Skip to content

Commit eb3600c

Browse files
committed
Restore compatibility of serve_forever with uvloop.
This is also a much better implementation. Refs #1608.
1 parent 8b5e767 commit eb3600c

4 files changed

Lines changed: 31 additions & 42 deletions

File tree

docs/project/changelog.rst

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,21 @@ fixing regressions shortly after a release.
2525
Only documented APIs are public. Undocumented, private APIs may change without
2626
notice.
2727

28-
.. _17.1:
28+
.. _17.0.1:
2929

30-
17.1
31-
----
30+
17.0.1
31+
------
3232

3333
*In development*
3434

35+
Bug fixes
36+
.........
37+
38+
* Restored compatibility of :meth:`~asyncio.server.Server.serve_forever` in the
39+
:mod:`asyncio` implementation with third-party event loops such as uvloop_.
40+
41+
.. _uvloop: https://uvloop.readthedocs.io/
42+
3543
.. _17.0:
3644

3745
17.0

src/websockets/asyncio/server.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -422,33 +422,21 @@ async def serve_forever(self) -> None:
422422
await server.serve_forever()
423423
424424
This is an alternative to using :func:`serve` as an asynchronous context
425-
manager. Shutdown is triggered by canceling :meth:`serve_forever`
426-
instead of exiting a :func:`serve` context.
425+
manager. Shutdown is triggered by canceling :meth:`serve_forever` or by
426+
calling :meth:`~Server.close` from another task.
427427
428428
"""
429-
# This is a copy-paste of asyncio.Server.serve_forever(), with
430-
# self.server instead of self, except it calls our close() and
431-
# wait_closed() when canceled to ensure a graceful shutdown.
432-
if self.server._serving_forever_fut is not None: # type: ignore[attr-defined]
433-
raise RuntimeError(
434-
f"server {self.server!r} is already being awaited on serve_forever()"
435-
)
436-
if self.server._sockets is None: # type: ignore[attr-defined]
437-
raise RuntimeError(f"server {self.server!r} is closed")
438-
439-
self.server._start_serving() # type: ignore[attr-defined]
440-
self.server._serving_forever_fut = self.server._loop.create_future() # type: ignore[attr-defined]
441-
442429
try:
443-
await self.server._serving_forever_fut # type: ignore[attr-defined]
430+
if not self.is_serving():
431+
await self.start_serving()
432+
# If close() is called, wait_closed() will return, and we'll exit.
433+
await self.wait_closed()
444434
except asyncio.CancelledError:
445435
try:
446436
self.close()
447437
await self.wait_closed()
448438
finally:
449439
raise
450-
finally:
451-
self.server._serving_forever_fut = None # type: ignore[attr-defined]
452440

453441
@property
454442
def sockets(self) -> tuple[socket.socket, ...]:

src/websockets/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
released = False
2222

23-
tag = version = commit = "17.1"
23+
tag = version = commit = "17.0.1"
2424

2525

2626
if not released: # pragma: no cover

tests/asyncio/test_server.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -599,19 +599,23 @@ async def test_serve_forever(self):
599599

600600
self.assertFalse(server.is_serving())
601601

602-
async def test_close_cancels_serve_forever(self):
603-
"""Closing the server cancels serve_forever()."""
604-
server = await serve(*args)
602+
async def test_serve_forever_starts_serving(self):
603+
"""serve_forever() starts serving when start_serving is False."""
604+
server = await serve(*args, start_serving=False)
605+
self.assertFalse(server.is_serving())
606+
605607
serve_forever_task = asyncio.get_running_loop().create_task(
606608
server.serve_forever()
607609
)
608610
await asyncio.sleep(0)
609611

610-
server.close()
612+
self.assertTrue(server.is_serving())
613+
614+
serve_forever_task.cancel()
611615
with self.assertRaises(asyncio.CancelledError):
612616
await serve_forever_task
613617

614-
async def test_serve_forever_closes_open_connections(self):
618+
async def test_canceling_serve_forever_closes_open_connections(self):
615619
"""Canceling serve_forever() closes open connections with code 1001."""
616620
server = await serve(*args)
617621
serve_forever_task = asyncio.get_running_loop().create_task(
@@ -631,29 +635,18 @@ async def test_serve_forever_closes_open_connections(self):
631635
with self.assertRaises(asyncio.CancelledError):
632636
await serve_forever_task
633637

634-
async def test_serve_forever_twice(self):
635-
"""Server rejects awaiting serve_forever() concurrently."""
638+
async def test_close_exits_serve_forever(self):
639+
"""Closing the server exits serve_forever()."""
636640
server = await serve(*args)
637641
serve_forever_task = asyncio.get_running_loop().create_task(
638642
server.serve_forever()
639643
)
640644
await asyncio.sleep(0)
645+
server.close()
641646

642-
with self.assertRaises(RuntimeError):
643-
await server.serve_forever()
644-
645-
serve_forever_task.cancel()
646-
with self.assertRaises(asyncio.CancelledError):
647+
async with asyncio.timeout(MS):
647648
await serve_forever_task
648649

649-
async def test_serve_forever_after_close(self):
650-
"""Server rejects serve_forever() after close()."""
651-
async with serve(*args) as server:
652-
pass
653-
654-
with self.assertRaises(RuntimeError):
655-
await server.serve_forever()
656-
657650
async def test_sockets(self):
658651
"""Server provides a sockets property."""
659652
async with serve(*args) as server:

0 commit comments

Comments
 (0)