implementation of user wishes and correction of shortcomings

implementation of user wishes and correction of shortcomings

A few days ago, we committed to version 5.8.1 of the SObjectizer open source project. In this article, we will talk about new features that appeared in SObjectizer thanks to the wishes of users, and we will mention the correction of a previously undetected flaw. Who is interested, I kindly ask under the cat.

For those who have never heard of SObjectizer, very briefly: it is a relatively small C++17 framework that allows the use of approaches such as Actor Model, Publish-Subscribe and Communicating Sequential Processes (CSP) in C++ programs. The main idea behind SObjectizer is to build a program from small agent entities that interact with each other through message exchange. SObjectizer is responsible for:

  • delivery of messages to recipient agents within one process;

  • management of working threads on which agents process messages addressed to them;

  • timer mechanism (in the form of delayed and periodic messages);

  • the possibility of setting the operating parameters of the mechanisms listed above.

You can get an impression of this tool by reading this review article.

New type of message sink: transform_then_redirect

The main innovation of the 5.8.0 release was message sinks: if previously only an agent could be a subscriber for a message, now it is possible to implement the interface abstract_message_sink_t and subscribe to anyone’s message. More details about this functionality were discussed in the previous article. There it was also said that over time, applications of message sinks that are not obvious to us may appear.

One such application was not long in coming: issue #67: bind_and_transform. The user wanted to be able to convert one message into another right at the moment of sending it to the recipient. Let’s say we have a message:

struct compound_data
{
   first_data_part m_first;
   second_data_part m_second;
};

It is sent to mbox_A.

And there is agent F who wants to receive not the whole message compound_data completely, but only compound_data::m_first. That is, agent F wants to receive a message of type first_data_part:

class F : public agent_t
{
   ...
   void so_define_agent() override {
      so_subscribe_self().event([](const first_data_part & msg) {...});
   }
};

And here the question arises: how to make it so that when sending a message compound_data message formation occurred in mbox_A first_data_part and sending it directly to agent F?

Connecting mbox_A to agent F’s own mbox is not difficult, there are helper classes for this single_sink_binding_t and multi_sink_binding_t:

const so_5::mbox_t mbox_A = ... // Получение mbox-а для compound_data.
const so_5::mbox_t mbox_F = ... // Получение mbox-а агента F.

so_5::multi_sink_binding_t<> binding;
binding.bind<compound_data>(mbox_A, so_5::wrap_to_msink(mbox_F));

But such communication delivers an output message to agent F compound_datawhile only need to be delivered compound_data::m_first.

That is, It is necessary to transform the original message into a new message of a different type.

And here we can mention that there is already a similar transformation in one place in SObjectizer. It is part of the mechanism of protection of agents against load:

class message_limits_demo final : public so_5::agent_t
{
public:
   message_limits_demo(context_t ctx)
      : so_5::agent_t{ ctx +
            // Говорим SObjectizer-у, что если количество
            // ждущих в очереди сообщений типа compound_data
            // будет больше 3-х, то новые сообщения нужно
            // преобразовать и переслать на другой mbox.
            limit_then_transform(3u, [this](const compound_data & msg) {
               return so_5::make_transformed<first_data_part>(
                  // Куда отсылать.
                  new_destination_mbox(),
                  // А это параметры для конструирования нового
                  // экземпляра сообщения first_data_part.
                  msg.m_first);
            })
            + ... }
       {}
...
};

That is, we already have a custom type in SObjectizer so_5::transformed_message_t<Msg> and auxiliary function so_5::make_transformed<Msg, ...Args> designed to convert messages with their subsequent forwarding. So why not take advantage of it?

As a result, an auxiliary function appeared so_5::bind_transformerwhich allows you to associate a lambda transformer with a message from a specific mbox. Thanks to bind_transformer our task is solved as follows:

const so_5::mbox_t mbox_A = ... // Получение mbox-а для compound_data.
const so_5::mbox_t mbox_F = ... // Получение mbox-а агента F.

so_5::multi_sink_binding_t<> binding;
so_5::bind_transformer(binding, mbox_A,
   [mbox_F](const compound_data & msg) {
      return so_5::make_transformed<first_data_part>(mbox_F, msg.m_first);
   });

The implementation of this feature left an interesting impression: according to subjective feelings (time accounting, of course, was not carried out), design and implementation took only about 1/10 of all costs. That is, testing and documenting the resulting implementation took about an order of magnitude more time. And even during the development of tests, it was possible to get VC++ into an internal compiler error, which has not been seen in our code for a long time. ten times more expensive than “just do”.

Methods agent_t::so_this_agent_disp_binder() and agent_t::so_this_coop_disp_binder()

To class so_5::agent_t added two methods that give access to disp_binders related to the agent and its cooperation. This may be necessary when creating subsidiary cooperatives.

Suppose we create a parent cooperation with the thread_pool manager:

// Этот агент будет затем создавать дочерние кооперации.
class parent_agent final : public so_5::agent_t {
   ...
};

