DecoratorNode: don't discard the result of an asynchronous child (root cause of flaky RepeatTestAsync) - #1206
Merged
Conversation
DecoratorNode::executeTick() resets a child found in SUCCESS/FAILURE after the decorator's tick(). A ThreadedAction child can complete right after tick() has seen it RUNNING: the reset then throws its result away, the decorator never sees the completion and the action is executed again. This is the cause of the flaky RepeatTestAsync (successCount 4 or 5 instead of 3). Apply the safety net only when the decorator itself has completed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
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.



Bug
RepeatTestAsync.RepeatTestAsyncis not just a slow-runner flake: it exposes a real race inDecoratorNode::executeTick():With a
ThreadedActionchild, the thread can complete after the decorator'stick()readRUNNINGand before this re-read. The child goesRUNNING -> SUCCESS -> IDLEwithout the decorator ever seeingSUCCESS, so on the next tick the action is started again: its side effects run twice. It affects any decorator with aThreadedActionchild, not onlyRepeat.Trace of a failing iteration (0 IDLE, 1 RUNNING, 2 SUCCESS):
Reproduction on Linux under 4x CPU oversubscription: 246 failures / 12000 iterations (
successCount4 or 5 in the first phase, stray successes in the second). TSan reports no data race, confirming that the extra executions are sequential re-runs, not overlapping threads.Fix
That reset is a safety net (from 2019) for decorators that do not reset a completed child. Keep it, but only when the decorator itself has completed; while the decorator is
RUNNING, the child's status is left for the nexttick()to consume. All the built-in decorators already callresetChild()explicitly on the paths where they keep running, and the full suite does not rely on the old behaviour.Behaviour change to be aware of: a third-party decorator that returns
RUNNINGafter its child completed and never callsresetChild()was implicitly covered by the old code; it now has to reset the child itself (as every built-in decorator does).Tests
Decorator.ChildCompletedAfterTickIsNotDiscarded: reproduces the window deterministically (no timing), fails on master, passes here.RepeatTestAsync(plus the other decorator tests) under the same load.🤖 Generated with Claude Code