Implement Subscription Heartbeat Logic
This implements the subscription heartbeat logic which will send the
message `RedfishServiceFunctional` periodically with the interval of
`HeartbeatIntervalMinutes` specified in subscription property [1][2], if
`SendHeartbeat` is enabled..
Note the heartbeat enablement is per event destination as DMTF specifies
[3] like
```
... This message shall only be sent if specifically requested by an event
destination during the creation of a subscription...
```
This also add `HeartbeatEvent` to supported registry prefixes like
```
curl -k -X GET https://${bmc}/redfish/v1/EventService/
{
...
"RegistryPrefixes": [
"Base",
"OpenBMC",
"TaskEvent",
"HeartbeatEvent"
],
"ResourceTypes": [
"Task",
"Heartbeat"
],
```
Tested:
1) A single subscription and heartbeat via Redfish Event Listener
- Create a subscription via Redfish Event Listener
- PATCH `SendHeartbeat=true` and `HeartbeatIntervalMinutes` like
```
curl -k -X PATCH https://${bmc}/redfish/v1/EventService/Subscriptions/${SUBID} \
-H "Content-Type: application/json" \
-d '{"SendHeartbeat":true, "HeartbeatIntervalMinutes":1}'
```
- Monitor the Redfish Event Listener and check the following heartbeat
messages periodically (per HeartbeatIntervalMinutes)
```
response_type: POST
headers: {'Host': '9.3.62.209', 'Content-Length': '230'}
response={
"@odata.type": "#Event.v1_4_0.Event",
"Events": [
{
"@odata.type": "#Message.v1_1_1.Message",
"EventId": "HeartbeatId",
"EventTimestamp": "2024-11-21T12:21:47+00:00",
"MemberId": "0",
"Message": "Redfish service is functional.",
"MessageArgs": [],
"MessageId": "HeartbeatEvent.1.0.1.RedfishServiceFunctional",
"MessageSeverity": "OK",
"OriginOfCondition": "/redfish/v1/EventService/Subscriptions/1521743607",
"Resolution": "None."
}
],
"Id": "HeartbeatId",
"Name": "Event Log"
}
```
- Change `SendHeartbeat` to false and see whether the heartbeat message
is stopped.
2) Multiple sbscribers with the different heartbeat setups
- create 2 event listeners with 2 different destinations (e.g., port
8080 and 8081).
- Patch sendheartbeat=true to only one subscriber.
- Check whether the only subscriber that enables `SendHeartbeat` is
receiving the heartbeat messages.
3) Redfish Service Validator passes
[1] https://github.com/openbmc/bmcweb/blob/02ea923f13de196726ac2f022766a6f80bee1c0a/redfish-core/schema/dmtf/json-schema/EventDestination.v1_15_0.json#L356
[2] https://redfish.dmtf.org/registries/HeartbeatEvent.1.0.1.json
[3] https://github.com/DMTF/Redfish/blob/d9e54fc8393d8930bd42e8b134741f5051a2680f/registries/HeartbeatEvent.1.0.1.json#L14
Change-Id: I8682e05f4459940913ba189f1ed016874e38dd4a
Signed-off-by: Myung Bae <myungbae@us.ibm.com>
diff --git a/redfish-core/src/subscription.cpp b/redfish-core/src/subscription.cpp
index 0b7a9fd..821077a 100644
--- a/redfish-core/src/subscription.cpp
+++ b/redfish-core/src/subscription.cpp
@@ -15,12 +15,14 @@
*/
#include "subscription.hpp"
+#include "dbus_singleton.hpp"
#include "event_log.hpp"
#include "event_logs_object_type.hpp"
#include "event_matches_filter.hpp"
#include "event_service_store.hpp"
#include "filter_expr_executor.hpp"
#include "generated/enums/log_entry.hpp"
+#include "heartbeat_messages.hpp"
#include "http_client.hpp"
#include "http_response.hpp"
#include "logging.hpp"
@@ -29,7 +31,9 @@
#include "ssl_key_handler.hpp"
#include "utils/time_utils.hpp"
+#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
+#include <boost/asio/steady_timer.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/system/errc.hpp>
#include <boost/url/format.hpp>
@@ -37,6 +41,7 @@
#include <nlohmann/json.hpp>
#include <algorithm>
+#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <ctime>
@@ -56,7 +61,7 @@
std::shared_ptr<persistent_data::UserSubscription> userSubIn,
const boost::urls::url_view_base& url, boost::asio::io_context& ioc) :
userSub{std::move(userSubIn)},
- policy(std::make_shared<crow::ConnectionPolicy>())
+ policy(std::make_shared<crow::ConnectionPolicy>()), hbTimer(ioc)
{
userSub->destinationUrl = url;
client.emplace(ioc, policy);
@@ -66,7 +71,7 @@
Subscription::Subscription(crow::sse_socket::Connection& connIn) :
userSub{std::make_shared<persistent_data::UserSubscription>()},
- sseConn(&connIn)
+ sseConn(&connIn), hbTimer(crow::connections::systemBus->get_io_context())
{}
// callback for subscription sendData
@@ -88,6 +93,7 @@
}
if (client->isTerminated())
{
+ hbTimer.cancel();
if (deleter)
{
BMCWEB_LOG_INFO("Subscription {} is deleted after MaxRetryAttempts",
@@ -97,6 +103,83 @@
}
}
+void Subscription::sendHeartbeatEvent()
+{
+ // send the heartbeat message
+ nlohmann::json eventMessage = messages::redfishServiceFunctional();
+
+ std::string heartEventId = std::to_string(eventSeqNum);
+ eventMessage["EventId"] = heartEventId;
+ eventMessage["EventTimestamp"] = time_utils::getDateTimeOffsetNow().first;
+ eventMessage["OriginOfCondition"] =
+ std::format("/redfish/v1/EventService/Subscriptions/{}", userSub->id);
+ eventMessage["MemberId"] = "0";
+
+ nlohmann::json::array_t eventRecord;
+ eventRecord.emplace_back(std::move(eventMessage));
+
+ nlohmann::json msgJson;
+ msgJson["@odata.type"] = "#Event.v1_4_0.Event";
+ msgJson["Name"] = "Heartbeat";
+ msgJson["Id"] = heartEventId;
+ msgJson["Events"] = std::move(eventRecord);
+
+ std::string strMsg =
+ msgJson.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
+ sendEventToSubscriber(std::move(strMsg));
+ eventSeqNum++;
+}
+
+void Subscription::scheduleNextHeartbeatEvent()
+{
+ hbTimer.expires_after(std::chrono::minutes(userSub->hbIntervalMinutes));
+ hbTimer.async_wait(
+ std::bind_front(&Subscription::onHbTimeout, this, weak_from_this()));
+}
+
+void Subscription::heartbeatParametersChanged()
+{
+ hbTimer.cancel();
+
+ if (userSub->sendHeartbeat)
+ {
+ scheduleNextHeartbeatEvent();
+ }
+}
+
+void Subscription::onHbTimeout(const std::weak_ptr<Subscription>& weakSelf,
+ const boost::system::error_code& ec)
+{
+ if (ec == boost::asio::error::operation_aborted)
+ {
+ BMCWEB_LOG_DEBUG("heartbeat timer async_wait is aborted");
+ return;
+ }
+ if (ec == boost::system::errc::operation_canceled)
+ {
+ BMCWEB_LOG_DEBUG("heartbeat timer async_wait canceled");
+ return;
+ }
+ if (ec)
+ {
+ BMCWEB_LOG_CRITICAL("heartbeat timer async_wait failed: {}", ec);
+ return;
+ }
+
+ std::shared_ptr<Subscription> self = weakSelf.lock();
+ if (!self)
+ {
+ BMCWEB_LOG_CRITICAL("onHbTimeout failed on Subscription");
+ return;
+ }
+
+ // Timer expired.
+ sendHeartbeatEvent();
+
+ // reschedule heartbeat timer
+ scheduleNextHeartbeatEvent();
+}
+
bool Subscription::sendEventToSubscriber(std::string&& msg)
{
persistent_data::EventServiceConfig eventServiceConfig =