diff --git a/src/health_monitoring_lib/BUILD b/src/health_monitoring_lib/BUILD index 9c138eee..89943f59 100644 --- a/src/health_monitoring_lib/BUILD +++ b/src/health_monitoring_lib/BUILD @@ -28,6 +28,8 @@ PROC_MACRO_DEPS = [ CC_SOURCES = [ "cpp/common.cpp", "cpp/deadline_monitor.cpp", + "cpp/heartbeat_monitor.cpp", + "cpp/logic_monitor.cpp", "cpp/health_monitor.cpp", ] @@ -35,6 +37,8 @@ CC_HDRS = [ "cpp/include/score/hm/common.h", "cpp/include/score/hm/tag.h", "cpp/include/score/hm/deadline/deadline_monitor.h", + "cpp/include/score/hm/heartbeat/heartbeat_monitor.h", + "cpp/include/score/hm/logic/logic_monitor.h", "cpp/include/score/hm/health_monitor.h", ] diff --git a/src/health_monitoring_lib/cpp/health_monitor.cpp b/src/health_monitoring_lib/cpp/health_monitor.cpp index ca4c8b2f..e022c548 100644 --- a/src/health_monitoring_lib/cpp/health_monitor.cpp +++ b/src/health_monitoring_lib/cpp/health_monitor.cpp @@ -18,6 +18,8 @@ extern "C" { using namespace score::hm; using namespace score::hm::internal; using namespace score::hm::deadline; +using namespace score::hm::heartbeat; +using namespace score::hm::logic; // Functions below must match functions defined in `crate::ffi`. @@ -30,9 +32,21 @@ FFICode health_monitor_builder_build(FFIHandle health_monitor_builder_handle, FFICode health_monitor_builder_add_deadline_monitor(FFIHandle health_monitor_builder_handle, const MonitorTag* monitor_tag, FFIHandle deadline_monitor_builder_handle); +FFICode health_monitor_builder_add_heartbeat_monitor(FFIHandle health_monitor_builder_handle, + const MonitorTag* monitor_tag, + FFIHandle heartbeat_monitor_builder_handle); +FFICode health_monitor_builder_add_logic_monitor(FFIHandle health_monitor_builder_handle, + const MonitorTag* monitor_tag, + FFIHandle logic_monitor_builder_handle); FFICode health_monitor_get_deadline_monitor(FFIHandle health_monitor_handle, const MonitorTag* monitor_tag, FFIHandle* deadline_monitor_handle_out); +FFICode health_monitor_get_heartbeat_monitor(FFIHandle health_monitor_handle, + const MonitorTag* monitor_tag, + FFIHandle* heartbeat_monitor_handle_out); +FFICode health_monitor_get_logic_monitor(FFIHandle health_monitor_handle, + const MonitorTag* monitor_tag, + FFIHandle* logic_monitor_handle_out); FFICode health_monitor_start(FFIHandle health_monitor_handle); FFICode health_monitor_destroy(FFIHandle health_monitor_handle); } @@ -71,6 +85,34 @@ HealthMonitorBuilder HealthMonitorBuilder::add_deadline_monitor(const MonitorTag return std::move(*this); } +HealthMonitorBuilder HealthMonitorBuilder::add_heartbeat_monitor(const MonitorTag& monitor_tag, + HeartbeatMonitorBuilder&& monitor) && +{ + auto monitor_handle = monitor.drop_by_rust(); + SCORE_LANGUAGE_FUTURECPP_PRECONDITION(monitor_handle.has_value()); + SCORE_LANGUAGE_FUTURECPP_PRECONDITION(health_monitor_builder_handle_.as_rust_handle().has_value()); + + auto result{health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle_.as_rust_handle().value(), &monitor_tag, monitor_handle.value())}; + SCORE_LANGUAGE_FUTURECPP_ASSERT(result == kSuccess); + + return std::move(*this); +} + +HealthMonitorBuilder HealthMonitorBuilder::add_logic_monitor(const MonitorTag& monitor_tag, + LogicMonitorBuilder&& monitor) && +{ + auto monitor_handle = monitor.drop_by_rust(); + SCORE_LANGUAGE_FUTURECPP_PRECONDITION(monitor_handle.has_value()); + SCORE_LANGUAGE_FUTURECPP_PRECONDITION(health_monitor_builder_handle_.as_rust_handle().has_value()); + + auto result{health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle_.as_rust_handle().value(), &monitor_tag, monitor_handle.value())}; + SCORE_LANGUAGE_FUTURECPP_ASSERT(result == kSuccess); + + return std::move(*this); +} + HealthMonitorBuilder HealthMonitorBuilder::with_internal_processing_cycle(std::chrono::milliseconds cycle_duration) && { internal_processing_cycle_duration_ = cycle_duration; @@ -122,6 +164,30 @@ score::cpp::expected HealthMonitor::get_deadline_monitor return score::cpp::expected(DeadlineMonitor{handle}); } +score::cpp::expected HealthMonitor::get_heartbeat_monitor(const MonitorTag& monitor_tag) +{ + FFIHandle handle{nullptr}; + auto result{health_monitor_get_heartbeat_monitor(health_monitor_, &monitor_tag, &handle)}; + if (result != kSuccess) + { + return score::cpp::unexpected(static_cast(result)); + } + + return score::cpp::expected(HeartbeatMonitor{handle}); +} + +score::cpp::expected HealthMonitor::get_logic_monitor(const MonitorTag& monitor_tag) +{ + FFIHandle handle{nullptr}; + auto result{health_monitor_get_logic_monitor(health_monitor_, &monitor_tag, &handle)}; + if (result != kSuccess) + { + return score::cpp::unexpected(static_cast(result)); + } + + return score::cpp::expected(LogicMonitor{handle}); +} + void HealthMonitor::start() { auto result{health_monitor_start(health_monitor_)}; diff --git a/src/health_monitoring_lib/cpp/heartbeat_monitor.cpp b/src/health_monitoring_lib/cpp/heartbeat_monitor.cpp new file mode 100644 index 00000000..7a4b6b87 --- /dev/null +++ b/src/health_monitoring_lib/cpp/heartbeat_monitor.cpp @@ -0,0 +1,57 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/hm/heartbeat/heartbeat_monitor.h" + +namespace { +extern "C" { +using namespace score::hm; +using namespace score::hm::internal; +using namespace score::hm::heartbeat; + +FFICode heartbeat_monitor_builder_create(uint32_t range_min_ms, uint32_t range_max_ms, FFIHandle* heartbeat_monitor_builder_handle_out); +FFICode heartbeat_monitor_builder_destroy(FFIHandle heartbeat_monitor_builder_handle); +FFICode heartbeat_monitor_destroy(FFIHandle heartbeat_monitor_builder_handle); +FFICode heartbeat_monitor_heartbeat(FFIHandle heartbeat_monitor_builder_handle); +} + +FFIHandle heartbeat_monitor_builder_create_wrapper(uint32_t range_min_ms, uint32_t range_max_ms) +{ + FFIHandle handle{nullptr}; + auto result{heartbeat_monitor_builder_create(range_min_ms, range_max_ms, &handle)}; + SCORE_LANGUAGE_FUTURECPP_ASSERT(result == kSuccess); + return handle; +} + +} + +namespace score::hm::heartbeat +{ +HeartbeatMonitorBuilder::HeartbeatMonitorBuilder(const TimeRange& range) + : monitor_builder_handle_{heartbeat_monitor_builder_create_wrapper(range.min_ms(), range.max_ms()), + &heartbeat_monitor_builder_destroy} +{ +} + +HeartbeatMonitor::HeartbeatMonitor(FFIHandle monitor_handle) + : monitor_handle_{monitor_handle, &heartbeat_monitor_destroy} +{ +} + +void HeartbeatMonitor::heartbeat() +{ + auto monitor_handle{monitor_handle_.as_rust_handle()}; + SCORE_LANGUAGE_FUTURECPP_PRECONDITION(monitor_handle.has_value()); + SCORE_LANGUAGE_FUTURECPP_ASSERT(heartbeat_monitor_heartbeat(monitor_handle.value()) == kSuccess); +} + +} // namespace score::hm::heartbeat diff --git a/src/health_monitoring_lib/cpp/include/score/hm/health_monitor.h b/src/health_monitoring_lib/cpp/include/score/hm/health_monitor.h index 369cd802..84fc9bdc 100644 --- a/src/health_monitoring_lib/cpp/include/score/hm/health_monitor.h +++ b/src/health_monitoring_lib/cpp/include/score/hm/health_monitor.h @@ -15,6 +15,8 @@ #include #include +#include +#include #include namespace score::hm @@ -42,6 +44,13 @@ class HealthMonitorBuilder final HealthMonitorBuilder add_deadline_monitor(const MonitorTag& monitor_tag, deadline::DeadlineMonitorBuilder&& monitor) &&; + /// Adds a heartbeat monitor for a specific identifier tag. + HealthMonitorBuilder add_heartbeat_monitor(const MonitorTag& monitor_tag, + heartbeat::HeartbeatMonitorBuilder&& monitor) &&; + + /// Adds a logic monitor for a specific identifier tag. + HealthMonitorBuilder add_logic_monitor(const MonitorTag& monitor_tag, logic::LogicMonitorBuilder&& monitor) &&; + /// Sets the cycle duration for supervisor API notifications. /// This duration determines how often the health monitor notifies the supervisor that the system is alive. HealthMonitorBuilder with_supervisor_api_cycle(std::chrono::milliseconds cycle_duration) &&; @@ -72,6 +81,8 @@ class HealthMonitor final ~HealthMonitor(); score::cpp::expected get_deadline_monitor(const MonitorTag& monitor_tag); + score::cpp::expected get_heartbeat_monitor(const MonitorTag& monitor_tag); + score::cpp::expected get_logic_monitor(const MonitorTag& monitor_tag); void start(); diff --git a/src/health_monitoring_lib/cpp/include/score/hm/heartbeat/heartbeat_monitor.h b/src/health_monitoring_lib/cpp/include/score/hm/heartbeat/heartbeat_monitor.h new file mode 100644 index 00000000..fbaa52e5 --- /dev/null +++ b/src/health_monitoring_lib/cpp/include/score/hm/heartbeat/heartbeat_monitor.h @@ -0,0 +1,84 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_HM_HEARTBEAT_HEARTBEAT_MONITOR_H +#define SCORE_HM_HEARTBEAT_HEARTBEAT_MONITOR_H + +#include +#include + +namespace score::hm +{ +// Forward declaration +class HealthMonitor; +class HealthMonitorBuilder; +} // namespace score::hm + +namespace score::hm::heartbeat +{ +// Forward declaration +class HeartbeatMonitor; + +/// Builder for `HeartbeatMonitor`. +class HeartbeatMonitorBuilder final : public internal::RustDroppable +{ + public: + /// Create a new `HeartbeatMonitorBuilder`. + /// + /// - `range` - time range between heartbeats. + HeartbeatMonitorBuilder(const TimeRange& range); + + HeartbeatMonitorBuilder(const HeartbeatMonitorBuilder&) = delete; + HeartbeatMonitorBuilder& operator=(const HeartbeatMonitorBuilder&) = delete; + + HeartbeatMonitorBuilder(HeartbeatMonitorBuilder&&) = default; + HeartbeatMonitorBuilder& operator=(HeartbeatMonitorBuilder&&) = delete; + + protected: + std::optional __drop_by_rust_impl() + { + return monitor_builder_handle_.drop_by_rust(); + } + + private: + internal::DroppableFFIHandle monitor_builder_handle_; + + // Allow to hide drop_by_rust implementation + friend class internal::RustDroppable; + + // Allow HealthMonitorBuilder to access drop_by_rust implementation + friend class ::score::hm::HealthMonitorBuilder; +}; + +class HeartbeatMonitor final +{ + public: + // Delete copy, allow move + HeartbeatMonitor(const HeartbeatMonitor&) = delete; + HeartbeatMonitor& operator=(const HeartbeatMonitor&) = delete; + + HeartbeatMonitor(HeartbeatMonitor&& other) noexcept = default; + HeartbeatMonitor& operator=(HeartbeatMonitor&& other) noexcept = default; + + void heartbeat(); + + private: + explicit HeartbeatMonitor(internal::FFIHandle monitor_handle); + + // Only `HealthMonitor` is allowed to create `HeartbeatMonitor` instances. + friend class score::hm::HealthMonitor; + internal::DroppableFFIHandle monitor_handle_; +}; + +} // namespace score::hm::heartbeat + +#endif // SCORE_HM_HEARTBEAT_HEARTBEAT_MONITOR_H diff --git a/src/health_monitoring_lib/cpp/include/score/hm/logic/logic_monitor.h b/src/health_monitoring_lib/cpp/include/score/hm/logic/logic_monitor.h new file mode 100644 index 00000000..26d367d7 --- /dev/null +++ b/src/health_monitoring_lib/cpp/include/score/hm/logic/logic_monitor.h @@ -0,0 +1,91 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_HM_LOGIC_LOGIC_MONITOR_H +#define SCORE_HM_LOGIC_LOGIC_MONITOR_H + +#include "score/hm/common.h" +#include "score/hm/tag.h" +#include + +namespace score::hm +{ +// Forward declaration +class HealthMonitor; +class HealthMonitorBuilder; +} // namespace score::hm + +namespace score::hm::logic +{ + +class LogicMonitorBuilder final : public internal::RustDroppable +{ + public: + /// Create a new `LogicMonitorBuilder`. + /// + /// - `initial_state` - starting point, implicitly added to the list of allowed states. + LogicMonitorBuilder(const StateTag& initial_state); + + LogicMonitorBuilder(const LogicMonitorBuilder&) = delete; + LogicMonitorBuilder& operator=(const LogicMonitorBuilder&) = delete; + + LogicMonitorBuilder(LogicMonitorBuilder&&) = default; + LogicMonitorBuilder& operator=(LogicMonitorBuilder&&) = delete; + + /// Add allowed state. + LogicMonitorBuilder add_state(const StateTag& state) &&; + + /// Add allowed transition. + LogicMonitorBuilder add_transition(const StateTag& from_state, const StateTag& to_state) &&; + + protected: + std::optional __drop_by_rust_impl() + { + return monitor_builder_handle_.drop_by_rust(); + } + + private: + internal::DroppableFFIHandle monitor_builder_handle_; + + // Allow to hide drop_by_rust implementation + friend class internal::RustDroppable; + + // Allow HealthMonitorBuilder to access drop_by_rust implementation + friend class ::score::hm::HealthMonitorBuilder; +}; + +class LogicMonitor final +{ + public: + LogicMonitor(const LogicMonitor&) = delete; + LogicMonitor& operator=(const LogicMonitor&) = delete; + + LogicMonitor(LogicMonitor&& other) noexcept = default; + LogicMonitor& operator=(LogicMonitor&& other) noexcept = default; + + /// Perform transition to a new state. + score::cpp::expected transition(const StateTag& state); + + /// Current monitor state. + score::cpp::expected state(); + + private: + explicit LogicMonitor(internal::FFIHandle monitor_handle); + + // Only `HealthMonitor` is allowed to create `LogicMonitor` instances. + friend class score::hm::HealthMonitor; + internal::DroppableFFIHandle monitor_handle_; +}; + +} // namespace score::hm::logic + +#endif // SCORE_HM_LOGIC_LOGIC_MONITOR_H diff --git a/src/health_monitoring_lib/cpp/include/score/hm/tag.h b/src/health_monitoring_lib/cpp/include/score/hm/tag.h index 5cb47d37..9ab32501 100644 --- a/src/health_monitoring_lib/cpp/include/score/hm/tag.h +++ b/src/health_monitoring_lib/cpp/include/score/hm/tag.h @@ -23,6 +23,9 @@ namespace score::hm class Tag { public: + /// Create an empty tag. + Tag() : data_{nullptr}, length_{0} {} + /// Create a new tag from a C-style string. template explicit Tag(const char (&tag)[N]) : data_(tag), length_(N - 1) @@ -49,6 +52,13 @@ class DeadlineTag : public Tag using Tag::Tag; }; +/// State tag. +class StateTag : public Tag +{ + public: + using Tag::Tag; +}; + } // namespace score::hm #endif // SCORE_HM_TAG_H diff --git a/src/health_monitoring_lib/cpp/logic_monitor.cpp b/src/health_monitoring_lib/cpp/logic_monitor.cpp new file mode 100644 index 00000000..0b820554 --- /dev/null +++ b/src/health_monitoring_lib/cpp/logic_monitor.cpp @@ -0,0 +1,103 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/hm/logic/logic_monitor.h" +#include + +namespace +{ +extern "C" { +using namespace score::hm; +using namespace score::hm::internal; +using namespace score::hm::logic; + +FFICode logic_monitor_builder_create(const StateTag* initial_state, FFIHandle* logic_monitor_builder_handle_out); +FFICode logic_monitor_builder_destroy(FFIHandle logic_monitor_builder_handle); +FFICode logic_monitor_builder_add_state(FFIHandle logic_monitor_builder_handle, const StateTag* state); +FFICode logic_monitor_builder_add_transition(FFIHandle logic_monitor_builder_handle, + const StateTag* from_state, + const StateTag* to_state); +FFICode logic_monitor_destroy(FFIHandle logic_monitor_handle); +FFICode logic_monitor_transition(FFIHandle logic_monitor_handle, const StateTag* to_state); +FFICode logic_monitor_state(FFIHandle logic_monitor_handle, StateTag* state_out); +} + +FFIHandle logic_monitor_builder_create_wrapper(const StateTag& initial_state) +{ + FFIHandle handle{nullptr}; + auto result{logic_monitor_builder_create(&initial_state, &handle)}; + SCORE_LANGUAGE_FUTURECPP_ASSERT(result == kSuccess); + return handle; +} +} // namespace + +namespace score::hm::logic +{ +LogicMonitorBuilder::LogicMonitorBuilder(const StateTag& initial_state) + : monitor_builder_handle_{logic_monitor_builder_create_wrapper(initial_state), &logic_monitor_builder_destroy} +{ +} + +LogicMonitorBuilder LogicMonitorBuilder::add_state(const StateTag& state) && +{ + auto monitor_builder_handle{monitor_builder_handle_.as_rust_handle()}; + SCORE_LANGUAGE_FUTURECPP_PRECONDITION(monitor_builder_handle.has_value()); + + auto result{logic_monitor_builder_add_state(monitor_builder_handle.value(), &state)}; + SCORE_LANGUAGE_FUTURECPP_ASSERT(result == kSuccess); + + return std::move(*this); +} + +LogicMonitorBuilder LogicMonitorBuilder::add_transition(const StateTag& from_state, const StateTag& to_state) && +{ + auto monitor_builder_handle{monitor_builder_handle_.as_rust_handle()}; + SCORE_LANGUAGE_FUTURECPP_PRECONDITION(monitor_builder_handle.has_value()); + + auto result{logic_monitor_builder_add_transition(monitor_builder_handle.value(), &from_state, &to_state)}; + SCORE_LANGUAGE_FUTURECPP_ASSERT(result == kSuccess); + + return std::move(*this); +} + +LogicMonitor::LogicMonitor(FFIHandle monitor_handle) : monitor_handle_{monitor_handle, &logic_monitor_destroy} {} + +score::cpp::expected LogicMonitor::transition(const StateTag& state) +{ + auto monitor_handle{monitor_handle_.as_rust_handle()}; + SCORE_LANGUAGE_FUTURECPP_PRECONDITION(monitor_handle.has_value()); + + auto result{logic_monitor_transition(monitor_handle.value(), &state)}; + if (result != kSuccess) + { + return score::cpp::unexpected(static_cast(result)); + } + + return score::cpp::expected(state); +} + +score::cpp::expected LogicMonitor::state() +{ + auto monitor_handle{monitor_handle_.as_rust_handle()}; + SCORE_LANGUAGE_FUTURECPP_PRECONDITION(monitor_handle.has_value()); + + StateTag state_tag; + auto result{logic_monitor_state(monitor_handle.value(), &state_tag)}; + if (result != kSuccess) + { + return score::cpp::unexpected(static_cast(result)); + } + + return score::cpp::expected(state_tag); +} + +} // namespace score::hm::logic diff --git a/src/health_monitoring_lib/cpp/tests/health_monitor_test.cpp b/src/health_monitoring_lib/cpp/tests/health_monitor_test.cpp index 4c39ec90..6ccb3afe 100644 --- a/src/health_monitoring_lib/cpp/tests/health_monitor_test.cpp +++ b/src/health_monitoring_lib/cpp/tests/health_monitor_test.cpp @@ -44,8 +44,22 @@ TEST_F(HealthMonitorTest, TestName) .add_deadline(DeadlineTag("deadline_2"), TimeRange(std::chrono::milliseconds(100), std::chrono::milliseconds(200))); + // Setup heartbeat monitor construction. + const MonitorTag heartbeat_monitor_tag{"heartbeat_monitor"}; + const TimeRange heartbeat_range{std::chrono::milliseconds{100}, std::chrono::milliseconds{200}}; + auto heartbeat_monitor_builder = heartbeat::HeartbeatMonitorBuilder(heartbeat_range); + + // Setup logic monitor construction. + const MonitorTag logic_monitor_tag{"logic_monitor"}; + StateTag from_state{"from_state"}; + StateTag to_state{"to_state"}; + auto logic_monitor_builder = + logic::LogicMonitorBuilder{from_state}.add_state(to_state).add_transition(from_state, to_state); + auto hm = HealthMonitorBuilder() .add_deadline_monitor(deadline_monitor_tag, std::move(deadline_monitor_builder)) + .add_heartbeat_monitor(heartbeat_monitor_tag, std::move(heartbeat_monitor_builder)) + .add_logic_monitor(logic_monitor_tag, std::move(logic_monitor_builder)) .with_internal_processing_cycle(std::chrono::milliseconds(50)) .with_supervisor_api_cycle(std::chrono::milliseconds(50)) .build(); @@ -62,9 +76,37 @@ TEST_F(HealthMonitorTest, TestName) auto deadline_mon = std::move(*deadline_monitor_res); + // Obtain heartbeat monitor from HMON. + auto heartbeat_monitor_res{hm.get_heartbeat_monitor(heartbeat_monitor_tag)}; + EXPECT_TRUE(heartbeat_monitor_res.has_value()); + + { + // Try again to get the same monitor. + auto heartbeat_monitor_res{hm.get_heartbeat_monitor(heartbeat_monitor_tag)}; + EXPECT_FALSE(heartbeat_monitor_res.has_value()); + } + + auto heartbeat_monitor{std::move(*heartbeat_monitor_res)}; + + // Obtain logic monitor from HMON. + auto logic_monitor_res{hm.get_logic_monitor(logic_monitor_tag)}; + EXPECT_TRUE(logic_monitor_res.has_value()); + + { + // Try again to get the same monitor. + auto logic_monitor_res{hm.get_logic_monitor(logic_monitor_tag)}; + EXPECT_FALSE(logic_monitor_res.has_value()); + } + + auto logic_monitor{std::move(*logic_monitor_res)}; + // Start HMON. hm.start(); + heartbeat_monitor.heartbeat(); + + EXPECT_TRUE(logic_monitor.transition(to_state).has_value()); + auto deadline_res = deadline_mon.get_deadline(DeadlineTag("deadline_1")); { diff --git a/src/health_monitoring_lib/rust/common.rs b/src/health_monitoring_lib/rust/common.rs index 82e4f887..a2313026 100644 --- a/src/health_monitoring_lib/rust/common.rs +++ b/src/health_monitoring_lib/rust/common.rs @@ -12,11 +12,14 @@ // ******************************************************************************* use crate::deadline::DeadlineEvaluationError; +use crate::heartbeat::HeartbeatEvaluationError; use crate::log::ScoreDebug; +use crate::logic::LogicEvaluationError; use crate::tag::MonitorTag; use core::hash::Hash; use core::time::Duration; use std::sync::Arc; +use std::time::Instant; /// Range of accepted time. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -48,8 +51,8 @@ pub(crate) trait Monitor { #[allow(dead_code)] pub(crate) enum MonitorEvaluationError { Deadline(DeadlineEvaluationError), - Heartbeat, - Logic, + Heartbeat(HeartbeatEvaluationError), + Logic(LogicEvaluationError), } impl From for MonitorEvaluationError { @@ -58,12 +61,25 @@ impl From for MonitorEvaluationError { } } +impl From for MonitorEvaluationError { + fn from(value: HeartbeatEvaluationError) -> Self { + MonitorEvaluationError::Heartbeat(value) + } +} + +impl From for MonitorEvaluationError { + fn from(value: LogicEvaluationError) -> Self { + MonitorEvaluationError::Logic(value) + } +} + /// Trait for evaluating monitors and reporting errors to be used by HealthMonitor. pub(crate) trait MonitorEvaluator { /// Run monitor evaluation. /// + /// - `hmon_starting_point` - starting point of all monitors. /// - `on_error` - error handling, containing tag of failing object and error code. - fn evaluate(&self, on_error: &mut dyn FnMut(&MonitorTag, MonitorEvaluationError)); + fn evaluate(&self, hmon_starting_point: Instant, on_error: &mut dyn FnMut(&MonitorTag, MonitorEvaluationError)); } /// Handle to a monitor evaluator, allowing for dynamic dispatch. @@ -78,7 +94,68 @@ impl MonitorEvalHandle { } impl MonitorEvaluator for MonitorEvalHandle { - fn evaluate(&self, on_error: &mut dyn FnMut(&MonitorTag, MonitorEvaluationError)) { - self.inner.evaluate(on_error) + fn evaluate(&self, hmon_starting_point: Instant, on_error: &mut dyn FnMut(&MonitorTag, MonitorEvaluationError)) { + self.inner.evaluate(hmon_starting_point, on_error) + } +} + +/// Get offset between HMON and monitor starting time points as [`u32`]. +pub(crate) fn hmon_time_offset(hmon_starting_point: Instant, monitor_starting_point: Instant) -> u32 { + let result = hmon_starting_point.checked_duration_since(monitor_starting_point); + let duration_since = result.expect("HMON starting point is earlier than monitor starting point"); + duration_to_u32(duration_since) +} + +/// Get duration as [`u32`]. +pub(crate) fn duration_to_u32(duration: Duration) -> u32 { + let millis = duration.as_millis(); + u32::try_from(millis).expect("Monitor running for too long") +} + +#[cfg(all(test, not(loom)))] +mod tests { + use crate::common::{duration_to_u32, hmon_time_offset}; + use core::time::Duration; + use std::time::Instant; + + #[test] + fn hmon_time_offset_valid() { + let monitor_starting_point = Instant::now(); + let hmon_starting_point = Instant::now(); + let offset = hmon_time_offset(hmon_starting_point, monitor_starting_point); + // Allow small offset. + assert!(offset < 10); + } + + #[test] + #[should_panic(expected = "HMON starting point is earlier than monitor starting point")] + fn hmon_time_offset_wrong_order() { + let hmon_starting_point = Instant::now(); + let monitor_starting_point = Instant::now(); + let _offset = hmon_time_offset(hmon_starting_point, monitor_starting_point); + } + + #[test] + #[should_panic(expected = "Monitor running for too long")] + fn hmon_time_offset_diff_too_large() { + const HUNDRED_DAYS_AS_SECS: u64 = 100 * 24 * 60 * 60; + let monitor_starting_point = Instant::now(); + let hmon_starting_point = Instant::now() + .checked_add(Duration::from_secs(HUNDRED_DAYS_AS_SECS)) + .unwrap(); + let _offset = hmon_time_offset(hmon_starting_point, monitor_starting_point); + } + + #[test] + fn duration_to_u32_valid() { + let result = duration_to_u32(Duration::from_millis(1234)); + assert_eq!(result, 1234); + } + + #[test] + #[should_panic(expected = "Monitor running for too long")] + fn duration_to_u32_too_large() { + const HUNDRED_DAYS_AS_SECS: u64 = 100 * 24 * 60 * 60; + let _result = duration_to_u32(Duration::from_secs(HUNDRED_DAYS_AS_SECS)); } } diff --git a/src/health_monitoring_lib/rust/deadline/deadline_monitor.rs b/src/health_monitoring_lib/rust/deadline/deadline_monitor.rs index 1f1c7cd8..b420f8bf 100644 --- a/src/health_monitoring_lib/rust/deadline/deadline_monitor.rs +++ b/src/health_monitoring_lib/rust/deadline/deadline_monitor.rs @@ -10,13 +10,12 @@ // // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -use crate::common::{Monitor, MonitorEvalHandle, MonitorEvaluationError, MonitorEvaluator}; +use crate::common::{duration_to_u32, Monitor, MonitorEvalHandle, MonitorEvaluationError, MonitorEvaluator, TimeRange}; use crate::deadline::common::{DeadlineTemplate, StateIndex}; use crate::deadline::deadline_state::{DeadlineState, DeadlineStateSnapshot}; use crate::log::{error, warn, ScoreDebug}; use crate::protected_memory::ProtectedMemoryAllocator; use crate::tag::{DeadlineTag, MonitorTag}; -use crate::TimeRange; use core::hash::Hash; use std::collections::HashMap; use std::sync::Arc; @@ -153,7 +152,7 @@ impl Deadline { /// Caller must ensure that deadline is not used until it's stopped. /// After this call You shall assure there's only a single owner of the `Deadline` instance and it does not call start before stopping. pub(super) unsafe fn start_internal(&mut self) -> Result<(), DeadlineError> { - let now = self.monitor.now(); + let now = duration_to_u32(self.monitor.monitor_starting_point.elapsed()); let max_time = now + self.range.max.as_millis() as u32; let mut is_broken = false; @@ -178,7 +177,7 @@ impl Deadline { } pub(super) fn stop_internal(&mut self) { - let now = self.monitor.now(); + let now = duration_to_u32(self.monitor.monitor_starting_point.elapsed()); let max = self.range.max.as_millis() as u32; let min = self.range.min.as_millis() as u32; @@ -260,7 +259,7 @@ struct DeadlineMonitorInner { } impl MonitorEvaluator for DeadlineMonitorInner { - fn evaluate(&self, on_error: &mut dyn FnMut(&MonitorTag, MonitorEvaluationError)) { + fn evaluate(&self, _hmon_starting_point: Instant, on_error: &mut dyn FnMut(&MonitorTag, MonitorEvaluationError)) { for (deadline_tag, deadline) in self.active_deadlines.iter() { let snapshot = deadline.snapshot(); if snapshot.is_underrun() { @@ -275,7 +274,7 @@ impl MonitorEvaluator for DeadlineMonitorInner { "Deadline snapshot cannot be both running and stopped" ); - let now = self.now(); + let now = duration_to_u32(self.monitor_starting_point.elapsed()); let expected = snapshot.timestamp_ms(); if now > expected { // Deadline missed, report @@ -336,13 +335,6 @@ impl DeadlineMonitorInner { Err(DeadlineMonitorError::DeadlineNotFound) } } - - fn now(&self) -> u32 { - let duration = self.monitor_starting_point.elapsed(); - // As u32 can hold up to ~49 days in milliseconds, this should be sufficient for our use case - // We still have a room up to 60bits timestamp if needed in future - u32::try_from(duration.as_millis()).expect("Monitor running for too long") - } } #[score_testing_macros::test_mod_with_log] @@ -410,6 +402,7 @@ mod tests { #[test] fn start_stop_deadline_within_range_works() { let monitor = create_monitor_with_deadlines(); + let hmon_starting_point = Instant::now(); let mut deadline = monitor.get_deadline(DeadlineTag::from("deadline_long")).unwrap(); let handle = deadline.start().unwrap(); @@ -417,49 +410,57 @@ mod tests { drop(handle); // stop the deadline - monitor.inner.evaluate(&mut |monitor_tag, deadline_failure| { - panic!( - "Deadline {:?} should not have failed or underrun({:?})", - monitor_tag, deadline_failure - ); - }); + monitor + .inner + .evaluate(hmon_starting_point, &mut |monitor_tag, deadline_failure| { + panic!( + "Deadline {:?} should not have failed or underrun({:?})", + monitor_tag, deadline_failure + ); + }); } #[test] fn start_stop_deadline_outside_ranges_is_error_when_dropped_before_evaluate() { let monitor = create_monitor_with_deadlines(); + let hmon_starting_point = Instant::now(); let mut deadline = monitor.get_deadline(DeadlineTag::from("deadline_long")).unwrap(); let handle = deadline.start().unwrap(); drop(handle); // stop the deadline - monitor.inner.evaluate(&mut |monitor_tag, deadline_failure| { - assert_eq!( - deadline_failure, - DeadlineEvaluationError::TooEarly.into(), - "Deadline {:?} should not have failed({:?})", - monitor_tag, - deadline_failure - ); - }); + monitor + .inner + .evaluate(hmon_starting_point, &mut |monitor_tag, deadline_failure| { + assert_eq!( + deadline_failure, + DeadlineEvaluationError::TooEarly.into(), + "Deadline {:?} should not have failed({:?})", + monitor_tag, + deadline_failure + ); + }); } #[test] fn deadline_outside_time_range_is_error_when_dropped_after_evaluate() { let monitor = create_monitor_with_deadlines(); + let hmon_starting_point = Instant::now(); let mut deadline = monitor.get_deadline(DeadlineTag::from("deadline_long")).unwrap(); let handle = deadline.start().unwrap(); // So deadline stop happens after evaluate, still it should be reported as failed - monitor.inner.evaluate(&mut |monitor_tag, deadline_failure| { - assert_eq!( - deadline_failure, - DeadlineEvaluationError::TooEarly.into(), - "Deadline {:?} should not have failed({:?})", - monitor_tag, - deadline_failure - ); - }); + monitor + .inner + .evaluate(hmon_starting_point, &mut |monitor_tag, deadline_failure| { + assert_eq!( + deadline_failure, + DeadlineEvaluationError::TooEarly.into(), + "Deadline {:?} should not have failed({:?})", + monitor_tag, + deadline_failure + ); + }); drop(handle); // stop the deadline } @@ -467,6 +468,7 @@ mod tests { #[test] fn deadline_failed_on_first_run_and_then_restarted_is_evaluated_as_error() { let monitor = create_monitor_with_deadlines(); + let hmon_starting_point = Instant::now(); let mut deadline = monitor.get_deadline(DeadlineTag::from("deadline_long")).unwrap(); let handle = deadline.start().unwrap(); @@ -477,39 +479,45 @@ mod tests { let handle = deadline.start(); assert_eq!(handle.err(), Some(DeadlineError::DeadlineAlreadyFailed)); - monitor.inner.evaluate(&mut |monitor_tag, deadline_failure| { - assert_eq!( - deadline_failure, - DeadlineEvaluationError::TooEarly.into(), - "Deadline {:?} should not have failed ({:?})", - monitor_tag, - deadline_failure - ); - }); + monitor + .inner + .evaluate(hmon_starting_point, &mut |monitor_tag, deadline_failure| { + assert_eq!( + deadline_failure, + DeadlineEvaluationError::TooEarly.into(), + "Deadline {:?} should not have failed ({:?})", + monitor_tag, + deadline_failure + ); + }); } #[test] fn start_stop_deadline_outside_ranges_is_evaluated_as_error() { let monitor = create_monitor_with_deadlines(); + let hmon_starting_point = Instant::now(); let mut deadline = monitor.get_deadline(DeadlineTag::from("deadline_fast")).unwrap(); let handle = deadline.start().unwrap(); drop(handle); // stop the deadline - monitor.inner.evaluate(&mut |monitor_tag, deadline_failure| { - assert_eq!( - deadline_failure, - DeadlineEvaluationError::TooLate.into(), - "Deadline {:?} should not have failed({:?})", - monitor_tag, - deadline_failure - ); - }); + monitor + .inner + .evaluate(hmon_starting_point, &mut |monitor_tag, deadline_failure| { + assert_eq!( + deadline_failure, + DeadlineEvaluationError::TooLate.into(), + "Deadline {:?} should not have failed({:?})", + monitor_tag, + deadline_failure + ); + }); } #[test] fn monitor_with_multiple_running_deadlines() { let monitor = create_monitor_with_multiple_running_deadlines(); + let hmon_starting_point = Instant::now(); let mut deadline = monitor.get_deadline(DeadlineTag::from("deadline_fast1")).unwrap(); let _handle1 = deadline.start().unwrap(); @@ -524,16 +532,18 @@ mod tests { let mut cnt = 0; - monitor.inner.evaluate(&mut |monitor_tag, deadline_failure| { - cnt += 1; - assert_eq!( - deadline_failure, - DeadlineEvaluationError::TooLate.into(), - "Deadline {:?} should not have failed({:?})", - monitor_tag, - deadline_failure - ); - }); + monitor + .inner + .evaluate(hmon_starting_point, &mut |monitor_tag, deadline_failure| { + cnt += 1; + assert_eq!( + deadline_failure, + DeadlineEvaluationError::TooLate.into(), + "Deadline {:?} should not have failed({:?})", + monitor_tag, + deadline_failure + ); + }); assert_eq!(cnt, 3, "All three deadlines should have been evaluated"); } diff --git a/src/health_monitoring_lib/rust/ffi.rs b/src/health_monitoring_lib/rust/ffi.rs index 38505c27..01c49e9b 100644 --- a/src/health_monitoring_lib/rust/ffi.rs +++ b/src/health_monitoring_lib/rust/ffi.rs @@ -12,6 +12,8 @@ // ******************************************************************************* use crate::deadline::ffi::DeadlineMonitorCpp; use crate::deadline::DeadlineMonitorBuilder; +use crate::heartbeat::HeartbeatMonitorBuilder; +use crate::logic::LogicMonitorBuilder; use crate::tag::MonitorTag; use crate::{HealthMonitor, HealthMonitorBuilder, HealthMonitorError}; use core::mem::ManuallyDrop; @@ -171,6 +173,73 @@ pub extern "C" fn health_monitor_builder_add_deadline_monitor( FFICode::Success } +#[no_mangle] +pub extern "C" fn health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle: FFIHandle, + monitor_tag: *const MonitorTag, + heartbeat_monitor_builder_handle: FFIHandle, +) -> FFICode { + if health_monitor_builder_handle.is_null() || monitor_tag.is_null() || heartbeat_monitor_builder_handle.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // `MonitorTag` type must be compatible between C++ and Rust. + let monitor_tag = unsafe { *monitor_tag }; + + // SAFETY: + // Validity of this pointer is ensured. + // It is assumed that the pointer was created by a call to `heartbeat_monitor_builder_create`. + // It is assumed that the pointer was not consumed by a call to `heartbeat_monitor_builder_destroy`. + let heartbeat_monitor_builder = + unsafe { Box::from_raw(heartbeat_monitor_builder_handle as *mut HeartbeatMonitorBuilder) }; + + // SAFETY: + // Validity of the pointer is ensured. + // It is assumed that the pointer was created by a call to `health_monitor_builder_create`. + // It is assumed that the pointer was not consumed by calls to `health_monitor_builder_destroy` or `health_monitor_builder_build`. + let mut health_monitor_builder = + FFIBorrowed::new(unsafe { Box::from_raw(health_monitor_builder_handle as *mut HealthMonitorBuilder) }); + + health_monitor_builder.add_heartbeat_monitor_internal(monitor_tag, *heartbeat_monitor_builder); + + FFICode::Success +} + +#[no_mangle] +pub extern "C" fn health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle: FFIHandle, + monitor_tag: *const MonitorTag, + logic_monitor_builder_handle: FFIHandle, +) -> FFICode { + if health_monitor_builder_handle.is_null() || monitor_tag.is_null() || logic_monitor_builder_handle.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // `MonitorTag` type must be compatible between C++ and Rust. + let monitor_tag = unsafe { *monitor_tag }; + + // SAFETY: + // Validity of this pointer is ensured. + // It is assumed that the pointer was created by a call to `logic_monitor_builder_create`. + // It is assumed that the pointer was not consumed by a call to `logic_monitor_builder_destroy`. + let logic_monitor_builder = unsafe { Box::from_raw(logic_monitor_builder_handle as *mut LogicMonitorBuilder) }; + + // SAFETY: + // Validity of the pointer is ensured. + // It is assumed that the pointer was created by a call to `health_monitor_builder_create`. + // It is assumed that the pointer was not consumed by calls to `health_monitor_builder_destroy` or `health_monitor_builder_build`. + let mut health_monitor_builder = + FFIBorrowed::new(unsafe { Box::from_raw(health_monitor_builder_handle as *mut HealthMonitorBuilder) }); + + health_monitor_builder.add_logic_monitor_internal(monitor_tag, *logic_monitor_builder); + + FFICode::Success +} + #[no_mangle] pub extern "C" fn health_monitor_get_deadline_monitor( health_monitor_handle: FFIHandle, @@ -202,6 +271,68 @@ pub extern "C" fn health_monitor_get_deadline_monitor( } } +#[no_mangle] +pub extern "C" fn health_monitor_get_heartbeat_monitor( + health_monitor_handle: FFIHandle, + monitor_tag: *const MonitorTag, + heartbeat_monitor_handle_out: *mut FFIHandle, +) -> FFICode { + if health_monitor_handle.is_null() || monitor_tag.is_null() || heartbeat_monitor_handle_out.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // `MonitorTag` type must be compatible between C++ and Rust. + let monitor_tag = unsafe { *monitor_tag }; + + // SAFETY: + // Validity of the pointer is ensured. + // It is assumed that the pointer was created by a call to `health_monitor_builder_build`. + // It is assumed that the pointer was not consumed by a call to `health_monitor_destroy`. + let mut health_monitor = FFIBorrowed::new(unsafe { Box::from_raw(health_monitor_handle as *mut HealthMonitor) }); + + if let Some(heartbeat_monitor) = health_monitor.get_heartbeat_monitor(monitor_tag) { + unsafe { + *heartbeat_monitor_handle_out = Box::into_raw(Box::new(heartbeat_monitor)).cast(); + } + FFICode::Success + } else { + FFICode::NotFound + } +} + +#[no_mangle] +pub extern "C" fn health_monitor_get_logic_monitor( + health_monitor_handle: FFIHandle, + monitor_tag: *const MonitorTag, + logic_monitor_handle_out: *mut FFIHandle, +) -> FFICode { + if health_monitor_handle.is_null() || monitor_tag.is_null() || logic_monitor_handle_out.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // `MonitorTag` type must be compatible between C++ and Rust. + let monitor_tag = unsafe { *monitor_tag }; + + // SAFETY: + // Validity of the pointer is ensured. + // It is assumed that the pointer was created by a call to `health_monitor_builder_build`. + // It is assumed that the pointer was not consumed by a call to `health_monitor_destroy`. + let mut health_monitor = FFIBorrowed::new(unsafe { Box::from_raw(health_monitor_handle as *mut HealthMonitor) }); + + if let Some(logic_monitor) = health_monitor.get_logic_monitor(monitor_tag) { + unsafe { + *logic_monitor_handle_out = Box::into_raw(Box::new(logic_monitor)).cast(); + } + FFICode::Success + } else { + FFICode::NotFound + } +} + #[no_mangle] pub extern "C" fn health_monitor_start(health_monitor_handle: FFIHandle) -> FFICode { if health_monitor_handle.is_null() { @@ -244,13 +375,41 @@ mod tests { deadline_monitor_builder_create, deadline_monitor_builder_destroy, deadline_monitor_destroy, }; use crate::ffi::{ - health_monitor_builder_add_deadline_monitor, health_monitor_builder_build, health_monitor_builder_create, + health_monitor_builder_add_deadline_monitor, health_monitor_builder_add_heartbeat_monitor, + health_monitor_builder_add_logic_monitor, health_monitor_builder_build, health_monitor_builder_create, health_monitor_builder_destroy, health_monitor_destroy, health_monitor_get_deadline_monitor, - health_monitor_start, FFICode, FFIHandle, + health_monitor_get_heartbeat_monitor, health_monitor_get_logic_monitor, health_monitor_start, FFICode, + FFIHandle, + }; + use crate::heartbeat::ffi::{ + heartbeat_monitor_builder_create, heartbeat_monitor_builder_destroy, heartbeat_monitor_destroy, }; - use crate::tag::MonitorTag; + use crate::logic::ffi::{ + logic_monitor_builder_add_state, logic_monitor_builder_add_transition, logic_monitor_builder_create, + logic_monitor_builder_destroy, logic_monitor_destroy, + }; + use crate::tag::{MonitorTag, StateTag}; use core::ptr::null_mut; + fn def_logic_monitor_builder() -> FFIHandle { + let mut logic_monitor_builder_handle = null_mut(); + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + let _ = logic_monitor_builder_add_transition( + logic_monitor_builder_handle, + &state1 as *const StateTag, + &state2 as *const StateTag, + ); + + logic_monitor_builder_handle + } + #[test] fn health_monitor_builder_create_succeeds() { let mut health_monitor_builder_handle: FFIHandle = null_mut(); @@ -455,6 +614,181 @@ mod tests { health_monitor_builder_destroy(health_monitor_builder_handle); } + #[test] + fn health_monitor_builder_add_heartbeat_monitor_succeeds() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let _ = heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + + let health_monitor_builder_add_heartbeat_monitor_result = health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle, + &heartbeat_monitor_tag as *const MonitorTag, + heartbeat_monitor_builder_handle, + ); + assert_eq!(health_monitor_builder_add_heartbeat_monitor_result, FFICode::Success); + + // Clean-up. + health_monitor_builder_destroy(health_monitor_builder_handle); + } + + #[test] + fn health_monitor_builder_add_heartbeat_monitor_null_hmon_builder() { + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let _ = heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + + let health_monitor_builder_add_heartbeat_monitor_result = health_monitor_builder_add_heartbeat_monitor( + null_mut(), + &heartbeat_monitor_tag as *const MonitorTag, + heartbeat_monitor_builder_handle, + ); + assert_eq!( + health_monitor_builder_add_heartbeat_monitor_result, + FFICode::NullParameter + ); + + // Clean-up. + heartbeat_monitor_builder_destroy(heartbeat_monitor_builder_handle); + } + + #[test] + fn health_monitor_builder_add_heartbeat_monitor_null_monitor_tag() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let _ = heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + + let health_monitor_builder_add_heartbeat_monitor_result = health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle, + null_mut(), + heartbeat_monitor_builder_handle, + ); + assert_eq!( + health_monitor_builder_add_heartbeat_monitor_result, + FFICode::NullParameter + ); + + // Clean-up. + heartbeat_monitor_builder_destroy(heartbeat_monitor_builder_handle); + health_monitor_builder_destroy(health_monitor_builder_handle); + } + + #[test] + fn health_monitor_builder_add_heartbeat_monitor_null_heartbeat_monitor_builder() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + + let health_monitor_builder_add_heartbeat_monitor_result = health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle, + &heartbeat_monitor_tag as *const MonitorTag, + null_mut(), + ); + assert_eq!( + health_monitor_builder_add_heartbeat_monitor_result, + FFICode::NullParameter + ); + + // Clean-up. + health_monitor_builder_destroy(health_monitor_builder_handle); + } + + #[test] + fn health_monitor_builder_add_logic_monitor_succeeds() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let initial_state = StateTag::from("initial_state"); + let _ = logic_monitor_builder_create( + &initial_state as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + + let health_monitor_builder_add_logic_monitor_result = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + assert_eq!(health_monitor_builder_add_logic_monitor_result, FFICode::Success); + + // Clean-up. + health_monitor_builder_destroy(health_monitor_builder_handle); + } + + #[test] + fn health_monitor_builder_add_logic_monitor_null_hmon_builder() { + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let initial_state = StateTag::from("initial_state"); + let _ = logic_monitor_builder_create( + &initial_state as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + + let health_monitor_builder_add_logic_monitor_result = health_monitor_builder_add_logic_monitor( + null_mut(), + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + assert_eq!(health_monitor_builder_add_logic_monitor_result, FFICode::NullParameter); + + // Clean-up. + logic_monitor_builder_destroy(logic_monitor_builder_handle); + } + + #[test] + fn health_monitor_builder_add_logic_monitor_null_monitor_tag() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let initial_state = StateTag::from("initial_state"); + let _ = logic_monitor_builder_create( + &initial_state as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + + let health_monitor_builder_add_logic_monitor_result = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + null_mut(), + logic_monitor_builder_handle, + ); + assert_eq!(health_monitor_builder_add_logic_monitor_result, FFICode::NullParameter); + + // Clean-up. + logic_monitor_builder_destroy(logic_monitor_builder_handle); + health_monitor_builder_destroy(health_monitor_builder_handle); + } + + #[test] + fn health_monitor_builder_add_logic_monitor_null_logic_monitor_builder() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + + let health_monitor_builder_add_logic_monitor_result = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + null_mut(), + ); + assert_eq!(health_monitor_builder_add_logic_monitor_result, FFICode::NullParameter); + + // Clean-up. + health_monitor_builder_destroy(health_monitor_builder_handle); + } + #[test] fn health_monitor_get_deadline_monitor_succeeds() { let mut health_monitor_builder_handle: FFIHandle = null_mut(); @@ -636,6 +970,363 @@ mod tests { health_monitor_destroy(health_monitor_handle); } + #[test] + fn health_monitor_get_heartbeat_monitor_succeeds() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let _ = heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + let _ = health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle, + &heartbeat_monitor_tag as *const MonitorTag, + heartbeat_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + let health_monitor_get_heartbeat_monitor_result = health_monitor_get_heartbeat_monitor( + health_monitor_handle, + &heartbeat_monitor_tag as *const MonitorTag, + &mut heartbeat_monitor_handle as *mut FFIHandle, + ); + assert!(!heartbeat_monitor_handle.is_null()); + assert_eq!(health_monitor_get_heartbeat_monitor_result, FFICode::Success); + + // Clean-up. + heartbeat_monitor_destroy(heartbeat_monitor_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn health_monitor_get_heartbeat_monitor_already_taken() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_1_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_2_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let _ = heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + let _ = health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle, + &heartbeat_monitor_tag as *const MonitorTag, + heartbeat_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + // First get. + let health_monitor_get_heartbeat_monitor_result_1 = health_monitor_get_heartbeat_monitor( + health_monitor_handle, + &heartbeat_monitor_tag as *const MonitorTag, + &mut heartbeat_monitor_1_handle as *mut FFIHandle, + ); + assert!(!heartbeat_monitor_1_handle.is_null()); + assert_eq!(health_monitor_get_heartbeat_monitor_result_1, FFICode::Success); + + // Second get. + let health_monitor_get_heartbeat_monitor_result_2 = health_monitor_get_heartbeat_monitor( + health_monitor_handle, + &heartbeat_monitor_tag as *const MonitorTag, + &mut heartbeat_monitor_2_handle as *mut FFIHandle, + ); + assert!(heartbeat_monitor_2_handle.is_null()); + assert_eq!(health_monitor_get_heartbeat_monitor_result_2, FFICode::NotFound); + + // Clean-up. + heartbeat_monitor_destroy(heartbeat_monitor_1_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn health_monitor_get_heartbeat_monitor_null_hmon() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let _ = heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + let _ = health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle, + &heartbeat_monitor_tag as *const MonitorTag, + heartbeat_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + let health_monitor_get_heartbeat_monitor_result = health_monitor_get_heartbeat_monitor( + null_mut(), + &heartbeat_monitor_tag as *const MonitorTag, + &mut heartbeat_monitor_handle as *mut FFIHandle, + ); + assert!(heartbeat_monitor_handle.is_null()); + assert_eq!(health_monitor_get_heartbeat_monitor_result, FFICode::NullParameter); + + // Clean-up. + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn health_monitor_get_heartbeat_monitor_null_monitor_tag() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let _ = heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + let _ = health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle, + &heartbeat_monitor_tag as *const MonitorTag, + heartbeat_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + let health_monitor_get_heartbeat_monitor_result = health_monitor_get_heartbeat_monitor( + health_monitor_handle, + null_mut(), + &mut heartbeat_monitor_handle as *mut FFIHandle, + ); + assert!(heartbeat_monitor_handle.is_null()); + assert_eq!(health_monitor_get_heartbeat_monitor_result, FFICode::NullParameter); + + // Clean-up. + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn health_monitor_get_heartbeat_monitor_null_heartbeat_monitor() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let _ = heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + let _ = health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle, + &heartbeat_monitor_tag as *const MonitorTag, + heartbeat_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + let health_monitor_get_heartbeat_monitor_result = health_monitor_get_heartbeat_monitor( + health_monitor_handle, + &heartbeat_monitor_tag as *const MonitorTag, + null_mut(), + ); + assert_eq!(health_monitor_get_heartbeat_monitor_result, FFICode::NullParameter); + + // Clean-up. + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn health_monitor_get_logic_monitor_succeeds() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder_handle = def_logic_monitor_builder(); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + let health_monitor_get_logic_monitor_result = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_handle as *mut FFIHandle, + ); + assert!(!logic_monitor_handle.is_null()); + assert_eq!(health_monitor_get_logic_monitor_result, FFICode::Success); + + // Clean-up. + logic_monitor_destroy(logic_monitor_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn health_monitor_get_logic_monitor_already_taken() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_1_handle: FFIHandle = null_mut(); + let mut logic_monitor_2_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder_handle = def_logic_monitor_builder(); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + // First get. + let health_monitor_get_logic_monitor_result_1 = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_1_handle as *mut FFIHandle, + ); + assert!(!logic_monitor_1_handle.is_null()); + assert_eq!(health_monitor_get_logic_monitor_result_1, FFICode::Success); + + // Second get. + let health_monitor_get_logic_monitor_result_2 = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_2_handle as *mut FFIHandle, + ); + assert!(logic_monitor_2_handle.is_null()); + assert_eq!(health_monitor_get_logic_monitor_result_2, FFICode::NotFound); + + // Clean-up. + logic_monitor_destroy(logic_monitor_1_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn health_monitor_get_logic_monitor_null_hmon() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder_handle = def_logic_monitor_builder(); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + let health_monitor_get_logic_monitor_result = health_monitor_get_logic_monitor( + null_mut(), + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_handle as *mut FFIHandle, + ); + assert!(logic_monitor_handle.is_null()); + assert_eq!(health_monitor_get_logic_monitor_result, FFICode::NullParameter); + + // Clean-up. + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn health_monitor_get_logic_monitor_null_monitor_tag() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder_handle = def_logic_monitor_builder(); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + let health_monitor_get_logic_monitor_result = health_monitor_get_logic_monitor( + health_monitor_handle, + null_mut(), + &mut logic_monitor_handle as *mut FFIHandle, + ); + assert!(logic_monitor_handle.is_null()); + assert_eq!(health_monitor_get_logic_monitor_result, FFICode::NullParameter); + + // Clean-up. + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn health_monitor_get_logic_monitor_null_logic_monitor() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder_handle = def_logic_monitor_builder(); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + + let health_monitor_get_logic_monitor_result = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + null_mut(), + ); + assert_eq!(health_monitor_get_logic_monitor_result, FFICode::NullParameter); + + // Clean-up. + health_monitor_destroy(health_monitor_handle); + } + #[test] fn health_monitor_start_succeeds() { let mut health_monitor_builder_handle: FFIHandle = null_mut(); diff --git a/src/health_monitoring_lib/rust/heartbeat/ffi.rs b/src/health_monitoring_lib/rust/heartbeat/ffi.rs new file mode 100644 index 00000000..4f170bae --- /dev/null +++ b/src/health_monitoring_lib/rust/heartbeat/ffi.rs @@ -0,0 +1,177 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use crate::ffi::{FFIBorrowed, FFICode, FFIHandle}; +use crate::heartbeat::{HeartbeatMonitor, HeartbeatMonitorBuilder}; +use crate::TimeRange; +use core::time::Duration; + +#[no_mangle] +pub extern "C" fn heartbeat_monitor_builder_create( + range_min_ms: u32, + range_max_ms: u32, + heartbeat_monitor_builder_handle_out: *mut FFIHandle, +) -> FFICode { + if heartbeat_monitor_builder_handle_out.is_null() { + return FFICode::NullParameter; + } + + let range_min = Duration::from_millis(range_min_ms as u64); + let range_max = Duration::from_millis(range_max_ms as u64); + let range = TimeRange::new(range_min, range_max); + + let heartbeat_monitor_builder = HeartbeatMonitorBuilder::new(range); + unsafe { + *heartbeat_monitor_builder_handle_out = Box::into_raw(Box::new(heartbeat_monitor_builder)).cast(); + } + + FFICode::Success +} + +#[no_mangle] +pub extern "C" fn heartbeat_monitor_builder_destroy(heartbeat_monitor_builder_handle: FFIHandle) -> FFICode { + if heartbeat_monitor_builder_handle.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // It is assumed that the pointer was created by a call to `heartbeat_monitor_builder_create`. + unsafe { + let _ = Box::from_raw(heartbeat_monitor_builder_handle as *mut HeartbeatMonitorBuilder); + } + + FFICode::Success +} + +#[no_mangle] +pub extern "C" fn heartbeat_monitor_destroy(heartbeat_monitor_handle: FFIHandle) -> FFICode { + if heartbeat_monitor_handle.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // It is assumed that the pointer was created by a call to `health_monitor_get_heartbeat_monitor`. + unsafe { + let _ = Box::from_raw(heartbeat_monitor_handle as *mut HeartbeatMonitor); + } + + FFICode::Success +} + +#[no_mangle] +pub extern "C" fn heartbeat_monitor_heartbeat(heartbeat_monitor_handle: FFIHandle) -> FFICode { + if heartbeat_monitor_handle.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of this pointer is ensured. + // It is assumed that the pointer was created by a call to `health_monitor_get_heartbeat_monitor`. + // It is assumed that the pointer was not consumed by a call to `heartbeat_monitor_destroy`. + let monitor = FFIBorrowed::new(unsafe { Box::from_raw(heartbeat_monitor_handle as *mut HeartbeatMonitor) }); + + monitor.heartbeat(); + + FFICode::Success +} + +#[score_testing_macros::test_mod_with_log] +#[cfg(all(test, not(loom)))] +mod tests { + use crate::ffi::{ + health_monitor_builder_add_heartbeat_monitor, health_monitor_builder_build, health_monitor_builder_create, + health_monitor_destroy, health_monitor_get_heartbeat_monitor, FFICode, FFIHandle, + }; + use crate::heartbeat::ffi::{ + heartbeat_monitor_builder_create, heartbeat_monitor_builder_destroy, heartbeat_monitor_destroy, + heartbeat_monitor_heartbeat, + }; + use crate::tag::MonitorTag; + use core::ptr::null_mut; + + #[test] + fn heartbeat_monitor_builder_create_succeeds() { + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + + let heartbeat_monitor_builder_create_result = + heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + assert!(!heartbeat_monitor_builder_handle.is_null()); + assert_eq!(heartbeat_monitor_builder_create_result, FFICode::Success); + + // Clean-up. + // NOTE: `heartbeat_monitor_builder_destroy` positive path is already tested here. + let heartbeat_monitor_builder_destroy_result = + heartbeat_monitor_builder_destroy(heartbeat_monitor_builder_handle); + assert_eq!(heartbeat_monitor_builder_destroy_result, FFICode::Success); + } + + #[test] + fn heartbeat_monitor_builder_create_null_builder() { + let heartbeat_monitor_builder_create_result = heartbeat_monitor_builder_create(100, 200, null_mut()); + assert_eq!(heartbeat_monitor_builder_create_result, FFICode::NullParameter); + } + + #[test] + fn heartbeat_monitor_builder_destroy_null_builder() { + let heartbeat_monitor_builder_destroy_result = heartbeat_monitor_builder_destroy(null_mut()); + assert_eq!(heartbeat_monitor_builder_destroy_result, FFICode::NullParameter); + } + + #[test] + fn heartbeat_monitor_destroy_null_monitor() { + let heartbeat_monitor_destroy_result = heartbeat_monitor_destroy(null_mut()); + assert_eq!(heartbeat_monitor_destroy_result, FFICode::NullParameter); + } + + #[test] + fn heartbeat_monitor_heartbeat_succeeds() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_builder_handle: FFIHandle = null_mut(); + let mut heartbeat_monitor_handle: FFIHandle = null_mut(); + + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let _ = heartbeat_monitor_builder_create(100, 200, &mut heartbeat_monitor_builder_handle as *mut FFIHandle); + let _ = health_monitor_builder_add_heartbeat_monitor( + health_monitor_builder_handle, + &heartbeat_monitor_tag as *const MonitorTag, + heartbeat_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + let _ = health_monitor_get_heartbeat_monitor( + health_monitor_handle, + &heartbeat_monitor_tag as *const MonitorTag, + &mut heartbeat_monitor_handle as *mut FFIHandle, + ); + + let heartbeat_monitor_heartbeat_result = heartbeat_monitor_heartbeat(heartbeat_monitor_handle); + assert_eq!(heartbeat_monitor_heartbeat_result, FFICode::Success); + + // Clean-up. + heartbeat_monitor_destroy(heartbeat_monitor_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn heartbeat_monitor_heartbeat_null_monitor() { + let heartbeat_monitor_heartbeat_result = heartbeat_monitor_heartbeat(null_mut()); + assert_eq!(heartbeat_monitor_heartbeat_result, FFICode::NullParameter); + } +} diff --git a/src/health_monitoring_lib/rust/heartbeat/heartbeat_monitor.rs b/src/health_monitoring_lib/rust/heartbeat/heartbeat_monitor.rs new file mode 100644 index 00000000..6fc13acb --- /dev/null +++ b/src/health_monitoring_lib/rust/heartbeat/heartbeat_monitor.rs @@ -0,0 +1,708 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use crate::common::{ + duration_to_u32, hmon_time_offset, Monitor, MonitorEvalHandle, MonitorEvaluationError, MonitorEvaluator, TimeRange, +}; +use crate::heartbeat::heartbeat_state::{HeartbeatState, HeartbeatStateSnapshot}; +use crate::log::{error, warn}; +use crate::protected_memory::ProtectedMemoryAllocator; +use crate::tag::MonitorTag; +use crate::HealthMonitorError; +use core::time::Duration; +use score_log::ScoreDebug; +use std::sync::Arc; +use std::time::Instant; + +/// Heartbeat evaluation errors. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, ScoreDebug)] +pub(crate) enum HeartbeatEvaluationError { + /// Finished too early. + TooEarly, + /// Finished too late. + TooLate, + /// Multiple heartbeats observed. + MultipleHeartbeats, +} + +/// Builder for [`HeartbeatMonitor`]. +#[derive(Debug)] +pub struct HeartbeatMonitorBuilder { + /// Time range between heartbeats. + range: TimeRange, +} + +impl HeartbeatMonitorBuilder { + /// Create a new [`HeartbeatMonitorBuilder`]. + /// + /// - `range` - time range between heartbeats. + pub fn new(range: TimeRange) -> Self { + Self { range } + } + + /// Build the [`HeartbeatMonitor`]. + /// + /// - `monitor_tag` - tag of this monitor. + /// - `internal_processing_cycle` - health monitor processing cycle. + /// - `_allocator` - protected memory allocator. + pub(crate) fn build( + self, + monitor_tag: MonitorTag, + internal_processing_cycle: Duration, + _allocator: &ProtectedMemoryAllocator, + ) -> Result { + // Check range is valid. + let range_min_ms = self.range.min.as_millis() as u64; + let internal_processing_cycle_ms = internal_processing_cycle.as_millis() as u64; + if range_min_ms * 2 <= internal_processing_cycle_ms { + error!( + "Internal processing cycle duration ({} ms) must be shorter than two shortest allowed ranges ({} ms).", + internal_processing_cycle_ms, range_min_ms + ); + return Err(HealthMonitorError::InvalidArgument); + } + + let inner = Arc::new(HeartbeatMonitorInner::new(monitor_tag, self.range)); + Ok(HeartbeatMonitor::new(inner)) + } +} + +/// Heartbeat monitor. +pub struct HeartbeatMonitor { + inner: Arc, +} + +impl HeartbeatMonitor { + /// Create a new [`HeartbeatMonitor`] instance. + fn new(inner: Arc) -> Self { + Self { inner } + } + + /// Provide a heartbeat. + pub fn heartbeat(&self) { + self.inner.heartbeat() + } +} + +impl Monitor for HeartbeatMonitor { + fn get_eval_handle(&self) -> crate::common::MonitorEvalHandle { + MonitorEvalHandle::new(Arc::clone(&self.inner)) + } +} + +/// Time range using [`u32`]. +#[derive(ScoreDebug)] +struct InternalRange { + min: u32, + max: u32, +} + +impl InternalRange { + /// Create range using provided values. + fn new(min: u32, max: u32) -> Self { + assert!(min <= max, "provided min is greater than provided max"); + Self { min, max } + } + + /// Create range with values offset by timestamp. + fn offset(&self, timestamp: u32) -> Self { + Self::new(self.min + timestamp, self.max + timestamp) + } +} + +impl From for InternalRange { + fn from(value: TimeRange) -> Self { + let min = duration_to_u32(value.min); + let max = duration_to_u32(value.max); + Self::new(min, max) + } +} + +pub(crate) struct HeartbeatMonitorInner { + /// Tag of this monitor. + monitor_tag: MonitorTag, + + /// Time range between heartbeats. + range: InternalRange, + + /// Monitor starting point. + /// Offset is calculated during evaluation in relation to provided health monitor starting point. + monitor_starting_point: Instant, + + /// Current heartbeat state. + /// Contains data in relation to [`Self::monitor_starting_point`]. + heartbeat_state: HeartbeatState, +} + +impl MonitorEvaluator for HeartbeatMonitorInner { + fn evaluate(&self, hmon_starting_point: Instant, on_error: &mut dyn FnMut(&MonitorTag, MonitorEvaluationError)) { + // Get current timestamp, with offset to HMON time. + let offset = hmon_time_offset(hmon_starting_point, self.monitor_starting_point); + let now = offset + duration_to_u32(hmon_starting_point.elapsed()); + + // Load current monitor state. + let snapshot = self.heartbeat_state.snapshot(); + + // Get and recalculate snapshot timestamps. + // IMPORTANT: first heartbeat is obtained when HMON time is unknown. + // It is necessary to: + // - use offset as cycle starting point. + // - get heartbeat snapshot in relation to zero point. + let (start_timestamp, heartbeat_timestamp) = if snapshot.post_init() { + let start_timestamp = snapshot.start_timestamp(); + let heartbeat_timestamp = start_timestamp + snapshot.heartbeat_timestamp_offset(); + (start_timestamp, heartbeat_timestamp) + } else { + let start_timestamp = offset; + let heartbeat_timestamp = snapshot.heartbeat_timestamp_offset(); + (start_timestamp, heartbeat_timestamp) + }; + + // Get allowed time range as absolute values. + let range = self.range.offset(start_timestamp); + + // Check current counter state. + let counter = snapshot.counter(); + // Disallow multiple heartbeats in same heartbeat cycle. + if counter > 1 { + warn!("Multiple heartbeats detected"); + on_error(&self.monitor_tag, HeartbeatEvaluationError::MultipleHeartbeats.into()); + return; + } + // Handle no heartbeats. + else if counter == 0 { + // Disallow no heartbeats when already out of time range. + // Stop execution if still in range. + if now > range.max { + let offset = now - range.max; + warn!("No heartbeat detected, observed after range: {}", offset); + on_error(&self.monitor_tag, HeartbeatEvaluationError::TooLate.into()); + } + // Either way - execution is stopped here. + return; + } + + // Check current heartbeat state. + // Heartbeat before allowed range. + if heartbeat_timestamp < range.min { + let offset = range.min - heartbeat_timestamp; + warn!("Heartbeat occurred too early, offset to range: {}", offset); + on_error(&self.monitor_tag, HeartbeatEvaluationError::TooEarly.into()); + } + // Heartbeat after allowed range. + else if heartbeat_timestamp > range.max { + let offset = heartbeat_timestamp - range.max; + warn!("Heartbeat occurred too late, offset to range: {}", offset); + on_error(&self.monitor_tag, HeartbeatEvaluationError::TooLate.into()); + } + // Heartbeat in allowed state. + else { + // Update heartbeat monitor state with a current heartbeat as a beginning of a new cycle. + let _ = self + .heartbeat_state + .update(|_| Some(HeartbeatStateSnapshot::new(heartbeat_timestamp))); + } + } +} + +impl HeartbeatMonitorInner { + fn new(monitor_tag: MonitorTag, range: TimeRange) -> Self { + let monitor_starting_point = Instant::now(); + let heartbeat_state_snapshot = HeartbeatStateSnapshot::default(); + let heartbeat_state = HeartbeatState::new(heartbeat_state_snapshot); + Self { + monitor_tag, + range: InternalRange::from(range), + monitor_starting_point, + heartbeat_state, + } + } + + /// Provide a heartbeat. + fn heartbeat(&self) { + // Get current timestamp. + let now = duration_to_u32(self.monitor_starting_point.elapsed()); + + // Set heartbeat timestamp and update counter. + let _ = self.heartbeat_state.update(|mut state| { + let start_ts = state.start_timestamp(); + state.set_heartbeat_timestamp_offset(now - start_ts); + state.increment_counter(); + Some(state) + }); + } +} + +#[cfg(test)] +mod test_common { + use crate::TimeRange; + use core::time::Duration; + use std::thread::sleep; + use std::time::Instant; + + pub(super) const TAG: &str = "heartbeat_monitor"; + + pub(super) fn sleep_until(target: Duration, start: Instant) { + let elapsed = start.elapsed(); + let diff = target.saturating_sub(elapsed); + sleep(diff) + } + + pub(super) fn range_from_ms(min: u64, max: u64) -> TimeRange { + TimeRange::new(Duration::from_millis(min), Duration::from_millis(max)) + } +} + +#[score_testing_macros::test_mod_with_log] +#[cfg(all(test, not(loom)))] +mod tests { + use crate::common::{MonitorEvaluationError, MonitorEvaluator, TimeRange}; + use crate::heartbeat::heartbeat_monitor::test_common::{range_from_ms, sleep_until, TAG}; + use crate::heartbeat::{HeartbeatEvaluationError, HeartbeatMonitor, HeartbeatMonitorBuilder}; + use crate::protected_memory::ProtectedMemoryAllocator; + use crate::tag::MonitorTag; + use crate::HealthMonitorError; + use core::sync::atomic::{AtomicBool, Ordering}; + use core::time::Duration; + use std::sync::Arc; + use std::thread::{sleep, spawn}; + use std::time::Instant; + + #[test] + fn heartbeat_monitor_builder_build_ok() { + let range = TimeRange::new(Duration::from_millis(500), Duration::from_millis(1000)); + let monitor_tag = MonitorTag::from("heartbeat_monitor"); + let internal_processing_cycle = Duration::from_millis(100); + let allocator = ProtectedMemoryAllocator {}; + let result = HeartbeatMonitorBuilder::new(range).build(monitor_tag, internal_processing_cycle, &allocator); + assert!(result.is_ok()); + } + + #[test] + fn heartbeat_monitor_builder_build_invalid_internal_processing_cycle() { + let range = TimeRange::new(Duration::from_millis(500), Duration::from_millis(1000)); + let monitor_tag = MonitorTag::from("heartbeat_monitor"); + let internal_processing_cycle = Duration::from_millis(1000); + let allocator = ProtectedMemoryAllocator {}; + let result = HeartbeatMonitorBuilder::new(range).build(monitor_tag, internal_processing_cycle, &allocator); + assert!(result.is_err_and(|e| e == HealthMonitorError::InvalidArgument)); + } + + fn create_monitor_single_cycle(range: TimeRange) -> HeartbeatMonitor { + let monitor_tag = MonitorTag::from(TAG); + let internal_processing_cycle = Duration::from_millis(1); + let allocator = ProtectedMemoryAllocator {}; + HeartbeatMonitorBuilder::new(range) + .build(monitor_tag, internal_processing_cycle, &allocator) + .unwrap() + } + + #[test] + fn heartbeat_monitor_no_beat_evaluate_early() { + let range = range_from_ms(80, 120); + let monitor = create_monitor_single_cycle(range); + let hmon_starting_point = Instant::now(); + + // No beat happened, no error is expected. + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}") + }); + } + + #[test] + fn heartbeat_monitor_no_beat_evaluate_in_range() { + let range = range_from_ms(80, 120); + let monitor = create_monitor_single_cycle(range); + let hmon_starting_point = Instant::now(); + + // Wait until middle of range. + sleep_until(Duration::from_millis(100), hmon_starting_point); + + // No beat happened, no error is expected. + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}") + }); + } + #[test] + fn heartbeat_monitor_no_beat_evaluate_late() { + let range = range_from_ms(80, 120); + let monitor = create_monitor_single_cycle(range); + let hmon_starting_point = Instant::now(); + + // Wait until late. + sleep_until(Duration::from_millis(150), hmon_starting_point); + + // No beat happened, too late error is expected. + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + assert_eq!(*monitor_tag, MonitorTag::from(TAG)); + assert_eq!(error, HeartbeatEvaluationError::TooLate.into()); + }); + } + + fn beat_eval_test( + beat_time: Duration, + eval_time: Duration, + on_error: &mut dyn FnMut(&MonitorTag, MonitorEvaluationError), + ) { + let range = range_from_ms(80, 120); + let monitor = create_monitor_single_cycle(range); + let hmon_starting_point = Instant::now(); + + // Wait and beat. + sleep_until(beat_time, hmon_starting_point); + monitor.heartbeat(); + + // Wait and evaluate. + sleep_until(eval_time, hmon_starting_point); + monitor.inner.evaluate(hmon_starting_point, on_error); + } + + fn beat_early_test(eval_time: Duration) { + beat_eval_test(Duration::from_millis(25), eval_time, &mut |monitor_tag, error| { + assert_eq!(*monitor_tag, MonitorTag::from(TAG)); + assert_eq!(error, HeartbeatEvaluationError::TooEarly.into()); + }); + } + + #[test] + fn heartbeat_monitor_beat_early_evaluate_early() { + beat_early_test(Duration::from_millis(50)); + } + + #[test] + fn heartbeat_monitor_beat_early_evaluate_in_range() { + beat_early_test(Duration::from_millis(100)); + } + + #[test] + fn heartbeat_monitor_beat_early_evaluate_late() { + beat_early_test(Duration::from_millis(150)); + } + + fn beat_in_range_test(eval_time: Duration) { + beat_eval_test(Duration::from_millis(90), eval_time, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}") + }); + } + + #[test] + fn heartbeat_monitor_beat_in_range_evaluate_in_range() { + beat_in_range_test(Duration::from_millis(100)); + } + + #[test] + fn heartbeat_monitor_beat_in_range_evaluate_late() { + beat_in_range_test(Duration::from_millis(150)); + } + + #[test] + fn heartbeat_monitor_beat_late_evaluate_late() { + beat_eval_test( + Duration::from_millis(150), + Duration::from_millis(200), + &mut |monitor_tag, error| { + assert_eq!(*monitor_tag, MonitorTag::from(TAG)); + assert_eq!(error, HeartbeatEvaluationError::TooLate.into()); + }, + ) + } + + fn multiple_beats_eval_test(beat_time: Duration, eval_time: Duration) { + let range = range_from_ms(80, 120); + let monitor = create_monitor_single_cycle(range); + let hmon_starting_point = Instant::now(); + + // Wait and beat. + sleep_until(beat_time, hmon_starting_point); + const NUM_BEATS: usize = 10; + for _ in 0..NUM_BEATS { + monitor.heartbeat(); + } + + // Wait and evaluate. + sleep_until(eval_time, hmon_starting_point); + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + assert_eq!(*monitor_tag, MonitorTag::from(TAG)); + assert_eq!(error, HeartbeatEvaluationError::MultipleHeartbeats.into()); + }); + } + + #[test] + fn heartbeat_monitor_multiple_beats_early_evaluate_early() { + multiple_beats_eval_test(Duration::from_millis(25), Duration::from_millis(50)) + } + + #[test] + fn heartbeat_monitor_multiple_beats_early_evaluate_in_range() { + multiple_beats_eval_test(Duration::from_millis(25), Duration::from_millis(100)) + } + + #[test] + fn heartbeat_monitor_multiple_beats_early_evaluate_late() { + multiple_beats_eval_test(Duration::from_millis(25), Duration::from_millis(150)) + } + + #[test] + fn heartbeat_monitor_multiple_beats_in_range_evaluate_in_range() { + multiple_beats_eval_test(Duration::from_millis(90), Duration::from_millis(100)) + } + + #[test] + fn heartbeat_monitor_multiple_beats_in_range_evaluate_late() { + multiple_beats_eval_test(Duration::from_millis(90), Duration::from_millis(150)) + } + + #[test] + fn heartbeat_monitor_multiple_beats_late_evaluate_late() { + multiple_beats_eval_test(Duration::from_millis(150), Duration::from_millis(200)) + } + + fn create_monitor_multiple_cycles(cycle: Duration) -> Arc { + let range = range_from_ms(80, 120); + let monitor_tag = MonitorTag::from(TAG); + let allocator = ProtectedMemoryAllocator {}; + let monitor = HeartbeatMonitorBuilder::new(range) + .build(monitor_tag, cycle, &allocator) + .unwrap(); + Arc::new(monitor) + } + + #[test] + fn heartbeat_monitor_cycle_early() { + let cycle = Duration::from_millis(20); + let monitor = create_monitor_multiple_cycles(cycle); + let hmon_starting_point = Instant::now(); + + // Run heartbeat thread. + let monitor_clone = monitor.clone(); + let heartbeat_finished = Arc::new(AtomicBool::new(false)); + let heartbeat_finished_clone = heartbeat_finished.clone(); + let heartbeat_thread = spawn(move || { + const NUM_BEATS: u32 = 3; + const BEAT_INTERVAL: Duration = Duration::from_millis(100); + for i in 1..NUM_BEATS { + sleep_until(i * BEAT_INTERVAL, hmon_starting_point); + monitor_clone.heartbeat(); + } + + // Perform a last heartbeat in shorter interval. + sleep_until( + NUM_BEATS * BEAT_INTERVAL - Duration::from_millis(40), + hmon_starting_point, + ); + monitor_clone.heartbeat(); + + heartbeat_finished_clone.store(true, Ordering::Release); + }); + + // Run evaluation thread. + while !heartbeat_finished.load(Ordering::Acquire) { + sleep(cycle); + // Too early error is expected. + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + assert_eq!(*monitor_tag, MonitorTag::from(TAG)); + assert_eq!(error, HeartbeatEvaluationError::TooEarly.into()); + }); + } + + heartbeat_thread.join().unwrap(); + } + + #[test] + fn heartbeat_monitor_cycle_in_range() { + let cycle = Duration::from_millis(20); + let monitor = create_monitor_multiple_cycles(cycle); + let hmon_starting_point = Instant::now(); + + // Run heartbeat thread. + let monitor_clone = monitor.clone(); + let heartbeat_finished = Arc::new(AtomicBool::new(false)); + let heartbeat_finished_clone = heartbeat_finished.clone(); + let heartbeat_thread = spawn(move || { + const NUM_BEATS: u32 = 3; + const BEAT_INTERVAL: Duration = Duration::from_millis(100); + for i in 1..=NUM_BEATS { + sleep_until(i * BEAT_INTERVAL, hmon_starting_point); + monitor_clone.heartbeat(); + } + heartbeat_finished_clone.store(true, Ordering::Release); + }); + + // Run evaluation thread. + while !heartbeat_finished.load(Ordering::Acquire) { + sleep(cycle); + // No error is expected. + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}") + }); + } + + heartbeat_thread.join().unwrap(); + } + + #[test] + fn heartbeat_monitor_cycle_late() { + let cycle = Duration::from_millis(20); + let monitor = create_monitor_multiple_cycles(cycle); + let hmon_starting_point = Instant::now(); + + // Run heartbeat thread. + let monitor_clone = monitor.clone(); + let heartbeat_finished = Arc::new(AtomicBool::new(false)); + let heartbeat_finished_clone = heartbeat_finished.clone(); + let heartbeat_thread = spawn(move || { + const NUM_BEATS: u32 = 3; + const BEAT_INTERVAL: Duration = Duration::from_millis(100); + for i in 1..NUM_BEATS { + sleep_until(i * BEAT_INTERVAL, hmon_starting_point); + monitor_clone.heartbeat(); + } + + // Perform a last heartbeat in shorter interval. + sleep_until( + NUM_BEATS * BEAT_INTERVAL + Duration::from_millis(40), + hmon_starting_point, + ); + monitor_clone.heartbeat(); + + heartbeat_finished_clone.store(true, Ordering::Release); + }); + + // Run evaluation thread. + while !heartbeat_finished.load(Ordering::Acquire) { + sleep(cycle); + // No heartbeat or too late error is expected. + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + assert_eq!(*monitor_tag, MonitorTag::from(TAG)); + assert_eq!(error, HeartbeatEvaluationError::TooLate.into()); + }); + } + + heartbeat_thread.join().unwrap(); + } + + #[test] + fn heartbeat_monitor_timestamp_offset() { + let range = range_from_ms(80, 120); + let monitor = create_monitor_single_cycle(range); + + // Move away monitor creation and HMON starting point. + sleep(Duration::from_millis(300)); + let hmon_starting_point = Instant::now(); + + // Wait and beat. + sleep_until(Duration::from_millis(90), hmon_starting_point); + monitor.heartbeat(); + + // Wait and evaluate. + sleep_until(Duration::from_millis(100), hmon_starting_point); + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}") + }); + } +} + +#[cfg(all(test, loom))] +mod loom_tests { + use crate::common::MonitorEvaluator; + use crate::heartbeat::heartbeat_monitor::test_common::{range_from_ms, sleep_until, TAG}; + use crate::heartbeat::{HeartbeatEvaluationError, HeartbeatMonitor, HeartbeatMonitorBuilder}; + use crate::protected_memory::ProtectedMemoryAllocator; + use crate::tag::MonitorTag; + use crate::TimeRange; + use core::time::Duration; + use loom::thread::spawn; + use std::sync::Arc; + use std::time::Instant; + + fn create_monitor_single_cycle(range: TimeRange) -> Arc { + let monitor_tag = MonitorTag::from(TAG); + let internal_processing_cycle = Duration::from_millis(1); + let allocator = ProtectedMemoryAllocator {}; + let monitor = HeartbeatMonitorBuilder::new(range) + .build(monitor_tag, internal_processing_cycle, &allocator) + .unwrap(); + Arc::new(monitor) + } + + #[test] + fn heartbeat_monitor_heartbeat_evaluate_too_early() { + loom::model(|| { + let range = range_from_ms(30, 70); + let monitor = create_monitor_single_cycle(range); + let hmon_starting_point = Instant::now(); + + // Perform heartbeat in a separate thread. + let monitor_clone = monitor.clone(); + let heartbeat_thread = spawn(move || monitor_clone.heartbeat()); + + // Evaluate. + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + assert_eq!(*monitor_tag, MonitorTag::from(TAG)); + assert_eq!(error, HeartbeatEvaluationError::TooEarly.into()); + }); + + heartbeat_thread.join().unwrap(); + }); + } + + #[test] + fn heartbeat_monitor_heartbeat_evaluate_in_range() { + loom::model(|| { + let range = range_from_ms(30, 70); + let monitor = create_monitor_single_cycle(range); + let hmon_starting_point = Instant::now(); + + // Wait until in range. + sleep_until(Duration::from_millis(50), hmon_starting_point); + + // Perform heartbeat in a separate thread. + let monitor_clone = monitor.clone(); + let heartbeat_thread = spawn(move || monitor_clone.heartbeat()); + + // Evaluate. + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}"); + }); + + heartbeat_thread.join().unwrap(); + }); + } + + #[test] + fn heartbeat_monitor_heartbeat_evaluate_too_late() { + loom::model(|| { + let range = range_from_ms(30, 70); + let monitor = create_monitor_single_cycle(range); + let hmon_starting_point = Instant::now(); + + // Wait until too late. + sleep_until(Duration::from_millis(100), hmon_starting_point); + + // Perform heartbeat in a separate thread. + let monitor_clone = monitor.clone(); + let heartbeat_thread = spawn(move || monitor_clone.heartbeat()); + + // Evaluate. + let mut error_detected = false; + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + assert_eq!(*monitor_tag, MonitorTag::from(TAG)); + assert_eq!(error, HeartbeatEvaluationError::TooLate.into()); + error_detected = true; + }); + + heartbeat_thread.join().unwrap(); + assert!(error_detected); + }); + } +} diff --git a/src/health_monitoring_lib/rust/heartbeat/heartbeat_state.rs b/src/health_monitoring_lib/rust/heartbeat/heartbeat_state.rs new file mode 100644 index 00000000..38c3fabc --- /dev/null +++ b/src/health_monitoring_lib/rust/heartbeat/heartbeat_state.rs @@ -0,0 +1,321 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use core::cmp::min; + +#[cfg(not(loom))] +use core::sync::atomic::{AtomicU64, Ordering}; +#[cfg(loom)] +use loom::sync::atomic::{AtomicU64, Ordering}; + +/// Snapshot of a heartbeat state. +/// Data layout: +/// - cycle start timestamp: 32 bits +/// - heartbeat timestamp offset: 29 bits +/// - heartbeat counter: 2 bits +/// - post-init flag: 1 bit +#[derive(Clone, Copy, Default)] +pub struct HeartbeatStateSnapshot(u64); + +const START_MASK: u64 = 0xFFFFFFFF_00000000; +const START_OFFSET: u32 = u32::BITS; +const BEAT_MASK: u64 = 0x00000000_FFFFFFF8; +const BEAT_OFFSET: u32 = 3; +const COUNT_MASK: u64 = 0b0110; +const COUNT_OFFSET: u32 = 1; +const POST_INIT_MASK: u64 = 0b0001; + +impl HeartbeatStateSnapshot { + /// Create a new snapshot with known starting point. + /// `post_init` flag is implicitly set to 1. + pub fn new(start_timestamp: u32) -> Self { + let mut snapshot = Self::default(); + snapshot.set_start_timestamp(start_timestamp); + snapshot.set_post_init(true); + snapshot + } + + /// Return underlying data. + pub fn as_u64(&self) -> u64 { + self.0 + } + + /// Cycle start timestamp. + pub fn start_timestamp(&self) -> u32 { + ((self.0 & START_MASK) >> START_OFFSET) as u32 + } + + /// Set cycle start timestamp. + pub fn set_start_timestamp(&mut self, value: u32) { + self.0 = ((value as u64) << START_OFFSET) | (self.0 & !START_MASK); + } + + /// Heartbeat timestamp offset. + pub fn heartbeat_timestamp_offset(&self) -> u32 { + ((self.0 & BEAT_MASK) >> BEAT_OFFSET) as u32 + } + + /// Set heartbeat timestamp offset. + /// Value is 29-bit, must be lower than 0x1FFFFFFF. + pub fn set_heartbeat_timestamp_offset(&mut self, value: u32) { + assert!(value < 1 << 29, "provided heartbeat offset is out of range"); + self.0 = ((value as u64) << BEAT_OFFSET) | (self.0 & !BEAT_MASK); + } + + /// Heartbeat counter. + pub fn counter(&self) -> u8 { + ((self.0 & COUNT_MASK) >> COUNT_OFFSET) as u8 + } + + /// Increment heartbeat counter. + /// Value is 2-bit, larger values are saturated to max value (3). + pub fn increment_counter(&mut self) { + let value = min(self.counter() + 1, 3); + self.0 = ((value as u64) << COUNT_OFFSET) | (self.0 & !COUNT_MASK); + } + + /// Post-init state. + /// This should be `false` only before first cycle is concluded. + pub fn post_init(&self) -> bool { + let value = self.0 & POST_INIT_MASK; + value != 0 + } + + /// Set post-init state. + pub fn set_post_init(&mut self, value: bool) { + self.0 = (value as u64) | (self.0 & !POST_INIT_MASK); + } +} + +impl From for HeartbeatStateSnapshot { + fn from(value: u64) -> Self { + Self(value) + } +} + +/// Atomic representation of [`HeartbeatStateSnapshot`]. +pub struct HeartbeatState(AtomicU64); + +impl HeartbeatState { + /// Create a new [`HeartbeatState`] using provided [`HeartbeatStateSnapshot`]. + pub fn new(snapshot: HeartbeatStateSnapshot) -> Self { + Self(AtomicU64::new(snapshot.as_u64())) + } + + /// Return a snapshot of the current heartbeat state. + pub fn snapshot(&self) -> HeartbeatStateSnapshot { + HeartbeatStateSnapshot::from(self.0.load(Ordering::Relaxed)) + } + + /// Update the heartbeat state using the provided closure. + /// Closure receives the current state and should return an [`Option`] containing a new state. + /// If [`None`] is returned then the state was not updated. + pub fn update Option>( + &self, + mut f: F, + ) -> Result { + // Prev values returned + self.0 + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |prev| { + let snapshot = HeartbeatStateSnapshot::from(prev); + f(snapshot).map(|new_snapshot| new_snapshot.as_u64()) + }) + .map(HeartbeatStateSnapshot::from) + .map_err(HeartbeatStateSnapshot::from) + } +} + +#[cfg(all(test, not(loom)))] +mod tests { + use crate::heartbeat::heartbeat_state::{HeartbeatState, HeartbeatStateSnapshot}; + use core::cmp::min; + use core::sync::atomic::Ordering; + + #[test] + fn snapshot_new_zero() { + let state = HeartbeatStateSnapshot::new(0); + + assert_eq!(state.as_u64(), 0x01); + assert_eq!(state.start_timestamp(), 0); + assert_eq!(state.heartbeat_timestamp_offset(), 0); + assert_eq!(state.counter(), 0); + assert!(state.post_init()); + } + + #[test] + fn snapshot_new_valid() { + let state = HeartbeatStateSnapshot::new(0xDEADBEEF); + + assert_eq!(state.as_u64(), (0xDEADBEEF << u32::BITS) + 0x01); + assert_eq!(state.start_timestamp(), 0xDEADBEEF); + assert_eq!(state.heartbeat_timestamp_offset(), 0); + assert_eq!(state.counter(), 0); + assert!(state.post_init()); + } + + #[test] + fn snapshot_new_max() { + let state = HeartbeatStateSnapshot::new(u32::MAX); + + assert_eq!(state.as_u64(), ((u32::MAX as u64) << u32::BITS) + 0x01); + assert_eq!(state.start_timestamp(), u32::MAX); + assert_eq!(state.heartbeat_timestamp_offset(), 0); + assert_eq!(state.counter(), 0); + assert!(state.post_init()); + } + + #[test] + fn snapshot_from_u64_zero() { + let state = HeartbeatStateSnapshot::from(0); + + assert_eq!(state.as_u64(), 0); + assert_eq!(state.start_timestamp(), 0); + assert_eq!(state.heartbeat_timestamp_offset(), 0); + assert_eq!(state.counter(), 0); + assert!(!state.post_init()); + } + + #[test] + fn snapshot_from_u64_valid() { + let state = HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEEF); + + assert_eq!(state.as_u64(), 0xDEADBEEF_DEADBEEF); + assert_eq!(state.start_timestamp(), 0xDEADBEEF); + assert_eq!(state.heartbeat_timestamp_offset(), 0xDEADBEEF >> 3); + assert_eq!(state.counter(), 3); + assert!(state.post_init()); + } + + #[test] + fn snapshot_from_u64_max() { + let state = HeartbeatStateSnapshot::from(u64::MAX); + + assert_eq!(state.as_u64(), u64::MAX); + assert_eq!(state.start_timestamp(), u32::MAX); + assert_eq!(state.heartbeat_timestamp_offset(), u32::MAX >> 3); + assert_eq!(state.counter(), 3); + assert!(state.post_init()); + } + + #[test] + fn snapshot_default() { + let state = HeartbeatStateSnapshot::default(); + + assert_eq!(state.as_u64(), 0); + assert_eq!(state.start_timestamp(), 0); + assert_eq!(state.heartbeat_timestamp_offset(), 0); + assert_eq!(state.counter(), 0); + assert!(!state.post_init()); + } + + #[test] + fn snapshot_set_start_timestamp() { + let mut state = HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEEF); + state.set_start_timestamp(0xCAFEBAAD); + + assert_eq!(state.start_timestamp(), 0xCAFEBAAD); + + // Check other parameters unchanged. + assert_eq!(state.heartbeat_timestamp_offset(), 0xDEADBEEF >> 3); + assert_eq!(state.counter(), 3); + assert!(state.post_init()); + } + + #[test] + fn snapshot_set_heartbeat_timestamp_valid() { + let mut state = HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEEF); + state.set_heartbeat_timestamp_offset(0x1CAFEBAD); + + assert_eq!(state.heartbeat_timestamp_offset(), 0x1CAFEBAD); + + // Check other parameters unchanged. + assert_eq!(state.start_timestamp(), 0xDEADBEEF); + assert_eq!(state.counter(), 3); + assert!(state.post_init()); + } + + #[test] + #[should_panic(expected = "provided heartbeat offset is out of range")] + fn snapshot_set_heartbeat_timestamp_out_of_range() { + let mut state = HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEEF); + state.set_heartbeat_timestamp_offset(0x20000000); + } + + #[test] + fn snapshot_counter_increment() { + let mut state = HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEE9); + + // Max value is 3, check if saturates. + for i in 1..=4 { + state.increment_counter(); + assert_eq!(state.counter(), min(i, 3)); + } + + // Check other parameters unchanged. + assert_eq!(state.start_timestamp(), 0xDEADBEEF); + assert_eq!(state.heartbeat_timestamp_offset(), 0xDEADBEEF >> 3); + assert!(state.post_init()); + } + + #[test] + fn snapshot_set_post_init() { + let mut state = HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEEF); + + state.set_post_init(false); + assert!(!state.post_init()); + state.set_post_init(true); + assert!(state.post_init()); + + // Check other parameters unchanged. + assert_eq!(state.start_timestamp(), 0xDEADBEEF); + assert_eq!(state.heartbeat_timestamp_offset(), 0xDEADBEEF >> 3); + assert_eq!(state.counter(), 3); + } + + #[test] + fn state_new() { + let state = HeartbeatState::new(HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEEF)); + assert_eq!(state.0.load(Ordering::Relaxed), 0xDEADBEEF_DEADBEEF); + } + + #[test] + fn state_snapshot() { + let state = HeartbeatState::new(HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEEF)); + assert_eq!(state.snapshot().as_u64(), 0xDEADBEEF_DEADBEEF); + } + + #[test] + fn state_update_some() { + let state = HeartbeatState::new(HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEEF)); + let _ = state.update(|prev_snapshot| { + // Make sure state is as expected. + assert_eq!(prev_snapshot.as_u64(), 0xDEADBEEF_DEADBEEF); + assert_eq!(prev_snapshot.start_timestamp(), 0xDEADBEEF); + assert_eq!(prev_snapshot.heartbeat_timestamp_offset(), 0xDEADBEEF >> 3); + assert_eq!(prev_snapshot.counter(), 3); + assert!(prev_snapshot.post_init()); + + Some(HeartbeatStateSnapshot::from(0)) + }); + + assert_eq!(state.snapshot().as_u64(), 0); + } + + #[test] + fn state_update_none() { + let state = HeartbeatState::new(HeartbeatStateSnapshot::from(0xDEADBEEF_DEADBEEF)); + let _ = state.update(|_| None); + + assert_eq!(state.snapshot().as_u64(), 0xDEADBEEF_DEADBEEF); + } +} diff --git a/src/health_monitoring_lib/rust/heartbeat/mod.rs b/src/health_monitoring_lib/rust/heartbeat/mod.rs new file mode 100644 index 00000000..a8b6e306 --- /dev/null +++ b/src/health_monitoring_lib/rust/heartbeat/mod.rs @@ -0,0 +1,21 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +mod heartbeat_monitor; +mod heartbeat_state; + +pub(crate) use heartbeat_monitor::HeartbeatEvaluationError; +pub use heartbeat_monitor::{HeartbeatMonitor, HeartbeatMonitorBuilder}; + +// FFI bindings +pub(super) mod ffi; diff --git a/src/health_monitoring_lib/rust/lib.rs b/src/health_monitoring_lib/rust/lib.rs index fcbcb945..4f0f038f 100644 --- a/src/health_monitoring_lib/rust/lib.rs +++ b/src/health_monitoring_lib/rust/lib.rs @@ -20,10 +20,14 @@ mod tag; mod worker; pub mod deadline; +pub mod heartbeat; +pub mod logic; use crate::common::{Monitor, MonitorEvalHandle}; use crate::deadline::{DeadlineMonitor, DeadlineMonitorBuilder}; +use crate::heartbeat::{HeartbeatMonitor, HeartbeatMonitorBuilder}; use crate::log::{error, ScoreDebug}; +use crate::logic::{LogicMonitor, LogicMonitorBuilder}; pub use common::TimeRange; use containers::fixed_capacity::FixedCapacityVec; use core::time::Duration; @@ -45,6 +49,8 @@ pub enum HealthMonitorError { #[derive(Default)] pub struct HealthMonitorBuilder { deadline_monitor_builders: HashMap, + heartbeat_monitor_builders: HashMap, + logic_monitor_builders: HashMap, supervisor_api_cycle: Duration, internal_processing_cycle: Duration, } @@ -54,6 +60,8 @@ impl HealthMonitorBuilder { pub fn new() -> Self { Self { deadline_monitor_builders: HashMap::new(), + heartbeat_monitor_builders: HashMap::new(), + logic_monitor_builders: HashMap::new(), supervisor_api_cycle: Duration::from_millis(500), internal_processing_cycle: Duration::from_millis(100), } @@ -72,6 +80,32 @@ impl HealthMonitorBuilder { self } + /// Add a [`HeartbeatMonitor`] for the given [`MonitorTag`]. + /// + /// - `monitor_tag` - unique tag for the [`HeartbeatMonitor`]. + /// - `monitor_builder` - monitor builder to finalize. + /// + /// # Note + /// + /// If a heartbeat monitor with the same tag already exists, it will be overwritten. + pub fn add_heartbeat_monitor(mut self, monitor_tag: MonitorTag, monitor_builder: HeartbeatMonitorBuilder) -> Self { + self.add_heartbeat_monitor_internal(monitor_tag, monitor_builder); + self + } + + /// Add a [`LogicMonitor`] for the given [`MonitorTag`]. + /// + /// - `monitor_tag` - unique tag for the [`LogicMonitor`]. + /// - `monitor_builder` - monitor builder to finalize. + /// + /// # Note + /// + /// If a logic monitor with the same tag already exists, it will be overwritten. + pub fn add_logic_monitor(mut self, monitor_tag: MonitorTag, monitor_builder: LogicMonitorBuilder) -> Self { + self.add_logic_monitor_internal(monitor_tag, monitor_builder); + self + } + /// Set the interval between supervisor API notifications. /// This duration determines how often the health monitor notifies the supervisor about system liveness. /// @@ -104,7 +138,9 @@ impl HealthMonitorBuilder { } // Check number of monitors. - let num_monitors = self.deadline_monitor_builders.len(); + let num_monitors = self.deadline_monitor_builders.len() + + self.heartbeat_monitor_builders.len() + + self.logic_monitor_builders.len(); if num_monitors == 0 { error!("No monitors have been added. HealthMonitor cannot be created."); return Err(HealthMonitorError::WrongState); @@ -120,8 +156,24 @@ impl HealthMonitorBuilder { deadline_monitors.insert(tag, Some(MonitorState::Available(monitor))); } + // Create heartbeat monitors. + let mut heartbeat_monitors = HashMap::new(); + for (tag, builder) in self.heartbeat_monitor_builders { + let monitor = builder.build(tag, self.internal_processing_cycle, &allocator)?; + heartbeat_monitors.insert(tag, Some(MonitorState::Available(monitor))); + } + + // Create logic monitors. + let mut logic_monitors = HashMap::new(); + for (tag, builder) in self.logic_monitor_builders { + let monitor = builder.build(tag, &allocator)?; + logic_monitors.insert(tag, Some(MonitorState::Available(monitor))); + } + Ok(HealthMonitor { deadline_monitors, + heartbeat_monitors, + logic_monitors, worker: worker::UniqueThreadRunner::new(self.internal_processing_cycle), supervisor_api_cycle: self.supervisor_api_cycle, }) @@ -137,6 +189,18 @@ impl HealthMonitorBuilder { self.deadline_monitor_builders.insert(monitor_tag, monitor_builder); } + pub(crate) fn add_heartbeat_monitor_internal( + &mut self, + monitor_tag: MonitorTag, + monitor_builder: HeartbeatMonitorBuilder, + ) { + self.heartbeat_monitor_builders.insert(monitor_tag, monitor_builder); + } + + pub(crate) fn add_logic_monitor_internal(&mut self, monitor_tag: MonitorTag, monitor_builder: LogicMonitorBuilder) { + self.logic_monitor_builders.insert(monitor_tag, monitor_builder); + } + pub(crate) fn with_supervisor_api_cycle_internal(&mut self, cycle_duration: Duration) { self.supervisor_api_cycle = cycle_duration; } @@ -162,6 +226,8 @@ type MonitorContainer = Option>; /// Health monitor. pub struct HealthMonitor { deadline_monitors: HashMap>, + heartbeat_monitors: HashMap>, + logic_monitors: HashMap>, worker: worker::UniqueThreadRunner, supervisor_api_cycle: Duration, } @@ -197,6 +263,26 @@ impl HealthMonitor { Self::get_monitor(&mut self.deadline_monitors, monitor_tag) } + /// Get and pass ownership of a [`HeartbeatMonitor`] for the given [`MonitorTag`]. + /// + /// - `monitor_tag` - unique tag for the [`HeartbeatMonitor`]. + /// + /// Returns [`Some`] containing [`HeartbeatMonitor`] if found and not taken. + /// Otherwise returns [`None`]. + pub fn get_heartbeat_monitor(&mut self, monitor_tag: MonitorTag) -> Option { + Self::get_monitor(&mut self.heartbeat_monitors, monitor_tag) + } + + /// Get and pass ownership of a [`LogicMonitor`] for the given [`MonitorTag`]. + /// + /// - `monitor_tag` - unique tag for the [`LogicMonitor`]. + /// + /// Returns [`Some`] containing [`LogicMonitor`] if found and not taken. + /// Otherwise returns [`None`]. + pub fn get_logic_monitor(&mut self, monitor_tag: MonitorTag) -> Option { + Self::get_monitor(&mut self.logic_monitors, monitor_tag) + } + fn collect_given_monitors( monitors_to_collect: &mut HashMap>, collected_monitors: &mut FixedCapacityVec, @@ -243,9 +329,11 @@ impl HealthMonitor { /// Health monitoring logic stops when the [`HealthMonitor`] is dropped. pub fn start(&mut self) -> Result<(), HealthMonitorError> { // Collect all monitors. - let num_monitors = self.deadline_monitors.len(); + let num_monitors = self.deadline_monitors.len() + self.heartbeat_monitors.len() + self.logic_monitors.len(); let mut collected_monitors = FixedCapacityVec::new(num_monitors); Self::collect_given_monitors(&mut self.deadline_monitors, &mut collected_monitors)?; + Self::collect_given_monitors(&mut self.heartbeat_monitors, &mut collected_monitors)?; + Self::collect_given_monitors(&mut self.logic_monitors, &mut collected_monitors)?; // Start monitoring logic. let monitoring_logic = worker::MonitoringLogic::new( @@ -267,15 +355,33 @@ impl HealthMonitor { #[score_testing_macros::test_mod_with_log] #[cfg(all(test, not(loom)))] mod tests { + use crate::common::TimeRange; use crate::deadline::DeadlineMonitorBuilder; - use crate::tag::MonitorTag; + use crate::heartbeat::HeartbeatMonitorBuilder; + use crate::logic::LogicMonitorBuilder; + use crate::tag::{MonitorTag, StateTag}; use crate::{HealthMonitorBuilder, HealthMonitorError}; use core::time::Duration; + fn def_heartbeat_monitor_builder() -> HeartbeatMonitorBuilder { + let range = TimeRange::new(Duration::from_millis(100), Duration::from_millis(200)); + HeartbeatMonitorBuilder::new(range) + } + + fn def_logic_monitor_builder() -> LogicMonitorBuilder { + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + LogicMonitorBuilder::new(state1) + .add_state(state2) + .add_transition((state1, state2)) + } + #[test] fn health_monitor_builder_new_succeeds() { let health_monitor_builder = HealthMonitorBuilder::new(); assert!(health_monitor_builder.deadline_monitor_builders.is_empty()); + assert!(health_monitor_builder.heartbeat_monitor_builders.is_empty()); + assert!(health_monitor_builder.logic_monitor_builders.is_empty()); assert_eq!(health_monitor_builder.supervisor_api_cycle, Duration::from_millis(500)); assert_eq!( health_monitor_builder.internal_processing_cycle, @@ -287,9 +393,15 @@ mod tests { fn health_monitor_builder_build_succeeds() { let deadline_monitor_tag = MonitorTag::from("deadline_monitor"); let deadline_monitor_builder = DeadlineMonitorBuilder::new(); + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let heartbeat_monitor_builder = def_heartbeat_monitor_builder(); + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder = def_logic_monitor_builder(); let result = HealthMonitorBuilder::new() .add_deadline_monitor(deadline_monitor_tag, deadline_monitor_builder) + .add_heartbeat_monitor(heartbeat_monitor_tag, heartbeat_monitor_builder) + .add_logic_monitor(logic_monitor_tag, logic_monitor_builder) .build(); assert!(result.is_ok()); } @@ -364,17 +476,135 @@ mod tests { assert!(result.is_none()); } + #[test] + fn health_monitor_get_heartbeat_monitor_available() { + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let heartbeat_monitor_builder = def_heartbeat_monitor_builder(); + let mut health_monitor = HealthMonitorBuilder::new() + .add_heartbeat_monitor(heartbeat_monitor_tag, heartbeat_monitor_builder) + .build() + .unwrap(); + + let result = health_monitor.get_heartbeat_monitor(heartbeat_monitor_tag); + assert!(result.is_some()); + } + + #[test] + fn health_monitor_get_heartbeat_monitor_taken() { + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let heartbeat_monitor_builder = def_heartbeat_monitor_builder(); + let mut health_monitor = HealthMonitorBuilder::new() + .add_heartbeat_monitor(heartbeat_monitor_tag, heartbeat_monitor_builder) + .build() + .unwrap(); + + let _ = health_monitor.get_heartbeat_monitor(heartbeat_monitor_tag); + let result = health_monitor.get_heartbeat_monitor(heartbeat_monitor_tag); + assert!(result.is_none()); + } + + #[test] + fn health_monitor_get_heartbeat_monitor_unknown() { + let heartbeat_monitor_builder = def_heartbeat_monitor_builder(); + let mut health_monitor = HealthMonitorBuilder::new() + .add_heartbeat_monitor(MonitorTag::from("heartbeat_monitor"), heartbeat_monitor_builder) + .build() + .unwrap(); + + let result = health_monitor.get_heartbeat_monitor(MonitorTag::from("undefined_monitor")); + assert!(result.is_none()); + } + + #[test] + fn health_monitor_get_heartbeat_monitor_invalid_state() { + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let heartbeat_monitor_builder = def_heartbeat_monitor_builder(); + let mut health_monitor = HealthMonitorBuilder::new() + .add_heartbeat_monitor(heartbeat_monitor_tag, heartbeat_monitor_builder) + .build() + .unwrap(); + + // Inject broken state - unreachable otherwise. + health_monitor.heartbeat_monitors.insert(heartbeat_monitor_tag, None); + + let result = health_monitor.get_heartbeat_monitor(heartbeat_monitor_tag); + assert!(result.is_none()); + } + + #[test] + fn health_monitor_get_logic_monitor_available() { + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder = def_logic_monitor_builder(); + let mut health_monitor = HealthMonitorBuilder::new() + .add_logic_monitor(logic_monitor_tag, logic_monitor_builder) + .build() + .unwrap(); + + let result = health_monitor.get_logic_monitor(logic_monitor_tag); + assert!(result.is_some()); + } + + #[test] + fn health_monitor_get_logic_monitor_taken() { + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder = def_logic_monitor_builder(); + let mut health_monitor = HealthMonitorBuilder::new() + .add_logic_monitor(logic_monitor_tag, logic_monitor_builder) + .build() + .unwrap(); + + let _ = health_monitor.get_logic_monitor(logic_monitor_tag); + let result = health_monitor.get_logic_monitor(logic_monitor_tag); + assert!(result.is_none()); + } + + #[test] + fn health_monitor_get_logic_monitor_unknown() { + let logic_monitor_builder = def_logic_monitor_builder(); + let mut health_monitor = HealthMonitorBuilder::new() + .add_logic_monitor(MonitorTag::from("logic_monitor"), logic_monitor_builder) + .build() + .unwrap(); + + let result = health_monitor.get_logic_monitor(MonitorTag::from("undefined_monitor")); + assert!(result.is_none()); + } + + #[test] + fn health_monitor_get_logic_monitor_invalid_state() { + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder = def_logic_monitor_builder(); + let mut health_monitor = HealthMonitorBuilder::new() + .add_logic_monitor(logic_monitor_tag, logic_monitor_builder) + .build() + .unwrap(); + + // Inject broken state - unreachable otherwise. + health_monitor.logic_monitors.insert(logic_monitor_tag, None); + + let result = health_monitor.get_logic_monitor(logic_monitor_tag); + assert!(result.is_none()); + } + #[test] fn health_monitor_start_succeeds() { let deadline_monitor_tag = MonitorTag::from("deadline_monitor"); let deadline_monitor_builder = DeadlineMonitorBuilder::new(); + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let heartbeat_monitor_builder = def_heartbeat_monitor_builder(); + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder = def_logic_monitor_builder(); let mut health_monitor = HealthMonitorBuilder::new() .add_deadline_monitor(deadline_monitor_tag, deadline_monitor_builder) + .add_heartbeat_monitor(heartbeat_monitor_tag, heartbeat_monitor_builder) + .add_logic_monitor(logic_monitor_tag, logic_monitor_builder) .build() .unwrap(); let _deadline_monitor = health_monitor.get_deadline_monitor(deadline_monitor_tag).unwrap(); + let _heartbeat_monitor = health_monitor.get_heartbeat_monitor(heartbeat_monitor_tag).unwrap(); + let _logic_monitor = health_monitor.get_logic_monitor(logic_monitor_tag).unwrap(); let result = health_monitor.start(); assert!(result.is_ok()); @@ -383,9 +613,13 @@ mod tests { #[test] fn health_monitor_start_monitors_not_taken() { let deadline_monitor_builder = DeadlineMonitorBuilder::new(); + let heartbeat_monitor_builder = def_heartbeat_monitor_builder(); + let logic_monitor_builder = def_logic_monitor_builder(); let mut health_monitor = HealthMonitorBuilder::new() .add_deadline_monitor(MonitorTag::from("deadline_monitor"), deadline_monitor_builder) + .add_heartbeat_monitor(MonitorTag::from("heartbeat_monitor"), heartbeat_monitor_builder) + .add_logic_monitor(MonitorTag::from("logic_monitor"), logic_monitor_builder) .build() .unwrap(); @@ -397,9 +631,15 @@ mod tests { fn health_monitor_start_not_taken_then_restart() { let deadline_monitor_tag = MonitorTag::from("deadline_monitor"); let deadline_monitor_builder = DeadlineMonitorBuilder::new(); + let heartbeat_monitor_tag = MonitorTag::from("heartbeat_monitor"); + let heartbeat_monitor_builder = def_heartbeat_monitor_builder(); + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let logic_monitor_builder = def_logic_monitor_builder(); let mut health_monitor = HealthMonitorBuilder::new() .add_deadline_monitor(deadline_monitor_tag, deadline_monitor_builder) + .add_heartbeat_monitor(heartbeat_monitor_tag, heartbeat_monitor_builder) + .add_logic_monitor(logic_monitor_tag, logic_monitor_builder) .build() .unwrap(); @@ -407,9 +647,13 @@ mod tests { let start_result = health_monitor.start(); assert!(start_result.is_err_and(|e| e == HealthMonitorError::WrongState)); - // Take monitor. + // Take monitors. let get_deadline_monitor_result = health_monitor.get_deadline_monitor(deadline_monitor_tag); assert!(get_deadline_monitor_result.is_some()); + let get_heartbeat_monitor_result = health_monitor.get_heartbeat_monitor(heartbeat_monitor_tag); + assert!(get_heartbeat_monitor_result.is_some()); + let get_logic_monitor_result = health_monitor.get_logic_monitor(logic_monitor_tag); + assert!(get_logic_monitor_result.is_some()); // Try to start again, this time should be successful. let start_result = health_monitor.start(); diff --git a/src/health_monitoring_lib/rust/logic/ffi.rs b/src/health_monitoring_lib/rust/logic/ffi.rs new file mode 100644 index 00000000..da7ef548 --- /dev/null +++ b/src/health_monitoring_lib/rust/logic/ffi.rs @@ -0,0 +1,649 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use crate::ffi::{FFIBorrowed, FFICode, FFIHandle}; +use crate::logic::{LogicMonitor, LogicMonitorBuilder}; +use crate::tag::StateTag; + +#[no_mangle] +pub extern "C" fn logic_monitor_builder_create( + initial_state: *const StateTag, + logic_monitor_builder_handle_out: *mut FFIHandle, +) -> FFICode { + if initial_state.is_null() || logic_monitor_builder_handle_out.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // `StateTag` type must be compatible between C++ and Rust. + let initial_state = unsafe { *initial_state }; + + let logic_monitor_builder = LogicMonitorBuilder::new(initial_state); + unsafe { + *logic_monitor_builder_handle_out = Box::into_raw(Box::new(logic_monitor_builder)).cast(); + } + + FFICode::Success +} + +#[no_mangle] +pub extern "C" fn logic_monitor_builder_destroy(logic_monitor_builder_handle: FFIHandle) -> FFICode { + if logic_monitor_builder_handle.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // It is assumed that the pointer was created by a call to `logic_monitor_builder_create`. + unsafe { + let _ = Box::from_raw(logic_monitor_builder_handle as *mut LogicMonitorBuilder); + } + + FFICode::Success +} + +#[no_mangle] +pub extern "C" fn logic_monitor_builder_add_state( + logic_monitor_builder_handle: FFIHandle, + state: *const StateTag, +) -> FFICode { + if logic_monitor_builder_handle.is_null() || state.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // `StateTag` type must be compatible between C++ and Rust. + let state = unsafe { *state }; + + // SAFETY: + // Validity of this pointer is ensured. + // It is assumed that the pointer was created by a call to `logic_monitor_builder_create`. + // It is assumed that the pointer was not consumed by a call to `logic_monitor_builder_destroy`. + let mut logic_monitor_builder = + FFIBorrowed::new(unsafe { Box::from_raw(logic_monitor_builder_handle as *mut LogicMonitorBuilder) }); + + logic_monitor_builder.add_state_internal(state); + + FFICode::Success +} + +#[no_mangle] +pub extern "C" fn logic_monitor_builder_add_transition( + logic_monitor_builder_handle: FFIHandle, + from_state: *const StateTag, + to_state: *const StateTag, +) -> FFICode { + if logic_monitor_builder_handle.is_null() || from_state.is_null() || to_state.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // `StateTag` type must be compatible between C++ and Rust. + let from_state = unsafe { *from_state }; + let to_state = unsafe { *to_state }; + + // SAFETY: + // Validity of this pointer is ensured. + // It is assumed that the pointer was created by a call to `logic_monitor_builder_create`. + // It is assumed that the pointer was not consumed by a call to `logic_monitor_builder_destroy`. + let mut logic_monitor_builder = + FFIBorrowed::new(unsafe { Box::from_raw(logic_monitor_builder_handle as *mut LogicMonitorBuilder) }); + + logic_monitor_builder.add_transition_internal((from_state, to_state)); + + FFICode::Success +} + +#[no_mangle] +pub extern "C" fn logic_monitor_destroy(logic_monitor_handle: FFIHandle) -> FFICode { + if logic_monitor_handle.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // It is assumed that the pointer was created by a call to `health_monitor_get_logic_monitor`. + unsafe { + let _ = Box::from_raw(logic_monitor_handle as *mut LogicMonitor); + } + + FFICode::Success +} + +#[no_mangle] +pub extern "C" fn logic_monitor_transition(logic_monitor_handle: FFIHandle, to_state: *const StateTag) -> FFICode { + if logic_monitor_handle.is_null() || to_state.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of the pointer is ensured. + // `StateTag` type must be compatible between C++ and Rust. + let to_state = unsafe { *to_state }; + + // SAFETY: + // Validity of this pointer is ensured. + // It is assumed that the pointer was created by a call to `health_monitor_get_logic_monitor`. + // It is assumed that the pointer was not consumed by a call to `logic_monitor_destroy`. + let monitor = FFIBorrowed::new(unsafe { Box::from_raw(logic_monitor_handle as *mut LogicMonitor) }); + + // `transition` method returns new state tag on success. + // This can be handled in C++ layer. + match monitor.transition(to_state) { + Ok(_) => FFICode::Success, + Err(_) => FFICode::Failed, + } +} + +#[no_mangle] +pub extern "C" fn logic_monitor_state(logic_monitor_handle: FFIHandle, state_out: *mut *const StateTag) -> FFICode { + if logic_monitor_handle.is_null() || state_out.is_null() { + return FFICode::NullParameter; + } + + // SAFETY: + // Validity of this pointer is ensured. + // It is assumed that the pointer was created by a call to `health_monitor_get_logic_monitor`. + // It is assumed that the pointer was not consumed by a call to `logic_monitor_destroy`. + let monitor = FFIBorrowed::new(unsafe { Box::from_raw(logic_monitor_handle as *mut LogicMonitor) }); + + match monitor.state() { + Ok(state) => { + unsafe { + *state_out = Box::into_raw(Box::new(state)); + } + FFICode::Success + }, + Err(_) => FFICode::Failed, + } +} + +#[score_testing_macros::test_mod_with_log] +#[cfg(test)] +mod tests { + use crate::ffi::{ + health_monitor_builder_add_logic_monitor, health_monitor_builder_build, health_monitor_builder_create, + health_monitor_destroy, health_monitor_get_logic_monitor, FFICode, FFIHandle, + }; + use crate::logic::ffi::{ + logic_monitor_builder_add_state, logic_monitor_builder_add_transition, logic_monitor_builder_create, + logic_monitor_builder_destroy, logic_monitor_destroy, logic_monitor_state, logic_monitor_transition, + }; + use crate::tag::StateTag; + use crate::MonitorTag; + use core::ptr::null_mut; + + #[test] + fn logic_monitor_builder_create_succeeds() { + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let initial_state = StateTag::from("initial_state"); + let logic_monitor_builder_create_result = logic_monitor_builder_create( + &initial_state as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + assert!(!logic_monitor_builder_handle.is_null()); + assert_eq!(logic_monitor_builder_create_result, FFICode::Success); + + // Clean-up. + // NOTE: `logic_monitor_builder_destroy` positive path is already tested here. + let logic_monitor_builder_destroy_result = logic_monitor_builder_destroy(logic_monitor_builder_handle); + assert_eq!(logic_monitor_builder_destroy_result, FFICode::Success); + } + + #[test] + fn logic_monitor_builder_create_null_builder() { + let initial_state = StateTag::from("initial_state"); + let logic_monitor_builder_create_result = + logic_monitor_builder_create(&initial_state as *const StateTag, null_mut()); + assert_eq!(logic_monitor_builder_create_result, FFICode::NullParameter); + } + + #[test] + fn logic_monitor_builder_create_null_initial_state() { + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let logic_monitor_builder_create_result = + logic_monitor_builder_create(null_mut(), &mut logic_monitor_builder_handle as *mut FFIHandle); + assert_eq!(logic_monitor_builder_create_result, FFICode::NullParameter); + + // Clean-up. + logic_monitor_builder_destroy(logic_monitor_builder_handle); + } + + #[test] + fn logic_monitor_builder_destroy_null_builder() { + let logic_monitor_builder_destroy_result = logic_monitor_builder_destroy(null_mut()); + assert_eq!(logic_monitor_builder_destroy_result, FFICode::NullParameter); + } + + #[test] + fn logic_monitor_builder_add_state_succeeds() { + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let state1 = StateTag::from("state1"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + + let state2 = StateTag::from("state2"); + let result = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + assert_eq!(result, FFICode::Success); + + // Clean-up. + logic_monitor_builder_destroy(logic_monitor_builder_handle); + } + + #[test] + fn logic_monitor_builder_add_state_null_builder() { + let state = StateTag::from("state"); + let result = logic_monitor_builder_add_state(null_mut(), &state as *const StateTag); + assert_eq!(result, FFICode::NullParameter); + } + + #[test] + fn logic_monitor_builder_add_state_null_tag() { + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let state1 = StateTag::from("state1"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + + let result = logic_monitor_builder_add_state(logic_monitor_builder_handle, null_mut()); + assert_eq!(result, FFICode::NullParameter); + + // Clean-up. + logic_monitor_builder_destroy(logic_monitor_builder_handle); + } + + #[test] + fn logic_monitor_builder_add_transition_succeeds() { + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + + let result = logic_monitor_builder_add_transition( + logic_monitor_builder_handle, + &state1 as *const StateTag, + &state2 as *const StateTag, + ); + assert_eq!(result, FFICode::Success); + + // Clean-up. + logic_monitor_builder_destroy(logic_monitor_builder_handle); + } + + #[test] + fn logic_monitor_builder_add_transition_null_builder() { + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let result = + logic_monitor_builder_add_transition(null_mut(), &state1 as *const StateTag, &state2 as *const StateTag); + assert_eq!(result, FFICode::NullParameter); + } + + #[test] + fn logic_monitor_builder_add_transition_null_from_state() { + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + + let result = + logic_monitor_builder_add_transition(logic_monitor_builder_handle, null_mut(), &state2 as *const StateTag); + assert_eq!(result, FFICode::NullParameter); + + // Clean-up. + logic_monitor_builder_destroy(logic_monitor_builder_handle); + } + + #[test] + fn logic_monitor_builder_add_transition_null_to_state() { + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + + let result = + logic_monitor_builder_add_transition(logic_monitor_builder_handle, &state1 as *const StateTag, null_mut()); + assert_eq!(result, FFICode::NullParameter); + + // Clean-up. + logic_monitor_builder_destroy(logic_monitor_builder_handle); + } + + #[test] + fn logic_monitor_destroy_null_monitor() { + let logic_monitor_destroy_result = logic_monitor_destroy(null_mut()); + assert_eq!(logic_monitor_destroy_result, FFICode::NullParameter); + } + + #[test] + fn logic_monitor_transition_succeeds() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + let mut logic_monitor_handle: FFIHandle = null_mut(); + + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + let _ = logic_monitor_builder_add_transition( + logic_monitor_builder_handle, + &state1 as *const StateTag, + &state2 as *const StateTag, + ); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + let _ = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_handle as *mut FFIHandle, + ); + + let result = logic_monitor_transition(logic_monitor_handle, &state2 as *const StateTag); + assert_eq!(result, FFICode::Success); + + // Clean-up. + logic_monitor_destroy(logic_monitor_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn logic_monitor_transition_fails() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + let mut logic_monitor_handle: FFIHandle = null_mut(); + + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + let _ = logic_monitor_builder_add_transition( + logic_monitor_builder_handle, + &state1 as *const StateTag, + &state2 as *const StateTag, + ); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + let _ = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_handle as *mut FFIHandle, + ); + + let state3 = StateTag::from("state3"); + let result = logic_monitor_transition(logic_monitor_handle, &state3 as *const StateTag); + assert_eq!(result, FFICode::Failed); + + // Clean-up. + logic_monitor_destroy(logic_monitor_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn logic_monitor_transition_null_monitor() { + let state1 = StateTag::from("state1"); + let result = logic_monitor_transition(null_mut(), &state1 as *const StateTag); + assert_eq!(result, FFICode::NullParameter); + } + + #[test] + fn logic_monitor_transition_null_to_state() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + let mut logic_monitor_handle: FFIHandle = null_mut(); + + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + let _ = logic_monitor_builder_add_transition( + logic_monitor_builder_handle, + &state1 as *const StateTag, + &state2 as *const StateTag, + ); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + let _ = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_handle as *mut FFIHandle, + ); + + let result = logic_monitor_transition(logic_monitor_handle, null_mut()); + assert_eq!(result, FFICode::NullParameter); + + // Clean-up. + logic_monitor_destroy(logic_monitor_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn logic_monitor_state_succeeds() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + let mut logic_monitor_handle: FFIHandle = null_mut(); + + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + let _ = logic_monitor_builder_add_transition( + logic_monitor_builder_handle, + &state1 as *const StateTag, + &state2 as *const StateTag, + ); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + let _ = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_handle as *mut FFIHandle, + ); + + let mut current_state: *const StateTag = null_mut(); + let result = logic_monitor_state(logic_monitor_handle, &mut current_state as *mut *const StateTag); + assert!(!current_state.is_null()); + assert_eq!(result, FFICode::Success); + + // Clean-up. + logic_monitor_destroy(logic_monitor_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn logic_monitor_state_fails() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + let mut logic_monitor_handle: FFIHandle = null_mut(); + + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + let _ = logic_monitor_builder_add_transition( + logic_monitor_builder_handle, + &state1 as *const StateTag, + &state2 as *const StateTag, + ); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + let _ = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_handle as *mut FFIHandle, + ); + + let state3 = StateTag::from("state3"); + let _ = logic_monitor_transition(logic_monitor_handle, &state3 as *const StateTag); + + let mut current_state: *const StateTag = null_mut(); + let result = logic_monitor_state(logic_monitor_handle, &mut current_state as *mut *const StateTag); + assert_eq!(result, FFICode::Failed); + + // Clean-up. + logic_monitor_destroy(logic_monitor_handle); + health_monitor_destroy(health_monitor_handle); + } + + #[test] + fn logic_monitor_state_null_monitor() { + let mut current_state: *const StateTag = null_mut(); + let result = logic_monitor_state(null_mut(), &mut current_state as *mut *const StateTag); + assert_eq!(result, FFICode::NullParameter); + } + + #[test] + fn logic_monitor_state_null_state() { + let mut health_monitor_builder_handle: FFIHandle = null_mut(); + let mut health_monitor_handle: FFIHandle = null_mut(); + let mut logic_monitor_builder_handle: FFIHandle = null_mut(); + let mut logic_monitor_handle: FFIHandle = null_mut(); + + let logic_monitor_tag = MonitorTag::from("logic_monitor"); + let _ = health_monitor_builder_create(&mut health_monitor_builder_handle as *mut FFIHandle); + let state1 = StateTag::from("state1"); + let state2 = StateTag::from("state2"); + let _ = logic_monitor_builder_create( + &state1 as *const StateTag, + &mut logic_monitor_builder_handle as *mut FFIHandle, + ); + let _ = logic_monitor_builder_add_state(logic_monitor_builder_handle, &state2 as *const StateTag); + let _ = logic_monitor_builder_add_transition( + logic_monitor_builder_handle, + &state1 as *const StateTag, + &state2 as *const StateTag, + ); + let _ = health_monitor_builder_add_logic_monitor( + health_monitor_builder_handle, + &logic_monitor_tag as *const MonitorTag, + logic_monitor_builder_handle, + ); + let _ = health_monitor_builder_build( + health_monitor_builder_handle, + 200, + 100, + &mut health_monitor_handle as *mut FFIHandle, + ); + let _ = health_monitor_get_logic_monitor( + health_monitor_handle, + &logic_monitor_tag as *const MonitorTag, + &mut logic_monitor_handle as *mut FFIHandle, + ); + + let result = logic_monitor_state(logic_monitor_handle, null_mut()); + assert_eq!(result, FFICode::NullParameter); + + // Clean-up. + logic_monitor_destroy(logic_monitor_handle); + health_monitor_destroy(health_monitor_handle); + } +} diff --git a/src/health_monitoring_lib/rust/logic/logic_monitor.rs b/src/health_monitoring_lib/rust/logic/logic_monitor.rs new file mode 100644 index 00000000..82747287 --- /dev/null +++ b/src/health_monitoring_lib/rust/logic/logic_monitor.rs @@ -0,0 +1,573 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use crate::common::{Monitor, MonitorEvalHandle, MonitorEvaluationError, MonitorEvaluator}; +use crate::log::{error, warn, ScoreDebug}; +use crate::protected_memory::ProtectedMemoryAllocator; +use crate::tag::{MonitorTag, StateTag}; +use crate::HealthMonitorError; +use core::hash::{Hash, Hasher}; +use core::sync::atomic::{AtomicU64, AtomicU8, Ordering}; +use std::hash::DefaultHasher; +use std::sync::Arc; +use std::time::Instant; + +/// Hashed representation of state. +#[derive(PartialEq, Eq)] +struct HashedState(u64); + +impl From for HashedState { + fn from(value: u64) -> Self { + Self(value) + } +} + +impl From for u64 { + fn from(value: HashedState) -> Self { + value.0 + } +} + +impl From for HashedState { + fn from(value: StateTag) -> Self { + Self::from(&value) + } +} + +impl From<&StateTag> for HashedState { + fn from(value: &StateTag) -> Self { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + Self(hasher.finish()) + } +} + +/// Internal OK state representation. +const OK_STATE: u8 = 0; + +/// Logic evaluation errors. +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, ScoreDebug)] +pub enum LogicEvaluationError { + /// State is unknown or cannot be determined. + InvalidState = OK_STATE + 1, + /// Transition is invalid. + InvalidTransition, +} + +impl From for u8 { + fn from(value: LogicEvaluationError) -> Self { + value as u8 + } +} + +impl From for LogicEvaluationError { + fn from(value: u8) -> Self { + match value { + value if value == LogicEvaluationError::InvalidState as u8 => LogicEvaluationError::InvalidState, + value if value == LogicEvaluationError::InvalidTransition as u8 => LogicEvaluationError::InvalidTransition, + _ => panic!("Invalid underlying value of logic evaluation error."), + } + } +} + +/// Builder for [`LogicMonitor`]. +#[derive(Debug)] +pub struct LogicMonitorBuilder { + /// Starting state. + initial_state: StateTag, + + /// List of allowed states. + allowed_states: Vec, + + /// List of allowed transitions between states. + allowed_transitions: Vec<(StateTag, StateTag)>, +} + +impl LogicMonitorBuilder { + /// Create a new [`LogicMonitorBuilder`]. + /// + /// - `initial_state` - starting point, implicitly added to the list of allowed states. + pub fn new(initial_state: StateTag) -> Self { + let allowed_states = vec![initial_state]; + Self { + initial_state, + allowed_states, + allowed_transitions: Vec::new(), + } + } + + /// Add allowed state. + pub fn add_state(mut self, state: StateTag) -> Self { + self.add_state_internal(state); + self + } + + /// Add allowed transition. + pub fn add_transition(mut self, transition: (StateTag, StateTag)) -> Self { + self.add_transition_internal(transition); + self + } + + /// Build the [`LogicMonitor`]. + /// + /// - `monitor_tag` - tag of this monitor. + /// - `_allocator` - protected memory allocator. + pub(crate) fn build( + self, + monitor_tag: MonitorTag, + _allocator: &ProtectedMemoryAllocator, + ) -> Result { + // Check number of transitions. + if self.allowed_transitions.is_empty() { + error!("No transitions have been added. LogicMonitor cannot be created."); + return Err(HealthMonitorError::WrongState); + } + + // Check transitions are between allowed states. + for (from, to) in self.allowed_transitions.iter() { + if !self.allowed_states.contains(from) { + error!("Invalid transition definition - 'from' state is unknown: {:?}", from); + return Err(HealthMonitorError::InvalidArgument); + } + if !self.allowed_states.contains(to) { + error!("Invalid transition definition - 'to' state is unknown: {:?}", to); + return Err(HealthMonitorError::InvalidArgument); + } + } + + let inner = Arc::new(LogicMonitorInner::new( + monitor_tag, + self.initial_state, + self.allowed_states, + self.allowed_transitions, + )); + Ok(LogicMonitor::new(inner)) + } + + // FFI internals. + + pub(crate) fn add_state_internal(&mut self, state: StateTag) { + if !self.allowed_states.contains(&state) { + self.allowed_states.push(state); + } + } + + pub(crate) fn add_transition_internal(&mut self, transition: (StateTag, StateTag)) { + if !self.allowed_transitions.contains(&transition) { + self.allowed_transitions.push(transition); + } + } +} + +/// Logic monitor. +pub struct LogicMonitor { + inner: Arc, +} + +impl LogicMonitor { + /// Create a new [`LogicMonitor`] instance. + fn new(inner: Arc) -> Self { + Self { inner } + } + + /// Perform transition to a new state. + pub fn transition(&self, state: StateTag) -> Result { + self.inner.transition(state) + } + + /// Current monitor state. + pub fn state(&self) -> Result { + self.inner.state() + } +} + +impl Monitor for LogicMonitor { + fn get_eval_handle(&self) -> MonitorEvalHandle { + MonitorEvalHandle::new(Arc::clone(&self.inner)) + } +} + +struct LogicMonitorInner { + /// Tag of this monitor. + monitor_tag: MonitorTag, + + /// Hashed current state. + current_state: AtomicU64, + + /// State of the monitor. + /// Contains zero for correct state. + /// Contains [`LogicEvaluationError`] if in erroneous state. + monitor_state: AtomicU8, + + /// List of allowed states. + allowed_states: Vec, + + /// List of allowed transitions between states. + allowed_transitions: Vec<(StateTag, StateTag)>, +} + +impl MonitorEvaluator for LogicMonitorInner { + fn evaluate(&self, _hmon_starting_point: Instant, on_error: &mut dyn FnMut(&MonitorTag, MonitorEvaluationError)) { + let monitor_state = self.monitor_state.load(Ordering::Relaxed); + if monitor_state != OK_STATE { + let error = LogicEvaluationError::from(monitor_state); + warn!("Invalid logic monitor state observed: {:?}", error); + on_error(&self.monitor_tag, error.into()); + } + } +} + +impl LogicMonitorInner { + fn new( + monitor_tag: MonitorTag, + initial_state: StateTag, + allowed_states: Vec, + allowed_transitions: Vec<(StateTag, StateTag)>, + ) -> Self { + let current_state = AtomicU64::new(HashedState::from(initial_state).into()); + let monitor_state = AtomicU8::new(0); + LogicMonitorInner { + monitor_tag, + current_state, + monitor_state, + allowed_states, + allowed_transitions, + } + } + + fn transition(&self, new_state: StateTag) -> Result { + // Get current state. + let current_state = self.state()?; + + // Check new state is valid. + if !self.allowed_states.contains(&new_state) { + // Move to `InvalidState` if requested state is not known. + warn!("Requested state transition to unknown state: {:?}", new_state); + let new_monitor_state = LogicEvaluationError::InvalidState; + self.monitor_state.store(new_monitor_state.into(), Ordering::Relaxed); + return Err(new_monitor_state); + } + + // Check transition is valid. + let transition = (current_state, new_state); + if !self.allowed_transitions.contains(&transition) { + // Move to `InvalidTransition` if requested transition is not known. + warn!( + "Requested state transition is invalid: {:?} -> {:?}", + current_state, new_state + ); + let new_monitor_state = LogicEvaluationError::InvalidTransition; + self.monitor_state.store(new_monitor_state.into(), Ordering::Relaxed); + return Err(new_monitor_state); + } + + // Change state and return it. + let hashed_new_state = HashedState::from(new_state); + self.current_state.store(hashed_new_state.into(), Ordering::Relaxed); + + Ok(new_state) + } + + fn state(&self) -> Result { + // Current state cannot be determined. + if self.monitor_state.load(Ordering::Relaxed) != OK_STATE { + warn!("Current logic monitor state cannot be determined"); + return Err(LogicEvaluationError::InvalidState); + } + + // Find current state. + let hashed_state = HashedState::from(self.current_state.load(Ordering::Relaxed)); + let result = self + .allowed_states + .iter() + .find(|e| HashedState::from(*e) == hashed_state); + + // Return current state if found. + // `None` indicates logic error - it should not be possible to successfully change state into an unknown. + match result { + Some(state) => Ok(*state), + None => Err(LogicEvaluationError::InvalidState), + } + } +} + +#[score_testing_macros::test_mod_with_log] +#[cfg(all(test, not(loom)))] +mod tests { + use crate::common::MonitorEvaluator; + use crate::logic::{LogicEvaluationError, LogicMonitorBuilder}; + use crate::protected_memory::ProtectedMemoryAllocator; + use crate::tag::{MonitorTag, StateTag}; + use crate::HealthMonitorError; + use std::time::Instant; + + #[test] + fn logic_monitor_builder_new_succeeds() { + let from_state = StateTag::from("from"); + let builder = LogicMonitorBuilder::new(from_state); + assert_eq!(builder.initial_state, from_state); + assert_eq!(builder.allowed_states, vec![from_state]); + assert!(builder.allowed_transitions.is_empty()); + } + + #[test] + fn logic_monitor_builder_build_succeeds() { + let allocator = ProtectedMemoryAllocator {}; + let monitor_tag = MonitorTag::from("logic_monitor"); + let from_state = StateTag::from("from"); + let to_state = StateTag::from("to"); + let result = LogicMonitorBuilder::new(from_state) + .add_state(to_state) + .add_transition((from_state, to_state)) + .build(monitor_tag, &allocator); + assert!(result.is_ok()); + } + + #[test] + fn logic_monitor_builder_build_no_transitions() { + let allocator = ProtectedMemoryAllocator {}; + let monitor_tag = MonitorTag::from("logic_monitor"); + let from_state = StateTag::from("from"); + let result = LogicMonitorBuilder::new(from_state).build(monitor_tag, &allocator); + assert!(result.is_err_and(|e| e == HealthMonitorError::WrongState)); + } + + #[test] + fn logic_monitor_builder_build_unknown_nodes() { + let allocator = ProtectedMemoryAllocator {}; + let monitor_tag = MonitorTag::from("logic_monitor"); + let from_state = StateTag::from("from"); + let to_state = StateTag::from("to"); + + // Unknown "from". + let result = LogicMonitorBuilder::new(from_state) + .add_state(to_state) + .add_transition((StateTag::from("unknown"), to_state)) + .build(monitor_tag, &allocator); + assert!(result.is_err_and(|e| e == HealthMonitorError::InvalidArgument)); + + // Unknown "to". + let result = LogicMonitorBuilder::new(from_state) + .add_state(to_state) + .add_transition((from_state, StateTag::from("unknown"))) + .build(monitor_tag, &allocator); + assert!(result.is_err_and(|e| e == HealthMonitorError::InvalidArgument)); + } + + #[test] + fn logic_monitor_transition_succeeds() { + let allocator = ProtectedMemoryAllocator {}; + let monitor_tag = MonitorTag::from("logic_monitor"); + let from_state = StateTag::from("from"); + let to_state = StateTag::from("to"); + let monitor = LogicMonitorBuilder::new(from_state) + .add_state(to_state) + .add_transition((from_state, to_state)) + .build(monitor_tag, &allocator) + .unwrap(); + + let result = monitor.transition(to_state); + assert!(result.is_ok_and(|s| s == to_state)); + } + + #[test] + fn logic_monitor_transition_unknown_node() { + let allocator = ProtectedMemoryAllocator {}; + let monitor_tag = MonitorTag::from("logic_monitor"); + let from_state = StateTag::from("from"); + let to_state = StateTag::from("to"); + let monitor = LogicMonitorBuilder::new(from_state) + .add_state(to_state) + .add_transition((from_state, to_state)) + .build(monitor_tag, &allocator) + .unwrap(); + + let result = monitor.transition(StateTag::from("unknown")); + assert!(result.is_err_and(|e| e == LogicEvaluationError::InvalidState)); + } + + #[test] + fn logic_monitor_transition_indeterminate_current_state() { + let allocator = ProtectedMemoryAllocator {}; + let monitor_tag = MonitorTag::from("logic_monitor"); + let from_state = StateTag::from("from"); + let to_state = StateTag::from("to"); + let monitor = LogicMonitorBuilder::new(from_state) + .add_state(to_state) + .add_transition((from_state, to_state)) + .build(monitor_tag, &allocator) + .unwrap(); + + // Trying to transition into unknown state causes monitor to move into indeterminate state. + let _ = monitor.transition(StateTag::from("unknown")); + + // Try to move to known state. + let result = monitor.transition(to_state); + assert!(result.is_err_and(|e| e == LogicEvaluationError::InvalidState)); + } + + #[test] + fn logic_monitor_transition_invalid_transition() { + let allocator = ProtectedMemoryAllocator {}; + let monitor_tag = MonitorTag::from("logic_monitor"); + let state1 = StateTag::from("state1"); + let state2: StateTag = StateTag::from("state2"); + let state3 = StateTag::from("state3"); + let monitor = LogicMonitorBuilder::new(state1) + .add_state(state2) + .add_state(state3) + .add_transition((state1, state2)) + .add_transition((state2, state3)) + .build(monitor_tag, &allocator) + .unwrap(); + + let result = monitor.transition(state3); + assert!(result.is_err_and(|e| e == LogicEvaluationError::InvalidTransition)); + } + + #[test] + fn logic_monitor_state_succeeds() { + let allocator = ProtectedMemoryAllocator {}; + let monitor_tag = MonitorTag::from("logic_monitor"); + let state1 = StateTag::from("state1"); + let state2: StateTag = StateTag::from("state2"); + let state3 = StateTag::from("state3"); + let monitor = LogicMonitorBuilder::new(state1) + .add_state(state2) + .add_state(state3) + .add_transition((state1, state2)) + .add_transition((state2, state3)) + .build(monitor_tag, &allocator) + .unwrap(); + + // Check state, perform transition to the next one. + let result = monitor.state(); + assert!(result.is_ok_and(|s| s == state1)); + + let _ = monitor.transition(state2); + let result = monitor.state(); + assert!(result.is_ok_and(|s| s == state2)); + + let _ = monitor.transition(state3); + let result = monitor.state(); + assert!(result.is_ok_and(|s| s == state3)); + } + + #[test] + fn logic_monitor_state_indeterminate_current_state() { + let allocator = ProtectedMemoryAllocator {}; + let monitor_tag = MonitorTag::from("logic_monitor"); + let from_state = StateTag::from("from"); + let to_state = StateTag::from("to"); + let monitor = LogicMonitorBuilder::new(from_state) + .add_state(to_state) + .add_transition((from_state, to_state)) + .build(monitor_tag, &allocator) + .unwrap(); + + // Trying to transition into unknown state causes monitor to move into indeterminate state. + let _ = monitor.transition(StateTag::from("unknown")); + + // Try to check state. + let result = monitor.state(); + assert!(result.is_err_and(|e| e == LogicEvaluationError::InvalidState)); + } + + #[test] + fn logic_monitor_evaluate_succeeds() { + let allocator = ProtectedMemoryAllocator {}; + let hmon_starting_point = Instant::now(); + let monitor_tag = MonitorTag::from("logic_monitor"); + let from_state = StateTag::from("from"); + let to_state = StateTag::from("to"); + let monitor = LogicMonitorBuilder::new(from_state) + .add_state(to_state) + .add_transition((from_state, to_state)) + .build(monitor_tag, &allocator) + .unwrap(); + + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}") + }); + + let _ = monitor.transition(to_state); + + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}") + }); + } + + #[test] + fn logic_monitor_evaluate_invalid_state() { + let allocator = ProtectedMemoryAllocator {}; + let hmon_starting_point = Instant::now(); + let monitor_tag = MonitorTag::from("logic_monitor"); + let from_state = StateTag::from("from"); + let to_state = StateTag::from("to"); + let monitor = LogicMonitorBuilder::new(from_state) + .add_state(to_state) + .add_transition((from_state, to_state)) + .build(monitor_tag, &allocator) + .unwrap(); + + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}") + }); + + let _ = monitor.transition(StateTag::from("unknown")); + + let mut error_happened = false; + monitor + .inner + .evaluate(hmon_starting_point, &mut |monitor_tag_internal, error| { + error_happened = true; + assert_eq!(*monitor_tag_internal, monitor_tag); + assert_eq!(error, LogicEvaluationError::InvalidState.into()) + }); + assert!(error_happened); + } + + #[test] + fn logic_monitor_evaluate_invalid_transition() { + let allocator = ProtectedMemoryAllocator {}; + let hmon_starting_point = Instant::now(); + let monitor_tag = MonitorTag::from("logic_monitor"); + let state1 = StateTag::from("state1"); + let state2: StateTag = StateTag::from("state2"); + let state3 = StateTag::from("state3"); + let monitor = LogicMonitorBuilder::new(state1) + .add_state(state2) + .add_state(state3) + .add_transition((state1, state2)) + .add_transition((state2, state3)) + .build(monitor_tag, &allocator) + .unwrap(); + + monitor.inner.evaluate(hmon_starting_point, &mut |monitor_tag, error| { + panic!("error happened, tag: {monitor_tag:?}, error: {error:?}") + }); + + let _ = monitor.transition(state3); + + let mut error_happened = false; + monitor + .inner + .evaluate(hmon_starting_point, &mut |monitor_tag_internal, error| { + error_happened = true; + assert_eq!(*monitor_tag_internal, monitor_tag); + assert_eq!(error, LogicEvaluationError::InvalidTransition.into()) + }); + assert!(error_happened); + } +} diff --git a/src/health_monitoring_lib/rust/logic/mod.rs b/src/health_monitoring_lib/rust/logic/mod.rs new file mode 100644 index 00000000..f30fe2c2 --- /dev/null +++ b/src/health_monitoring_lib/rust/logic/mod.rs @@ -0,0 +1,19 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +mod logic_monitor; + +pub use logic_monitor::{LogicEvaluationError, LogicMonitor, LogicMonitorBuilder}; + +// FFI bindings +pub(super) mod ffi; diff --git a/src/health_monitoring_lib/rust/tag.rs b/src/health_monitoring_lib/rust/tag.rs index e3013edc..7b436e45 100644 --- a/src/health_monitoring_lib/rust/tag.rs +++ b/src/health_monitoring_lib/rust/tag.rs @@ -151,10 +151,45 @@ impl From<&str> for DeadlineTag { } } +/// State tag. +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +#[repr(C)] +pub struct StateTag(Tag); + +impl fmt::Debug for StateTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // SAFETY: the underlying data was created from a valid `&str`. + let bytes = unsafe { core::slice::from_raw_parts(self.0.data, self.0.length) }; + let s = unsafe { core::str::from_utf8_unchecked(bytes) }; + write!(f, "StateTag({})", s) + } +} + +impl log::ScoreDebug for StateTag { + fn fmt(&self, f: log::Writer, _spec: &log::FormatSpec) -> Result<(), log::Error> { + // SAFETY: the underlying data was created from a valid `&str`. + let bytes = unsafe { core::slice::from_raw_parts(self.0.data, self.0.length) }; + let s = unsafe { core::str::from_utf8_unchecked(bytes) }; + log::score_write!(f, "StateTag({})", s) + } +} + +impl From for StateTag { + fn from(value: String) -> Self { + Self(Tag::from(value)) + } +} + +impl From<&str> for StateTag { + fn from(value: &str) -> Self { + Self(Tag::from(value)) + } +} + #[cfg(all(test, not(loom)))] mod tests { use crate::log::score_write; - use crate::tag::{DeadlineTag, MonitorTag, Tag}; + use crate::tag::{DeadlineTag, MonitorTag, StateTag, Tag}; use core::fmt::Write; use core::hash::{Hash, Hasher}; use score_log::fmt::{Error, FormatSpec, Result as FmtResult, ScoreWrite}; @@ -371,4 +406,34 @@ mod tests { let tag = DeadlineTag::from(example_str); compare_tag(tag.0, example_str); } + + #[test] + fn state_tag_debug() { + let example_str = "EXAMPLE"; + let tag = StateTag::from(example_str.to_string()); + assert_eq!(format!("{:?}", tag), "StateTag(EXAMPLE)"); + } + + #[test] + fn state_tag_score_debug() { + let example_str = "EXAMPLE"; + let tag = StateTag::from(example_str.to_string()); + let mut writer = StringWriter::new(); + assert!(score_write!(&mut writer, "{:?}", tag).is_ok()); + assert_eq!(writer.get(), "StateTag(EXAMPLE)"); + } + + #[test] + fn state_tag_from_string() { + let example_str = "EXAMPLE"; + let tag = StateTag::from(example_str.to_string()); + compare_tag(tag.0, example_str); + } + + #[test] + fn state_tag_from_str() { + let example_str = "EXAMPLE"; + let tag = StateTag::from(example_str); + compare_tag(tag.0, example_str); + } } diff --git a/src/health_monitoring_lib/rust/worker.rs b/src/health_monitoring_lib/rust/worker.rs index b7091ad0..0723e054 100644 --- a/src/health_monitoring_lib/rust/worker.rs +++ b/src/health_monitoring_lib/rust/worker.rs @@ -45,11 +45,11 @@ impl MonitoringLogic { } } - fn run(&mut self) -> bool { + fn run(&mut self, hmon_starting_point: Instant) -> bool { let mut has_any_error = false; for monitor in self.monitors.iter() { - monitor.evaluate(&mut |monitor_tag, error| { + monitor.evaluate(hmon_starting_point, &mut |monitor_tag, error| { has_any_error = true; match error { @@ -59,8 +59,18 @@ impl MonitoringLogic { monitor_tag, deadline_evaluation_error ) }, - MonitorEvaluationError::Heartbeat => unimplemented!(), - MonitorEvaluationError::Logic => unimplemented!(), + MonitorEvaluationError::Heartbeat(heartbeat_evaluation_error) => { + warn!( + "Heartbeat monitor with tag {:?} reported error: {:?}.", + monitor_tag, heartbeat_evaluation_error + ) + }, + MonitorEvaluationError::Logic(logic_evaluation_error) => { + warn!( + "Logic monitor with tag {:?} reported error: {:?}.", + monitor_tag, logic_evaluation_error + ) + }, } }); } @@ -105,6 +115,7 @@ impl UniqueThreadRunner { std::thread::spawn(move || { info!("Monitoring thread started."); + let hmon_starting_point = Instant::now(); let mut next_sleep_time = interval; // TODO Add some checks and log if cyclicly here is not met. @@ -113,7 +124,7 @@ impl UniqueThreadRunner { let now = Instant::now(); - if !monitoring_logic.run() { + if !monitoring_logic.run(hmon_starting_point) { info!("Monitoring logic failed, stopping thread."); break; } @@ -168,6 +179,7 @@ mod tests { use core::sync::atomic::{AtomicUsize, Ordering}; use core::time::Duration; use std::sync::Arc; + use std::time::Instant; #[derive(Clone)] struct MockSupervisorAPIClient { @@ -211,6 +223,7 @@ mod tests { fn monitoring_logic_report_error_when_deadline_failed() { let deadline_monitor = create_monitor_with_deadlines(); let alive_mock = MockSupervisorAPIClient::new(); + let hmon_starting_point = Instant::now(); let mut logic = MonitoringLogic::new( { @@ -229,7 +242,7 @@ mod tests { drop(handle); - assert!(!logic.run()); + assert!(!logic.run(hmon_starting_point)); assert_eq!(alive_mock.get_notify_count(), 0); } @@ -237,6 +250,7 @@ mod tests { fn monitoring_logic_report_alive_on_each_call_when_no_error() { let deadline_monitor = create_monitor_with_deadlines(); let alive_mock = MockSupervisorAPIClient::new(); + let hmon_starting_point = Instant::now(); let mut logic = MonitoringLogic::new( { @@ -253,11 +267,11 @@ mod tests { .unwrap(); let _handle = deadline.start().unwrap(); - assert!(logic.run()); - assert!(logic.run()); - assert!(logic.run()); - assert!(logic.run()); - assert!(logic.run()); + assert!(logic.run(hmon_starting_point)); + assert!(logic.run(hmon_starting_point)); + assert!(logic.run(hmon_starting_point)); + assert!(logic.run(hmon_starting_point)); + assert!(logic.run(hmon_starting_point)); assert_eq!(alive_mock.get_notify_count(), 5); } @@ -266,6 +280,7 @@ mod tests { fn monitoring_logic_report_alive_respect_cycle() { let deadline_monitor = create_monitor_with_deadlines(); let alive_mock = MockSupervisorAPIClient::new(); + let hmon_starting_point = Instant::now(); let mut logic = MonitoringLogic::new( { @@ -283,19 +298,19 @@ mod tests { let _handle = deadline.start().unwrap(); std::thread::sleep(Duration::from_millis(30)); - assert!(logic.run()); + assert!(logic.run(hmon_starting_point)); std::thread::sleep(Duration::from_millis(30)); - assert!(logic.run()); + assert!(logic.run(hmon_starting_point)); std::thread::sleep(Duration::from_millis(30)); - assert!(logic.run()); + assert!(logic.run(hmon_starting_point)); std::thread::sleep(Duration::from_millis(30)); - assert!(logic.run()); + assert!(logic.run(hmon_starting_point)); std::thread::sleep(Duration::from_millis(30)); - assert!(logic.run()); + assert!(logic.run(hmon_starting_point)); assert_eq!(alive_mock.get_notify_count(), 5); }