_run_to_completion passed _CLEANUP_TIMEOUT_SECONDS to each asyncio.wait()
call, so every re-delivered CancelledError re-armed a fresh 10s window and
the deadline never elapsed. anyio re-delivers cancellation on every event
loop iteration (CancelScope._deliver_cancellation reschedules itself via
call_soon until the task leaves the scope), and Starlette's
BaseHTTPMiddleware -- registered for every request in main.py -- wraps the
request in an anyio task group. So on the API path the abandon valve was
dead exactly where it was needed: a wedged connection pinned the request
task, hot-spinning the loop, for as long as the storm lasted.
Compute one monotonic deadline before the loop and derive each wait timeout
from the remaining time.
Tests: storm helper now re-cancels at anyio's call_soon cadence instead of
5 sparse cancels (sparse cancels leave quiet ticks where a per-wait timeout
re-arms and cleanup finishes, which is why the existing storm test passed
either way). New test_run_to_completion_deadline_holds_under_cancel_storm
fails on the old loop ("not abandoned after 5.0s") and passes with the fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>