|
2 | 2 | import sys |
3 | 3 | import types |
4 | 4 | import asyncio |
| 5 | +import threading |
5 | 6 | import trio |
6 | 7 | import trio.testing |
7 | 8 | import trio_asyncio |
@@ -307,3 +308,62 @@ async def iterate_one(label, extra=""): |
307 | 308 | } |
308 | 309 | finally: |
309 | 310 | sys.unraisablehook = prev_hook |
| 311 | + |
| 312 | + |
| 313 | +def test_async_handle_cancelled_before_dequeue(): |
| 314 | + """ |
| 315 | + Regression test for the wedge where an AsyncHandle queued onto _q_send |
| 316 | + and then cancelled before _main_loop_one dequeues it would never resolve |
| 317 | + its result_future. |
| 318 | +
|
| 319 | + Run trio.run in a daemon thread with a hard 5s join timeout: when the bug |
| 320 | + is present, both run_aio_future and the open_loop shutdown wedge on the |
| 321 | + dropped AsyncHandle, so an in-trio timeout cannot recover the test. |
| 322 | + """ |
| 323 | + |
| 324 | + async def trio_main(): |
| 325 | + async with trio_asyncio.open_loop() as loop: |
| 326 | + # Define a trio function for the AsyncHandle to wrap. |
| 327 | + async def trio_proc(): |
| 328 | + return 42 |
| 329 | + |
| 330 | + # Queue an AsyncHandle on _q_send. trio_as_future is sync — it |
| 331 | + # creates the AsyncHandle, calls _queue_handle (= _q_send.send_nowait), |
| 332 | + # and returns the asyncio.Future immediately. _main_loop hasn't |
| 333 | + # had a chance to run yet because we haven't yielded. |
| 334 | + f = loop.trio_as_future(trio_proc) |
| 335 | + assert not f.done() |
| 336 | + |
| 337 | + # Cancel the future BEFORE yielding. wrapped_cancel marks the |
| 338 | + # AsyncHandle._cancelled = True but leaves f PENDING. |
| 339 | + assert f.cancel() is True |
| 340 | + assert not f.done() # the bug is here: f is PENDING despite cancel |
| 341 | + |
| 342 | + # Now yield. _main_loop dequeues the cancelled handle. Without |
| 343 | + # the fix, _main_loop_one drops it at `if obj._cancelled: return` |
| 344 | + # and f stays PENDING forever. With the fix, _run is called and |
| 345 | + # marks f cancelled. |
| 346 | + try: |
| 347 | + await loop.run_aio_future(f) |
| 348 | + except asyncio.CancelledError: |
| 349 | + pass |
| 350 | + |
| 351 | + assert f.done() |
| 352 | + assert f.cancelled() |
| 353 | + |
| 354 | + error = [] |
| 355 | + |
| 356 | + def runner(): |
| 357 | + try: |
| 358 | + trio.run(trio_main) |
| 359 | + except BaseException as e: |
| 360 | + error.append(e) |
| 361 | + |
| 362 | + thread = threading.Thread(target=runner, daemon=True) |
| 363 | + thread.start() |
| 364 | + thread.join(timeout=5.0) |
| 365 | + |
| 366 | + if thread.is_alive(): |
| 367 | + pytest.fail("AsyncHandle.result_future was never resolved within 5s") |
| 368 | + if error: |
| 369 | + raise error[0] |
0 commit comments