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:
+#### 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