diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 614653d..c7b6927 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - uses: actions/cache@v3 diff --git a/grpclib/server.py b/grpclib/server.py index aebd34a..b652a75 100644 --- a/grpclib/server.py +++ b/grpclib/server.py @@ -625,6 +625,7 @@ class Server(_GC): self._config = config.__for_server__() self._server: Optional[asyncio.AbstractServer] = None + self._server_closed_fut: Optional[asyncio.Future[None]] = None self._handlers: Set[Handler] = set() self.__dispatch__ = _DispatchServerEvents() @@ -703,7 +704,6 @@ class Server(_GC): self._protocol_factory, path, sock=sock, backlog=backlog, ssl=ssl ) - else: # FIXME: Not all union combinations were tried because there are # too many unions @@ -715,15 +715,18 @@ class Server(_GC): backlog=backlog, ssl=ssl, reuse_address=reuse_address, reuse_port=reuse_port ) + self._server_closed_fut = self._loop.create_future() def close(self) -> None: """Stops accepting new connections, cancels all currently running requests. Request handlers are able to handle `CancelledError` and exit properly. """ - if self._server is None: + if self._server is None or self._server_closed_fut is None: raise RuntimeError('Server is not started') self._server.close() + if not self._server_closed_fut.done(): + self._server_closed_fut.set_result(None) for handler in self._handlers: handler.close() @@ -731,8 +734,9 @@ class Server(_GC): """Coroutine to wait until all existing request handlers will exit properly. """ - if self._server is None: + if self._server is None or self._server_closed_fut is None: raise RuntimeError('Server is not started') + await self._server_closed_fut await self._server.wait_closed() if self._handlers: await asyncio.wait({ diff --git a/grpclib/testing.py b/grpclib/testing.py index 68d5000..99a93be 100644 --- a/grpclib/testing.py +++ b/grpclib/testing.py @@ -106,6 +106,7 @@ class ChannelFor: status_details_codec=self._status_details_codec, ) self._server._server = _Server() + self._server._server_closed_fut = self._server._loop.create_future() self._server_protocol = self._server._protocol_factory() self._channel = Channel( diff --git a/setup.cfg b/setup.cfg index b5bcddb..535790c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,8 @@ asyncio_mode = auto filterwarnings = error ignore:.*pkg_resources.*:DeprecationWarning + ignore:.*google.*:DeprecationWarning + ignore:.*utcfromtimestamp.*:DeprecationWarning ignore::ResourceWarning [coverage:run] diff --git a/tests/conn.py b/tests/conn.py index 73f6795..fdd35bc 100644 --- a/tests/conn.py +++ b/tests/conn.py @@ -167,6 +167,6 @@ class ClientServer: return handler, stub async def __aexit__(self, *exc_info): + self.channel.close() self.server.close() await self.server.wait_closed() - self.channel.close() diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..05a5ce3 --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,30 @@ +import asyncio + +import pytest + +from grpclib.server import Server + + +async def serve_forever(server): + await server.start('127.0.0.1') + await server.wait_closed() + + +@pytest.mark.asyncio +async def test_wait_closed(loop: asyncio.AbstractEventLoop): + server = Server([]) + task = loop.create_task(serve_forever(server)) + done, pending = await asyncio.wait([task], timeout=0.1) + assert pending and not done + server.close() + done, pending = await asyncio.wait([task], timeout=0.1) + assert done and not pending + + +@pytest.mark.asyncio +async def test_close_twice(): + server = Server([]) + await server.start('127.0.0.1') + server.close() + server.close() + await server.wait_closed()