-
Notifications
You must be signed in to change notification settings - Fork 14.3k
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
AIP-84 Convert async route to sync routes #43797
AIP-84 Convert async route to sync routes #43797
Conversation
Good catch,, I forgot the private API. Updated thanks Brent. |
Hey @pierrejeambrun, I've one concern on this - if we use sync path funcs, the FastAPI requests will run in a threadpool (one request per thread), consuming more memory per request and will limit throughput on our new APIs, as there's a default limit of 40 threads on the threadpool, please see: encode/starlette#1724 (context: AnyIO is now used for async IO by Starlette, which in turn is used by FastAPI to handle http requests). So, just a suggestion - instead of changing all the path funcs from async to sync, it'll be great if wrap the blocking (sync) function calls inside the path funcs to make them async, using |
Hm, I don't get why this code: @route.get(...)
def handler(...):
something = session.get(...)
other = session.select(...)
await asyncio.to_thread(handler) does limit thoughput, but this doesn't: @route.get(...)
async def handler(...):
something = await asyncio.to_thread(session.get, ...)
other = await asyncio.to_thread(session.select, ...) Could you please elaborate? |
I agree with @dolfinus, running in a separate thread manually or leveraging FastAPI to do so is more or less the same. (just less work and more code maintainability to let FastAPI handle that). Long term we will rewrite that with full async support, in the meantime FastAPI is just |
Hey @dolfinus, yes, the throughput in this case would be very similar for both snippets, because when calling Example: As our new APIs have a mix of CPU-bound (e.g. common params resolution, data checks, Pydantic validations, etc.) and IO-bound (e.g. DB queries, network calls) activities, I guess we could tune it something like this: @route.get(...)
async def handler(...):
# CPU bound
if some_check:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Not Found")
# IO bound, sent to it's own thread
something = await asyncio.to_thread(session.get, ...)
# CPU bound again
return SomePydanticModel(something) We could alternatively use |
That would be lovely, thank you! :) |
I think that would be a lot of work to maintain + code becomes hard to read + 1 mistake (someone forget to manually put into the threadpool a blocking IO call) and then the main event loop is blocked... I think we can start like that, and if it's not enough we can go deeper into the fine tuning of what is executed in the main even loop and what is run in a separate thread. I believe CPU bound operations run in a separate thread won't bottleneck. (And if they do, most likely the main even loop would struggle too, so we would have another problem here) |
Yes sure, sounds good! Thanks @pierrejeambrun 👍🏽 |
* AIP-84 convert async route to sync routes * Update following code review * Fix CI
As discussed in #43718 (comment), routes with blocking I/O code should be sync to not block main event loop. (db access, disk read, network call, etc...)
More information in FastAPI documentation https://fastapi.tiangolo.com/async/#path-operation-functions.
This PR converts all of the endpoints, all that can me converted to
async def
when we implement full async support.(I recall Maybe one or two endpoints that are purely in memory and could stay async but I didn't bother to make an exception for them)