v.5.8.0 - Stiffstream/sobjectizer GitHub Wiki
This page describes changes and new features of v.5.8.0.
- Deprecated stuff removed
- The noexcept attribute is now more widely used than in previous versions
- The so_5::abstract_message_box_t interface has been changed
- A new message_sink abstraction has been introduced
- unique_subscribers mbox is now a part of SObjectizer Core
- New method so_5::environment_t::introduce_named_mbox() has been added
- New method so_5::agent_t::so_low_level_exec_as_event_handler has been added
- Several steps towards noexcept-ness of coop deregistration procedure
- Format of so_5::environment_params_t::default_disp_params() getter changed
- Refactoring of SObjectizer's internals
Functions/methods/constants marked as [[deprecated]]
in previous versions are now removed.
NOTE. It's a breaking change: if such a stuff was used in the code then switching to v.5.8.0 will break the compilation.
Several functions/methods that weren't noexcept
in previous versions are noexcept
since v.5.8.0. For example: so_5::environment_t::stop()
, so_5::environment_infrastructure_t::stop()
, so_5::environment_infrastructure_t::final_dereg_coop()
.
The so_5::agent_t::so_exception_reaction()
method is noexcept
starting from v.5.8.0. The main reason for this: SObjectizer can't handle an exception thrown from an event handler if the user's so_exception_reaction
throws.
NOTE. It's a breaking change: If there are agent types with overridden so_exception_reaction
then the compilation will fail and it's required to fix the prototype of so_exception_reaction
method.
There are several breaking changes in so_5::abstract_message_box_t
interface:
- a new format of
subscribe_event_handler
method. Now it accepts onlytype_index
and a reference to subscriber'smessage_sink
; - old method
unsubscribe_event_handlers
renamed tounsubscribe_event_handler
. It's nownoexcept
. It now receives a reference to subscriber'smessage_sink
instead a reference to subscribed agent; - a new format of
do_deliver_message
method; - there are no more helper
do_deliver_message_from_timer
anddelegate_deliver_message_from_timer
protected methods; - there are no more comparison operators for
abstract_message_box_t
.
Those changes may affect existing code if the code uses mbox's methods directly or if there are implementations of custom mbox types.
A flaw in the message delivery scheme was discovered: if a message was sent from timer to an overloaded mbox and the redirect/transform overload reaction redirects this message to a full size-limited mchain, then the timer thread is blocked. This caused the normal work of the timer thread to be broken. Fortunately, this case hasn't been found in the wild.
This flaw was fixed by the introduction of an additional parameter delivery_mode
to the so_5::abstract_message_sink_t::do_deliver_message
method. This parameter has a nonblocking
value if the message is being sent by the timer. In that case the do_deliver_message
can't block the caller even if the final destination is the full size-limited mchain. The message has to be discarded in that case or the custom implementation of mbox/mchain can do something else (like terminating the application). But the caller shouldn't be blocked.
If the delivery_mode
parameter is ordinary
then the caller of do_deliver_message
can be blocked.
NOTE. It's a breaking change: if the code contains custom implementations of mboxes/mchains then the new delivery_mode
parameter should be taken into account and must be handled properly.
Implementations of mboxes in previous versions of SObjectizer had to deal with message limits: if a limit was set for a subscription then that limit had to be stored with the subscription description and had to be handled properly within the do_deliver_message implementation.
Since v.5.8.0 the handling of message limits has been moved to message_sinks. This has made writing custom mboxes much easier.
NOTE. It's a breaking change: if the code contains custom implementations of mboxes then the handling of message limits has to be removed.
Only agents can be used as subscribers to messages in previous versions of SObjectizer. A new abstraction message_sink has been added in v.5.8.0. It allows the delivery of messages not only to agents, but also to arbitrary types of message receivers in an application. For example, a message sent to mbox A can now be simply redirected to mbox B (it's a like a subscription of mbox B to messages from mbox A).
The new abstraction message_sink is represented as the abstract class so_5::abstract_message_sink_t
.
Two new helper classes so_5::single_sink_binding_t
and so_5::multi_sink_binding_t
simplify redirection of messages sent into one mbox to one or several other mboxes. For example:
class coordinator final : public so_5::agent_t
{
// A MPMC mbox created elsewhere.
const so_5::mbox_t broadcasting_mbox_;
so_5::single_sink_binding_t bindings_;
...
void on_some_event(mhood_t<msg_some_command> cmd) {
// Create a child coop and bind an agent to broadcasting mbox.
so_5::introduce_child_coop(*this, [](so_5::coop_t & coop) {
auto * worker = coop.make_agent<worker>(...);
auto worker_msink = so_5::wrap_to_msink(worker->so_direct_mbox());
// Ask for redirection of msg_some_data messages from
// the broadcasting mbox to the direct mbox of the worker.
bindings_.bind<msg_some_data>(broadcasting_mbox_, worker_msink);
...
});
}
};
The unique_subscribers mboxes were introduced in the v.1.5.0 of a companion project so5extra. Since v.5.8.0 they are part of SObjectizer Core. So they can be used directly without a need to add so5extra to your project:
#include <so_5/all.hpp>
...
so_5::environment_t & env = ...;
auto mbox = so_5::make_unique_subscribers_mbox(env);
The ordinary so_5::environment_t::create_mbox()
creates the standard MPMC mbox with a name. Sometimes a user may want an instance of different mbox type to be used as a named mbox. For example, an instance of unique_subscribers mbox or an instance of custom user type. Version v.5.8.0 provides a way to do that via new introduce_named_mbox()
method in so_5::environment_t
interface:
class first_participant final : public so_5::agent_t {
const so_5::mbox_t broadcast_mbox_;
...
public:
first_participant(context_t ctx)
: so_5::agent_t{std::move(ctx)}
, broadcast_mbox_{so_environment().introduce_named_mbox(
so_5::mbox_namespace_name_t{"demo"},
"message-board",
[this]() { return so_5::make_unique_subscribers_mbox(so_environment()); } )
}
{}
...
};
class second_participant final : public so_5::agent_t {
const so_5::mbox_t broadcast_mbox_;
...
public:
second_participant(context_t ctx)
: so_5::agent_t{std::move(ctx)}
, broadcast_mbox_{so_environment().introduce_named_mbox(
so_5::mbox_namespace_name_t{"demo"},
"message-board",
[this]() { return so_5::make_unique_subscribers_mbox(so_environment()); } )
}
{}
...
};
There is a new method in so_5::agent_t
that can be used when an agent is bound to a special dispatcher like asio_thread_pool or asio_one_thread from a companion so5extra project:
class agent_that_uses_asio : public so_5::agent_t
{
state_t st_not_ready{this};
state_t st_ready{this};
asio::io_context & io_ctx_;
public:
...
void so_evt_start() override
{
auto resolver = std::make_shared<asio::ip::tcp::resolver>(io_ctx_);
resolver->async_resolve("some.host.name", "",
asio::ip::tcp::numeric_service | asio::ip::tcp::address_configured,
// IO completion handler to be run on agent's worker thread.
[resolver, this](auto ec, auto results) {
// It's necessary to wrap the block of code, otherwise
// modification of the agent's state (or managing of subscriptions)
// will be prohibited because SObjectizer doesn't see
// the IO completion handler as event handler.
so_low_level_exec_as_event_handler( [&]() {
...
st_ready.activate();
});
});
}
}
NOTE. SObjectizer can't check that this method is used properly, so all responsibility falls on the user who calls this method.
Sometimes coops are being deregistered in noexcept contexts like destructors or catch blocks. Unfortunately the deregistration procedure may throw due to several factors (some of them aren't under the control of SObjectizer). But several steps have been made in v.5.8.0 towards the total noexcept-ness of this procedure.
When a coop is being deregistered it must be placed in a special chain (queue) of coops that are ready to the final step of the deregistration. In previous versions of SObjectizer, this chain was implemented as an ordinary mchain, and adding another coop to this chain required sending a special message. This required a memory allocation and could lead to an std::bad_alloc exception.
The implementation of chain of coops for the final deregistration has been rewritten in v.5.8.0 and now this chain is implemented as an intrusive list of so_5::coop_t
objects. Because such objects are already allocated the addition of a coop to this chain doesn't require memory allocation any more.
The so_5::event_queue_t
interface has been changed and now it contains three pure virtual methods instead of just one in previous versions. Two new methods push_evt_start
and push_evt_finish
have been added to so_5::event_queue_t
.
Please note that two methods, push
and push_evt_start
aren't noexcept
. They can throw and the SObjectizer expects that they may throw.
But the third method, push_evt_finish
, is noexcept
. If this method throws (std::bad_alloc
as a good example) then the whole application will be terminated.
NOTE. It's a breaking change: if the code contains custom implementations of a dispatcher then the implementation of event_queue for such a dispatcher has to be modified.
A key moment in noexcept-ness of the coop deregistration procedure is non-throwing pushing of a special evt_finish demand to queues for all agents of the deregistered coop. Unfortunately, the standard SObjectizer's dispatchers from previous versions (and Asio-based dispatchers from so5extra companion projects) can't guarantee that pushing of evt_finish demand won't throw. At least std::bad_alloc
can be raised because all those dispatchers use demand queues based on dynamic memory allocation.
Two new dispatchers that provide noexcept-guarantee for evt_finish demand have been added in v.5.8.0 (nef_ means no-except for evt_finish):
- nef_one_thread. It's like the classical one_thread dispatcher: all agents work on the same worker threads;
- nef_thread_pool. It's like the classical thread_pool dispatcher: a thread pool is used and an agent can handle events on different threads from the pool.
Those dispatchers use preallocated memory blocks for evt_finish demands and linked lists from demands queues. It avoids std::bad_alloc during pushing of eve_finish, but at the price of slightly less efficiency. So if you don't care about noexcept-ness of deregister_coop
and can tolerate std::terminate
if std::bad_alloc
is thrown during deregister_coop
then you can still use the classical dispatchers. But if you have to recover from std::bad_alloc
and need a safe non-throwing deregister_coop
(in a complex GUI application, for example), then you can use new nef_-dispatchers.
An instance of nef_one_thread dispatcher can be set as the default dispatcher for the whole SObjectizer Environment (when the default multi-threaded environment infrastructure is used).
The so_5::environment_params_t::default_disp_params()
getter method now returns a new type: so_5::environment_params_t::default_disp_params_t
. It's a sum type (in the form of std::variant
) that contains so_5::disp::one_thread::disp_params_t
or so_5::disp::nef_one_thread::disp_params_t
.
Note also that since v.5.8.0 there are two default_disp_params
setters in so_5::environment_params_t
:
so_5::environment_params_t &
default_disp_params( so_5::disp::one_thread::disp_params_t params )
so_5::environment_params_t &
default_disp_params( so_5::disp::nef_one_thread::disp_params_t params )
Free (sometimes friend) functions swap
defined for some SObjectizer's classes now have the classical format:
void swap(T &, T &) noexcept;
Similarly, some old implementation of operator=
(copy- and move-operator) have been fixed and now have the classical format:
T & operator=(const T&);
T & operator=(T &&) noexcept;
Implicit type conversion operator for so_5::outliving_reference_t
has been removed. The reference to the underlying object should be obtained explicitly via the get()
method.
NOTE. It's a breaking change: if the code uses some SObjectizer's internal classes directly then this change can break the compilation.
NOTE. There could be more outliving_reference_t
-related changes that breaks compilation after switching to v.5.8.0 if SObjectizer's internal classes/functions were used, because now outliving_reference_t<T>
is used in several places instead of T&
.