Skip to content

Commit d6c33e4

Browse files
committed
chore: Remove support for non-RTC model; Preparing processing_loop for new version
1 parent e25a88d commit d6c33e4

10 files changed

Lines changed: 233 additions & 221 deletions

File tree

docs/processing_model.md

Lines changed: 5 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,8 @@ In the literature, It's expected that all state-machine events should execute on
1010
1111
The main point is: What should happen if the state machine triggers nested events while processing a parent event?
1212

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.
13+
This library atheres to the {ref}`RTC model` to be compliant with the specs, where the {ref}`event` is put on a
14+
queue before processing.
2615

2716
Consider this state machine:
2817

@@ -60,13 +49,13 @@ Consider this state machine:
6049

6150
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.
6251

63-
If the machine is in `rtc` mode, the event is put on a queue.
52+
Internally, the events are put on a queue before processing.
6453

6554
```{note}
66-
While processing the queue items, if others events are generated, they will be processed sequentially.
55+
While processing the queue items, if others events are generated, they will be processed sequentially in FIFO order.
6756
```
6857

69-
Running the above state machine will give these results on the RTC model:
58+
Running the above state machine will give these results:
7059

7160
```py
7261
>>> sm = ServerConnection()
@@ -89,50 +78,3 @@ after 'connection_succeed' from 'connecting' to 'connected'
8978
Note that the events `connect` and `connection_succeed` are executed sequentially, and the `connect.after` runs on the expected order.
9079
```
9180

92-
## Non-RTC model
93-
94-
```{deprecated} 2.3.2
95-
`StateMachine.rtc` option is deprecated. We'll keep only the **run-to-completion** (RTC) model.
96-
```
97-
98-
In contrast, in a non-RTC (synchronous) processing model, the state machine starts executing nested events
99-
while processing a parent event. This means that when an event is triggered, the state machine
100-
chains the processing when another event was triggered as a result of the first event.
101-
102-
```{warning}
103-
This can lead to complex and unpredictable behavior in the system if your state-machine definition triggers **nested
104-
events**.
105-
```
106-
107-
If your state machine does not trigger nested events while processing a parent event,
108-
and you plan to use the API in an _imperative programming style_, you can consider using the synchronous mode (non-RTC).
109-
110-
In this model, you can think of events as analogous to simple method calls.
111-
112-
```{note}
113-
While processing the {ref}`event`, if others events are generated, they will also be processed immediately, so a **nested** behavior happens.
114-
```
115-
116-
Running the above state machine will give these results on the non-RTC (synchronous) model:
117-
118-
```py
119-
>>> sm = ServerConnection(rtc=False)
120-
enter 'disconnected' from '' given '__initial__'
121-
122-
>>> sm.send("connect")
123-
exit 'disconnected' to 'connecting' given 'connect'
124-
on 'connect' from 'disconnected' to 'connecting'
125-
enter 'connecting' from 'disconnected' given 'connect'
126-
exit 'connecting' to 'connected' given 'connection_succeed'
127-
on 'connection_succeed' from 'connecting' to 'connected'
128-
enter 'connected' from 'connecting' given 'connection_succeed'
129-
after 'connection_succeed' from 'connecting' to 'connected'
130-
after 'connect' from 'disconnected' to 'connecting'
131-
['on_transition', 'on_connect']
132-
133-
```
134-
135-
```{note}
136-
Note that the events `connect` and `connection_succeed` are nested, and the `connect.after`
137-
unexpectedly only runs after `connection_succeed.after`.
138-
```

statemachine/engines/async_.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,15 @@
44

55
from ..event_data import EventData
66
from ..event_data import TriggerData
7-
from ..exceptions import InvalidDefinition
87
from ..exceptions import TransitionNotAllowed
98
from ..i18n import _
109
from .base import BaseEngine
1110

1211
if TYPE_CHECKING:
13-
from ..statemachine import StateMachine
1412
from ..transition import Transition
1513

1614

1715
class AsyncEngine(BaseEngine):
18-
def __init__(self, sm: "StateMachine", rtc: bool = True):
19-
if not rtc:
20-
raise InvalidDefinition(_("Only RTC is supported on async engine"))
21-
super().__init__(sm=sm, rtc=rtc)
22-
2316
async def activate_initial_state(self):
2417
"""
2518
Activate the initial state.
@@ -35,16 +28,7 @@ async def activate_initial_state(self):
3528
async def processing_loop(self):
3629
"""Process event triggers.
3730
38-
The simplest implementation is the non-RTC (synchronous),
39-
where the trigger will be run immediately and the result collected as the return.
40-
41-
.. note::
42-
43-
While processing the trigger, if others events are generated, they
44-
will also be processed immediately, so a "nested" behavior happens.
45-
46-
If the machine is on ``rtc`` model (queued), the event is put on a queue, and only the
47-
first event will have the result collected.
31+
The event is put on a queue, and only the first event will have the result collected.
4832
4933
.. note::
5034
While processing the queue items, if others events are generated, they

