Pull to refresh

Comments 11

Thanks for the article. It's interesting to know that actor frameworks for C++ are continuously evolving.


However, there are two points I miss in your article:


  1. I think there should be at least two pictures/schemes that can make the article more clear. The first picture could show the relationship(s) between actors. The second picture could show stages of the lifetime of an actor as a diagram. Such pictures can be a good addition to textual explanations and can make reading your description easier.
  2. It's a pity that you show the usage of out-of-box plugins but say nothing about the plugin subsystem itself. What is a plugin? Which tasks can be solved by using plugins? Can a user write its own plugin?

And I want to ask you: is it a good idea to write some clarification about the mentioned SObjectizer-related parts in a comment for the article?

The Russian version of the article is coming, so, I'll add to it missing pictures, thanks for the advice.


Let me answer the 2nd question here. The abstract answer to the question "What is a plugin", is: actor is a behavioral aspect of an actor, i.e. how actor reacts on particular message (or group of related messages), may be exposing some convenient API for sending messages.


It's better to give a few examples of build-in plugins: The address_maker plugin is a shortcut for asking actor's supervisor for creating a new address; the init_shutdown plugin does proper actor housekeeping on init and shutdown requests (messages); the child_manager is a supervisor-specific plugin, which allows to spawn child-actors, as well as reacts on their init- and shutdown- responses.


Can a user write its own plugin? Yes, but from my experience there is no need of that, as the resources_plugin covers actor with external event-loop interactions, and all other plugins are quite rotor-internals specific. There is a nuance, when you'd like to inspect messages flow, and the build-in messages dumper is not OK for your (e.g. you'd like to enable messages traffic only for a specific supervisor, or add your own filtering logic or decorations for your custom messages) — in that case, you'd need to write a plugin (and own supervisor, and insert it there). Again, this is depths of the rotor, and currently, I'd tend to view plugin system is not a public API, hence, it is not documented.


Yes, please, clarify sobjectizer related parts… I'll update the current article as well as the planed Russian one. I think it would be valuable for users of the both — of sobjectizer and rotor.

Again, this is depths of the rotor, and currently, I'd tend to view plugin system is not a public API, hence, it is not documented.

In that case, "pluginization" of the rotor is just an implementation detail that could not be seen as a valuable addition for a user. You, as a maintainer of the framework, can have significant benefits from "pluginization", but end user will see your standard plugins just as subparts of the tool.

While it is not documented how to allow user write own plugins, it is supposed that a user will use the shipped plugins, e.g. as in the example below:


resources->acquire(resource::db_connection);

You are right in the sense, that it is part of actor API, and not of it's underlying implementation (plugin API).

My notice is more related to the promotion of the new version of the rotor. I think that materials used for the promotion of the new version should emphasize the things valuable for the end-users. If your plugins can be seen mostly as a part of public API then there is no difference for an end-user between the actual implementation of that API: implemented it as a plugin or as a hardcoded-and-not-changeable part.

Yes, please, clarify sobjectizer related parts…

Agents in SObjectizer also have I- and S-phases.


I-phase is implemented by two virtual methods of agent_t class. The first is so_define_agent method. This method is called during the preparation of resources needed for an agent. Usually so_define_agent is used for the creation of subscriptions for the agent and for switching the agent to the initial state. It's important to note that during the call to so_define_agent the agent is not a part of the SObjectizer Environment yet. And if some error occurs during this stage then the agent will automatically be removed and all associated resources will automatically be freed.


The second method is so_evt_start. It is the first method called for an agent after the successful registration of an agent in the SObjectizer Environment. This method is called on the context of the dispatcher the agent is bound to. And this method is usually used for performing initial actions like sending messages of registration of new agents.


When so_evt_start is called the agent is already a part of the SObjectizer Environment. It means that a failure inside so_evt_start (e.g. an exception is thrown out from that method) is treated like any other agent's failure.


The S-phase is implemented via the so_evt_finish method. This is the last method called for an agent on the context of the agent's dispatcher. It's rarely used, but if so it is usually used for sending some messages or performing some cleanup action that can only be performed on the context of agent's dispatcher.


So the agents in SObjectizer have their lifetimes in the form "not_registered" -> "in_registration_phase" -> "registered" -> "deregistering" -> "destroyed". Where so_define_agent is called during the switch from "not_registered" to "in_registration_phase", so_evt_start is called during the switch from "in_registration_phase" to "registered", so_evt_finish is called during the switch "registered" -> "deregistering".


The shutdowner from so5extra (as well as stop_guards) is intended for a different task: preventing of the fast shutdown of the SObjectizer Environment after the invocation of environment_t::stop method if some agents need to perform long-lasting shutdown-related actions.

Agents in SObjectizer also have I- and S-phases.

I think there is some misunderstanding… The so_evt_start in sobjectizer (and on_start in rotor) are not designed to be part of the initialization, instead, the method purpose it to let agent (actor) play trigger some activity, e.g. request something or do some I/O. Otherwise, without the methods, as actors are passive/reactive by their nature, there will be nobody to initiate activity.


So, the so_define_agent is the only proper location for initialization and resources acquisition. However, since so_define_agent and so_evt_start they are passive (from the agent perspective, as they are called outside) — they are just a lifetime callbacks, not phases.


Let me give an abstract example, what I mean:


struct actor_t {
    void on_init() { ... }
    void on_start() { ... }
}

Here is the on_init method. After it the actor is either initialized or not, is has no chance to do "long" (aka asynchonous) initialization. This is because it is simple callback, not a phase/process. Any resources, if needed, can be acquired synchously only. Contrary, the following actor:


