Skip to content

fix queued events dispatched in stale state when anonymous transitions are pending (#542)#671

Merged
kris-jusiak merged 2 commits into
boost-ext:masterfrom
PavelGuzenfeld:fix/issue-542-queued-events-dispatched-in-stale-state
May 24, 2026
Merged

fix queued events dispatched in stale state when anonymous transitions are pending (#542)#671
kris-jusiak merged 2 commits into
boost-ext:masterfrom
PavelGuzenfeld:fix/issue-542-queued-events-dispatched-in-stale-state

Conversation

@PavelGuzenfeld
Copy link
Copy Markdown
Contributor

Problem

When an action used back::process<E> to enqueue a new event, process_queued_events drained the entire process queue in a single while-loop pass. The outer event-processing loop never got to run anonymous (guard-only) transitions between consecutive queued-event dispatches.

Example: given a machine with

*state<sa> + event<e>[!g] / (++n; enqueue e{count-1})
 state<sa> + on_entry<_>  / (n = 0)          // reset on entry
 state<sa> [g]            = state<sb>         // anonymous: leave sa when n>0

the first event fires in sa and enqueues a second event. With the old code, the second event is dispatched immediately, still in sa, before the anonymous transition to sb runs. The action increments n again, guard !g is now false, and every subsequent queued event is silently dropped.

This is the root cause reported in #542. The same bug affects any state machine where the active state changes via an anonymous transition after an action enqueues further events.

Fix

Change the while-loop in process_queued_events to an if-block so that each call dispatches exactly one queued event and returns true if there was something to dispatch. The outer do { ... } while (process_queued_events(...)) loop already calls process_queued_events repeatedly (see the triple-nested do-while structure in process_event), so all queued events are still eventually drained—but now anonymous transitions and defer-queue drains run between each pair of consecutive queued events.

⚠️ Behaviour change

Previously all queued events were dispatched atomically before any anonymous transitions fired. After this fix, each queued event is followed by a full anonymous-transition + defer-drain cycle. State machines that implicitly relied on the old batch-drain semantics will observe different transition ordering. This is the minimal change required to make the semantics correct, but consumers should be aware.

Test

Regression test added in test/ft/actions_process.cpp (process_queue_anonymous_transitions_between_queued_events): a four-state cycling machine where each state's on-entry resets a counter used by an anonymous guard. Processing e{5} from the initial state enqueues five more events recursively; with the bug only two fire, with the fix all six fire.

Existing tests (including actions_process_n_defer) pass without modification.

Fixes #542.

@PavelGuzenfeld PavelGuzenfeld force-pushed the fix/issue-542-queued-events-dispatched-in-stale-state branch from 1ca8e55 to bd6dbab Compare May 23, 2026 21:48
…s pending (boost-ext#542)

process_queued_events previously drained the entire process queue in a
single while-loop pass.  When actions queued new events via back::process,
those events were dispatched immediately inside the same pass without giving
the outer event-processing loop a chance to run anonymous (guard-only)
transitions between dispatches.

Example: state sa [guard] = sb has an anonymous transition that fires after
any event in sa satisfies the guard.  If an action in sa enqueues a second
event, the old code dispatched the second event while still in sa (before
the anonymous transition moved to sb), causing guard failures and silent
event drops.

Fix: change the while-loop to an if-block so process_queued_events dispatches
exactly one queued event per call and returns true if there was anything to
dispatch.  The outer do-while loop already calls process_queued_events
repeatedly (see the do-while structure in process_event), so all queued
events are still drained -- but now anonymous transitions and defer-queue
drains run between each pair of consecutive queued events.

BEHAVIOUR CHANGE: Previously all queued events were dispatched atomically
before any anonymous transitions fired.  After this fix each queued event
is followed by a full anonymous-transition + defer-drain cycle.  State
machines that rely on the old atomic-batch semantics will observe different
transition ordering.

Fixes boost-ext#542.  Regression test added in test/ft/actions_process.cpp.
@PavelGuzenfeld PavelGuzenfeld force-pushed the fix/issue-542-queued-events-dispatched-in-stale-state branch from 81c43e3 to 162c65a Compare May 23, 2026 22:18
…valid)

Upstream master commit e4fdeb2 added a regression test for issue boost-ext#504 that
used sm.is<sml::state<sub504>>(sml::X) — sml::state<sub504> is a value
expression, not a type, so 'is<T>' rejects it on all compilers.

The same bad assertion appears in this branch because dependencies.cpp is
inherited from the base commit.  Remove the assertion; the action lambda
already calls expect(99 == d.val) which is the correct liveness check.

(Fixes are tracked by PR boost-ext#675.)
@kris-jusiak kris-jusiak merged commit 801253c into boost-ext:master May 24, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

back::defer<Event> or back::process<Event> failed to propagate event recursively?

2 participants