Introduction.md 6.5 KB

Intoduction

Modeling your applications with actors, you can get two benefits: messaging and supervising between actors.

Actor is a reactive entity with a state, which can receive and send messages; actor state is usually changed with new messages. Every message has destination (one or more actors) and user-defined payload. Messaging is send with fire-and-forget approach, i.e. sender does not care about further delivery and processing of the message, like with UDP protocol.

Like in UDP a message can be "lost", because the destination queue if full. Unlike UDP, this can be checked, howerver.

The message delivery is asynchronous, as the message is put into the queue, and will be delivered some time later.

With all that properties it is possible to assemble concurrent programs.

Supervising is more about proper actor initialization order, synchronization and failure handling. Synchronization makes it sure that starting sending messages to actors is performed when actors are ready. Failure handling allows to use different restart strategies of a failing actor, or group of actors, or even escalate locally unresolvable problem into upper layers... upto the whole board restart.

rotor-light is platform-neutral framework, i.e. it can be build and run on any plaform, which conforms the requirements (C++17, basically).

rotor and rotor-light

There is a successful C++ actor microframework rotor, when a few colleagues of mine asked me to have something similar for embedded systems.

The following table highlights the differences

rotor light rotor
max. number of actors 64 unlimited, runtime
max. number of messages compile-time defined unlimited, runtime
thread-safety no yes
message priorities yes no
request-response pattern no yes
actors discovery & linking no yes
multiple I/O backends no (1) yes
timers yes (1) yes
non-intrusiveness (2) yes yes
dynamic allocations no yes
C++ standard C++17 C++17
dependencies no (except, stdlib++) boost, event-loops
multiple addresses per actor no yes
multiple recipients yes, via broadcasting yes
uses RTTI no yes

(1) rotor-light needs only "now" function from timer, the time point and time interval are abstract notions, have to be defined by end-user of the library.

(2) Non-intrusiveness means, that a framework does not owns execution thread: when all messages have been processed, it just exits as there is nothing to do. That way it is possible to integrate a framework with other event-loops or enter into power-save mode for MCU or even let it go into sleep and wakeup on external event (interrupt).

There are a lot of useful patterns and explanations in rotor documentation, it is worth familiarize self with it to get some insights.

building

rotor-light uses cmake build system. It has no dependencies, except C++17 core libraries (e.g. <tuple>,<type_traits> etc.). Basically, the framework can be build via

mkdir build
cd build
cmake ..
make -j4

There are few customization options:

  • ROTOR_LIGHT_FW_QUEUE - the queue/priority to be used for internal rotor-light messaging, 0 by default. This might be useful, to have a dedicated queue for rotor-light messages, to avoid messages loss due to queue overfill, or have lower or higher priorities for rotor-light messages.

  • ROTOR_LIGHT_QUEUE_SZ - allows to measure maximum queue filling with messages via the max_items() method. As this is mostly useful during development the option is ON for Debug build types and OFF otherwise by default. Enable it manually if needed.

  • ROTOR_LIGHT_ACTOR - defines the type to be used as actor id, the default is uint64_t, which means the limitation of 64 actors in total. If you know a priory, that there are less than 32/16/8 actors in total, you can save bits here.

  • ROTOR_LIGHT_TIMEPOINT defines the type to be used as timepoint. The default value is int64_t.

  • ROTOR_LIGHT_DURATION defines the type to be used as time interval. The default value is std::int_fast32_t.

  • ROTOR_LIGHT_MESSAGE - defines the type to be used as message type id, the default is std::uint_fast16_t; you can tune that if you have more than 2^16 different message types.

  • ROTOR_LIGHT_EVENT defines the type to be used as event id. The default value is std::uint16_t; tune if there is a chance of having more than 2^16 active events at one time.

  • ROTOR_LIGHT_INDEX defines the type to be used as queue index. The default value is std::uint_fast8_t.

  • ROTOR_LIGHT_DOC - generate doxygen docs, false by default.

Usage example:

cmake -DROTOR_LIGHT_TIMEPOINT=int32_t ..

error handling

If any internal rotor-light message is not delivered, this is a critical error. In the case the following function is invoked in the rotor_ligth namespace:

namespace rotor_light {

__attribute__((weak)) void on_queue_full() {
  for (;;) {
    __asm__("nop");
  }
}

If there is need to turn on red LED or something like that, then you should define your own function void on_queue_full() (without weak specifier) in the namespace.

Custom message delivery faulure does not causes on_queue_full() invokation, it should be done manually if needed.

A few advices to avoid the critical error:

  • inrease queue size

  • use different queue/prirority for rotor_light messages, if it is OK to loose your custom messeages, as it is fatal to loose rotor_ligth messages. See ROTOR_LIGHT_FW_QUEUE build option.