fix: stop idle embedded servers promptly on should_exit
Fixes #3216.
What this fixes
An idle server (no accepted connections) took about 202 ms to shut down:
- The main loop polled
should_exitevery 100 ms withasyncio.sleep(0.1) shutdown()added another unconditionalasyncio.sleep(0.1)after
callingconnection.shutdown()on each open connection, even when
there were none
Changes
1. should_exit is now a property
Setting server.should_exit = True (externally or from the SIGINT handler)
immediately sets self._exit_event, a lazily-created asyncio.Event.
2. main_loop() wakes up on the event
# before
await asyncio.sleep(0.1)
# after, wakes immediately when should_exit flips; falls through after 0.1 s
with contextlib.suppress(asyncio.TimeoutError):
await asyncio.wait_for(self._exit_event.wait(), timeout=0.1)
The event is created at the start of main_loop() so the first should_exit = True before main_loop runs (during startup) is still handled correctly, the
setter checks self._exit_event is not None before calling .set().
3. shutdown() skips the drain sleep when there are no connections
# before
for connection in list(self.server_state.connections):
connection.shutdown()
await asyncio.sleep(0.1) # always
# after
for connection in list(self.server_state.connections):
connection.shutdown()
if self.server_state.connections: # only when something needs draining
await asyncio.sleep(0.1)
Verification
Using the reproduction script from the issue against the patched code
(macOS 27, Python 3.14, no accepted connections):
Shutdown took: 0.1 ms # was ~202 ms before the patch
The property setter is backward-compatible: external code that assigns
server.should_exit = True continues to work and now also wakes up the
loop immediately.
🤖 Generated with Claude Code