Skip to content

Python: fix(workflow): finish in-flight checkpoint saves when a sibling step fails - #9644

Open
ShiYuan Xiong (hehe764) wants to merge 5 commits into
microsoft:mainfrom
hehe764:fix/9615-checkpoint-save-cancellation
Open

ShiYuan Xiong (hehe764) wants to merge 5 commits into
microsoft:mainfrom
hehe764:fix/9615-checkpoint-save-cancellation

Conversation

@hehe764

@hehe764 ShiYuan Xiong (hehe764) commented Oct 23, 2026 •

Copy link
Copy Markdown

Motivation & Context

A functional workflow with checkpoint storage can lose the per-step checkpoint of a step that already completed when a parallel sibling step fails, so a resumed run re-runs the completed step and its side effects. Whether the checkpoint is lost depends on the storage implementation's await timing, not on user code. Fixes #9615 (analysis and approach proposed in the linked issue comment).

Description & Review Guide

  • What are the major changes?

    • The per-step checkpoint save now runs as its own task, with both the normal and the drain await wrapped in asyncio.shield, so a cancellation of the step task during the framework's failure cleanup after a sibling raises cannot reach the save through the await.
    • RunContext tracks in-flight per-step saves in a dedicated set; _cancel_workflow_tasks expands protection through the task-parent map to include storage-created descendants, and drains these tasks rather than cancelling them.
    • Regression test test_completed_parallel_step_is_checkpointed_when_sibling_fails covers inline asynchronous writes, asyncio.create_task writes, and asyncio.gather writes; all three resume without repeating the completed step.
  • What is the impact of these changes?

    • Failure cleanup no longer interrupts an in-flight save or its storage-created child tasks. When storage completes successfully, the persisted result can be replayed without repeating the completed step. Storage failures, direct cancellation, and process termination are outside this guarantee.
    • Run teardown waits for in-flight storage saves. An adapter that never finishes can delay teardown indefinitely; storage timeouts remain the adapter's responsibility.
  • What do you want reviewers to focus on?

    • The interaction between the loop task factory (which registers the save task) and _cancel_workflow_tasks: the sweep protects _checkpoint_save_tasks and their descendants from cancellation but still awaits them in its drain gather.
    • Whether waiting for in-flight saves in the failure sweep is acceptable, versus an alternative where storages are individually responsible for surviving cancellation.

Related Issue

Fixes #9615

AI Assistance

  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: AI assistance was used for the root-cause analysis, implementation, tests, and this PR description. All changes were reviewed, executed, and verified locally by the contributor (reproduction script, regression test red/green against unmodified main, full tests/workflow/ suite on the merged head (14445d9e): 1282 passed, 6 skipped, 1 xfailed; ruff and pyright passed). The descendant-task regression failed for task/gather storage before this follow-up fix. The complete core suite was not rerun for the follow-up.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change.

…fails

A step that completed and emitted executor_completed before a sibling
failed could have its per-step checkpoint save cancelled by run teardown.
asyncio.gather cancels the completed step's task as soon as the sibling
raises, and the failure-path task sweep cancelled the save task itself
(the loop task factory tracks every task), so a storage whose save
awaits I/O before writing lost the checkpoint. The latest checkpoint
then held no trace of the finished step and a resumed run re-ran it and
its side effects.

Run the save as its own task, shield both awaits so a cancellation of
the step task cannot reach it, and have the task sweep wait for
in-flight saves instead of cancelling them. A completed step is now
restored from the checkpoint whatever storage is used.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Oct 23, 2026
@github-actions github-actions Bot changed the title fix(workflow): finish in-flight checkpoint saves when a sibling step fails Python: fix(workflow): finish in-flight checkpoint saves when a sibling step fails Oct 23, 2026
@hehe764

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

This branch was successfully deployed

1 active deployment
github-app-auth — c6414f15 Deployed Oct 23, 2026 by hehe764 via add_label #26047
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Functional workflow drops a finished @step's checkpoint when a sibling step fails during the save, so resuming runs the step again

2 participants