Skip to content

Make asyncio.to_thread() work, with the same semantics as normal asyncio#930

Merged
4383 merged 8 commits intomasterfrom
929-eventlet-asyncio-hub-does-not-run-as-documented
Mar 14, 2024
Merged

Make asyncio.to_thread() work, with the same semantics as normal asyncio#930
4383 merged 8 commits intomasterfrom
929-eventlet-asyncio-hub-does-not-run-as-documented

Conversation

@itamarst
Copy link
Copy Markdown
Contributor

Fixes #929

@itamarst itamarst linked an issue Mar 11, 2024 that may be closed by this pull request
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 11, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 56%. Comparing base (48c259c) to head (31ee83b).

Additional details and impacted files
@@          Coverage Diff          @@
##           master   #930   +/-   ##
=====================================
- Coverage      56%    56%   -1%     
=====================================
  Files          89     89           
  Lines        9756   9764    +8     
  Branches     1817   1818    +1     
=====================================
+ Hits         5472   5473    +1     
- Misses       3908   3918   +10     
+ Partials      376    373    -3     
Flag Coverage Δ
ipv6 23% <37%> (+<1%) ⬆️
py310asyncio 52% <100%> (-1%) ⬇️
py310epolls 53% <37%> (-1%) ⬇️
py310poll 53% <37%> (-1%) ⬇️
py310selects 53% <37%> (-1%) ⬇️
py311asyncio 52% <100%> (-1%) ⬇️
py311epolls 53% <37%> (+<1%) ⬆️
py312asyncio 50% <100%> (-1%) ⬇️
py312epolls 51% <37%> (-1%) ⬇️
py37asyncio 50% <62%> (-1%) ⬇️
py37epolls 51% <37%> (+<1%) ⬆️
py38asyncio 51% <62%> (-1%) ⬇️
py38epolls 53% <37%> (+<1%) ⬆️
py38openssl 51% <37%> (-1%) ⬇️
py38poll 53% <37%> (-1%) ⬇️
py38selects 53% <37%> (-1%) ⬇️
py39asyncio 51% <100%> (-1%) ⬇️
py39dnspython1 51% <37%> (-1%) ⬇️
py39epolls 53% <37%> (-1%) ⬇️
py39poll 53% <37%> (-1%) ⬇️
py39selects 52% <37%> (-1%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@itamarst itamarst requested a review from 4383 March 11, 2024 17:29
@4383
Copy link
Copy Markdown
Member

4383 commented Mar 13, 2024

Yanks my previous comment here as the previous discussion has been resolved and so the comment is hidden...

This sentence: As the hubs are event loops made to dispatch I/O events and to schedule threads come from the Eventlet doc https://eventlet.readthedocs.io/en/latest/hubs.html#how-the-hubs-work

I agree with you, and with your previous analyze, we can't runs blocking things in the event loop threads, else it would block all the rest.

However, one thing related to the internal design of aiohttp bothers me.

The DNS lookups made by aiohttp and that we can observe in the stack trace in the description of #929, rely of aiohttp's ThreadedResolver.

The threaded resolver is the default used by aiodns:

This is a by design choice because its authors think that AsyncResolver of aiodns works in 99% of cases but sometimes demonstrate different behavior in very rare conditions (aio-libs/aiohttp#2237, aio-libs/aiohttp#559): aio-libs/aiohttp#2254.

A comment in the ThreadedExecutor says:

uses an Executor for synchronous getaddrinfo() calls. concurrent.futures.ThreadPoolExecutor is used by default.

But, internally, this executor calls asyncio.loop.getaddrinfo which is an asynchronous version of socket.getaddrinfo(). See: https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.getaddrinfo. asyncio.loop.getaddrinfo is a coroutine... I won't expect it would block...

So, one thing is not clear for me. Why aiohttp should execute this coroutine in a threaded way?

Also I didn't find where the resolver is launched. I found several reference to asyncio.loop.run_in_executor (https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor) in aiohttp, maybe one of them launch this resolver, but if yes, as this resolver use asyncio.loop.getaddrinfo, and should not block, it is not clear to me why it should be made in a thread... .

Leading us to the bug we observed and to this PR...

In the example given in the issue at the origin of this PR, if one using the AsyncResolver it would surely have hidden this problem:

ClientSession(connector=aiohttp.TCPConnector(resolver=aiohttp.AsyncResolver()))

Fortunately the ThreadedResolver is the default one.

@itamarst
Copy link
Copy Markdown
Contributor Author

Coroutines never run in a thread other than the main event loop. Blocking DNS lookups (and other blocking APIs) will run in a thread pool.

@itamarst
Copy link
Copy Markdown
Contributor Author

To expand: concurrent.futures.thread thread pool gives you API that returns Future instances, which can be turned into asyncio-style Future instances, which can then be await-ed. But there's no coroutine involved.

@itamarst itamarst requested a review from 4383 March 13, 2024 17:48
@itamarst itamarst changed the title Make asyncio.to_pool() work, with the same semantics as normal asyncio Make asyncio.to_thread() work, with the same semantics as normal asyncio Mar 13, 2024
@4383
Copy link
Copy Markdown
Member

4383 commented Mar 13, 2024

Coroutines never run in a thread other than the main event loop. Blocking DNS lookups (and other blocking APIs) will run in a thread pool.

Yeah, that's my point... asyncio.loop.getaddrinfo is a coroutine (https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.getaddrinfo), so why aiohttp run it in thread pool?

IMO, using the main event loop to run asyncio.loop.getaddrinfo would have been ok.


``concurrent.futures.thread`` just uses normal threads, not Eventlet's special threads.
Similarly, ``asyncio.to_pool()`` specifically requires regular blocking code, it won't work correctly with Eventlet code.
Similarly, ``asyncio.to_thread()`` specifically requires regular blocking code, it won't work correctly with Eventlet code.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could be worth adding a link toward the official python doc https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread

It could be added with a follow up patch.

@4383 4383 merged commit 520c6e2 into master Mar 14, 2024
@4383 4383 deleted the 929-eventlet-asyncio-hub-does-not-run-as-documented branch March 14, 2024 08:56
@4383 4383 mentioned this pull request Mar 15, 2024
clrpackages pushed a commit to clearlinux-pkgs/pypi-eventlet that referenced this pull request Jul 10, 2024
…rsion 0.36.1

0.36.1
======

* [fix] eventlet.websocket is not always used from eventlet.wsgi, so do not assume eventlet.set_idle exists eventlet/eventlet#949

0.36.0
======

* [fix] Make sure asyncio hub doesn't use greendns for asyncio DNS APIs eventlet/eventlet#938
* [fix] Make asyncio.to_thread work with the same semantics as normal asyncio eventlet/eventlet#930
* [fix] Refactor congruence checks based on assert at runtime eventlet/eventlet#932
* [tests] Run tests on macOS in CI, and some fixes to get it in reasonable state (#list eventlet/eventlet#934
* [fix] Fix wsgi.server shutdown for in-flight requests eventlet/eventlet#912
* [feature] Add debug convenience helpers - asyncio, threads eventlet/eventlet#925
* [fix] Handle errors better. eventlet/eventlet#923

(NEWS truncated at 15 lines)

CVEs fixed in this build:
CVE-2023-29483
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.

eventlet asyncio hub does not run as documented

3 participants