diff --git a/score/mw/com/dependability/requirements/component_requirements/component_requirements_ipc.trlc b/score/mw/com/dependability/requirements/component_requirements/component_requirements_ipc.trlc index 63588b928..49ca2680a 100644 --- a/score/mw/com/dependability/requirements/component_requirements/component_requirements_ipc.trlc +++ b/score/mw/com/dependability/requirements/component_requirements/component_requirements_ipc.trlc @@ -648,7 +648,7 @@ section "com" { /* broken_link_c/issue/14137269 */ ScoreReq.CompReq ProxyEventCopySemantics { - description = '''The {{ProxyEvent}} class shall neither be copy-constructable nor copy-assignable.''' + description = '''The {{ProxyEvent}} class shall neither be copy-constructable nor copy-assignable.''' safety = ScoreReq.Asil.B derived_from = [Communication.EventType@1, Communication.DataLoss@1] version = 1 @@ -1590,7 +1590,7 @@ section "com" { The signature of an SubscriptionStateChangeHandler is: - {{{void(SubscriptionState)}}} + {{{bool(SubscriptionState)}}} The type chosen for {{SubscriptionStateChangeHandler}} by the {{mw::com}} provider shall allow construction from any callable, with the given signature.''' safety = ScoreReq.Asil.B @@ -2307,8 +2307,7 @@ section "com" { /* broken_link_c/issue/21466369 */ ScoreReq.CompReq BehaviourOfSetSubscriptionStateChangeHandler { description = '''The {{SubscriptionStateChangeHandler}} (See SubscriptionStateChangeHandler) - {{SetSubscriptionStateChangeHandler}} - that is passed to shall be called every time the {{SubscriptionState}} (See SubscriptionState) changes according to the requirements: + that is passed to {{SetSubscriptionStateChangeHandler}} shall be called every time the {{SubscriptionState}} (See SubscriptionState) changes according to the requirements: * Calls to SubscriptionStateChangeHandler with kSubscriptionPending @@ -2356,9 +2355,17 @@ section "com" { version = 1 } + ScoreReq.CompReq SubscriptionStateChangeHandlerReturnValue { + description = '''In case the user provided handler returns true, the {{SubscriptionStateChangeHandler}} will be kept registered and will be called on the next {{SubscriptionState}} change again. + In case the user provided handler returns false, the {{SubscriptionStateChangeHandler}} will be unregistered and won't be called on the next {{SubscriptionState}} change again.''' + safety = ScoreReq.Asil.B + derived_from = [Communication.SupportForTimeBasedArchitecture@2] + version = 1 + } + /* broken_link_c/issue/21466236 */ ScoreReq.CompReq BehaviourOfUnsetSubscriptionStateChangeHandler { - description = '''{{UnsetSubscriptionStateChangeHandler}} {{UnsetSubscriptionStateChangeHandler}}{{SubscriptionStateChangeHandler}}will unregister the {{SubscriptionStateChangeHandler}} (See SubscriptionStateChangeHandler) that was registered by the class instance with a call to {{SetSubscriptionStateChangeHandler}} After calling the will no longer be triggered when the {{SubscriptionState}} (See progress-cursor SubscriptionState changes. + description = '''{{UnsetSubscriptionStateChangeHandler}} will unregister the {{SubscriptionStateChangeHandler}} (See SubscriptionStateChangeHandler) that was registered by the class instance with a call to {{SetSubscriptionStateChangeHandler}} After calling the will no longer be triggered when the {{SubscriptionState}} (See progress-cursor SubscriptionState changes. {{UnsetSubscriptionStateChangeHandler}} shall fulfill the following requirement: Explicit Lifetime ending by API calls.''' safety = ScoreReq.Asil.B diff --git a/score/mw/com/design/events_fields/README.md b/score/mw/com/design/events_fields/README.md index eefe598c5..040373f75 100644 --- a/score/mw/com/design/events_fields/README.md +++ b/score/mw/com/design/events_fields/README.md @@ -346,6 +346,36 @@ The structural model of the state machine design is as follows: PROXY_EVENT_STATE_MACHINE_MODEL +#### User access to the subscription state + +The user can access the subscription state of a `ProxyEvent` or `ProxyField` instance via method `GetSubscriptionState()`. +This method dispatches to the binding (`ProxyEventBindingBase::GetSubscriptionState()`), which in the case of the `LoLa` +binding dispatches to `lola::SubscriptionStateMachine` (method `GetCurrentState()`) shown above. + +Additionally a user can register a callback, which gets called as soon as the subscription state changes. This is done +via method `ProxyEvent::SetSubscriptionStateChangeHandler`. This method dispatches to the binding +(`ProxyEventBindingBase::SetSubscriptionStateChangeHandler()`), which in the case of the `LoLa` binding dispatches to +`lola::SubscriptionStateMachine` (method `SetSubscriptionStateChangeHandler()`) shown above. The +`lola::SubscriptionStateMachine` stores the user provided callback and calls it as soon as the subscription state changes. + +The call to the user-provided handler happens under the same lock, which protects the state change and the state change +notification. Thus, there are two important implications: +1. The user provided handler gets called synchronously within the state change call. This means, that the state change + call only returns after the user provided handler has returned. +2. Since the user provided handler gets called under the lock, it must not do any call, which could lead to a deadlock. E.g. + it must not call `ProxyEvent::Unsubscribe` or `ProxyEvent::Subscribe` again, as these calls also acquire the same + lock. + +The implications/recommendations are given in the public API description of the `SubscriptionStateChangeHandler`. +We explicitly allow, that the user unsets/unregisters the `SubscriptionStateChangeHandler` from within the handler itself. +To avoid any potential deadlock, we do **not** allow to unset the `SubscriptionStateChangeHandler` via the +`UnsetSubscriptionStateChangeHandler()`! Instead the `SubscriptionStateChangeHandler` signature is designed to control +the unsetting via its return value. I.e. if the handler returns `false`, it will be automatically unset after it has been +called. This avoids introduction of recursive mutexes/locking on the existing mutex of the state machine, which would +add a lot of complexity and potential for deadlocks. The solution via the return code is very lightweight: After the state +machine called the handler, and before it releases the lock, it checks the return value. If it is `false`, it just resets +– still under state-machine mutex lock – the internal `std::optional`, which holds the handler. + ### Event Update Notification Event Notification is a good showcase for the "smart" behavior of `lola::MessagePassingFacade` as already mentioned (see diff --git a/score/mw/com/impl/BUILD b/score/mw/com/impl/BUILD index d677fab28..44bf059a7 100644 --- a/score/mw/com/impl/BUILD +++ b/score/mw/com/impl/BUILD @@ -483,7 +483,7 @@ cc_library( tags = ["FFI"], visibility = [ "//score/mw/com:__pkg__", - "//score/mw/com/impl/mocking:__pkg__", + "//score/mw/com/impl:__subpackages__", ], ) @@ -584,6 +584,7 @@ cc_library( ":sample_reference_tracker", ":scoped_event_receive_handler", ":subscription_state", + ":subscription_state_change_handler", "//score/mw/com/impl/configuration", "//score/mw/com/impl/plumbing:sample_ptr", "//score/mw/com/impl/tracing:i_tracing_runtime", @@ -833,6 +834,21 @@ cc_library( deps = ["@score_baselibs//score/language/futurecpp"], ) +cc_library( + name = "subscription_state_change_handler", + srcs = ["subscription_state_change_handler.cpp"], + hdrs = ["subscription_state_change_handler.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + "//score/mw/com:__subpackages__", + ], + deps = [ + ":subscription_state", + "@score_baselibs//score/language/futurecpp", + ], +) + cc_library( name = "i_service_discovery", srcs = ["i_service_discovery.cpp"], diff --git a/score/mw/com/impl/bindings/lola/BUILD b/score/mw/com/impl/bindings/lola/BUILD index ca386bdff..d730fa611 100644 --- a/score/mw/com/impl/bindings/lola/BUILD +++ b/score/mw/com/impl/bindings/lola/BUILD @@ -509,6 +509,8 @@ cc_library( ":transaction_log_set", "//score/mw/com/impl:runtime", "//score/mw/com/impl:scoped_event_receive_handler", + "//score/mw/com/impl:subscription_state", + "//score/mw/com/impl:subscription_state_change_handler", "//score/mw/com/impl/bindings/lola/messaging:i_message_passing_service", ], ) diff --git a/score/mw/com/impl/bindings/lola/generic_proxy_event.cpp b/score/mw/com/impl/bindings/lola/generic_proxy_event.cpp index 0414e16bc..ca5a39903 100644 --- a/score/mw/com/impl/bindings/lola/generic_proxy_event.cpp +++ b/score/mw/com/impl/bindings/lola/generic_proxy_event.cpp @@ -97,6 +97,16 @@ Result GenericProxyEvent::UnsetReceiveHandler() noexcept return proxy_event_common_.UnsetReceiveHandler(); } +Result GenericProxyEvent::SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept +{ + return proxy_event_common_.SetSubscriptionStateChangeHandler(std::move(handler)); +} + +Result GenericProxyEvent::UnsetSubscriptionStateChangeHandler() noexcept +{ + return proxy_event_common_.UnsetSubscriptionStateChangeHandler(); +} + pid_t GenericProxyEvent::GetEventSourcePid() const noexcept { return proxy_event_common_.GetEventSourcePid(); diff --git a/score/mw/com/impl/bindings/lola/generic_proxy_event.h b/score/mw/com/impl/bindings/lola/generic_proxy_event.h index 4fb2943f7..c38f477d0 100644 --- a/score/mw/com/impl/bindings/lola/generic_proxy_event.h +++ b/score/mw/com/impl/bindings/lola/generic_proxy_event.h @@ -68,6 +68,9 @@ class GenericProxyEvent final : public GenericProxyEventBinding Result SetReceiveHandler(std::weak_ptr handler) noexcept override; Result UnsetReceiveHandler() noexcept override; + Result SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept override; + Result UnsetSubscriptionStateChangeHandler() noexcept override; + pid_t GetEventSourcePid() const noexcept; ElementFqId GetElementFQId() const noexcept; std::optional GetMaxSampleCount() const noexcept override; diff --git a/score/mw/com/impl/bindings/lola/proxy_event.h b/score/mw/com/impl/bindings/lola/proxy_event.h index 625375315..73c3e4bc2 100644 --- a/score/mw/com/impl/bindings/lola/proxy_event.h +++ b/score/mw/com/impl/bindings/lola/proxy_event.h @@ -99,6 +99,14 @@ class ProxyEvent final : public ProxyEventBinding { return proxy_event_common_.UnsetReceiveHandler(); } + Result SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept override + { + return proxy_event_common_.SetSubscriptionStateChangeHandler(std::move(handler)); + } + Result UnsetSubscriptionStateChangeHandler() noexcept override + { + return proxy_event_common_.UnsetSubscriptionStateChangeHandler(); + } std::optional GetMaxSampleCount() const noexcept override { return proxy_event_common_.GetMaxSampleCount(); diff --git a/score/mw/com/impl/bindings/lola/proxy_event_common.cpp b/score/mw/com/impl/bindings/lola/proxy_event_common.cpp index 1071ad29e..552265166 100644 --- a/score/mw/com/impl/bindings/lola/proxy_event_common.cpp +++ b/score/mw/com/impl/bindings/lola/proxy_event_common.cpp @@ -64,20 +64,7 @@ void ProxyEventCommon::Unsubscribe() SubscriptionState ProxyEventCommon::GetSubscriptionState() const noexcept { const auto current_state = subscription_event_state_machine_.GetCurrentState(); - if (current_state == SubscriptionStateMachineState::NOT_SUBSCRIBED_STATE) - { - return SubscriptionState::kNotSubscribed; - } - else if (current_state == SubscriptionStateMachineState::SUBSCRIPTION_PENDING_STATE) - { - return SubscriptionState::kSubscriptionPending; - } - else - { - SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(current_state == SubscriptionStateMachineState::SUBSCRIBED_STATE, - "Invalid subscription state machine state."); - return SubscriptionState::kSubscribed; - } + return SubscriptionStateMachineStateToSubscriptionState(current_state); } Result ProxyEventCommon::GetNumNewSamplesAvailable() const noexcept @@ -114,6 +101,18 @@ Result ProxyEventCommon::UnsetReceiveHandler() return {}; } +Result ProxyEventCommon::SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept +{ + subscription_event_state_machine_.SetSubscriptionStateChangeHandler(std::move(handler)); + return {}; +} + +Result ProxyEventCommon::UnsetSubscriptionStateChangeHandler() noexcept +{ + subscription_event_state_machine_.UnsetSubscriptionStateChangeHandler(); + return {}; +} + pid_t ProxyEventCommon::GetEventSourcePid() const noexcept { return parent_.GetSourcePid(); diff --git a/score/mw/com/impl/bindings/lola/proxy_event_common.h b/score/mw/com/impl/bindings/lola/proxy_event_common.h index 55565b1dc..7086baa6c 100644 --- a/score/mw/com/impl/bindings/lola/proxy_event_common.h +++ b/score/mw/com/impl/bindings/lola/proxy_event_common.h @@ -84,6 +84,9 @@ class ProxyEventCommon final Result SetReceiveHandler(std::weak_ptr handler); Result UnsetReceiveHandler(); + Result SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept; + Result UnsetSubscriptionStateChangeHandler() noexcept; + pid_t GetEventSourcePid() const noexcept; ElementFqId GetElementFQId() const noexcept { diff --git a/score/mw/com/impl/bindings/lola/subscription_helpers.cpp b/score/mw/com/impl/bindings/lola/subscription_helpers.cpp index 559f36300..f039455d5 100644 --- a/score/mw/com/impl/bindings/lola/subscription_helpers.cpp +++ b/score/mw/com/impl/bindings/lola/subscription_helpers.cpp @@ -57,6 +57,21 @@ void EventReceiveHandlerManager::Unregister() noexcept } } +SubscriptionState SubscriptionStateMachineStateToSubscriptionState(SubscriptionStateMachineState state) noexcept +{ + switch (state) + { + case SubscriptionStateMachineState::NOT_SUBSCRIBED_STATE: + return SubscriptionState::kNotSubscribed; + case SubscriptionStateMachineState::SUBSCRIBED_STATE: + return SubscriptionState::kSubscribed; + case SubscriptionStateMachineState::SUBSCRIPTION_PENDING_STATE: + return SubscriptionState::kSubscriptionPending; + default: + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(false, "Invalid subscription state"); + } +} + std::string CreateLoggingString(std::string&& string, const ElementFqId& element_fq_id, const SubscriptionStateMachineState current_state) diff --git a/score/mw/com/impl/bindings/lola/subscription_helpers.h b/score/mw/com/impl/bindings/lola/subscription_helpers.h index 22ac30dc3..0899ceceb 100644 --- a/score/mw/com/impl/bindings/lola/subscription_helpers.h +++ b/score/mw/com/impl/bindings/lola/subscription_helpers.h @@ -17,6 +17,7 @@ #include "score/mw/com/impl/bindings/lola/slot_collector.h" #include "score/mw/com/impl/bindings/lola/subscription_state_machine_states.h" #include "score/mw/com/impl/scoped_event_receive_handler.h" +#include "score/mw/com/impl/subscription_state.h" #include #include @@ -87,6 +88,8 @@ std::string CreateLoggingString(std::string&& string, const ElementFqId& element_fq_id, const SubscriptionStateMachineState current_state); +SubscriptionState SubscriptionStateMachineStateToSubscriptionState(SubscriptionStateMachineState state) noexcept; + } // namespace score::mw::com::impl::lola #endif // SCORE_MW_COM_IMPL_BINDINGS_LOLA_SUBSCRIPTION_HELPERS_H diff --git a/score/mw/com/impl/bindings/lola/subscription_state_machine.cpp b/score/mw/com/impl/bindings/lola/subscription_state_machine.cpp index d48ffe220..969c987be 100644 --- a/score/mw/com/impl/bindings/lola/subscription_state_machine.cpp +++ b/score/mw/com/impl/bindings/lola/subscription_state_machine.cpp @@ -110,6 +110,18 @@ void SubscriptionStateMachine::UnsetReceiveHandler() noexcept GetCurrentEventState().UnsetReceiveHandler(); } +void SubscriptionStateMachine::SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept +{ + std::lock_guard lock{state_mutex_}; + subscription_state_change_handler_ = std::move(handler); +} + +void SubscriptionStateMachine::UnsetSubscriptionStateChangeHandler() noexcept +{ + std::lock_guard lock{state_mutex_}; + subscription_state_change_handler_.reset(); +} + std::optional SubscriptionStateMachine::GetMaxSampleCount() const noexcept { std::lock_guard lock{state_mutex_}; @@ -136,6 +148,17 @@ void SubscriptionStateMachine::TransitionToState(const SubscriptionStateMachineS GetCurrentEventState().OnExit(); current_state_idx_ = newState; GetCurrentEventState().OnEntry(); + if (subscription_state_change_handler_.has_value()) + { + // We call the user-provided handler under state_mutex_ lock, which has always been acquired within this method. + // This is documented in the AoUs of SubscriptionStateChangeHandler! + const auto keep_handler = + subscription_state_change_handler_.value()(SubscriptionStateMachineStateToSubscriptionState(newState)); + if (!keep_handler) + { + subscription_state_change_handler_.reset(); + } + } } } // namespace score::mw::com::impl::lola diff --git a/score/mw/com/impl/bindings/lola/subscription_state_machine.h b/score/mw/com/impl/bindings/lola/subscription_state_machine.h index e17055ca0..f87f3d234 100644 --- a/score/mw/com/impl/bindings/lola/subscription_state_machine.h +++ b/score/mw/com/impl/bindings/lola/subscription_state_machine.h @@ -24,6 +24,7 @@ #include "score/mw/com/impl/bindings/lola/transaction_log_registration_guard.h" #include "score/mw/com/impl/bindings/lola/transaction_log_set.h" #include "score/mw/com/impl/scoped_event_receive_handler.h" +#include "score/mw/com/impl/subscription_state_change_handler.h" #include "score/result/result.h" @@ -99,10 +100,12 @@ class SubscriptionStateMachine : public std::enable_shared_from_this handler) noexcept; void UnsetReceiveHandler() noexcept; + void SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept; + void UnsetSubscriptionStateChangeHandler() noexcept; std::optional GetMaxSampleCount() const noexcept; @@ -139,6 +142,7 @@ class SubscriptionStateMachine : public std::enable_shared_from_this> event_receiver_handler_; + std::optional subscription_state_change_handler_; EventReceiveHandlerManager event_receive_handler_manager_; ConsumerEventDataControlLocalView<>& event_data_control_local_; EventSubscriptionControl<>& subscription_control_; diff --git a/score/mw/com/impl/bindings/lola/subscription_state_machine_events_test.cpp b/score/mw/com/impl/bindings/lola/subscription_state_machine_events_test.cpp index 16d6e4cab..38d8cd819 100644 --- a/score/mw/com/impl/bindings/lola/subscription_state_machine_events_test.cpp +++ b/score/mw/com/impl/bindings/lola/subscription_state_machine_events_test.cpp @@ -59,6 +59,7 @@ class StateMachineEventsFixture : public LolaProxyEventResources void TearDown() override { + state_machine_.UnsetSubscriptionStateChangeHandler(); // We call Unsubscribe in the tear down to make sure that the state machine is correctly cleaned up. // Specifically, it's important that the Unsubscribe is recorded so that when ~TransactionLogRegistrationGuard // unregisters the TransactionLog, there are no open transactions. @@ -124,6 +125,7 @@ class StateMachineEventsFixture : public LolaProxyEventResources SubscriptionStateMachine state_machine_; std::vector transaction_log_registration_guards_{}; pid_t new_event_source_pid_{kDummyPid + 1}; + std::optional observed_subscription_state_{}; }; using StateMachineNotSubscribedStateFixture = StateMachineEventsFixture; @@ -391,5 +393,138 @@ TEST_F(StateMachineSubscribedStateFixture, CallingReOfferEventDoesNothing) EXPECT_EQ(state_machine_.GetCurrentState(), SubscriptionStateMachineState::SUBSCRIBED_STATE); } +using StateMachineStateChangeFixture = StateMachineEventsFixture; +TEST_F(StateMachineStateChangeFixture, CallingSetSubscriptionStateChangeHandlerSucceeds) +{ + // Given a state-machine in its default state and a subscription-state-change-handler + auto subscription_state_change_handler = [](SubscriptionState new_subscription_state) { + return true; + }; + + // Calling SetSubscriptionStateChangeHandler doesn't crash. + state_machine_.SetSubscriptionStateChangeHandler(subscription_state_change_handler); +} + +TEST_F(StateMachineStateChangeFixture, CallingUnsetSubscriptionStateChangeHandlerSucceeds) +{ + // Given a state-machine in its default state + + // Calling UnsetSubscriptionStateChangeHandler without having set a handler before doesn't crash. + state_machine_.UnsetSubscriptionStateChangeHandler(); +} + +TEST_F(StateMachineStateChangeFixture, SubscriptionStateChangeHandlerGetsInvoked) +{ + + // Given a state-machine with a set subscription-state-change-handler + auto subscription_state_change_handler = [this](SubscriptionState new_subscription_state) { + observed_subscription_state_ = new_subscription_state; + return true; + }; + state_machine_.SetSubscriptionStateChangeHandler(subscription_state_change_handler); + + // when the state-machine switches to SUBSCRIBED + EnterSubscribed(max_num_slots_); + + // expect, that the handler has been called with the new state SUBSCRIBED + EXPECT_TRUE(observed_subscription_state_.has_value()); + EXPECT_EQ(observed_subscription_state_, SubscriptionState::kSubscribed); +} + +TEST_F(StateMachineStateChangeFixture, SubscriptionStateChangeHandlerNotInvokedInCaseNoStateChange) +{ + // Given a state machine already in SUBSCRIBED state + EnterSubscribed(max_num_slots_); + + // and a set subscription-state-change-handler + auto subscription_state_change_handler = [this](SubscriptionState new_subscription_state) { + observed_subscription_state_ = new_subscription_state; + return true; + }; + state_machine_.SetSubscriptionStateChangeHandler(subscription_state_change_handler); + + // when the state-machine gets again a subscribe-event + EnterSubscribed(max_num_slots_); + + // expect, that the handler has NOT been called as no state change happened. + EXPECT_FALSE(observed_subscription_state_.has_value()); +} + +TEST_F(StateMachineStateChangeFixture, SubscriptionStateChangeHandlerStaysRegisteredWhenReturningTrue) +{ + // Given a state-machine with a set subscription-state-change-handler + auto subscription_state_change_handler = [this](SubscriptionState new_subscription_state) { + observed_subscription_state_ = new_subscription_state; + return true; + }; + state_machine_.SetSubscriptionStateChangeHandler(subscription_state_change_handler); + + // when the state-machine switches to SUBSCRIBED + EnterSubscribed(max_num_slots_); + + // expect, that the handler has been called with the new state SUBSCRIBED + EXPECT_TRUE(observed_subscription_state_.has_value()); + EXPECT_EQ(observed_subscription_state_, SubscriptionState::kSubscribed); + + // and when another state-change happens + observed_subscription_state_.reset(); + state_machine_.StopOfferEvent(); + + // expect, that the handler has been called with the new state kSubscriptionPending + EXPECT_TRUE(observed_subscription_state_.has_value()); + EXPECT_EQ(observed_subscription_state_, SubscriptionState::kSubscriptionPending); +} + +TEST_F(StateMachineStateChangeFixture, SubscriptionStateChangeHandlerGetsUnsetWhenReturningFalse) +{ + // Given a state-machine with a set subscription-state-change-handler, which returns false after being called + auto subscription_state_change_handler = [this](SubscriptionState new_subscription_state) { + observed_subscription_state_ = new_subscription_state; + return false; + }; + state_machine_.SetSubscriptionStateChangeHandler(subscription_state_change_handler); + + // when the state-machine switches to SUBSCRIBED + EnterSubscribed(max_num_slots_); + + // expect, that the handler has been called with the new state SUBSCRIBED + EXPECT_TRUE(observed_subscription_state_.has_value()); + EXPECT_EQ(observed_subscription_state_, SubscriptionState::kSubscribed); + + // and when another state-change happens + observed_subscription_state_.reset(); + state_machine_.StopOfferEvent(); + + // expect, that the handler has not been called anymore as it was unregistered. + EXPECT_FALSE(observed_subscription_state_.has_value()); +} + +TEST_F(StateMachineStateChangeFixture, UnsetSubscriptionStateChangeHandlerWorks) +{ + // Given a state-machine with a set subscription-state-change-handler, which returns true after being called + auto subscription_state_change_handler = [this](SubscriptionState new_subscription_state) { + observed_subscription_state_ = new_subscription_state; + return true; + }; + state_machine_.SetSubscriptionStateChangeHandler(subscription_state_change_handler); + + // when the state-machine switches to SUBSCRIBED + EnterSubscribed(max_num_slots_); + + // expect, that the handler has been called with the new state SUBSCRIBED + EXPECT_TRUE(observed_subscription_state_.has_value()); + EXPECT_EQ(observed_subscription_state_, SubscriptionState::kSubscribed); + + // and after unsetting the handler + state_machine_.UnsetSubscriptionStateChangeHandler(); + + // and when another state-change happens + observed_subscription_state_.reset(); + state_machine_.StopOfferEvent(); + + // expect, that the handler has not been called anymore as it was unregistered. + EXPECT_FALSE(observed_subscription_state_.has_value()); +} + } // namespace } // namespace score::mw::com::impl::lola diff --git a/score/mw/com/impl/bindings/mock_binding/generic_proxy_event.h b/score/mw/com/impl/bindings/mock_binding/generic_proxy_event.h index be6502ee1..6204137e8 100644 --- a/score/mw/com/impl/bindings/mock_binding/generic_proxy_event.h +++ b/score/mw/com/impl/bindings/mock_binding/generic_proxy_event.h @@ -56,6 +56,11 @@ class GenericProxyEvent : public GenericProxyEventBinding (noexcept, override)); MOCK_METHOD(Result, SetReceiveHandler, (std::weak_ptr), (noexcept, override)); MOCK_METHOD(Result, UnsetReceiveHandler, (), (noexcept, override)); + MOCK_METHOD(Result, + SetSubscriptionStateChangeHandler, + (SubscriptionStateChangeHandler), + (noexcept, override)); + MOCK_METHOD(Result, UnsetSubscriptionStateChangeHandler, (), (noexcept, override)); MOCK_METHOD(std::optional, GetMaxSampleCount, (), (const, noexcept, override)); MOCK_METHOD(BindingType, GetBindingType, (), (const, noexcept, override)); MOCK_METHOD(void, NotifyServiceInstanceChangedAvailability, (bool, pid_t), (noexcept, override)); diff --git a/score/mw/com/impl/bindings/mock_binding/proxy_event.h b/score/mw/com/impl/bindings/mock_binding/proxy_event.h index 41de6d9e6..d8e6267a9 100644 --- a/score/mw/com/impl/bindings/mock_binding/proxy_event.h +++ b/score/mw/com/impl/bindings/mock_binding/proxy_event.h @@ -40,6 +40,11 @@ class ProxyEventBase : public ProxyEventBindingBase MOCK_METHOD(Result, GetNumNewSamplesAvailable, (), (const, noexcept, override)); MOCK_METHOD(Result, SetReceiveHandler, (std::weak_ptr), (noexcept, override)); MOCK_METHOD(Result, UnsetReceiveHandler, (), (noexcept, override)); + MOCK_METHOD(Result, + SetSubscriptionStateChangeHandler, + (SubscriptionStateChangeHandler), + (noexcept, override)); + MOCK_METHOD(Result, UnsetSubscriptionStateChangeHandler, (), (noexcept, override)); MOCK_METHOD(std::optional, GetMaxSampleCount, (), (const, noexcept, override)); MOCK_METHOD(BindingType, GetBindingType, (), (const, noexcept, override)); MOCK_METHOD(void, NotifyServiceInstanceChangedAvailability, (bool, pid_t), (noexcept, override)); @@ -74,6 +79,11 @@ class ProxyEvent : public ProxyEventBinding (noexcept, override)); MOCK_METHOD(Result, SetReceiveHandler, (std::weak_ptr), (noexcept, override)); MOCK_METHOD(Result, UnsetReceiveHandler, (), (noexcept, override)); + MOCK_METHOD(Result, + SetSubscriptionStateChangeHandler, + (SubscriptionStateChangeHandler), + (noexcept, override)); + MOCK_METHOD(Result, UnsetSubscriptionStateChangeHandler, (), (noexcept, override)); MOCK_METHOD(std::optional, GetMaxSampleCount, (), (const, noexcept, override)); MOCK_METHOD(BindingType, GetBindingType, (), (const, noexcept, override)); MOCK_METHOD(void, NotifyServiceInstanceChangedAvailability, (bool, pid_t), (noexcept, override)); @@ -160,6 +170,14 @@ class ProxyEventFacade : public ProxyEventBinding { return proxy_event_.UnsetReceiveHandler(); } + Result SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept override + { + return proxy_event_.SetSubscriptionStateChangeHandler(std::move(handler)); + } + Result UnsetSubscriptionStateChangeHandler() noexcept override + { + return proxy_event_.UnsetSubscriptionStateChangeHandler(); + } std::optional GetMaxSampleCount() const noexcept override { return proxy_event_.GetMaxSampleCount(); diff --git a/score/mw/com/impl/mocking/BUILD b/score/mw/com/impl/mocking/BUILD index fbeb338d8..c720bfb2a 100644 --- a/score/mw/com/impl/mocking/BUILD +++ b/score/mw/com/impl/mocking/BUILD @@ -48,6 +48,7 @@ cc_library( deps = [ "//score/mw/com/impl:event_receive_handler", "//score/mw/com/impl:subscription_state", + "//score/mw/com/impl:subscription_state_change_handler", "//score/mw/com/impl/plumbing:sample_ptr", "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/result", diff --git a/score/mw/com/impl/mocking/i_proxy_event.h b/score/mw/com/impl/mocking/i_proxy_event.h index 124074d98..d48280a04 100644 --- a/score/mw/com/impl/mocking/i_proxy_event.h +++ b/score/mw/com/impl/mocking/i_proxy_event.h @@ -16,6 +16,7 @@ #include "score/mw/com/impl/event_receive_handler.h" #include "score/mw/com/impl/plumbing/sample_ptr.h" #include "score/mw/com/impl/subscription_state.h" +#include "score/mw/com/impl/subscription_state_change_handler.h" #include "score/result/result.h" @@ -39,6 +40,8 @@ class IProxyEventBase virtual Result GetNumNewSamplesAvailable() = 0; virtual Result SetReceiveHandler(EventReceiveHandler) = 0; virtual Result UnsetReceiveHandler() = 0; + virtual Result SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) = 0; + virtual Result UnsetSubscriptionStateChangeHandler() = 0; protected: IProxyEventBase(const IProxyEventBase&) = default; diff --git a/score/mw/com/impl/mocking/proxy_event_mock.h b/score/mw/com/impl/mocking/proxy_event_mock.h index d69cd04c6..cafe71217 100644 --- a/score/mw/com/impl/mocking/proxy_event_mock.h +++ b/score/mw/com/impl/mocking/proxy_event_mock.h @@ -33,6 +33,8 @@ class ProxyEventMock : public IProxyEvent MOCK_METHOD(Result, GetNumNewSamplesAvailable, (), (override)); MOCK_METHOD(Result, SetReceiveHandler, (EventReceiveHandler), (override)); MOCK_METHOD(Result, UnsetReceiveHandler, (), (override)); + MOCK_METHOD(Result, SetSubscriptionStateChangeHandler, (SubscriptionStateChangeHandler), (override)); + MOCK_METHOD(Result, UnsetSubscriptionStateChangeHandler, (), (override)); MOCK_METHOD(Result, GetNewSamples, (Callback&&, const std::size_t), (override)); }; diff --git a/score/mw/com/impl/proxy_event_base.cpp b/score/mw/com/impl/proxy_event_base.cpp index 4e748c0d8..faa9f8650 100644 --- a/score/mw/com/impl/proxy_event_base.cpp +++ b/score/mw/com/impl/proxy_event_base.cpp @@ -30,7 +30,7 @@ namespace score::mw::com::impl { -// Initialization of static thread_local variable! +// Initialization of static thread_local variables! thread_local bool ProxyEventBase::is_in_receive_handler_context = false; /// \brief Helper class which registers the ProxyEventBindingBase with its parent proxy (ProxyBinding) and unregisters @@ -171,6 +171,18 @@ void ProxyEventBase::Unsubscribe() noexcept } } +Result ProxyEventBase::SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept +{ + binding_base_->SetSubscriptionStateChangeHandler(std::move(handler)); + return {}; +} + +Result ProxyEventBase::UnsetSubscriptionStateChangeHandler() noexcept +{ + binding_base_->UnsetSubscriptionStateChangeHandler(); + return {}; +} + std::size_t ProxyEventBase::GetFreeSampleCount() const noexcept { if (proxy_event_base_mock_ != nullptr) diff --git a/score/mw/com/impl/proxy_event_base.h b/score/mw/com/impl/proxy_event_base.h index 1e8ccf594..55ebcb83a 100644 --- a/score/mw/com/impl/proxy_event_base.h +++ b/score/mw/com/impl/proxy_event_base.h @@ -19,6 +19,7 @@ #include "score/mw/com/impl/proxy_event_binding_base.h" #include "score/mw/com/impl/sample_reference_tracker.h" #include "score/mw/com/impl/subscription_state.h" +#include "score/mw/com/impl/subscription_state_change_handler.h" #include "score/mw/com/impl/tracing/proxy_event_tracing_data.h" #include "score/mw/com/impl/mocking/i_proxy_event.h" @@ -119,6 +120,22 @@ class ProxyEventBase */ void Unsubscribe() noexcept; + /** + * \api + * \brief Sets/Registers a SubscriptionStateChangeHandler for this event. This handler will be called whenever the + * subscription state of this event changes. + * \note An already set/registered SubscriptionStateChangeHandler will be silently overridden. + * \param handler + */ + Result SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept; + + /** + * \api + * \brief Unsets/Unregisters a SubscriptionStateChangeHandler for this event. After this method returns, it is + * guaranteed, that the previously registered handler is neither active nor will be called anymore. + */ + Result UnsetSubscriptionStateChangeHandler() noexcept; + /** * \api * \brief Get the number of samples that can still be received by the user of this event. diff --git a/score/mw/com/impl/proxy_event_binding_base.h b/score/mw/com/impl/proxy_event_binding_base.h index 2ee2db8bb..6709741b2 100644 --- a/score/mw/com/impl/proxy_event_binding_base.h +++ b/score/mw/com/impl/proxy_event_binding_base.h @@ -16,6 +16,7 @@ #include "score/mw/com/impl/binding_type.h" #include "score/mw/com/impl/scoped_event_receive_handler.h" #include "score/mw/com/impl/subscription_state.h" +#include "score/mw/com/impl/subscription_state_change_handler.h" #include "score/result/result.h" @@ -73,6 +74,15 @@ class ProxyEventBindingBase /// \brief Remove any receive handler registered via SetReceiveHandler() virtual Result UnsetReceiveHandler() noexcept = 0; + /// \brief Sets/Registers a SubscriptionStateChangeHandler for this event. This handler will be called whenever the + /// subscription state of this event changes. + /// \note An already set/registered SubscriptionStateChangeHandler will be silently overridden. + /// @param handler The callback to be called in case of a subscription state change. + virtual Result SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept = 0; + + /// \brief Remove any receive handler registered via SetSubscriptionStateChangeHandler() + virtual Result UnsetSubscriptionStateChangeHandler() noexcept = 0; + /// \brief Returns the number of new samples a call to GetNewSamples() would currently provide if the /// max_sample_count set in the Subscribe call and GetNewSamples call were both infinitely high. /// \see ProxyEvent::GetNumNewSamplesAvailable() diff --git a/score/mw/com/impl/proxy_event_binding_base_test.cpp b/score/mw/com/impl/proxy_event_binding_base_test.cpp index 99d05a66e..5d690c75a 100644 --- a/score/mw/com/impl/proxy_event_binding_base_test.cpp +++ b/score/mw/com/impl/proxy_event_binding_base_test.cpp @@ -40,6 +40,14 @@ class DummyProxyEventBinding final : public ProxyEventBindingBase { return {}; } + Result SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept override + { + return {}; + } + Result UnsetSubscriptionStateChangeHandler() noexcept override + { + return {}; + } Result GetNumNewSamplesAvailable() const noexcept override { return {}; diff --git a/score/mw/com/impl/proxy_field_base.h b/score/mw/com/impl/proxy_field_base.h index dc49bcaee..88068b96e 100644 --- a/score/mw/com/impl/proxy_field_base.h +++ b/score/mw/com/impl/proxy_field_base.h @@ -86,6 +86,28 @@ class ProxyFieldBase proxy_event_base_dispatch_->Unsubscribe(); } + /** + * \api + * \brief Sets/Registers a SubscriptionStateChangeHandler for this event. This handler will be called whenever the + * subscription state of this event changes. + * \note An already set/registered SubscriptionStateChangeHandler will be silently overridden. + * \param handler + */ + Result SetSubscriptionStateChangeHandler(SubscriptionStateChangeHandler handler) noexcept + { + return proxy_event_base_dispatch_->SetSubscriptionStateChangeHandler(std::move(handler)); + } + + /** + * \api + * \brief Unsets/Unregisters a SubscriptionStateChangeHandler for this field. After this method returns, it is + * guaranteed, that the previously registered handler is neither active nor will be called anymore. + */ + Result UnsetSubscriptionStateChangeHandler() noexcept + { + return proxy_event_base_dispatch_->UnsetSubscriptionStateChangeHandler(); + } + /** * \api * \brief Get the number of samples that can still be received by the user of this field. diff --git a/score/mw/com/impl/subscription_state_change_handler.cpp b/score/mw/com/impl/subscription_state_change_handler.cpp new file mode 100644 index 000000000..a2f073655 --- /dev/null +++ b/score/mw/com/impl/subscription_state_change_handler.cpp @@ -0,0 +1,13 @@ +/******************************************************************************** + * Copyright (c) 2025 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/mw/com/impl/subscription_state_change_handler.h" diff --git a/score/mw/com/impl/subscription_state_change_handler.h b/score/mw/com/impl/subscription_state_change_handler.h new file mode 100644 index 000000000..becbe8188 --- /dev/null +++ b/score/mw/com/impl/subscription_state_change_handler.h @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2025 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_MW_COM_IMPL_SUBSCRIPTION_STATE_CHANGE_HANDLER_H +#define SCORE_MW_COM_IMPL_SUBSCRIPTION_STATE_CHANGE_HANDLER_H + +#include "score/mw/com/impl/subscription_state.h" + +#include + +namespace score::mw::com::impl +{ + +/// \brief Callback for event/field subscription state change notifications on proxy side. +/// \details This callback may be called under lock of the internal state-machine. This in general means, that the user +/// shall not do any long-running activities within this handler as it will prolonge this lock. +/// The user also shall not call any methods on the same event/field instance for which the handler was set, as they +/// might also require the same lock, which is not guaranteed to be recursive, thus leading to a deadlock. If the user +/// intends to do such activities from this callback, he shall dispatch it to a separate thread. +/// However, unsetting the handler within its call is a reasonable/supported use-case. Instead of calling +/// UnsetSubscriptionStateChangeHandler, the user shall return false from the handler. See return-value description. +/// \param new_state new subscription state. +/// \return true if the registered handler shall be kept, false if it shall be unset/unregistered. +using SubscriptionStateChangeHandler = score::cpp::callback; + +} // namespace score::mw::com::impl + +#endif // SCORE_MW_COM_IMPL_SUBSCRIPTION_STATE_CHANGE_HANDLER_H