statemachine/engines/base.py

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
from itertools import chain
12
from queue import PriorityQueue
23
from queue import Queue
34
from threading import Lock
45
from typing import TYPE_CHECKING
56
from weakref import proxy
67

8+
from statemachine.orderedset import OrderedSet
9+
710
from ..event import BoundEvent
11+
from ..event_data import EventData
812
from ..event_data import TriggerData
913
from ..exceptions import TransitionNotAllowed
1014
from ..state import State
@@ -14,43 +18,64 @@
1418
from ..statemachine import StateMachine
1519

1620

21+
class EventQueue:
22+
def __init__(self):
23+
self.queue: Queue = PriorityQueue()
24+
25+
def empty(self):
26+
return self.queue.qsize() == 0
27+
28+
def put(self, trigger_data: TriggerData):
29+
"""Put the trigger on the queue without blocking the caller."""
30+
self.queue.put(trigger_data)
31+
32+
def pop(self):
33+
"""Pop a trigger from the queue without blocking the caller."""
34+
return self.queue.get(block=False)
35+
36+
def clear(self):
37+
with self.queue.mutex:
38+
self.queue.queue.clear()
39+
40+
def remove(self, send_id: str):
41+
# We use the internal `queue` to make thins faster as the mutex
42+
# is protecting the block below
43+
with self.queue.mutex:
44+
self.queue.queue = [
45+
trigger_data
46+
for trigger_data in self.queue.queue
47+
if trigger_data.send_id != send_id
48+
]
49+
50+
1751
class BaseEngine:
18-
def __init__(self, sm: "StateMachine", rtc: bool = True):
52+
def __init__(self, sm: "StateMachine"):
1953
self.sm: StateMachine = proxy(sm)
20-
self._external_queue: Queue = PriorityQueue()
54+
self.external_queue = EventQueue()
55+
self.internal_queue = EventQueue()
2156
self._sentinel = object()
22-
self._rtc = rtc
2357
self._running = True
2458
self._processing = Lock()
2559

2660
def empty(self):
27-
return self._external_queue.qsize() == 0
61+
return self.external_queue.empty()
2862

2963
def put(self, trigger_data: TriggerData):
3064
"""Put the trigger on the queue without blocking the caller."""
3165
if not self._running and not self.sm.allow_event_without_transition:
3266
raise TransitionNotAllowed(trigger_data.event, self.sm.current_state)
3367

34-
self._external_queue.put(trigger_data)
68+
self.external_queue.put(trigger_data)
3569

3670
def pop(self):
37-
return self._external_queue.get(block=False)
71+
return self.external_queue.pop()
3872

3973
def clear(self):
40-
with self._external_queue.mutex:
41-
self._external_queue.queue.clear()
74+
self.external_queue.clear()
4275

4376
def cancel_event(self, send_id: str):
4477
"""Cancel the event with the given send_id."""
45-
46-
# We use the internal `queue` to make thins faster as the mutex
47-
# is protecting the block below
48-
with self._external_queue.mutex:
49-
self._external_queue.queue = [
50-
trigger_data
51-
for trigger_data in self._external_queue.queue
52-
if trigger_data.send_id != send_id
53-
]
78+
self.external_queue.remove(send_id)
5479

5580
def start(self):
5681
if self.sm.current_state_value is not None:

statemachine/engines/sync.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from time import time
33
from typing import TYPE_CHECKING
44

5+
from statemachine.orderedset import OrderedSet
6+
57
from ..event_data import EventData
68
from ..event_data import TriggerData
79
from ..exceptions import TransitionNotAllowed
@@ -31,27 +33,13 @@ def activate_initial_state(self):
3133
def processing_loop(self):
3234
"""Process event triggers.
3335
34-
The simplest implementation is the non-RTC (synchronous),
35-
where the trigger will be run immediately and the result collected as the return.
36-
37-
.. note::
38-
39-
While processing the trigger, if others events are generated, they
40-
will also be processed immediately, so a "nested" behavior happens.
41-
42-
If the machine is on ``rtc`` model (queued), the event is put on a queue, and only the
43-
first event will have the result collected.
36+
The event is put on a queue, and only the first event will have the result collected.
4437
4538
.. note::
4639
While processing the queue items, if others events are generated, they
4740
will be processed sequentially (and not nested).
4841
4942
"""
50-
if not self._rtc:
51-
# The machine is in "synchronous" mode
52-
trigger_data = self.pop()
53-
return self._trigger(trigger_data)
54-
5543
# We make sure that only the first event enters the processing critical section,
5644
# next events will only be put on the queue and processed by the same loop.
5745
if not self._processing.acquire(blocking=False):
@@ -127,7 +115,7 @@ def _activate(self, trigger_data: TriggerData, transition: "Transition"): # noq
127115

