Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to using asyncio.timeout instead of asyncio.wait_for #1307

Merged
merged 5 commits into from
Apr 2, 2023

Conversation

bdraco
Copy link
Contributor

@bdraco bdraco commented Mar 7, 2023

asyncio.wait_for creates a task whereas asyncio.timeout avoids doing this which decreases latency.

Fallback to using async_timeout when the python version is too old (<3.11)

asyncio.timeout will become the underlying implementation for async.wait_for in cpython 3.12
python/cpython#98518

@bdraco bdraco force-pushed the async_timeout branch 3 times, most recently from c6e2028 to 03ede5b Compare March 7, 2023 22:02
@bdraco bdraco marked this pull request as ready for review March 7, 2023 22:05
@aaugustin
Copy link
Member

Thank you for the suggestion! I think it's a good idea. Let's do it.

Until now, websockets has no dependencies. This removes an entire category of concerns like managing versions that, as a single maintainer, I don't want to deal with. I understand that this may not look like a big benefit but it is valuable to me.

In order to keep this benefit, I am tempted to do one of the following:

  1. create a small compatibility layer to use asyncio.wait_for on Python < 3.11 and asyncio.timeout on Python ≥ 3.12
  2. vendor CPython's or async-timeout's implementation — they're probably identical if async-timeout was merged into CPython.

Option 1 could be as simple as:

if sys.version_info[:2] >= (3, 12):

    async def asyncio_wait_for(coro, timeout):
        async with asyncio.timeout(timeout):
            await coro

else:

    asyncio_wait_for = asyncio.wait_for

In all cases, version compatibility code should go in websockets.legacy.compatibility rather than a new module.

@aaugustin
Copy link
Member

I'm aware that my option 1 doesn't yield performance benefits on Python < 3.12 while your version does. IMO those who care about performance optimizations available in Python 3.12 should upgrade to Python 3.12. It isn't my job to fake that for them.

@bdraco
Copy link
Contributor Author

bdraco commented Mar 8, 2023

I'll work on option 2 since the races in async.wait_for and the issue tickets they generated for cpython were part of the motivation for cpython switching out the implementation. It think it would be best if everything behaved the same regardless of the python version.

ex:
python/cpython#86296
python/cpython#81839
python/cpython#96764

@bdraco
Copy link
Contributor Author

bdraco commented Mar 8, 2023

Its passing locally now. Hopefully it should be good to go now

@@ -929,6 +929,7 @@ def test_answer_ping_does_not_crash_if_connection_closed(self):
self.receive_frame(Frame(True, OP_PING, b"test"))
self.receive_eof()

self.loop.run_until_complete(asyncio.sleep(MS))
Copy link
Contributor Author

@bdraco bdraco Mar 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is no task being created in protocol.close() anymore its happening faster. The test previously relied on the wait_for yielding to the event loop

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not surprised that you're finding this sort of issue in the test suite.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be best to use self.run_loop_once(). It's faster than waiting 1ms.

Yes, it's just 1ms, but over hundreds of tests, delays add up!

Copy link
Member

@aaugustin aaugustin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you again for the pull request. We're close to being able to merge it.

I left a few comments to polish it. Let me know if you'd like to do it or if I should finalize.

If you think this optimization deserves an entry in changelog.rst, feel free to add it.

@@ -929,6 +929,7 @@ def test_answer_ping_does_not_crash_if_connection_closed(self):
self.receive_frame(Frame(True, OP_PING, b"test"))
self.receive_eof()

self.loop.run_until_complete(asyncio.sleep(MS))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be best to use self.run_loop_once(). It's faster than waiting 1ms.

Yes, it's just 1ms, but over hundreds of tests, delays add up!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to put the code in src/websockets/legacy/async_timeout.py. We have no reason to nest it in under async_timeout/__init__.py.


if sys.version_info[:2] < (3, 11):
from .async_timeout import timeout as asyncio_timeout # pragma: no cover
else: # pragma: no cover
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made changes in 399db68 so you can drop the # pragma: no cover on the two lines above.

from asyncio import timeout as asyncio_timeout


__all__ = ["asyncio_timeout", "loop_if_py_lt_38"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't needed because aren't doing from websockets.legacy.compatibility import * anywhere.

Did I miss a reason why it would be useful? Or did you just add it for consistency?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it avoids this warning from mypy:

src/websockets/legacy/protocol.py:56: error: Module "websockets.legacy.compatibility" does not explicitly export attribute "asyncio_timeout" [attr-defined]

tox.ini Outdated
@@ -18,13 +18,13 @@ commands = python -W error::DeprecationWarning -W error::PendingDeprecationWarni
commands =
python -m coverage erase
python -m coverage run --source {envsitepackagesdir}/websockets,tests -m unittest {posargs}
python -m coverage report --show-missing --fail-under=100
python -m coverage report --show-missing --omit src/websockets/legacy/async_timeout/__init__.py --fail-under=100
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made changes in 399db68 to configure this in setup.cfg rather than in all coverage commands. It should be possible to remove this change.

@aaugustin
Copy link
Member

Also, It would be great if you could rebase and squash into a final commit to clean up the PR.

@bdraco
Copy link
Contributor Author

bdraco commented Mar 29, 2023

Late here. I'll take a look in the morning if the Home Assistant beta isn't too busy.

@aaugustin
Copy link
Member

No hurry :-)

I just finished #1245. I'm planning to make a release in a few days. I'd like to include this in the release.

It doesn't matter if we need a few more days — the last release was in October :-)

@aaugustin
Copy link
Member

aaugustin commented Apr 2, 2023

I squashed your commits and I'm finalizing the changes here: #1325.

EDIT - I just found out that I could push to you branch. I'm closing the other PR.

bdraco and others added 4 commits April 2, 2023 09:15
asyncio.wait_for creates a task whereas asyncio.timeout doesn't.

Fallback to a vendored version of async_timeout on Python < 3.11.

async.timeout will become the underlying implementation for
async.wait_for in Python 3.12:
python/cpython#98518
Some instances were missed in a previous commit.

Also update documentation.
They relied (accidentally) on wait_for() creating a task,
causing the event loop to run once when calling close().
@aaugustin aaugustin merged commit 901e434 into python-websockets:main Apr 2, 2023
@aaugustin
Copy link
Member

I filed nedbat/coveragepy#1598 for the coverage problem.

Thank you for your contribution!

@bdraco
Copy link
Contributor Author

bdraco commented Apr 4, 2023

I had this on my followup list and just returned to it and found you already got it over the finish line. Many thanks 👍 🐬 🥇

@aaugustin
Copy link
Member

This caused encode/uvicorn#1927.

@bdraco
Copy link
Contributor Author

bdraco commented Jul 29, 2023

Looks like you have already determined whats going on in encode/uvicorn#1927 (comment)

Let me know if I can help in any way.

Cheers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants