fix: stop idle embedded servers promptly on should_exit

Closed
SW
3d ago
+41-2

Fixes #3216.

What this fixes

An idle server (no accepted connections) took about 202 ms to shut down:

  • The main loop polled should_exit every 100 ms with asyncio.sleep(0.1)
  • shutdown() added another unconditional asyncio.sleep(0.1) after
    calling connection.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