3. Event State Machine#
3.1. Introduction and Purpose#
Within palaestrAI, many processes are based on events and transitions between states. The Event State Machine (ESM) is a utility class that helps to handle all event-based state transitions within palaestrAI. It transparently handles the most common use cases:
dealing with ZMQ messages
monitoring processes
reacting to signals
handling exceptions that rise (from user code).
The ESM works by decorating the target class (called the monitored class). It also injects some methods/properties into the target class. However, the target class’ code does not need to interact with the ESM; it can remain as-is, without being subjected to a certain programming paradigm.
3.2. Class Documentation#
- class palaestrai.core.EventStateMachine(monitored: Any)[source]#
An event-triggered state machine
The EventStateMachine (ESM) can be used to transparently handle events within palaestrAI. An ESM wraps another class and callbacks can be defined with method decorators for events. Events are:
A message received,
a signal received (SIGCHLD, SIGTERM, etc.)
setup
enter (the initial event)
teardown
The initial event enter is issued immediately after the main event/state loop commences in order to provide an entrypoint for operation. The enter event can be used to, e.g., send out the first request. For example:
@ESM.monitor() class Foo: @ESM.enter async def _enter(self): _ = await self._request_initialization() @ESM.requests async def _request_initialization(self): # ... return InitRequest( # ... )
It is not strictly necessary to provide an enter event. If the monitored class is exclusively an MDP worker, then there is no need for the enter event, because the worker reacts on the first request it receives and not on its own volition.
In order to make a class use the ESM, you must decorate it with ::~.monitor. The
monitor
decorator can also inject all necessary code to handle ZMQ MDP workers.If the monitored class does not have a
run
method, the ESM will also inject it. Therun
method then serves as an event/state loop that continues until it is stopped. At the start of therun
method, the target objectssetup
method is called if it exists. Likewise, ateardown
method will be called immediately after the loop ends.The ESM also adds a
stop
method to the target object. It serves to terminate the event/state loop.In order to react to a specific event, users of the ESM can decorate their methods with
on(event)
. The ::~.on decorator takes as parameter the class of what is handled. E.g., the class of a particular message, orsignal.SIGCHLD
to react to a process that has ended. For example:from palaestrai.core import EventStateMachine as ESM import signal @ESM.monitor() class Foo: @ESM.on(SomeRequest) async def handle_some_request(self, request): # ... pass @ESM.on(signal.SIGCHLD) async def handle_process_termination(self, process): # ... pass
Spawning processes is also handled through a decorator:
spawns
. If a method decorated withspawns
returns a ::Process object, this process will automatically be monitored. E.g.,:# ... @ESM.spawns def start_some_fancy_process(self): p = multiprocessing.Process(target=somefunc) p.start() return p
The ESM also handles the sending of requests. ESM-monitored classes do not need to instantiate and monitor MDP client objects themselves. Instead, they simply need methods to be decorated with
requests
. The so decorated method must return a message object that has thereceiver
property, so that ::~.requests can handle sending. E.g.,:# ... @ESM.requests def get_something_from_a_worker(self): req = SomeRequest() req.receiver = "Foo" return req @ESM.on(SomeResponse) # also handle the response! def handle_response_from_worker(self, response): # ... pass
The ESM also supports classes that act as workers. For this, the ESM’s
monitor
decorator needs the flagis_mdp_worker=True
. Then, the ESM injects the propertymdp_service
. Setting this property connects the MDP worker, andESM.on
can be used to handle requests from clients. For example:@ESM.monitor(is_mdp_worker=True) class Foo: async def setup(self): self.mdp_service = "Foo" @ESM.on(SomeRequest) def handle_request_from_client(self, req): do_something_with(request) rsp = SomeResponse() rsp.receiver = req.sender return rsp