// Регистрируем родительскую кооперацию.
env.introduce_coop(
   // Этот диспетчер будет использоваться для родительской кооперации
   // по умолчанию (т.е. если агент не привязан к какому-то другому
   // диспетчеру явно, то будет использоваться этот диспетчер).
   so_5::disp::thread_pool::make_dispatcher(env, 8u).binder(),
   [&](so_5::coop_t & coop) {
      // Этот агент будет использовать собственный диспетчер.
      coop.make_agent_with_binder<parent_agent>(
         so_5::disp::one_thread::make_dispatcher(env).binder(),
         ...);

      // Этот агент будет привязан к thread_pool-диспетчеру
      // кооперации, поскольку собственного disp_binder-а ему
      // не дали.
      coop.make_agent<worker>(...);

      ... // Создание остальных агентов родительской кооперации.
   });

We need the new child co-ops to be bound to the same thread_pool manager that was used for the parent co-op. Now it is enough to use a new method for this agent_t::so_this_coop_disp_binder():

void parent_agent::make_new_child_coop() {
   so_5::introduce_child_coop(*this,
      // Новая кооперация будет использовать тот же диспетчер,
      // что и родительская кооперация.
      so_this_coop_disp_binder(),
      [&](so_5::coop_t & coop) {
         ... // Наполнение дочерней кооперации.
      });
}

Method so_this_agent_disp_binder() returns the disp_binder used by the agent. And this disp_binder may differ from the cooperative’s disp_binder. So if in our example we write:

void parent_agent::make_new_child_coop() {
   so_5::introduce_child_coop(*this,
      // Новая кооперация будет использовать тот же диспетчер,
      // что и этот конкретный агент.
      so_this_agent_disp_binder(),
      [&](so_5::coop_t & coop) {
         ... // Наполнение дочерней кооперации.
      });
}

then the child cooperation will not be tied to the thread_pool manager of the parent cooperation, but to the one_thread manager of the agent parent_agent. Which, probably, will give a fundamentally different effect.

It must be said that we were first asked for this feature quite a long time ago, back in 2020. So we had to wait three years, as predicted by popular wisdom 🥺

As an excuse, it remains to say that at that time the agent did not know at all which disp_binder is used to bind the agent to the dispatcher. This information was only in cooperation, and for hypothetical implementation so_this_agent_disp_binder() it would be necessary to slightly complicate the implementation of the relations of the agent and its cooperation.

However, recently, in version 5.7.5, the agent took disp_binder into its own ownership to address issues with premature object deletion. So now the implementation so_this_agent_disp_binder() it became apparent that was used during the work on version 5.8.1.

It can be said that we did not ignore or forget the old request, but fulfilled it when the opportunity presented itself.

Fixed something that was not paid attention to for a long time

In the process of working on so_5::bind_transformer an unfortunate flaw was discovered that no one paid attention to for many years: the method limit_then_transform could not be used with mutable messages. That is, write like this:

class message_limits_demo final : public so_5::agent_t
{
public:
   message_limits_demo(context_t ctx)
      : so_5::agent_t{ ctx +
            // Говорим SObjectizer-у, что если количество
            // ждущих в очереди сообщений типа compound_data
            // будет больше 3-х, то новые сообщения нужно
            // преобразовать и переслать на другой mbox.
            limit_then_transform<so_5::mutable_msg<compound_data>>(3u,
               [this](compound_data & msg) {
                   return so_5::make_transformed<first_data_part>(
                      // Куда отсылать.
                      new_destination_mbox(),
                      // А это параметры для конструирования нового
                      // экземпляра сообщения first_data_part.
                      std::move(msg.m_first));
               })
            + ... }
       {}
...
};

before version 5.8.1 you simply can’t: a compile-time error would occur.

How did it happen that this problem has been around for many years, and no one has discovered it yet?

At first we overlooked it, then nobody used it limit_then_transform for mutable messages Somehow.

But now it has been found and fixed. Better late…

Conclusion

This release perfectly illustrates a point that has been made many times before in articles about SObjectizer: if you tell us what you think is missing from SObjectizer, then there is a chance that it will appear someday. Even after three years 😉 But if you don’t tell us what you would like to see, then it most likely won’t happen.

So please voice your wishes in issues or in discussions on GitHub. Or in the Google group.

By the way, if you are missing something in SObjectizer, you can take a look at the so5extra companion project, we have collected various useful things there that we did not want to put in the core of SObjectizer.


It is with great pleasure that I recall the series of articles about SObjectizer that Marco Arena (some may know him from the Italian C++ Community) began to publish. You can find this series on Marko’s blog or on the dev.to website. I myself was very interested in reading the articles written by him, as if you are looking at things that have been familiar to you for a long time from a completely different angle. So I recommend it. Three parts have been published so far, but this is just the beginning.


And finally, on the right of self-promotion: I invent bicycles for myself, I can invent them for you too.

Related posts