freezegun icon indicating copy to clipboard operation
freezegun copied to clipboard

Improve asyncio support to avoid hangs of asyncio.sleep()

Open marcinsulikowski opened this issue 2 years ago • 0 comments

Clean up asyncio tests

We finish the cleanup of test_asyncio.py done in c60dd14b05f09c482d4:

  • We remove test_time_freeze_async_def because since c60dd14b05f09c48 it is exactly the same as test_time_freeze_coroutine.
  • We remove remnants of support of Pythons with no asyncio module because all Python interpreters supported by freezegun already support asyncio.

Remove Python 3.6 from tox.ini

PR #455 removed support for Python 3.6 so we remove it from tox.ini. This should allow us to use Python 3.7+-only features, like asyncio.run.

Avoid warning when running FreezeGun tests on Python 3.10+

To avoid this warning:

freezegun/tests/test_asyncio.py:12: DeprecationWarning: There is no current event loop
  asyncio.get_event_loop().run_until_complete(frozen_coroutine())

on Python 3.10+, we modify tests to use asyncio.run instead of asyncio.get_event_loop().run_until_complete, which [1] recommends:

asyncio.get_event_loop()
  (...)
  Consider also using the asyncio.run() function instead of using
  lower level functions to manually create and close an event loop.

  Deprecated since version 3.10: Deprecation warning is emitted
  if there is no running event loop. In future Python releases,
  this function will be an alias of get_running_loop().

asyncio.run has been added in Python 3.7 but we no longer support 3.6 (see #455) so we can use it.

[1] https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_event_loop

Avoid asyncio.sleep() hanging forever when time is frozen

The following code:

async def test():
    with freeze_time("2020-01-01"):
        await asyncio.sleep(0.01)

hangs forever since FreezeGun 1.1.0 because 1.1.0 started patching time.monotonic() (see #369) which is used internally by asyncio event loops to schedule code for execution in the future. This breaks many projects that uses FreezeGun to test asynchronous code.

We fix this by changing freeze_time to patch asyncio event loop's time() method in a way that it uses real monotonic time instead of the frozen one. Note that we couldn't achieve this by adding asyncio to DEFAULT_IGNORE_LIST in freezegun/config.py because any running async code has functions from the asyncio module on its stack -- adding asyncio to the ignore list would just disable freezing time in any async code. This is why we patch one method of a specific class instead.

This change not only fixes asyncio.sleep() but also things like asyncio.get_running_loop().call_later (for scheduling task execution in the future) which in turn makes things like timeouts work in async code while time is frozen. This may not be desired because some users may expect that execution of events scheduled to happen in the future can be controlled using FreezeGun. However, it's not easy to distinguish between things that users would like to see frozen time and those which should not (like asyncio.sleep()) because all of them use the same clock. Therefore, we opt for making all asyncio internals not affected by FreezeGun.

We also add more tests that verify how FreezeGun interacts with asyncio code, including tests that cover the scenario described in #437 which we aim to fix.

Closes #401 Closes #437

marcinsulikowski avatar Sep 14 '22 19:09 marcinsulikowski