BUG-HUNT: [concurrency] AsyncResourceTracker.close_all() swallows asyncio.CancelledError, breaking task cancellation #6389

Open
opened 2026-04-09 21:00:06 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: Concurrency — close_all() Swallows CancelledError

Severity Assessment

  • Impact: If the asyncio task running close_all() is cancelled (a common shutdown pattern), the CancelledError is silently swallowed inside the except (Exception, asyncio.CancelledError) block. The task will never propagate cancellation to its caller, making graceful shutdown impossible and potentially causing the application to hang or stall indefinitely during shutdown sequences that rely on cooperative task cancellation.
  • Likelihood: High — task cancellation is the standard asyncio shutdown mechanism. Any code using async with AsyncResourceTracker() or explicitly calling close_all() from a cancellable task is affected.
  • Priority: High

Location

  • File: src/cleveragents/core/async_cleanup.py
  • Function: close_all
  • Lines: 116–125

Description

The close_all() method iterates over registered resources and catches exceptions from each resource.close() call. The exception handler is:

except (Exception, asyncio.CancelledError):
    logger.exception(
        "Error closing async resource '%s'",
        name,
    )

asyncio.CancelledError inherits from BaseException, not Exception. When asyncio.wait_for(resource.close(), timeout=timeout) is running and the parent task receives a cancellation signal, the CancelledError is delivered to the await inside wait_for. The except (Exception, asyncio.CancelledError) block catches this cancellation signal and logs it — but does not re-raise it.

This breaks the cooperative cancellation protocol required by asyncio:

Per the Python asyncio docs: Coroutines should let CancelledError propagate unless they have a specific reason to suppress it and immediately re-raise it.

Consequence: If the application is shutting down and calls cancel() on a task that is mid-close_all(), the shutdown will stall. The task will finish iterating all resources (ignoring cancellation) and return normally, leaving the caller's cancel() semantics broken.

Evidence

# src/cleveragents/core/async_cleanup.py lines 107–125
for name, resource in snapshot.items():
    try:
        await asyncio.wait_for(resource.close(), timeout=timeout)
        logger.info("Closed async resource '%s'", name)
    except TimeoutError:
        self.timed_out_resources.append(name)
        logger.warning(
            "Forced termination: resource '%s' did not close "
            "within timeout of %.1f s",
            name,
            timeout,
        )
    except (Exception, asyncio.CancelledError):   # ← BUG: CancelledError swallowed
        logger.exception(
            "Error closing async resource '%s'",
            name,
        )

Expected Behavior

asyncio.CancelledError should be re-raised immediately after logging, to allow the surrounding task to honour cancellation. Only resource-level errors (not task cancellations) should be caught and swallowed.

Actual Behavior

A CancelledError raised during resource.close() is caught and silently dropped. The close_all() method continues iterating remaining resources and returns normally, making the outer task's cancellation invisible.

Suggested Fix

except asyncio.CancelledError:
    logger.warning(
        "close_all() cancelled while closing resource '%s'",
        name,
    )
    raise  # Always re-raise CancelledError
except Exception:
    logger.exception(
        "Error closing async resource '%s'",
        name,
    )

Category

concurrency

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: Concurrency — `close_all()` Swallows `CancelledError` ### Severity Assessment - **Impact**: If the asyncio task running `close_all()` is cancelled (a common shutdown pattern), the `CancelledError` is silently swallowed inside the `except (Exception, asyncio.CancelledError)` block. The task will never propagate cancellation to its caller, making graceful shutdown impossible and potentially causing the application to hang or stall indefinitely during shutdown sequences that rely on cooperative task cancellation. - **Likelihood**: High — task cancellation is the standard asyncio shutdown mechanism. Any code using `async with AsyncResourceTracker()` or explicitly calling `close_all()` from a cancellable task is affected. - **Priority**: High ### Location - **File**: `src/cleveragents/core/async_cleanup.py` - **Function**: `close_all` - **Lines**: 116–125 ### Description The `close_all()` method iterates over registered resources and catches exceptions from each `resource.close()` call. The exception handler is: ```python except (Exception, asyncio.CancelledError): logger.exception( "Error closing async resource '%s'", name, ) ``` `asyncio.CancelledError` inherits from `BaseException`, **not** `Exception`. When `asyncio.wait_for(resource.close(), timeout=timeout)` is running and the parent task receives a cancellation signal, the `CancelledError` is delivered to the `await` inside `wait_for`. The `except (Exception, asyncio.CancelledError)` block catches this cancellation signal and logs it — but **does not re-raise it**. This breaks the cooperative cancellation protocol required by asyncio: > Per the [Python asyncio docs](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel): Coroutines should let `CancelledError` propagate unless they have a specific reason to suppress it and immediately re-raise it. **Consequence**: If the application is shutting down and calls `cancel()` on a task that is mid-`close_all()`, the shutdown will stall. The task will finish iterating all resources (ignoring cancellation) and return normally, leaving the caller's `cancel()` semantics broken. ### Evidence ```python # src/cleveragents/core/async_cleanup.py lines 107–125 for name, resource in snapshot.items(): try: await asyncio.wait_for(resource.close(), timeout=timeout) logger.info("Closed async resource '%s'", name) except TimeoutError: self.timed_out_resources.append(name) logger.warning( "Forced termination: resource '%s' did not close " "within timeout of %.1f s", name, timeout, ) except (Exception, asyncio.CancelledError): # ← BUG: CancelledError swallowed logger.exception( "Error closing async resource '%s'", name, ) ``` ### Expected Behavior `asyncio.CancelledError` should be re-raised immediately after logging, to allow the surrounding task to honour cancellation. Only resource-level errors (not task cancellations) should be caught and swallowed. ### Actual Behavior A `CancelledError` raised during `resource.close()` is caught and silently dropped. The `close_all()` method continues iterating remaining resources and returns normally, making the outer task's cancellation invisible. ### Suggested Fix ```python except asyncio.CancelledError: logger.warning( "close_all() cancelled while closing resource '%s'", name, ) raise # Always re-raise CancelledError except Exception: logger.exception( "Error closing async resource '%s'", name, ) ``` ### Category `concurrency` ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-09 21:09:08 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6389
No description provided.