Skip to content

fix: call task.uncancel() after catching CancelledError in shield loops (Python 3.11+)#1523

Open
brucearctor wants to merge 1 commit into
temporalio:mainfrom
brucearctor:fix/uncancel-shielded-child-workflow
Open

fix: call task.uncancel() after catching CancelledError in shield loops (Python 3.11+)#1523
brucearctor wants to merge 1 commit into
temporalio:mainfrom
brucearctor:fix/uncancel-shielded-child-workflow

Conversation

@brucearctor
Copy link
Copy Markdown

Summary

On Python 3.11+, asyncio.Task tracks a cancellation counter via Task.cancelling() / Task.uncancel(). When CancelledError is caught inside a while True / asyncio.shield loop without calling uncancel(), the counter stays elevated and Python re-throws CancelledError at every subsequent await.

This causes:

  1. Duplicate commands (e.g. RequestCancelExternalWorkflow) sent to the Temporal server
  2. Spurious ERROR-level log lines ("exception in shielded future") from temporalio.worker._workflow_instance

Fix

Adds task.uncancel() (guarded by hasattr for Python ≤3.10 compatibility) after each CancelledError catch in all 6 affected shield loops:

  • run_activity() in _outbound_schedule_activity
  • run_child() in _outbound_start_child_workflow
  • start-wait loop in _outbound_start_child_workflow
  • operation_handle_fn() in _outbound_start_nexus_operation
  • start-wait loop in _outbound_start_nexus_operation
  • _signal_external_workflow

After uncancel(), the cancellation counter returns to 0 so the next await asyncio.shield(...) blocks normally until the future resolves, rather than immediately re-raising CancelledError. The while True loop then works as designed — it waits for the result after the cancel has been handled.

Fixes #1504

On Python 3.11+, asyncio.Task tracks a cancellation counter via
Task.cancelling()/Task.uncancel(). When CancelledError is caught
inside a while-True/asyncio.shield loop without calling uncancel(),
the counter stays elevated and Python re-throws CancelledError at
every subsequent await. This causes:

1. Duplicate commands (e.g. RequestCancelExternalWorkflow) sent to
   the Temporal server
2. Spurious ERROR-level 'exception in shielded future' log lines
   from temporalio.worker._workflow_instance

The fix adds task.uncancel() (guarded by hasattr for Python <=3.10
compatibility) after each CancelledError catch in all 6 affected
shield loops:

- run_activity() in _outbound_schedule_activity
- run_child() in _outbound_start_child_workflow
- start-wait loop in _outbound_start_child_workflow
- operation_handle_fn() in _outbound_start_nexus_operation
- start-wait loop in _outbound_start_nexus_operation
- _signal_external_workflow

Fixes temporalio#1504
@brucearctor brucearctor requested a review from a team as a code owner May 14, 2026 02:44
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 14, 2026

CLA assistant check
All committers have signed the CLA.

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.

[Bug] spurious "exception in shielded future" warnings on Python 3.11+

2 participants