Skip to content

fix(core): reset FAILED JOIN tasks to IN_PROGRESS during retry walk-up - #1274

Open
nthmost-orkes wants to merge 7 commits into
conductor-oss:mainfrom
nthmost-orkes:fix/retry-join-reset-on-parent-walk
Open

nthmost-orkes wants to merge 7 commits into
conductor-oss:mainfrom
nthmost-orkes:fix/retry-join-reset-on-parent-walk

Conversation

@nthmost-orkes

@nthmost-orkes nthmost-orkes commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • During updateAndPushParents, FAILED and TIMED_OUT JOIN tasks were silently skipped in the sibling-processing loop.
  • A subworkflowChanged-flag race in CI allowed decide() to run AFTER the sibling retry but BEFORE the flag was consumed, finding a FAILED built-in JOIN and throwing TerminateWorkflowException — collapsing the parent workflow that should have been resuming.
  • Fix: add an else if branch resetting FAILED/TIMED_OUT JOIN tasks to IN_PROGRESS and queuing them immediately during updateAndPushParents, unconditionally, so no subsequent decide() can encounter a FAILED built-in JOIN during the retry walk-up.

Root cause

updateAndPushParents loops over the parent's tasks and:

  • retries UNSUCCESSFUL SUB_WORKFLOW siblings
  • resets CANCELED JOINs to IN_PROGRESS

But it had no handling for FAILED or TIMED_OUT JOIN tasks. They remained FAILED in Redis after the walk-up completed. adjustStateIfSubWorkflowChanged is supposed to clean them up via resetUnsuccessfulJoinTasks, but that mechanism consumes exactly one subworkflowChanged flag per decide() call. Under fast execution (redis-es8 CI), a decide() call can arrive for the parent workflow while some flags are still unconsumed and find inner_join = FAILED — DeciderService.retry() sees a FAILED built-in task and throws TerminateWorkflowException, terminating the parent.

Fix

Added an else if ((task.getStatus() == FAILED || task.getStatus() == TIMED_OUT) && TASK_TYPE_JOIN.equals(task.getTaskType())) branch in updateAndPushParents's sibling loop that resets the JOIN to IN_PROGRESS and calls addTaskToQueue, identical to what resetUnsuccessfulJoinTasks does.

User impact

Retrying a nested sub-workflow (e.g. workflowClient.retryWorkflow(...)) could cause parent workflows to become TERMINATED instead of RUNNING when running against a Redis/ES8 backend, because FAILED JOIN tasks upstream were not cleaned up before the next decide cycle ran.

Orkes parity

No Orkes-parallel file — this is a bug fix to an existing method in WorkflowExecutorOps.java.

In updateAndPushParents, FAILED and TIMED_OUT JOIN tasks (e.g. inner_join,
top_join) were silently skipped during the sibling-processing loop. The
existing adjustStateIfSubWorkflowChanged mechanism tries to reset them via
resetUnsuccessfulJoinTasks, but it consumes exactly one subworkflowChanged
flag per decide() call. Under load (redis-es8 CI), decide() can run before
all flags are consumed, finding a FAILED built-in JOIN and throwing
TerminateWorkflowException — collapsing the parent workflow that should be
resuming after the child retry.

Fix: add an else-if branch for FAILED/TIMED_OUT JOIN tasks in the loop,
resetting status to IN_PROGRESS and queuing them immediately. This matches
what resetUnsuccessfulJoinTasks does but happens unconditionally during the
walk-up, removing the race with subworkflowChanged flag consumption.

Fixes testRetryNestedSubWorkflowsReschedulesSiblingsAtEveryLevel (e2e redis-es8).
@nthmost-orkes
nthmost-orkes requested a review from v1r3n July 17, 2026 17:26

This branch has not been deployed

No deployments
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.

1 participant