Conversation
When a run was evicted, Core queued a cancel for each of its in-flight local activities and produced the eviction activation in the same pass. If lang completed the eviction before its activity poll reached the cancel, invalidating the run made the LA manager drop the cancel as untracked, so lang kept running the local activity while Core reported local activity shutdown complete (temporalio/sdk-python#1837). The eviction activation is now withheld while any of the run's in-flight local activities has a queued, not yet polled cancel, and the LA manager wakes the run once the last one is handed to lang. Cancels are queued once per attempt, a local activity still waiting for dispatch resolves as cancelled instead of starting, and an attempt that fails after its cancel was requested is not retried locally. Local activity results that arrive while the eviction is withheld are discarded, as they already were once the eviction activation was outstanding, so the cancellation the eviction caused never reaches workflow code as an activation. CancelAllInRun yields no immediate resolutions for the same reason: it is only sunk for runs that are terminating or evicting, and applying them surfaced spurious cancellations for backing-off or never-dispatched local activities.
051b82d to
b25d552
Compare
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The cancellation and eviction handoff spans concurrent activity and workflow streams and warrants final human validation.
Review effort: Balanced
Findings: None
What changed in this PR
Fixes a shutdown hang by ensuring local-activity cancellations are dispatched before workflow eviction removes their tracking state.
Changes:
- Tracks cancellation delivery and blocks eviction until queued cancellations are polled.
- Prevents retries and spurious resolutions after cancellation or eviction.
- Adds regression coverage and user-facing changelog entries.
| File | Description |
|---|---|
crates/sdk-core/src/worker/workflow/workflow_stream.rs |
Handles cancellation-delivery notifications. |
crates/sdk-core/src/worker/workflow/mod.rs |
Prioritizes local-activity notifications. |
crates/sdk-core/src/worker/workflow/managed_run.rs |
Withholds eviction while cancellations remain queued. |
crates/sdk-core/src/worker/mod.rs |
Connects the notification channel. |
crates/sdk-core/src/worker/activities/local_activities.rs |
Tracks cancellation state and suppresses retries. |
crates/sdk-core/src/core_tests/workflow_tasks.rs |
Adds eviction and cancellation regressions. |
crates/sdk-core/CHANGELOG.md |
Documents Core behavior changes. |
CHANGELOG.md |
Documents Rust SDK behavior changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| * Local activities still running when their workflow run is evicted (cache full, workflow | ||
| completion, worker shutdown, task failures) now have their cancellation delivered to the | ||
| activity poller before the run's eviction activation is issued, so a local activity can no | ||
| longer be left running without ever receiving its cancellation. Evictions no longer | ||
| deliver duplicate cancellation tasks for the same attempt, a local activity attempt that fails | ||
| after cancellation due to run eviction is no longer retried locally, | ||
| and a local activity cancelled before the activity poller picked it up now resolves as cancelled | ||
| immediately instead of starting. Workflow code is no longer activated with cancellations or | ||
| results of local activities that only came about because their run was being evicted or had | ||
| completed (previously a local activity backing off between retries at that point surfaced a | ||
| cancellation the workflow never requested). A worker with local activities enabled must keep | ||
| polling activity tasks until the poll reports shutdown: evictions of runs with in-flight local | ||
| activities wait for that poll. |
There was a problem hiding this comment.
This is a lot of detail for a changelog entry—it is nearly twice as long as any existing entry. Do users need all of the duplicate-cancel, retry, undispatched-activity, and backing-off edge cases here, or can this be trimmed to the essential user-facing behavior and operational impact?
What was changed
A run's eviction activation is withheld until every queued cancel for its in-flight local activities has been returned from an activity poll. Cancels are queued once per attempt, an undispatched local activity resolves as cancelled, and an attempt failing after its cancel was requested is not retried locally, matching the server's
RETRY_STATE_CANCEL_REQUESTED.CancelAllInRunno longer returns immediate resolutions, as it is only sunk for terminating or evicting runsWhy
_check_more_activationssinksCancelAllInRun, which only queues cancels, then produces the eviction activation. Once lang completes it,InvalidateRundrops the run's LAs from the outstanding set andnext_pendingdiscards the queued cancel as untracked, so lang keeps running the LA while Core reports shutdown complete. This was the hang I was originally looking at in temporalio/sdk-python#1837.Testing
New
la_cancel_delivered_before_eviction_activationfails without the fix; six more cover withheld-eviction results, undispatched and backing-off LAs, and no retry after cancel. If any look low-value just lmk.