Skip to content

fix: guard against proactor accept-loop race on Windows (Python 3.14+)#2

Merged
PolyphonyRequiem merged 1 commit intoinstall-combinedfrom
fix/proactor-shutdown-race
Apr 24, 2026
Merged

fix: guard against proactor accept-loop race on Windows (Python 3.14+)#2
PolyphonyRequiem merged 1 commit intoinstall-combinedfrom
fix/proactor-shutdown-race

Conversation

@PolyphonyRequiem
Copy link
Copy Markdown
Owner

Bug

Web dashboard crashes with AssertionError in Python 3.14 asyncio proactor event loop on Windows when the server accepts a new connection during shutdown.

Root cause: The proactor accept callback fires after Server.close() sets _sockets = None — a race between the shutdown path and the accept loop.

Fix

Two-layer guard, both narrowly scoped to only suppress AssertionError when the uvicorn server is in shutdown state (should_exit = True):

  1. _guarded_serve() — wraps server.serve() to catch the error if it propagates through the coroutine chain
  2. Custom event-loop exception handler — catches the error when it surfaces through the accept callback path

_is_proactor_shutdown_race() validates: AssertionError type, server shutdown state, and asyncio-originating traceback frames. The original exception handler is saved and restored in stop().

Why not a separate thread?

Considered running uvicorn in a dedicated thread with a selector event loop, but cross-thread communication would break existing engine contracts (asyncio.Event/Queue used by workflow engine). Much higher complexity for the same outcome.

Tests

9 new tests covering race detection, exception handler delegation, guarded serve behavior, and edge cases. All 58 server tests pass.

On Windows with Python 3.14+, the proactor event loop's accept callback
can fire after Server.close() sets _sockets = None during shutdown,
causing an AssertionError in base_events.py:_attach that crashes the
workflow process.

Fix:
- Add _guarded_serve() wrapper that catches AssertionError when the
  uvicorn server is in shutdown state (should_exit = True)
- Install a custom event-loop exception handler during server lifetime
  that suppresses the same race when it surfaces through callbacks
- _is_proactor_shutdown_race() validates: AssertionError type, server
  shutdown state, and asyncio-originating traceback frames
- Restore original exception handler in stop()

The guard is narrowly scoped: only AssertionError during server shutdown
is suppressed. All other exceptions delegate to the original handler.

Tests: 9 new tests covering the race detection, exception handler
delegation, guarded serve behavior, and edge cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PolyphonyRequiem PolyphonyRequiem merged commit 7e3b940 into install-combined Apr 24, 2026
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