Skip to content

fix(defer): propagate CancelledError into background event loop#1688

Open
no0ktheali3n wants to merge 1 commit into
agent0ai:mainfrom
no0ktheali3n:fix/defer-cancellation-propagation
Open

fix(defer): propagate CancelledError into background event loop#1688
no0ktheali3n wants to merge 1 commit into
agent0ai:mainfrom
no0ktheali3n:fix/defer-cancellation-propagation

Conversation

@no0ktheali3n
Copy link
Copy Markdown

DeferredTask.result() awaits on loop.run_in_executor(None, _get_result), where _get_result is a blocking call to self._future.result(timeout). When the outer asyncio task is cancelled (for example a wall-clock timeout in the caller fires), only the outer task receives the CancelledError. The underlying concurrent.futures.Future continues running on the singleton background event loop, and the thread-pool worker stays parked in _get_result waiting for it. Every cancelled result() leaks one worker thread until the default ThreadPoolExecutor saturates and subsequent result() calls block indefinitely.

Fix: catch asyncio.CancelledError in result(), call self.kill() to cancel the underlying future and drain the singleton background event loop, then re-raise. The worker thread is released as the future resolves to CancelledError; any in-flight coroutine tasks on the background loop are cancelled too.

Adds tests/test_defer_cancellation.py asserting that after a cancelled result(), the underlying future reaches done() within 500ms

  • a thread-leak regression guard.

Reproduces in upstream main and earlier releases - same code shape.

DeferredTask.result() awaits on loop.run_in_executor(None, _get_result),
where _get_result is a blocking call to self._future.result(timeout).
When the outer asyncio task is cancelled (for example a wall-clock
timeout in the caller fires), only the outer task receives the
CancelledError. The underlying concurrent.futures.Future continues
running on the singleton background event loop, and the thread-pool
worker stays parked in _get_result waiting for it. Every cancelled
result() leaks one worker thread until the default ThreadPoolExecutor
saturates and subsequent result() calls block indefinitely.

Fix: catch asyncio.CancelledError in result(), call self.kill() to
cancel the underlying future and drain the singleton background event
loop, then re-raise. The worker thread is released as the future
resolves to CancelledError; any in-flight coroutine tasks on the
background loop are cancelled too.

Adds tests/test_defer_cancellation.py asserting that after a
cancelled result(), the underlying future reaches done() within 500ms
- a thread-leak regression guard.

Reproduces in upstream main and earlier releases - same code shape.
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