Summary
The Listener is besides the WaitSet one of the building blocks to realize a push approach to detect and react to certain events. The Listener offers the user the ability to attach objects with a corresponding event and callback. Whenever the object receives the specified event the corresponding callback is called in the Listener background thread as a reaction.
The two key differences to the WaitSet are that the Listener is event-driven and not event- and state-driven as the WaitSet and that the Listener creates a separate background thread in which the event-callbacks are executed in contrast to the WaitSet where the user has to call the event-callbacks explicitly.
Terminology
- Condition Variable Used by an attached object to inform the Listener/WaitSet that an event has occurred.
- event is changing the state of an object.
- event driven a one time reaction which is caused directly by an event. Example: a new sample has been delivered to a subscriber.
- state predefined values to which the members of an object are set.
- state driven a repeating reaction which is continued as long as the state persists. Example: a subscriber has stored samples which were not inspected by the user.
Design
The Listener is a variation of the reactor pattern and the usage should be similar to the WaitSet with a key difference - it should be event driven and not a mixture of event and state driven, depending on which event is attached, like in the WaitSet.
Requirements
- Whenever an event occurs the corresponding callback should be called once as soon as possible.
- If an event occurs multiple times before the callback was called, the callback should be called once.
- If an event occurs while the callback is being executed the callback should be called again once.
- Thread-safety: attaching an event at any time from any thread.
- Thread-safety: detaching an event at any time from any thread.
- If the callback is currently running
detachEventblocks until the callback is finished. - After the
detachEventcall the event-callback is not called anymore, even when the event was signalled whiledetachEventwas running and the callback was not yet executed. - If the callback is detached from within the event-callback then
detachEventis non blocking. The event is detached right after thedetachEventcall. - One can attach at most one callback to a specific event of a specific object.
- Usually defined with an enum by the developer. One example is
SubscriberEvent::DATA_RECEIVED. - Attaching a callback to an event where a callback has been already attached results in an error.
- One can attach the same event to different objects at the same time.
- One can attach multiple different events to a single object
- When the Listener goes out of scope it detaches itself from every attached object via a callback provided by the attached object (like in the WaitSet).
- When the class which is attached to the Listener goes out of scope it detaches itself from the Listener via a callback provided by the Listener (like in the WaitSet).
Solution
Class diagram
+---------------------------+
| ConditionVariableData |
| - m_semaphore |
| - m_runtimeName |
| - m_toBeDestroyed |
| - m_activeNotifications |
+---------------------------+
| 1 | 1
| |
| 1 | n
+-----------------------------------------------+ +--------------------------------------------------+
| ConditionListener | | ConditionNotifier |
| ConditionListener(ConditionVariableData & ) | | ConditionNotifier(ConditionVariableData &, |
| | | uint64_t notificationIndex) |
| bool wasNotified() | | |
| void destroy() | | void notify() |
| NotificationVector_t wait() | | |
| NotificationVector_t timedWait() | | - m_condVarDataPtr : ConditionVariableData* |
| | | - m_notificationIndex |
| - m_condVarDataPtr : ConditionVariableData* | +--------------------------------------------------+
| - m_toBeDestroyed : std::atomic_bool | | 1
+-----------------------------------------------+ |
| 1 | n
| +--------------------------------+
| 1 | TriggerHandle |
+-------------------------------------------------+ | bool isValid() |
| Listener | | bool wasTriggered() |
| attachEvent(Triggerable, EventType, Callback) | | void trigger() |
| detachEvent(Triggerable, EventType) | | void reset() |
| | | void invalidate() |
| - m_events : Event_t[] | | void getUniqueId() |
| - m_thread : std::thread | | |
| - m_conditionListener : ConditionListener | | - m_conditionVariableDataPtr |
| | | - m_resetCallback |
| +----------------------------+ | | - m_uniqueTriggerId |
| | Event_t | | +--------------------------------+
| | void executeCallback() | | | 1
| | bool reset() | | |
| | bool init(...) | | | n
| | | | +-------------------------------------------------------+
| | - m_origin | | | Triggerable (e.g. Subscriber) |
| | - m_callback | | | |
| | - m_invalidationCallback | | | void invalidateTrigger(const uint64_t triggerId) |
| | - m_eventId | | | void enableEvent(TriggerHandle&&, const EventEnum ) |
| +----------------------------+ | | void enableEvent(TriggerHandle&&) |
+-------------------------------------------------+ | void disableEvent(const EventEnum) |
| void disableEvent() |
| |
| - m_triggerHandle : TriggerHandle |
+-------------------------------------------------------+
The Triggerable does not need to implement all enableEvent,
disableEvent variations, only the ones which are required by the use
case. The enableEvent and disableEvent, without the distincting EventEnum,
can be used when there is only a single event which can be triggered.
Class Interactions
- Creating Listener: a
ConditionVariableDatais created in the shared memory TheListeneruses theConditionListenerto wait for incoming events.
PoshRuntime
Listener |
| getMiddlewareConditionVariable : var |
| --------------------------------------> |
| ConditionListener(var) | ConditionListener
| ----------------------------------------+-----------------> |
| wait() : vector<uint64_t> | |
| ----------------------------------------+-----------------> |
- Attaching Triggerable Event (SubscriberEvent::DATA_RECEIVED) to Listener:
The Listener creates a TriggerHandle and provides it to the Triggerable (Subscriber)
via
enableEventso that the Triggerable owns the handle. Whenever the event occurs the Triggerable can use thetrigger()method of the TriggerHandle to notify the Listener.
User Listener Triggerable
| attachEvent() | |
| ------------------> | TriggerHandle |
| | create | |
| | ---------> | |
| | enableEvent(std::move(TriggerHandle)) |
| | -----------+--------------------------------------> |
- Signal an event from Triggerable:
TriggerHandle::trigger()is called, the Listener is returning from theConditionListener.wait()call and retrieves a list of all the signal notifications. The corresponding event-callbacks are called.
Triggerable TriggerHandle ConditionNotifier ConditionListener Listener Event_t
| trigger() | | | wait() : notificationIds | |
| -------------> | notify() | | <------------------------- | |
| | -------------> | .... unblocks .... | blocks | exeuteCallback() |
| | | | | ------------------------> |
| | | | | m_events[notificationId] |
- Triggerable goes out of scope: The TriggerHandle is a member of the
Triggerable, therefore the d'tor of the TriggerHandle is called which removes
the trigger from the Listener via the
resetCallback
Triggerable TriggerHandle Listener Event_t
| ~TriggerHandle | | |
| ----------------> | removeTrigger() | |
| | ----------------> | reset() |
| | via resetCallback | ------------> |
- Listener goes out of scope: The d'tor of the
Event_tinvalidates the Trigger inside the Triggerable via theinvalidationCallback
Listener Event_t Triggerable
| ~Event_t() | |
| -----------> | invalidateTrigger() |
| | -----------------------> |
| | via invalidationCallback |
TriggerHandle
- Problem: The Triggerable should be able to notify a Listener/WaitSet without having any knowledge about those class so that circular dependencies can be prevented. Furthermore, the Triggerable must be able to remove its attached events when it goes out of scope.
- Solution: The dependency inversion principle, create an abstraction which
is known by both, the TriggerHandle. Created by the Listener/WaitSet and
attached to the Triggerable so that it can notify the Listener/WaitSet via
the underlying
ConditionNotifierwithTriggerHandle::notify(). The cleanup task is performed by them_resetCallbackso that the Triggerable has no dependencies to any Notifyable.
Condition Variable
The ConditionListener and ConditionNotifier are two different interfaces to
the same class which state is stored in the ConditionVariableData class. The
intention of the separation is to provide one side (e.g. Triggerable) only
an API to notify the Notifyable (e.g. Listener) whereas the Notifyable can only
wait on events. So the contract is reflected in the design.
- Problem: Since the Listener reacts on events and not states it requires the knowledge by whom it was notified.
- Solution:
- Every TriggerHandle has a unique id which is used as index in
ConditionNotifier. - When
ConditionNotifier::notifyis called the Listener is informed via theNotificationVector_treturn value fromConditionListener::wait()which index notified him. Hence it is the same as the unique id of the TriggerHandle the Listener knows which Triggerable notified him.
Event_t
Concurrency
The Listener must be able to attach, detach events concurrently. Additionally, it supports that a callback can attach or detach further events or detach its corresponding event concurrently. Furthermore, the Listener supports that a callback is called concurrently while events are being attached/detached.
To realize this we created the Event_t abstraction which is stored in an
array called m_events. If we would like to attach or detach an event we either
initialize Event_t::init() or reset Event_t::reset() the corresponding entry
in the m_events array. The array has the advantage that the data structure
itself never changes during runtime therefore it does not have to be thread-safe.
The thread-safety must then be ensured by the Event_t class itself. Since
every concurrent action is contained in Event_t we can use
concurrent::smart_lock in combination with a std::recursive_mutex to
guarantee the thread-safe access.
- Concurrent attach/detach event and callback execution, ensured by
thread-safe
Event_t. - Detaching itself from within a callback is ensured via the
std::recursive_mutex. - Attaching/detaching arbitrary events from within a callback is ensured by
securing every
Event_tobject inm_eventswith aconcurrent::smart_lock. This would not be possible if the data structure itself had to be thread-safe.
Lifetime
Since the event contains everything which is required to handle events it has
the responsibility to ensure the life-time of the TriggerHandle. This is done
by the m_invalidationCallback which is called in Event_t::reset() to
invalidate the TriggerHandle in the corresponding Triggerable. This is either
done when an event is detached or the Listener goes out of scope.
Triggerable (e.g. Subscriber)
The Triggerable is a set of classes of which events can be attached to the Listener.
It is possible to either attach a specific event of a class to the Listener or attach the class without providing an event.
The basic idea is that the Listener creates a TriggerHandle whenever an event is attached and provides that TriggerHandle to the corresponding Triggerable. The Triggerable uses then the TriggerHandle to notify the Listener about events.
Triggerable With Single Event
Every Triggerable requires:
- The private methods:
void enableEvent(iox::popo::TriggerHandle&& triggerHandle) noexcept;
void disableEvent() noexcept;
void invalidateTrigger(const uint64_t uniqueTriggerId) noexcept;
Triggerable With Multiple Events
Every Triggerable requires:
- An
enum classwhich usesiox::popo::EventEnumIdentifieras underlying type.
enum class EventEnum : iox::popo::EventEnumIdentifier {
EVENT_IDENTIFIER,
ANOTHER_EVENT_IDENTIFIER,
};
- The private methods:
void enableEvent(iox::popo::TriggerHandle&& triggerHandle, const EventEnum event) noexcept;
void disableEvent(const EventEnum event) noexcept;
void invalidateTrigger(const uint64_t uniqueTriggerId) noexcept;
The methods above are used by the Listener to transfer the ownership of the TriggerHandle to the Triggerable. The Triggerable should have one TriggerHandle member for every attachable event/state. The TriggerHandle is then used to notify the Listener by the Triggerable that a certain event has occurred.
- It must be friend with
iox::popo::NotificationAttorney. It is possible to provide public access to the previous methods but then the user has the ability to call methods which should only used by the Listener.