StateChart 3.1.1

May 15, 2026

Bug fixes in 3.1.1

Thread-safety hardening of the configuration cache

Two races in Configuration (introduced indirectly by the cache + no-copy design in 3.1.0) have been fixed. Both surfaced under concurrent reads of machine.configuration while another thread is sending events to the same state machine instance, a scenario explicitly supported by the sync engine.

  1. Cache read race. Configuration.states checked self._cached is not None and then returned self._cached. Another thread invalidating between the check and the return could cause the property to return None, leading to a TypeError in callers that iterate the result (e.g., list(machine.configuration)). The getter now snapshots the cache fields locally before the freshness check. #620.

  2. In-place mutation race. Configuration.add() and Configuration.discard() mutated the OrderedSet stored on the model in place and rewrote the same reference. A concurrent reader iterating .configuration could observe a partially mutated set (raising RuntimeError: Set changed size during iteration) or read back a stale cached resolution missing the new state. Both methods now use copy-on-write, producing a fresh OrderedSet per call. This affects only StateChart (where atomic_configuration_update=False is the default to support parallel regions). The atomic update path used by StateMachine was never affected. #620.

Both fixes are covered by new stress tests in tests/test_threading.py::TestThreadSafety: test_concurrent_send_and_read_configuration and test_concurrent_parallel_region_send_and_read, plus a deterministic copy-on-write contract test test_add_discard_produce_fresh_orderedset.

Performance impact

Copy-on-write in add() / discard() reintroduces an O(n) shallow copy of the active configuration on every state entry and exit. For the typical configuration sizes used in practice (1–7 states), this is sub-microsecond.

Measured on macOS / Python 3.14, pytest-benchmark median, vs 3.1.0:

Benchmark

3.1.0

3.1.1

Δ

test_parallel_region_events

175.2 μs

184.5 μs

+5.3%

test_many_transitions_reset

125.9 μs

139.5 μs

+10.9%

test_guarded_transitions

70.0 μs

75.7 μs

+8.2%

test_history_pause_resume

88.4 μs

91.4 μs

+3.4%

test_many_transitions_full_cycle

156.9 μs

162.1 μs

+3.3%

test_flat_self_transition

38.7 μs

39.1 μs

+1.0%

Overall 4.7x–7.7x event throughput improvement vs 3.0.0 (declared in 3.1.0 release notes) is unchanged.