I am currently working on refactoring a large application partially to rely on the actor framework. The application executes a scripting language to run tests. Therefore large part of the application relies on strictly sequential execution of the given commands. The scripting is currently implemented on a queued state machine with one central command queue. I plan to distribute different responsibilities, e.g. a larger number of hardware drivers and analysis modules into different actors and dispatch commands to the corresponding actors. The downside of the design is that without synchronisation I run into race condition issues because I loose the central Queue/Handler Loop as inherently sequential execution module.
This brings me to a more general question about actor application design. How do I enforce a step sequence in the same level of an actor tree, without hardwiring everything by coding to messages from a specific other actor? To make example: If I have three actors on the same level below the root which are connected to hardware and I need Actor A in some defined state before executing a command on B. And then wait with a command for C until B has also achieved some defined state. I can always hardwire this by sending a message from A to B and from B to C but this would require that I reflect all possible possible sequences somehow in the structure of the individual actors. This seems not very loosely coupled.
Otherwise I can build a central sequencer actor sending out commands to the actors and receiving "execution confirmed" MSGs from all actors. This would be my current design idea. But this seems a lot like the synchronous "reply message" method which is strongly not recommended in manuals and postings from this forum.
To put all this back in a Question:
What is the best architecture to achieve a strict sequence of actions from multiple actors which is configurable at runtime, e.g. by a script?
Thanks a lot
Your second option will work fine, and is probably the most correct way of doing it.
For example, suppose the sequencer actor sends a command to one of its sub-actors. Once the sub-actor sends a message back saying it's done (I would recommend a "zero-couple" message for this), the sequencer actor sends the next command to the appropiate sub-actor and so on. It may seem like it's synchronous, but because the sequencer is not blocked waiting for a response, it can handle other messages while the sub-actor does the work (such as a pause, stop, or some other message).
Second option is what I use as well: a main sequencer actor which sends requests and receives replies from a range of zero-coupled actors. The replies from actors are of two main types: (1) An acknowledgement that a command has been carried out (or an error that something could not be done), or (2) Some data, for example a measurement value from a test instrument actor.
For replies, I always override the Actor Core of the sequencer actor and use events to receive the reply (or replies) from the sub-actor(s). The replies are then verified and a decision is made on the next state to go to. The sequencer actor always uses messages to self to move to the next state.
My sequencer is a bit more elaborate in that, depending on the sequencer state, some actions can be carried out concurrently. An example from a current program I'm working on is a 'Configure Instruments' state, where (for example) I will send commands to an oscilloscope actor and AWG actor at the same time, and receive the replies using a single event case. This is more complex though as it means the sequencer must have a way of differentiating the replies it gets. I do this by embedding a message ID in each message, which the sub-actors extract and send back to the sequencer with each reply. (I'm not saying you would need to do this for your application though, as you may not need the complexity).
The design is still asynchronous (outside of the fact that it is implementing synchronous state-machine-like functionality) i.e. when it sends commands it waits for asynchronous replies. I also have full pause and abort functionality in my programs. But one thing you may still need is time-out functionality. I've been playing with a time-out actor which I can kick off when I send commands to sub-actors; this sends an event to the sequencer if a sub-actor doesn't reply within a time limit, and the sequencer can then abort (or carry out some other operation).
I had designed an aproach like the 2nd option too. Main difference is that the 'called' sub-actors (which carry out some specific tests) shall inherit from a 'Tests' class and as such should be plugins.
But their messages to receive and to reply are well-defined by the ancester class.
This is still more a concept that working code. And I have not much time for this project at the moment...