Repository navigation
Fix cancelling a local activity during its retry backoff - #1972
Merged
Sushisource merged 1 commit intoOct 15, 2026
Merged
Conversation
|
|
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 #1971.
When Core resolves a local activity with
DoBackoff,_resolve_backoffmoves the handle to a new seq and sets_started = False. TheScheduleLocalActivitycommand for that seq is only sent after the backoffworkflow_sleepinrun_activity. If the handle is cancelled during that sleep (a client-side workflow cancel,handle.cancel(), orasyncio.wait_foraroundexecute_local_activity),cancel()sees_startedas false and calls_request_cancel(). That emitsRequestCancelLocalActivityfor a seq Core never saw, and every workflow task after that fails with[TMPRL1100] Nondeterminism error: Missing associated machine for LocalActivity(N). The workflow staysRUNNINGand the backoff timer is never cancelled._startedonly exists socancel()can send a command for a handle whose coroutine has not run yet and so cannot trap the cancel. During the backoff sleep the coroutine is running, so this change stops resetting it in_resolve_backoff, andcancel()falls through to a plain task cancel. That cancels the backoff timer the same way as any otherworkflow.sleep.run_activitynow sets_startedonce instead of on every loop iteration.After the fix, a cancel during timer backoff raises
asyncio.CancelledErrorfrom the handle (a workflow cancel ends the workflow cancelled,wait_forraisesTimeoutError), while a cancel during a running attempt still raisesActivityErrorwith aCancelledErrorcause. That is the same split as the TypeScript and .NET SDKs, which sleep on a timer during backoff and only send a local activity cancel for a seq that is still pending. Cancels outside the backoff window produce the same commands as before. Onmainthis path always fails the workflow task, so no completed history contains it.test_workflow_local_activity_cancel_during_backoffraisesApplicationErrorwithnext_retry_delayabove the default 60slocal_retry_threshold, so Core switches to timer backoff after the first attempt with onlystart_to_close_timeoutset. It waits forTIMER_STARTED, then cancels in two ways:CancelledErrorcausehandle.cancel(), andawait handlemust raiseasyncio.CancelledErrorBoth assert
TIMER_CANCELEDand noWORKFLOW_TASK_FAILEDin history. Both fail onmainat the 60s pytest timeout, with the nondeterminism error above logged on every workflow task retry, and pass with the fix. Puttingself._started = Falseback in_resolve_backoff, or droppinghandle._started = Truefromrun_activity, fails both again.Validation, run locally on macOS (Apple Silicon) with Python 3.12, Rust bridge built with
poe build-develop:poe lint: ruff import and format checks clean, pyright 0 errors, mypy no issues in 272 source files, basedpyright 0 errors, pydocstyle cleanpytest -n auto --dist=worksteal -v -o timeout_func_only=false --timeout 180: 1488 passed, 9 skippedpytest tests/worker/test_workflow.py tests/worker/test_replayer.py -n auto --dist=worksteal: 258 passed, 1 skippedpytest tests/worker/test_workflow.py -k "local_activity or cancel_activity or backoff" --workflow-environment time-skipping: 13 passedpytest tests/worker/test_workflow.py -k test_workflow_local_activity_cancel_during_backoff: 2 passed, run twice (2 failed onmainat the 60s timeout)mainprintsstatus: RUNNINGwith the TMPRL1100 task failures, this branch printsstatus: CANCELEDandtimer cancelledNot run: Python versions other than 3.12, Linux and Windows.