fix: clear the transition when the context is canceled - #117
Merged
maxekman merged 1 commit intoAug 27, 2026
Conversation
transitionFunc returns early when ctx.Err() != nil, but does not clear f.transition on the way out — only the completed path below it does. Since f.transition was already published before the context was ever checked, the FSM is left in transition permanently: every later Event returns InTransitionError, Can() returns false, and Transition() cannot help either, because it re-runs this same closure and takes this same early return. Only constructing a new FSM recovers. A state whose leave callback cancels is spared, because leaveStateCallbacks returning CanceledError clears the flag. A state with no callbacks at all is not — so whether a canceled context bricks the FSM depends on whether the state you happen to be leaving has a callback registered. Clears it with the same lock the completed path takes, and leaves e.Err untouched, so Event still returns the context error to the caller as before. Fixes looplab#115. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
|
Thanks for the fix, will check it out. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #115.
transitionFuncreturns early whenctx.Err() != nil, but only the completed path below it clearsf.transition:Since
f.transitionis published before the context is ever checked, a canceled context leaves the FSM in transition — and permanently, because there is no way back:EventreturnsInTransitionError,Can()returns false (ok && (f.transition == nil)),Transition()cannot help either: it re-runs this same closure, which captured the same dead context and takes the same early return.Only constructing a new FSM recovers.
Why it looks intermittent
Whether a canceled context bricks the FSM depends on which state is being left, which is not something a caller would think to check:
leaveStateCallbacksreturningCanceledErrordoes clear the flag;So the same cancellation is harmless in one state and terminal in another.
The fix
Clear
f.transitionon the canceled path, taking the samestateMulock the completed path takes.e.Erris left untouched, soEventstill returns the context error to the caller exactly as before — this only stops the FSM being unusable afterwards.Test
TestCanceledContextDoesNotLeaveTheFSMInTransition— an FSM with no callbacks, one event with an already-canceled context, then a second event with a live one. Before the fix it fails with:After it passes, and the full suite passes under
-race.Where this came from
A cart-fleet sync service, one FSM per sync rule, with the context being the gRPC stream. A stream dying while an event was in flight left that rule's handler refusing every transition until it was rebuilt, so it silently stopped syncing and queued everything it received instead. It showed up on 34 devices in three days, and was hard to see because the resulting error is indistinguishable from ordinary "wrong state for this event" noise unless you look at the wrapped cause.