Skip to content

Commit 685d35b

Browse files
committed
Test that memory streams don't raise incorrectly (e.g. dropping items) when calling *_nowait() immediately after cancelling send()/receive()
* In the send_nowait() + receive() case, this bug could drop items. * In the send() + receive_nowait() case, this bug could cause send() to raise even though it succeeded.
1 parent 8e8b7f5 commit 685d35b

File tree

2 files changed

+131
-16
lines changed

2 files changed

+131
-16
lines changed

Diff for: tests/streams/test_memory.py

+112-16
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
fail_after,
1818
wait_all_tasks_blocked,
1919
)
20-
from anyio.abc import ObjectReceiveStream, ObjectSendStream
20+
from anyio.abc import ObjectReceiveStream, ObjectSendStream, TaskStatus
2121
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
2222

2323
if sys.version_info < (3, 11):
@@ -298,34 +298,130 @@ async def receiver() -> None:
298298
receive.close()
299299

300300

301-
async def test_cancel_during_receive() -> None:
301+
async def test_cancel_during_receive_after_send_nowait() -> None:
302302
"""
303-
Test that cancelling a pending receive() operation does not cause an item in the
304-
stream to be lost.
303+
Test that cancelling a pending receive() operation immediately after an item has
304+
been sent to that receiver does not cause the item to be lost.
305305
306306
"""
307-
receiver_scope = None
308307

309-
async def scoped_receiver() -> None:
310-
nonlocal receiver_scope
308+
async def scoped_receiver(task_status: TaskStatus[CancelScope]) -> None:
311309
with CancelScope() as receiver_scope:
310+
task_status.started(receiver_scope)
312311
received.append(await receive.receive())
313312

314313
assert receiver_scope.cancel_called
315314

316315
received: list[str] = []
317316
send, receive = create_memory_object_stream[str]()
318-
async with create_task_group() as tg:
319-
tg.start_soon(scoped_receiver)
320-
await wait_all_tasks_blocked()
321-
send.send_nowait("hello")
322-
assert receiver_scope is not None
323-
receiver_scope.cancel()
317+
with send, receive:
318+
async with create_task_group() as tg:
319+
receiver_scope = await tg.start(scoped_receiver)
320+
await wait_all_tasks_blocked()
321+
send.send_nowait("hello")
322+
receiver_scope.cancel()
324323

325-
assert received == ["hello"]
324+
assert received == ["hello"]
326325

327-
send.close()
328-
receive.close()
326+
327+
async def test_cancel_during_receive_before_send_nowait() -> None:
328+
"""
329+
Test that cancelling a pending receive() operation immediately before an item is
330+
sent to that receiver does not cause the item to be lost.
331+
332+
Note: AnyIO's memory stream behavior here currently differs slightly from Trio's
333+
memory channel behavior. Neither will lose items in this case, but Trio's memory
334+
channels use abort_fn to have an extra stage during cancellation delivery, so with a
335+
Trio memory channel send_nowait() will raise WouldBlock even if the receive()
336+
operation has not raised Cancelled yet. This test is intended only as a regression
337+
test for the bug where AnyIO dropped items in this situation; addressing the
338+
(possible) issue where AnyIO behaves slightly differently from Trio in this
339+
situation (in terms of when cancellation is delivered) will involve modifying this
340+
test. See #728.
341+
342+
"""
343+
344+
async def scoped_receiver(task_status: TaskStatus[CancelScope]) -> None:
345+
with CancelScope() as receiver_scope:
346+
task_status.started(receiver_scope)
347+
received.append(await receive.receive())
348+
349+
assert receiver_scope.cancel_called
350+
351+
received: list[str] = []
352+
send, receive = create_memory_object_stream[str]()
353+
with send, receive:
354+
async with create_task_group() as tg:
355+
receiver_scope = await tg.start(scoped_receiver)
356+
await wait_all_tasks_blocked()
357+
receiver_scope.cancel()
358+
send.send_nowait("hello")
359+
360+
assert received == ["hello"]
361+
362+
363+
async def test_cancel_during_send_after_receive_nowait() -> None:
364+
"""
365+
Test that cancelling a pending send() operation immediately after its item has been
366+
received does not cause send() to raise cancelled after successfully sending the
367+
item.
368+
369+
"""
370+
sender_woke = False
371+
372+
async def scoped_sender(task_status: TaskStatus[CancelScope]) -> None:
373+
nonlocal sender_woke
374+
with CancelScope() as sender_scope:
375+
task_status.started(sender_scope)
376+
await send.send("hello")
377+
sender_woke = True
378+
379+
send, receive = create_memory_object_stream[str]()
380+
with send, receive:
381+
async with create_task_group() as tg:
382+
sender_scope = await tg.start(scoped_sender)
383+
await wait_all_tasks_blocked()
384+
assert receive.receive_nowait() == "hello"
385+
sender_scope.cancel()
386+
387+
assert sender_woke
388+
389+
390+
async def test_cancel_during_send_before_receive_nowait() -> None:
391+
"""
392+
Test that cancelling a pending send() operation immediately before its item is
393+
received does not cause send() to raise cancelled after successfully sending the
394+
item.
395+
396+
Note: AnyIO's memory stream behavior here currently differs slightly from Trio's
397+
memory channel behavior. Neither will allow send() to successfully send an item but
398+
still raise cancelled after, but Trio's memory channels use abort_fn to have an
399+
extra stage during cancellation delivery, so with a Trio memory channel
400+
receive_nowait() will raise WouldBlock even if the send() operation has not raised
401+
Cancelled yet. This test is intended only as a regression test for the bug where
402+
send() incorrectly raised cancelled in this situation; addressing the (possible)
403+
issue where AnyIO behaves slightly differently from Trio in this situation (in terms
404+
of when cancellation is delivered) will involve modifying this test. See #728.
405+
406+
"""
407+
sender_woke = False
408+
409+
async def scoped_sender(task_status: TaskStatus[CancelScope]) -> None:
410+
nonlocal sender_woke
411+
with CancelScope() as sender_scope:
412+
task_status.started(sender_scope)
413+
await send.send("hello")
414+
sender_woke = True
415+
416+
send, receive = create_memory_object_stream[str]()
417+
with send, receive:
418+
async with create_task_group() as tg:
419+
sender_scope = await tg.start(scoped_sender)
420+
await wait_all_tasks_blocked()
421+
sender_scope.cancel()
422+
assert receive.receive_nowait() == "hello"
423+
424+
assert sender_woke
329425

330426

331427
async def test_close_receive_after_send() -> None:

Diff for: tests/test_synchronization.py

+19
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,25 @@ async def setter() -> None:
209209
assert setter_started
210210
assert waiter_woke
211211

212+
async def test_event_wait_before_cancel_before_set(self) -> None:
213+
setter_started = waiter_woke = False
214+
215+
async def setter() -> None:
216+
nonlocal setter_started
217+
setter_started = True
218+
assert not event.is_set()
219+
tg.cancel_scope.cancel()
220+
event.set()
221+
222+
event = Event()
223+
async with create_task_group() as tg:
224+
tg.start_soon(setter)
225+
await event.wait()
226+
waiter_woke = True
227+
228+
assert setter_started
229+
assert not waiter_woke
230+
212231
async def test_statistics(self) -> None:
213232
async def waiter() -> None:
214233
await event.wait()

0 commit comments

Comments
 (0)