Skip to content

Commit 6e6bc2b

Browse files
committed
Handle ConnectionClosed exception in keepalive_ping.
Fix #551. Thanks @Harmon758 for diagnosing the root cause and proposing a fix.
1 parent c0c31b8 commit 6e6bc2b

File tree

3 files changed

+38
-5
lines changed

3 files changed

+38
-5
lines changed

docs/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ Also:
5959
* Improved support for sending fragmented messages by accepting asynchronous
6060
iterators in :meth:`~protocol.WebSocketCommonProtocol.send`.
6161

62+
* Prevented spurious log messages about :exc:`~exceptions.ConnectionClosed`
63+
exceptions in keepalive ping task.
64+
6265
* Avoided a crash of a ``extra_headers`` callable returns ``None``.
6366

6467
* Enabled readline in the interactive client.

src/websockets/protocol.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,7 @@ async def keepalive_ping(self) -> None:
980980
981981
This coroutine exits when the connection terminates and one of the
982982
following happens:
983+
983984
- :meth:`ping` raises :exc:`ConnectionClosed`, or
984985
- :meth:`close_connection` cancels :attr:`keepalive_ping_task`.
985986
@@ -991,11 +992,12 @@ async def keepalive_ping(self) -> None:
991992
while True:
992993
await asyncio.sleep(self.ping_interval, loop=self.loop)
993994

994-
# ping() cannot raise ConnectionClosed, only CancelledError:
995-
# - If the connection is CLOSING, keepalive_ping_task will be
996-
# canceled by close_connection() before ping() returns.
997-
# - If the connection is CLOSED, keepalive_ping_task must be
998-
# canceled already.
995+
# ping() raises CancelledError if the connection is closed,
996+
# when close_connection() cancels self.keepalive_ping_task.
997+
998+
# ping() raises ConnectionClosed if the connection is lost,
999+
# when connection_lost() calls abort_keepalive_pings().
1000+
9991001
ping_waiter = await self.ping()
10001002

10011003
if self.ping_timeout is not None:
@@ -1011,6 +1013,9 @@ async def keepalive_ping(self) -> None:
10111013
except asyncio.CancelledError:
10121014
raise
10131015

1016+
except ConnectionClosed:
1017+
pass
1018+
10141019
except Exception:
10151020
logger.warning("Unexpected exception in keepalive ping task", exc_info=True)
10161021

tests/test_protocol.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,31 @@ def test_keepalive_ping_stops_when_connection_closed(self):
10741074
# The keepalive ping task terminated.
10751075
self.assertTrue(self.protocol.keepalive_ping_task.cancelled())
10761076

1077+
def test_keepalive_ping_does_not_crash_when_connection_lost(self):
1078+
self.restart_protocol_with_keepalive_ping()
1079+
# Clog incoming queue. This lets connection_lost() abort pending pings
1080+
# with a ConnectionClosed exception before transfer_data_task
1081+
# terminates and close_connection cancels keepalive_ping_task.
1082+
self.protocol.max_queue = 1
1083+
self.receive_frame(Frame(True, OP_TEXT, b"1"))
1084+
self.receive_frame(Frame(True, OP_TEXT, b"2"))
1085+
# Ping is sent at 3ms.
1086+
self.loop.run_until_complete(asyncio.sleep(4 * MS))
1087+
ping_waiter, = tuple(self.protocol.pings.values())
1088+
# Connection drops.
1089+
self.receive_eof()
1090+
self.loop.run_until_complete(self.protocol.wait_closed())
1091+
1092+
# The ping waiter receives a ConnectionClosed exception.
1093+
with self.assertRaises(ConnectionClosed):
1094+
ping_waiter.result()
1095+
# The keepalive ping task terminated properly.
1096+
self.assertIsNone(self.protocol.keepalive_ping_task.result())
1097+
1098+
# Unclog incoming queue to terminate the test quickly.
1099+
self.loop.run_until_complete(self.protocol.recv())
1100+
self.loop.run_until_complete(self.protocol.recv())
1101+
10771102
def test_keepalive_ping_with_no_ping_interval(self):
10781103
self.restart_protocol_with_keepalive_ping(ping_interval=None)
10791104

0 commit comments

Comments
 (0)