Skip to content

Commit fef04d8

Browse files
committed
Fix backpressure in the Trio implementation.
Previously, saturating the buffer would crash, due to attempting 1/ to re-acquire a lock and 2/ to release it from another task. Test backpressure in the asyncio and threading implementations.
1 parent eb3600c commit fef04d8

5 files changed

Lines changed: 115 additions & 5 deletions

File tree

docs/project/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ Bug fixes
3838
* Restored compatibility of :meth:`~asyncio.server.Server.serve_forever` in the
3939
:mod:`asyncio` implementation with third-party event loops such as uvloop_.
4040

41+
* Prevented the Trio implementation from crashing when backpressure kicks in,
42+
i.e. when receiving data faster than the application can process it.
43+
4144
.. _uvloop: https://uvloop.readthedocs.io/
4245

4346
.. _17.0:

src/websockets/trio/connection.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ def __init__(
8484
"""Opening handshake response."""
8585

8686
# Lock stopping reads when the assembler buffer is full.
87-
self.recv_flow_control = trio.Lock()
87+
self.recv_flow_control: trio.Event | None = None
8888

8989
# Assembler turning frames into messages and serializing reads.
9090
self.recv_messages = Assembler(
9191
max_queue_high,
9292
max_queue_low,
93-
pause=self.recv_flow_control.acquire_nowait,
94-
resume=self.recv_flow_control.release,
93+
pause=self.pause_reading,
94+
resume=self.resume_reading,
9595
)
9696

9797
# Deadline for the closing handshake.
@@ -882,8 +882,8 @@ async def recv_events(self) -> None:
882882
while True:
883883
try:
884884
# If the assembler buffer is full, block until it drains.
885-
async with self.recv_flow_control:
886-
pass
885+
if self.recv_flow_control is not None:
886+
await self.recv_flow_control.wait()
887887
data = await self.stream.receive_some()
888888
except Exception as exc:
889889
if self.debug:
@@ -966,6 +966,16 @@ async def recv_events(self) -> None:
966966
# This isn't expected to raise an exception.
967967
await self.close_stream()
968968

969+
def pause_reading(self) -> None:
970+
"""Pause recv_events() until resume_reading() is called."""
971+
self.recv_flow_control = trio.Event()
972+
973+
def resume_reading(self) -> None:
974+
"""Resume recv_events() after pause_reading() was called."""
975+
assert self.recv_flow_control is not None
976+
self.recv_flow_control.set()
977+
self.recv_flow_control = None
978+
969979
@contextlib.asynccontextmanager
970980
async def send_context(
971981
self,

tests/asyncio/test_connection.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,38 @@ async def test_close_reason(self):
12551255
"""Connection has a close_reason attribute."""
12561256
self.assertIsNone(self.connection.close_reason)
12571257

1258+
# Test backpressure.
1259+
1260+
async def test_backpressure(self):
1261+
"""Connection stops reading from the network when the buffer fills up."""
1262+
self.connection.recv_messages.high = 1
1263+
self.connection.recv_messages.low = 0
1264+
frames_buffer = self.connection.recv_messages.frames
1265+
1266+
await self.remote_connection.send("A")
1267+
await self.remote_connection.send("B")
1268+
1269+
# Assembler buffer is above the high water mark. Backpressure kicks in.
1270+
await self.wait_for_remote_side()
1271+
self.assertFalse(self.transport.is_reading())
1272+
self.assertEqual(len(frames_buffer), 2)
1273+
1274+
await self.remote_connection.send("C")
1275+
1276+
# A third frame is sent by the peer but the connection doesn't read it.
1277+
await self.wait_for_remote_side()
1278+
self.assertEqual(len(frames_buffer), 2)
1279+
1280+
self.assertEqual(await self.connection.recv(), "A")
1281+
self.assertEqual(await self.connection.recv(), "B")
1282+
1283+
# Draining the assembler buffer below the low water mark resumes reading.
1284+
await self.wait_for_remote_side()
1285+
self.assertTrue(self.transport.is_reading())
1286+
self.assertEqual(len(frames_buffer), 1)
1287+
1288+
self.assertEqual(await self.connection.recv(), "C")
1289+
12581290
# Test reporting of network errors.
12591291

12601292
async def test_writing_in_data_received_fails(self):

tests/sync/test_connection.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,38 @@ def test_close_reason(self):
949949
"""Connection has a close_reason attribute."""
950950
self.assertIsNone(self.connection.close_reason)
951951

952+
# Test backpressure.
953+
954+
def test_backpressure(self):
955+
"""Connection stops reading from the network when the buffer fills up."""
956+
self.connection.recv_messages.high = 1
957+
self.connection.recv_messages.low = 0
958+
frames_buffer = self.connection.recv_messages.frames
959+
960+
self.remote_connection.send("A")
961+
self.remote_connection.send("B")
962+
963+
# Assembler buffer is above the high water mark. Backpressure kicks in.
964+
self.wait_for_remote_side()
965+
self.assertTrue(self.connection.recv_flow_control.locked())
966+
self.assertEqual(frames_buffer.qsize(), 2)
967+
968+
self.remote_connection.send("C")
969+
970+
# A third frame is sent by the peer but the connection doesn't read it.
971+
self.wait_for_remote_side()
972+
self.assertEqual(frames_buffer.qsize(), 2)
973+
974+
self.assertEqual(self.connection.recv(), "A")
975+
self.assertEqual(self.connection.recv(), "B")
976+
977+
# Draining the assembler buffer below the low water mark resumes reading.
978+
self.wait_for_remote_side()
979+
self.assertFalse(self.connection.recv_flow_control.locked())
980+
self.assertEqual(frames_buffer.qsize(), 1)
981+
982+
self.assertEqual(self.connection.recv(), "C")
983+
952984
# Test reporting of network errors.
953985

954986
def test_writing_in_recv_events_fails(self):

tests/trio/test_connection.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,39 @@ async def test_close_reason(self):
12101210
"""Connection has a close_reason attribute."""
12111211
self.assertIsNone(self.connection.close_reason)
12121212

1213+
# Test backpressure.
1214+
1215+
async def test_backpressure(self):
1216+
"""Connection stops reading from the network when the buffer fills up."""
1217+
self.connection.recv_messages.high = 1
1218+
self.connection.recv_messages.low = 0
1219+
frames_buffer = self.connection.recv_messages.recv_frames
1220+
1221+
await self.remote_connection.send("A")
1222+
await self.remote_connection.send("B")
1223+
1224+
# Assembler buffer is above the high water mark. Backpressure kicks in.
1225+
await trio.testing.wait_all_tasks_blocked()
1226+
self.assertIsNotNone(self.connection.recv_flow_control)
1227+
self.assertFalse(self.connection.recv_flow_control.is_set())
1228+
self.assertEqual(frames_buffer.statistics().current_buffer_used, 2)
1229+
1230+
await self.remote_connection.send("C")
1231+
1232+
# A third frame is sent by the peer but the connection doesn't read it.
1233+
await trio.testing.wait_all_tasks_blocked()
1234+
self.assertEqual(frames_buffer.statistics().current_buffer_used, 2)
1235+
1236+
self.assertEqual(await self.connection.recv(), "A")
1237+
self.assertEqual(await self.connection.recv(), "B")
1238+
1239+
# Draining the assembler buffer below the low water mark resumes reading.
1240+
await trio.testing.wait_all_tasks_blocked()
1241+
self.assertIsNone(self.connection.recv_flow_control)
1242+
self.assertEqual(frames_buffer.statistics().current_buffer_used, 1)
1243+
1244+
self.assertEqual(await self.connection.recv(), "C")
1245+
12131246
# Test reporting of network errors.
12141247

12151248
async def test_writing_in_recv_events_fails(self):

0 commit comments

Comments
 (0)