Skip to content

v3.1.1

Choose a tag to compare

@fgmacedo fgmacedo released this 16 May 21:30
· 25 commits to main since this release

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.