128116
result += self.sm._callbacks.call(transition.on.key, *args, **kwargs)
129117

130-
self.sm.current_state = target
118+
self.sm.configuration = OrderedSet([target])
131119
event_data.state = target
132120
kwargs["state"] = target
133121

statemachine/io/scxml/actions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,6 @@ def __init__(
417417
model: Any = None,
418418
state_field: str = "state",
419419
start_value: Any = None,
420-
rtc: bool = True,
421420
allow_event_without_transition: bool = True,
422421
listeners: "List[object] | None" = None,
423422
):
@@ -431,7 +430,6 @@ def __init__(
431430
model,
432431
state_field=state_field,
433432
start_value=start_value,
434-
rtc=rtc,
435433
allow_event_without_transition=allow_event_without_transition,
436434
listeners=listeners,
437435
)

statemachine/orderedset.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import itertools
2+
from typing import Iterable
3+
from typing import Iterator
4+
from typing import MutableSet
5+
from typing import TypeVar
6+
7+
T = TypeVar("T")
8+
9+
10+
class OrderedSet(MutableSet[T]):
11+
"""A set that preserves insertion order by internally using a dict.
12+
13+
>>> OrderedSet([1, 2, "foo"])
14+
OrderedSet([1, 2, 'foo'])
15+
16+
17+
>>> OrderedSet([1, 2, 3, 3, 2, 1, 'a', 'b', 'a', 'c'])
18+
OrderedSet([1, 2, 3, 'a', 'b', 'c'])
19+
20+
>>> s = OrderedSet([1, 2, 3])
21+
>>> s.add(4)
22+
>>> s
23+
OrderedSet([1, 2, 3, 4])
24+
25+
>>> s = OrderedSet([1, 2, 3])
26+
>>> "foo" in s
27+
False
28+
29+
>>> 1 in s
30+
True
31+
32+
>>> list(s)
33+
[1, 2, 3]
34+
35+
>>> s == OrderedSet([1, 2, 3])
36+
True
37+
38+
>>> s > OrderedSet([1, 2]) # set is a superset of other
39+
True
40+
41+
>>> s & {2}
42+
OrderedSet([2])
43+
44+
>>> s | {4}
45+
OrderedSet([1, 2, 3, 4])
46+
47+
>>> s - {2}
48+
OrderedSet([1, 3])
49+
50+
>>> s - {1}
51+
OrderedSet([2, 3])
52+
53+
>>> {1} - s
54+
OrderedSet([])
55+
56+
>>> s ^ {2}
57+
OrderedSet([1, 3])
58+
59+
>>> s[1]
60+
2
61+
62+
>>> s[2]
63+
3
64+
65+
>>> eval(repr(OrderedSet(['a', 'b', 'c'])))
66+
OrderedSet(['a', 'b', 'c'])
67+
68+
69+
70+
"""
71+
72+
__slots__ = ("_d",)
73+
74+
def __init__(self, iterable: Iterable[T] | None = None):
75+
self._d = dict.fromkeys(iterable) if iterable else {}
76+
77+
def add(self, x: T) -> None:
78+
self._d[x] = None
79+
80+
def clear(self) -> None:
81+
self._d.clear()
82+
83+
def discard(self, x: T) -> None:
84+
self._d.pop(x, None)
85+
86+
def __getitem__(self, index) -> T:
87+
try:
88+
return next(itertools.islice(self._d, index, index + 1))
89+
except StopIteration as e:
90+
raise IndexError(f"index {index} out of range") from e
91+
92+
def __contains__(self, x: object) -> bool:
93+
return self._d.__contains__(x)
94+
95+
def __len__(self) -> int:
96+
return self._d.__len__()
97+
98+
def __iter__(self) -> Iterator[T]:
99+
return self._d.__iter__()
100+
101+
def __str__(self):
102+
return f"{{{', '.join(str(i) for i in self)}}}"
103+
104+
def __repr__(self):
105+
return f"OrderedSet({list(self._d.keys())})"

0 commit comments

Comments
 (0)