|
| 1 | +# Processing model |
| 2 | + |
| 3 | +In the literature, It's expected that all state-machine events should execute on a |
| 4 | +[run-to-completion](https://en.wikipedia.org/wiki/UML_state_machine#Run-to-completion_execution_model) |
| 5 | +(RTC) model. |
| 6 | + |
| 7 | +> All state machine formalisms, including UML state machines, universally assume that a state machine |
| 8 | +> completes processing of each event before it can start processing the next event. This model of |
| 9 | +> execution is called run to completion, or RTC. |
| 10 | +
|
| 11 | +The main point is: What should happen if the state machine triggers nested events while processing a parent event? |
| 12 | + |
| 13 | +```{hint} |
| 14 | +The importance of this decision depends on your state machine definition. Also the difference between RTC |
| 15 | +and non-RTC processing models is more pronounced in a multi-threaded system than in a single-threaded system. |
| 16 | +In other words, even if you run in {ref}`Non-RTC model`, only one external {ref}`event` will be |
| 17 | +handled at a time and all internal events will run before the next external event is called, |
| 18 | +so you only notice the difference if your state machine definition has nested event triggers while |
| 19 | +processing these external events. |
| 20 | +``` |
| 21 | + |
| 22 | +There are two distinct models for processing events in the library. The default is to run in |
| 23 | +{ref}`RTC model` to be compliant with the specs, where the {ref}`event` is put on a |
| 24 | +queue before processing. You can also configure your state machine to run in |
| 25 | +{ref}`Non-RTC model`, where the {ref}`event` will be run immediately. |
| 26 | + |
| 27 | +Consider this state machine: |
| 28 | + |
| 29 | +```py |
| 30 | +>>> from statemachine import StateMachine, State |
| 31 | + |
| 32 | +>>> class ServerConnection(StateMachine): |
| 33 | +... disconnected = State(initial=True) |
| 34 | +... connecting = State() |
| 35 | +... connected = State() |
| 36 | +... |
| 37 | +... connect = disconnected.to(connecting, after="connection_succeed") |
| 38 | +... connection_succeed = connecting.to(connected) |
| 39 | +... |
| 40 | +... def on_connect(self): |
| 41 | +... return "on_connect" |
| 42 | +... |
| 43 | +... def on_enter_state(self, event: str, state: State, source: State): |
| 44 | +... print(f"enter '{state.id}' from '{source.id if source else ''}' given '{event}'") |
| 45 | +... |
| 46 | +... def on_exit_state(self, event: str, state: State, target: State): |
| 47 | +... print(f"exit '{state.id}' to '{target.id}' given '{event}'") |
| 48 | +... |
| 49 | +... def on_transition(self, event: str, source: State, target: State): |
| 50 | +... print(f"on '{event}' from '{source.id}' to '{target.id}'") |
| 51 | +... return "on_transition" |
| 52 | +... |
| 53 | +... def after_transition(self, event: str, source: State, target: State): |
| 54 | +... print(f"after '{event}' from '{source.id}' to '{target.id}'") |
| 55 | +... return "after_transition" |
| 56 | + |
| 57 | +``` |
| 58 | + |
| 59 | +## RTC model |
| 60 | + |
| 61 | +In a run-to-completion (RTC) processing model (**default**), the state machine executes each event to completion before processing the next event. This means that the state machine completes all the actions associated with an event before moving on to the next event. This guarantees that the system is always in a consistent state. |
| 62 | + |
| 63 | +If the machine is in `rtc` mode, the event is put on a queue. |
| 64 | + |
| 65 | +```{note} |
| 66 | +While processing the queue items, if others events are generated, they will be processed sequentially. |
| 67 | +``` |
| 68 | + |
| 69 | +Running the above state machine will give these results on the RTC model: |
| 70 | + |
| 71 | +```py |
| 72 | +>>> sm = ServerConnection() |
| 73 | +enter 'disconnected' from '' given '__initial__' |
| 74 | + |
| 75 | +>>> sm.send("connect") |
| 76 | +exit 'disconnected' to 'connecting' given 'connect' |
| 77 | +on 'connect' from 'disconnected' to 'connecting' |
| 78 | +enter 'connecting' from 'disconnected' given 'connect' |
| 79 | +after 'connect' from 'disconnected' to 'connecting' |
| 80 | +exit 'connecting' to 'connected' given 'connection_succeed' |
| 81 | +on 'connection_succeed' from 'connecting' to 'connected' |
| 82 | +enter 'connected' from 'connecting' given 'connection_succeed' |
| 83 | +after 'connection_succeed' from 'connecting' to 'connected' |
| 84 | +['on_transition', 'on_connect'] |
| 85 | + |
| 86 | +``` |
| 87 | + |
| 88 | +```{note} |
| 89 | +Note that the events `connect` and `connection_succeed` are executed sequentially, and the `connect.after` runs on the expected order. |
| 90 | +``` |
| 91 | + |
| 92 | +## Non-RTC model |
| 93 | + |
| 94 | +In contrast, in a non-RTC (synchronous) processing model, the state machine starts executing nested events |
| 95 | +while processing a parent event. This means that when an event is triggered, the state machine |
| 96 | +chains the processing when another event was triggered as a result of the first event. |
| 97 | + |
| 98 | +```{warning} |
| 99 | +This can lead to complex and unpredictable behavior in the system if your state-machine definition triggers **nested |
| 100 | +events**. |
| 101 | +``` |
| 102 | + |
| 103 | +If your state machine does not trigger nested events while processing a parent event, |
| 104 | +and you plan to use the API in an _imperative programming style_, you can consider using the synchronous mode (non-RTC). |
| 105 | + |
| 106 | +In this model, you can think of events as analogous to simple method calls. |
| 107 | + |
| 108 | +```{note} |
| 109 | +While processing the {ref}`event`, if others events are generated, they will also be processed immediately, so a **nested** behavior happens. |
| 110 | +``` |
| 111 | + |
| 112 | +Running the above state machine will give these results on the non-RTC (synchronous) model: |
| 113 | + |
| 114 | +```py |
| 115 | +>>> sm = ServerConnection(rtc=False) |
| 116 | +enter 'disconnected' from '' given '__initial__' |
| 117 | + |
| 118 | +>>> sm.send("connect") |
| 119 | +exit 'disconnected' to 'connecting' given 'connect' |
| 120 | +on 'connect' from 'disconnected' to 'connecting' |
| 121 | +enter 'connecting' from 'disconnected' given 'connect' |
| 122 | +exit 'connecting' to 'connected' given 'connection_succeed' |
| 123 | +on 'connection_succeed' from 'connecting' to 'connected' |
| 124 | +enter 'connected' from 'connecting' given 'connection_succeed' |
| 125 | +after 'connection_succeed' from 'connecting' to 'connected' |
| 126 | +after 'connect' from 'disconnected' to 'connecting' |
| 127 | +['on_transition', 'on_connect'] |
| 128 | + |
| 129 | +``` |
| 130 | + |
| 131 | +```{note} |
| 132 | +Note that the events `connect` and `connection_succeed` are nested, and the `connect.after` |
| 133 | +unexpectedly only runs after `connection_succeed.after`. |
| 134 | +``` |
0 commit comments