| 
1 | 1 | import asyncio  | 
 | 2 | +import sys  | 
2 | 3 | import time  | 
3 | 4 | 
 
  | 
4 | 5 | import pytest  | 
5 | 6 | 
 
  | 
6 |  | -from async_timeout import timeout, timeout_at  | 
 | 7 | +from async_timeout import Timeout, timeout, timeout_at  | 
7 | 8 | 
 
  | 
8 | 9 | 
 
  | 
9 | 10 | @pytest.mark.asyncio  | 
@@ -344,3 +345,55 @@ async def test_deprecated_with() -> None:  | 
344 | 345 |     with pytest.warns(DeprecationWarning):  | 
345 | 346 |         with timeout(1):  | 
346 | 347 |             await asyncio.sleep(0)  | 
 | 348 | + | 
 | 349 | + | 
 | 350 | +@pytest.mark.skipif(sys.version_info < (3, 7), reason="Not supported in 3.6")  | 
 | 351 | +@pytest.mark.asyncio  | 
 | 352 | +async def test_race_condition_cancel_before() -> None:  | 
 | 353 | +    """Test race condition when cancelling before timeout.  | 
 | 354 | +
  | 
 | 355 | +    If cancel happens immediately before the timeout, then  | 
 | 356 | +    the timeout may overrule the cancellation, making it  | 
 | 357 | +    impossible to cancel some tasks.  | 
 | 358 | +    """  | 
 | 359 | + | 
 | 360 | +    async def test_task(deadline: float, loop: asyncio.AbstractEventLoop) -> None:  | 
 | 361 | +        # We need the internal Timeout class to specify the deadline (not delay).  | 
 | 362 | +        # This is needed to create the precise timing to reproduce the race condition.  | 
 | 363 | +        with Timeout(deadline, loop):  | 
 | 364 | +            await asyncio.sleep(10)  | 
 | 365 | + | 
 | 366 | +    loop = asyncio.get_running_loop()  | 
 | 367 | +    deadline = loop.time() + 1  | 
 | 368 | +    t = asyncio.create_task(test_task(deadline, loop))  | 
 | 369 | +    loop.call_at(deadline, t.cancel)  | 
 | 370 | +    await asyncio.sleep(1.1)  | 
 | 371 | +    # If we get a TimeoutError, then the code is broken.  | 
 | 372 | +    with pytest.raises(asyncio.CancelledError):  | 
 | 373 | +        await t  | 
 | 374 | + | 
 | 375 | + | 
 | 376 | +@pytest.mark.skipif(sys.version_info < (3, 7), reason="Not supported in 3.6")  | 
 | 377 | +@pytest.mark.asyncio  | 
 | 378 | +async def test_race_condition_cancel_after() -> None:  | 
 | 379 | +    """Test race condition when cancelling after timeout.  | 
 | 380 | +
  | 
 | 381 | +    Similarly to the previous test, if a cancel happens  | 
 | 382 | +    immediately after the timeout (but before the __exit__),  | 
 | 383 | +    then the explicit cancel can get overruled again.  | 
 | 384 | +    """  | 
 | 385 | + | 
 | 386 | +    async def test_task(deadline: float, loop: asyncio.AbstractEventLoop) -> None:  | 
 | 387 | +        # We need the internal Timeout class to specify the deadline (not delay).  | 
 | 388 | +        # This is needed to create the precise timing to reproduce the race condition.  | 
 | 389 | +        with Timeout(deadline, loop):  | 
 | 390 | +            await asyncio.sleep(10)  | 
 | 391 | + | 
 | 392 | +    loop = asyncio.get_running_loop()  | 
 | 393 | +    deadline = loop.time() + 1  | 
 | 394 | +    t = asyncio.create_task(test_task(deadline, loop))  | 
 | 395 | +    loop.call_at(deadline + 0.000001, t.cancel)  | 
 | 396 | +    await asyncio.sleep(1.1)  | 
 | 397 | +    # If we get a TimeoutError, then the code is broken.  | 
 | 398 | +    with pytest.raises(asyncio.CancelledError):  | 
 | 399 | +        await t  | 
0 commit comments