Skip to content

fix: never retry CancelledError under retry_if_not_exception_type - #661

Open
MohammedAnasNathani wants to merge 5 commits into
jd:mainfrom
MohammedAnasNathani:fix/cancelled-error-no-retry
Open

fix: never retry CancelledError under retry_if_not_exception_type#661
MohammedAnasNathani wants to merge 5 commits into
jd:mainfrom
MohammedAnasNathani:fix/cancelled-error-no-retry

Conversation

@MohammedAnasNathani

Copy link
Copy Markdown
Contributor

Summary

Fixes #529.

retry_if_not_exception_type(ValueError) currently retries any non-ValueError, including asyncio.CancelledError. That breaks cancellation:

@retry(retry=retry_if_not_exception_type(ValueError), reraise=True, ...)
async def work():
    await asyncio.sleep(10)

await asyncio.wait_for(work(), 0.05)  # should stop; used to retry then finish

Default retry_if_exception_type() only retries Exception subclasses, so cancel already worked there. The bug is specific to the if_not / unless predicates.

Fix

Central helper _is_control_flow_exception — never retry:

  • asyncio.CancelledError
  • KeyboardInterrupt / SystemExit / GeneratorExit

Applied in:

  • retry_if_not_exception_type
  • retry_unless_exception_type

Tests

  • wait_for + retry_if_not_exception_typeone attempt only
  • direct CancelledError → not retried
  • sync KeyboardInterrupt → not retried

Why this is important

Without this, any asyncio.timeout / wait_for around a tenacity-wrapped coroutine with a custom retry_if_not_exception_type can hang or run past the deadline.

retry_if_not_exception_type(T) treated every non-T exception as
retryable, including asyncio.CancelledError. That broke asyncio.wait_for
/ timeout cancellation: the task was retried instead of stopping.

Exclude control-flow exceptions (CancelledError, KeyboardInterrupt,
SystemExit, GeneratorExit) before applying the user type filter. Same
guard on retry_unless_exception_type. Async + sync regression tests.

Fixes jd#529.
Copilot AI review requested due to automatic review settings July 25, 2026 12:50

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.

Required for green CI under current ruff: preview RUF036 flags
None | X unions in public retry signatures. Purely mechanical
reordering; no behavior change.
CI runs ruff format --check; wrap long async test def and
normalize spacing around the new control-flow cases.
…ator

Double @asynctest made asyncio.run receive None (wrapper result),
failing wait_for coverage. Also restore @asynctest on
test_iscoroutinefunction after insert.
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.

retry_if_not_exception_type swallows asyncio.CancelledError

2 participants