v.5.8.0 - Stiffstream/sobjectizer GitHub Wiki

This page describes changes and new features of v.5.8.0.

Table of Contents

Deprecated stuff removed

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.

The noexcept attribute is now more widely used than in previous versions

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() is noexcept now

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.

The so_5::abstract_message_box_t interface has been changed

There are several breaking changes in so_5::abstract_message_box_t interface:

  • a new format of subscribe_event_handler method. Now it accepts only type_index and a reference to subscriber's message_sink;
  • old method unsubscribe_event_handlers renamed to unsubscribe_event_handler. It's now noexcept. It now receives a reference to subscriber's message_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 and delegate_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.

New type message_delivery_mode_t and delivery_mode parameter for do_deliver_message method

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.

Mboxes do not deal with message limits since v.5.8.0

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.

A new message_sink abstraction has been introduced

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.

New helper classes so_5::single_sink_binding_t and and so_5::multi_sink_binding_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);
        ...
      });
  }
};

unique_subscribers mbox is now a part of SObjectizer Core

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);

New method so_5::environment_t::introduce_named_mbox() has been added

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()); } )
      }
  {}
  ...
};

New method so_5::agent_t::so_low_level_exec_as_event_handler has been added

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.

Several steps towards noexcept-ness of coop deregistration procedure

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.

Refactored chain of coops for final deregistration

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

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 couple of new dispatchers: nef_one_thread and nef_thread_pool

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).

Format of so_5::environment_params_t::default_disp_params() getter changed

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 )

Refactoring of SObjectizer's internals

Format of several swap and operator= have been fixed

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;

Modification of so_5::outliving_reference_t

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&.

⚠️ **GitHub.com Fallback** ⚠️