This specification describes Actor4j's core concepts. A brief introduction to the actor model and reactive systems is given. Under Related Work, the used thread pool architecture specially designed for message passing is described. Requirements for actors, actor types, life cycle (& directives), monitoring, persistence, and execution are defined. Then the non-functional requirements follow. A brief overall architectural concept is given.
This text is published under a Creative Commons License (CC BY). The reference implementation is licensed under Apache License 2.0.
Revision | Date | Author(s) | Comments |
---|---|---|---|
Initial | Jan 27, 2020 | David A. Bauer | Initial draft |
v0.1 | Feb 15, 2020 | David A. Bauer | Theoretical Background, Related Works, Use Cases |
v0.2 | Feb 16, 2020 | David A. Bauer | Non-functional Requirements, Functional Requirements (Actor, Actor Types, Life Cycle, Life Cycle - Directives) |
v0.3 | Feb 17, 2020 | David A. Bauer | Functional Requirements ( Actor Types, Monitoring, Supervision, Persistence, Execution) |
v0.4 | Feb 18, 2020 | David A. Bauer | Conception, Architectural Overall Concept |
v0.5 | Oct 25, 2020 | David A. Bauer | Functional Requirements (Actor) |
v0.6 | Nov 26, 2020 | David A. Bauer | Conception, Architectural Overall Concept |
v0.7 | Oct 12, 2021 | David A. Bauer | Objectives |
This specification is mainly based on the paper by David A. Bauer et al. [1] in 2018 and from the general documentation of Actor4j [6]. Actor4j has been under development since the end of 2015.
D. A. Bauer and J. Mäkiö, "Hybrid Cloud – Architecture for Administration Shells with RAMI4.0 Using Actor4j," 2019 IEEE 17th International Conference on Industrial Informatics (INDIN), Helsinki, Finland 2019, pp. 79-86, [Online]. Available from: https://doi.org/10.1109/INDIN41052.2019.8972075
D. A. Bauer and J. Mäkiö, “Actor4j: A Software Framework for the Actor Model Focusing on the Optimization of Message Passing,” AICT 2018: The Fourteenth Advanced International Conference on Telecommunications, IARIA, Barcelona, Spain 2018, pp. 125-134, [Online]. Available from: http://www.thinkmind.org/download.php?articleid=aict_2018_8_10_10087
The actor model is a mathematical model for describing concurrent and distributed processes (the actors themselves) [2][4]. Carl Hewitt [3][4] is considered as the inventor of the actor model. Actors have three basic properties [2]. An actor can create additional actors [2]. Actors can exchange messages with each other, and these are immutable messages [2]. This guarantees that actors do not have to share common resources in order to exchange information but only do so via immutable messages [2]. This avoids possible synchronization problems that occur when accessing resources together when running concurrently [2]. Actors are also characterized by behavior, which can be passive, reactive, or proactive [2]. That means an actor can react to a message by changing its behavior, which can influence the processing of the next message that the actor receives [2]. An actor can also send a message to itself, which in turn can cause its behavior to change. The actor model is used, for example, in the programming language Erlang, which is used in WhatsApp, RabbitMQ, CouchDB, and Amazon SimpleDB [6]. The actor model is also suitable for simple modeling of complex communicative systems (high level of communication between actors of an associated actor group). It is ideal for simulating agent systems but also suitable for today's requirements, such as the Internet of Things (networking of devices) [6]. Erlang (programming language with comparable concepts to the actor model) also introduces the supervision concept, which contributes particularly to the resilience of the system [5]. In Erlang, a process corresponds to an actor. In the event of an error in a child's process, the supervisor process (parent process) is notified, which decides how to proceed [5]. The process can be restarted or ended, or even all direct child nodes of the supervisor process [5].
A reactive system (see Figure 1) should be responsive, i.e., a system should be able to respond adequately depending on the requirements. The response times must be within the defined range, in particular, to meet quality requirements and usability. The system must be resilient, i.e., fail-safe when an error occurs, and responsiveness must be further ensured. For example, replication, partitioning, and the concept of supervision are the means of choice to counteract a failure. It is elastic, i.e., even when the system load changes, the system remains responsive by adapting the system to the new requirements (e.g., by using replication, partitioning, or rebalancing). Reactive systems are message-oriented. They use asynchronous message communication and are characterized by non-blocking behavior. This means that the system is always ready to respond [7].
Fig. 1: The Reactive Manifesto [7]
Actor4j is a Java framework based on the actor model. Actor4j is based on Akka as a reference implementation. Akka is, in turn, influenced by Erlang, especially by the supervision concept. A new thread pool architecture was designed (see Figure 2), specially designed for the exchange of messages between the actors. In contrast to Akka, with Actor4j, not every actor has its own queue, but several task-specific queues are localized to the assigned thread. Incoming messages are injected via the corresponding thread at the actor. Each actor is permanently assigned to a thread. With this new thread pool architecture, Actor4j performs significantly better than Akka. By default, Akka uses a ForkJoinPool from the Java Concurrency library internally [1].
Fig. 2: Thread pool architecture of Actor4j [1]
In the standard thread pool architecture of Actor4j, four task-specific queues are provided for each thread, one for accepting messages from actors belonging to the same thread, one for accepting messages from actors of another thread, one for accepting messages from the server, and a special prioritized one queue to process internal directives. This procedure makes it possible to dispense with synchronization means, particularly when exchanging messages on the same thread, which significantly increases the performance. All queues are served equally, except the directive queue, so a queue's message processing is not blocked. Another mechanism built-in is two-level queues, one with synchronization means for external access (external thread access) and one for internal use on the same thread without synchronization means. This also contributes to better performance depending on the application. Necessary blocking operations must be outsourced to special ResourceActors
to ensure the system is ready to respond. ResourceActors
run in their own thread pool [1].
- Multi-agent systems (MAS)
- Simulations
- Games (MMOG, MMORPG)
- Business Process Modeling (BPM)
- Business logic (for Client-Server architectures)
- Caching
- In-memory databases
- Databases (see CouchDB, Amazon SimpleDB)
- Messaging (see WhatsApp, RabbitMQ)
- Reactive Streams (with back pressure)
- Reactive Systems (responsive, elastic, resilient, and message-driven)
- Internet of Things
- Digital representatives of devices (see Device Shadows, AWS IoT)
- Functions (here actors) as nano services
- Transaction processing
- Blockchain
- Batch-processing and stream-processing
(Adapted list from [6])
Objective 1: Implementing a Java framework based on the actor model.
Objective 2: Introduction of a novel thread pool architecture with significantly better performance, specially designed for exchanging messages between the actors. Message queues are located on the thread.
Objective 3: Ensuring that the actor semantics [8] and the principles of reactive manifesto [7] are taken into account when designing the overall architecture.
The following keywords are highlighted: MUST
, SHOULD
, and CAN
. MUST
means that the requirement must be fully met. SHOULD
means that the requirement can be deviated from in justified cases. CAN
means that it is an optional requirement.
Actor 1: Every actor MUST
be associated with an actor cell that contains the actor.
Actor 2: The system logic of the actor MUST
be implemented within the actor cell, which acts as a wrapper.
Actor 3: Every actor MUST
have a unique ID.
Actor 4: Every actor MUST
have a parent actor.
Actor 5: Every actor MUST
be addressable through a path.
Actor 6: Every actor MUST
have a method handler for receiving messages. The corresponding message is injected.
Actor 7: Every actor MUST
have the ability to send messages to other actors.
Actor 8: An alias MUST
be associable with an actor for easier addressing.
Actor 9: Every actor MUST
have the ability to create child actors.
Actor 10: A message MUST
consist of a payload, a tag for differentiating between messages, sender address, receiver address, interaction ID, interaction protocol, and an associated domain.
Actor Types 1: Actors within the actor system MUST
be derived from the class Actor
. Actors of this type MUST
not have blocking behavior.
Actor Types 2: Workload tasks MUST
not be performed within the actor system. Because they block the reactive system, and it is no longer responsive. Therefore the class ResourceActor
is provided. These special actors are executed in a separate thread pool, thus avoiding disturbances within the actor system. It SHOULD
be distinguished between stateless and stateful actors. The advantage of this distinction lies in the fact that stateless actors can be executed in parallel.
Actor Types 3: A PseudoActor
is a mediator between the outside world and the actor system. It allows communication with the actors within the actor system from the outside. Unlike the other actors, the PseudoActor
MUST
have its own message queue, in which the messages of other actors can then be stored by the actor system.
Actor Types 4: To persist the state of an actor, this MUST
be derived from the PersistentActor
class. A PersistentActor
is characterized by events and a state which can be saved, depending on the use case.
Actor Types 5: An EmbeddedActor
provides a lightweight alternative to the classical actor. They MUST
run in a classical host actor. A queue for delivering messages is not necessarily needed for the embedded actors, only if it addresses some embedded actor within the host actor. The receiving method for messages for that embedded actor is simply called synchronously within the host actor.
Life Cycle 1: Every actor MUST
be activatable or can be deactivated (ignoring incoming messages, except internal messages).
Life Cycle 1: After instantiation, the actor MUST
call the method preStart
. This method is used for the first initializations of the actor.
Life Cycle 2: If the actor is a derivative of the PersistenActor
, it MUST
execute the recovery protocol. The actor MUST
be deactivated until the actor gets the recovered data from the persistent system. After that, the method recover
MUST
be called. This method then recovers the state of the actor.
Life Cycle 3: An actor MUST
also be restartable, usually triggered by an exception. In this case, on the old instance, preRestart
MUST
be called first. Then a new instance MUST
be generated with the dependency injection container. The old instance MUST
be replaced by the new instance, and the method postRestart
MUST
be called by the new instance. The preRestart
and postRestart
methods are used so that the actor can react adequately to a restart situation. The marking (UUID) of the original actor MUST
be retained. This guarantees that references from other actors to this actor will stay valid.
Life Cycle 4: An actor MUST
be stoppable either by calling the stop
method or by receiving the STOP
or POISONPILL
message.
There MUST
be supported eight directives: RESUME
, STOP
, TERMINATED
, RESTART
, ESCALATE
, RECOVER
, ACTIVATE
and DEACTIVATE
. Stopping, restarting, and recovering the actors MUST
be asynchronous.
RESUME
: In this case, the supervisor remains passive. The actor can continue its activities undisturbed.STOP
:- To all children, the message
STOP
is sent (recursive process, if the children also have children) so they can terminate. Use ofwatch
to observe that all children have terminated. - Call of
postStop
.
- To all children, the message
TERMINATED
: The actor is stopped.RESTART
:PreRestart
is called at the current instance.- To all children, the message
STOP
is sent (recursive process, if the children also have children) so they can terminate. Use ofwatch
, to observe that all children have terminated. - Call of
postStop
at the current instance; after all, children have finished and confirmed this with theTERMINATED
message. - Instantiate a new instance with the dependency injection container. It is ensured that the
UUID
is maintained. - Call of
postRestart
(withpreStart
(with optionalrecover
) for the new instance.
ESCALATE
: If a supervisor is unclear about the correct strategy in the event of a specific error, he can pass it on to his superior supervisor for clarification.RECOVER
: The actor will be recovered to its last state. Novel events can lead to an update of the actor's state.ACTIVATE
andDEACTIAVTE
: Activates or deactivates the actor (messages will be or no longer be processed). The current explained directives remain deliverable, even when the actor is deactivated.
Fig. 3: Extended representation of the life cycle of an actor [6]
An actor MUST
also have the option to monitor another actor for that it has not yet terminated itself. If the observed actor is terminated, a message TERMINATED
is sent to the observer.
Supervision 1: The supervisor actor MUST
monitors its child actors. In the event of an error, they are resumed or restarted, or stopped by them.
Supervision 2: Two strategies MUST
be foreseen (see Fig. 4). In the OneForOne-Strategy
, only the affected actor MUST
be considered. In the OneForAll-Strategy
, on the other hand, not only the affected actor MUST
be considered but also the neighboring actors (below the supervisor actor).
Fig. 4: OneForOne-Strategy and OneForAll-Strategy [6]
Persistence 1: The principles of event sourcing MUST
be followed.
Persistence 2: Events and states snapshots of an actor MUST
be saveable.
Persistence 3: A recovery of the last state MUST
be possible by replaying the events since the last state snapshot in case of an error.
Execution 1: Each actor MUST
be permanently assigned to a thread.
Execution 2: Four task-specific queues SHOULD
be provided for each thread, one for accepting messages from actors belonging to the same thread, one for accepting messages from actors of another thread, one for accepting messages from the server, and a special prioritized one queue to process internal directives.
Execution 3: All queues SHOULD
be served equally (by defining a fixed throughput), except the directive queue, so that the message processing of a queue is not blocked.
Execution 4: The directive queue MUST
be served first to maintain the system's consistency.
Execution 5: Two-level queues architecture SHOULD
be established, one with synchronization means for external access (external thread access), and one for internal use on the same thread without synchronization means.
Execution 6: Incoming messages MUST
be injected via the corresponding thread at the actor.
NF 1: Ensures High Reponsiveness
for their clients (see the concept of thread pool architecture, actors are receiving messages in sequential order from the sender).
NF 2: Ensures Non-Blocking Behavior
through asynchronous communication style (also related to responsiveness, for computationally intensive tasks, there exists an extra thread pool, in Actor4j, these actors are called resource actors).
NF 3: Ensures High Resilience
(Robustness) through the supervision concept (see also Erlang, restarting of actors in the occurrence of failure)
NF 4: Ensures Simplicity
in modeling an actor in a concurrent environment (the key concept of the actor model, all actors are executed in a safe place, with no corruption in the state possible by using immutable objects for communication between actors).
The actor semantics [8] and the principles of reactive manifesto [7] MUST
be taken into account when designing the architecture. Akka is used as a co-reference implementation (for not reinventing the wheel).
Advantages | Disadvantages |
---|---|
Responsive (non-blocking behavior) | Susceptible for message flooding (not more responsive), slows down the system; to counteract this, the count of newly created messages between actors at any time in the system should be less or equal to the count of actors |
Asynchronous message passing (temporal decoupling) | There is no guarantee for a rendezvous between sender and receiver. |
Resilient (supervision concept) | Intense computational tasks (blocking behavior; to counteract this, use an extra thread pool) |
Thread-safe (actors are isolated, usage of immutable messages) | More latency through asynchronous message communication style (instead of calling pure methods synchronously); to counteract this, use embedded actors |
Easier concurrency (through simplicity) | Possibility of logical errors due to increasing complexity, the occurrence of undesired cycles, unreachable or dead states; to counteract this, use verification |
Using lock-free queues | |
Potential mobility can be realized through the underlying recovery functionality |
Tab. 1: Advantages & Disadvantages of the Architecture
The reference implementation for Actor4j can be found under the following link: https://github.com/relvaner/actor4j-core, as well as additional libraries for Actor4j (see: https://github.com/relvaner?utf8=%E2%9C%93&tab=repositories&q=actor4j&type=&language=).
The focus of this brief specification is to give an overall impression of how actor-oriented systems like Actor4j can be designed.
[1] D. A. Bauer and J. Mäkiö, “Actor4j: A Software Framework for the Actor Model Focusing on the Optimization of Message Passing,” AICT 2018: The Fourteenth Advanced International Conference on Telecommunications, IARIA, Barcelona, Spain 2018, pp. 125-134, [Online]. Available from: http://www.thinkmind.org/download.php?articleid=aict_2018_8_10_10087
[2] D. A. Bauer and J. Mäkiö, "Hybrid Cloud – Architecture for Administration Shells with RAMI4.0 Using Actor4j," 2019 IEEE 17th International Conference on Industrial Informatics (INDIN), Helsinki, Finland 2019, pp. 79-86, [Online]. Available from: https://doi.org/10.1109/INDIN41052.2019.8972075
[4] C. Hewitt, “Actor Model of Computation: Scalable Robust Information Systems,“ v38. arXiv, 2015.
[6] D. A. Bauer, "Actor4j," 2019, [Online]. Available from: https://github.com/relvaner/actor4j-core
[7] J. Bonér, D. Farley, R. Kuhn, M. Thompson, and Community, “The Reactive Manifesto,” 2014, [Online]. Available from: http://www.reactivemanifesto.org/
[8] R. K. Karmani, A. Shali, and G. Agha, “Actor frameworks for the JVM platform: a comparative analysis,” in PPPJ, ACM, 2009, [Online]. Available from: http://doi.acm.org/10.1145/1596655.1596658