-
-
Notifications
You must be signed in to change notification settings - Fork 524
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
Conversation
c6e2028
to
03ede5b
Compare
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:
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. |
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. |
I'll work on option 2 since the races in ex: |
Its passing locally now. Hopefully it should be good to go now |
tests/legacy/test_protocol.py
Outdated
@@ -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)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this 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.
tests/legacy/test_protocol.py
Outdated
@@ -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)) |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
Also, It would be great if you could rebase and squash into a final commit to clean up the PR. |
Late here. I'll take a look in the morning if the Home Assistant beta isn't too busy. |
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 :-) |
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. |
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().
I filed nedbat/coveragepy#1598 for the coverage problem. Thank you for your contribution! |
I had this on my followup list and just returned to it and found you already got it over the finish line. Many thanks 👍 🐬 🥇 |
This caused encode/uvicorn#1927. |
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 |
asyncio.wait_for
creates a task whereasasyncio.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 forasync.wait_for
in cpython 3.12python/cpython#98518