struct actor_async_t {
    void on_start() { ... }
    void on_init(init_token_t t) {
        ...; // subscribe to messages or trigger I/O on event loop
        token = t;
    }

    void on_message_or_event_loop_event(...) {
        token.init_complete();
    }

    void on_other_message_or_fail_event_loop_event(...) {
        token.init_fail(error_code);
    }`

    init_token_t token;
}

After on_init the asynс actor is still initializing, and might postpones the initialization decision, until it gets the whole picture (i.e. until it receives enough messages/events to make init-decision).


Here is a more concrete example: I'm writing torrent-like client. There is a protocol: the client should announce self in the remote server, before searching other peers. The announce message contains public endpoint (i.e. reacheable IP:port pair, which must be previously opened on the router via UPNP-protocol).


Using the init-phase it could be done as following. The communiactor_actor (which announces self and searches for other peers), during initialization phase discovers and links to upnp_actor (alternative: it can spawn & link to upnp_actor), and then makes an request for the public endpoint to the upnp_actor. The upnp_actor actually makes an HTTP-request, receives HTTP-response, parses it and replies back to the communiactor_actor. If everything is OK, communiactor_actor announces self, and only then completes its initialization (and then can be asked for searching peers). The entire process is async and non-blocking.


In the reality, there might be a few other layers of indirection, like adding resolver_actor and http_actor, which also participate in the I-phase of upnp_actor. So, I-phase is scalable in that sense; as well as you can see it is a process, not a single callback invokation.


I really don't know how to achieve that with the simple actor lifetime callbacks. The only way I see it to do in so_define_agent() is drop actors layering/communication (because it is not part of the Environment), and do HTTP-request, synchronously (including resolving and connecting), and synchronously wait for HTTP-response.


That's why in the article it is told, that shutdowner in sobjectizer mimics the S-phase in rotor, because it is "long lasting" and not a simple callback.

It seems the root of misunderstanding is in point of view to lifetime. There are at least two different viewpoints:


  • viewpoint of actor framework;
  • viewpoint of end-user who writes actual actors.

From actor framework viewpoint actor is initialized when all related resources are allocated and bound to the actor and actor is able to receive and handle messages sent to it. I am convinced that in that viewpoint initialization phase can't be asynchronous and can't last long.


From the user viewpoint the initialization sometimes can require a lot of interaction with other actors and can take a long time. But the actor framework itself sees the actor as fully initialized and working. So such initialization is a part of business logic, not a part of lifetime from actor framework point of view.


In SObjectizer if an actor requires a long-lasting initialization phase it is implemented via agent's states. This is also true for long-lasting deinitialization phase.


Shutdowner and stop-guards are necessary in SObjectizer just because a call to environment_t::stop can finish all message-processing almost immediately without any chances to send or receive new messages.


It also seems that your choice is to integrate a concept of initialization and deinitialization phases (from business logic's point of view) into the framework.

Thanks a lot for explanation. I have published the update to the article, fixing sobjectizer related corrections, based on your comments. The Russian version of the article will be published with fixed information.


I completely agree with your statements.


The underlying reason for rotor that it's initialization and shutdown phases where asynchronous since the beginning, because subscription to addresses was made as messaging too, which is non-atomic; especially if address belongs to to different threads/event loops. I consider this as on of the rotor features, however if you don't need it it brings unneeded complexity, and then better to use sobjectizer :)

I want to clarify one thing relate to the rotor. Let's assume we have service actor that receives some do_something messages. That actor processes those messages and sends replies to the address specified inside do_something message. Something like:


struct do_something {
   ... // Some parameters.
   rotor::address_ptr_t reply_to_; // Address for the result.
};

class service : public rotor::actor_base_t {
   ...
   void on_do_something(rotor::message_t<do_something> cmd) {
      ... // Actual processing.
      send<some_result>(cmd->payload.reply_to_);
   }
};

I suspect that the usage of that pattern isn't safe in rotor because at the time of calling send in service the destination of the some_result can go away. And the safe way of using such a pattern is establishing a link between service and destination actors before sending some_result.


PS. There could be more complex scenarios like actor A sends do_something to service actor with the address of actor B in do_something::reply_to.

Yes, you correctly catch the issue and the solution about virtual linking. However, usage pattern is slightly wrong, as there is no need of direct shipping of reply_to in the messages (it is indirectly included if request/response pattern is used.) For regular messages it would look like;


namespace payload {
struct do_something_t { ... };
}

namespace message {
using do_something_t = rotor::message_t<payload::do_something_t>;
}

struct client_actor_t {
   void configure(...) {
        // discover and link to server;  
   } 

  void on_start() {
     // safe to do as it is already linked to server
     send<payload:: do_something_t>(server, ...);
  }

  rotor::address_ptr_t server;
};

struct server_actor_t {
  void configure(...) {
      // register self in a registry and subscribe
  }

  void on_do_something(message::do_something_t) {
      ...;
  }
};

In the case of request/response it will be changed a litte bit:


namespace payload {
struct request_t { ... };
struct response_t { ... };
}

namespace message {
using request_t = rotor::request_traits_t<payload::request_t>::request::message_t;
using response_t = rotor::request_traits_t<payload::response_t>::response::message_t;
}

struct client_actor_t {
   void configure(...) {
        // discover and link to server;  subscribe
   } 

  void on_start() {
     // safe to do as it is already linked to server
     request<payload::request_t>(server, ...).send(timeout);
  }

  void on_response(message::response_t) {
    ...
  }

  rotor::address_ptr_t server;
};

struct server_actor_t {
  void configure(...) {
      // register self in a registry and subscribe
  }

  void on_request(message::request_t& req) {
    ...;
    // usually it is safe to reply
    reply(req, ...);
  }
;}
Sign up to leave a comment.