diff --git a/tests/test_server.py b/tests/test_server.py index eaede18df..f693344b0 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -276,3 +276,20 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) as extract_json_body: assert await extract_json_body(large_request) == {} assert await extract_json_body(SIMPLE_GET_REQUEST) == {} + + +async def test_should_exit_setter_clears_exit_event() -> None: + """Setting should_exit=False clears the exit event, allowing a paused main_loop to resume.""" + config = Config(app=app) + server = Server(config=config) + loop = asyncio.get_running_loop() + server._exit_loop = loop + server._exit_event = asyncio.Event() + + server.should_exit = True + await asyncio.sleep(0) + assert server._exit_event.is_set() + + server.should_exit = False + await asyncio.sleep(0) + assert not server._exit_event.is_set() diff --git a/uvicorn/server.py b/uvicorn/server.py index 380e70a78..ab226138d 100644 --- a/uvicorn/server.py +++ b/uvicorn/server.py @@ -70,12 +70,28 @@ def __init__(self, config: Config) -> None: self.server_state = ServerState() self.started = False - self.should_exit = False + self._should_exit = False + self._exit_event: asyncio.Event | None = None + self._exit_loop: asyncio.AbstractEventLoop | None = None self.force_exit = False self.last_notified = 0.0 self._captured_signals: list[int] = [] + @property + def should_exit(self) -> bool: + return self._should_exit + + @should_exit.setter + def should_exit(self, value: bool) -> None: + self._should_exit = value + loop = self._exit_loop + if self._exit_event is not None and loop is not None and not loop.is_closed(): + if value: + loop.call_soon_threadsafe(self._exit_event.set) + else: + loop.call_soon_threadsafe(self._exit_event.clear) + @functools.cached_property def limit_max_requests(self) -> int | None: if self.config.limit_max_requests is None: @@ -240,12 +256,18 @@ def _log_started_message(self, listeners: Sequence[socket.SocketType]) -> None: ) async def main_loop(self) -> None: + self._exit_loop = asyncio.get_running_loop() + self._exit_event = asyncio.Event() counter = 0 should_exit = await self.on_tick(counter) while not should_exit: counter += 1 counter = counter % 864000 - await asyncio.sleep(0.1) + try: + async with asyncio.timeout(0.1): + await self._exit_event.wait() + except TimeoutError: + pass should_exit = await self.on_tick(counter) async def on_tick(self, counter: int) -> bool: