-
-
Notifications
You must be signed in to change notification settings - Fork 547
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
FastAPI and Strawberry, Losing Concurrency when Following Intro Example #3187
Comments
Thanks for the repo! I think that's quite useful, I'll double check that soon 😊 My understand is that simple resolver (ie resolver not doing anything with network) can be sync as it shouldn't impact the concurrency, but maybe I'm wrong? |
Can confirm that, I think it should be expected from async frameworks that doing any synchronous IO would block the event loop if it's not mentioned otherwise in the documentation 🤔 |
@ThirVondukr thanks for your comment! From the docs of FastAPI:
The "right thing" in case of a sync path operation function for FastAPI is to run that path operation function in a thread pool. I disagree with your statement that running sync resolvers in a threadpool would be inefficient: Enabling concurrency through a threadpool is one way, enabling concurrency using async IO and an event loop is another way. Both ways have their own performance characteristics. Simply not getting any concurrency (as in the case of using sync resolvers naively in Strawberry) is certainly worse than using a threadpool, in most scenarios. To illustrate, I've expanded my testing app, and done a performance test with concurrency on a sync FastAPI route (not involving Strawberry). I suggest to make one of two possible improvements:
Keen to hear your thoughts! |
The issue with making sync resolvers magically async is that graphql potentially would have to resolve thousands of them per single request 🤔, that's why I think it's inefficient By the way, currently starwberry runs resolvers using async def await_maybe(value: AwaitableOrValue[T]) -> T:
if inspect.isawaitable(value):
return await value
return cast(T, value) |
Ah, yes, now I understand your concern :) I agree, making all sync resolvers magically async (by using a threadpool) is not a good idea. Maybe apply it to the top-level queries and mutations only? |
yes, and the current implementation of gather is quite slow for function that are not async (might be better in 3.12 or 3.13 though) Also, every single field is a resolver, for example: query {
user { # this is a call to a resolver
field_a # also this
field_b # and this one
}
}
I think I'd prefer to recommend to users to use async when possible more than us using threadpools, so that they can also leverage things like dataloaders Also I wonder if we should extend your benchmarks to use |
@patrick91 Same behavior: @strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
time.sleep(1)
return "Hello World" doctor@main MINGW64 /c/dev/python/sandbox
$ ./hey -c 100 -z 10s -D body.txt -m POST -T "application/json" http://127.0.0.1:8000/graphql
Summary:
Total: 23.9953 secs
Slowest: 19.0458 secs
Fastest: 1.0125 secs
Average: 10.0322 secs
Requests/sec: 4.5426
Total data: 646 bytes
Size/request: 34 bytes
Response time histogram:
1.013 [1] |■■■■■■■■■■■■■■■■■■■■
2.816 [1] |■■■■■■■■■■■■■■■■■■■■
4.619 [2] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
6.423 [2] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
8.226 [2] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
10.029 [1] |■■■■■■■■■■■■■■■■■■■■
11.832 [2] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
13.636 [2] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
15.439 [2] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
17.242 [2] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
19.046 [2] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
Latency distribution:
10% in 3.0163 secs
25% in 6.0296 secs
50% in 11.0347 secs
75% in 16.0415 secs
90% in 19.0458 secs
0% in 0.0000 secs
0% in 0.0000 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0010 secs, 1.0125 secs, 19.0458 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
req write: 0.0002 secs, 0.0000 secs, 0.0005 secs
resp wait: 10.0309 secs, 1.0117 secs, 19.0439 secs
resp read: 0.0000 secs, 0.0000 secs, 0.0001 secs
Status code distribution:
[200] 19 responses
Error distribution:
[6] Post "http://127.0.0.1:8000/graphql": EOF
[84] Post "http://127.0.0.1:8000/graphql": context deadline exceeded (Client.Timeout exceeded while awaiting headers) |
interesting, I think it might be also worth doing tests with a database 😊 We should probably have a test suite somewhere for this, what do you think? |
If you're using synchronous IO it wouldn't matter if it's |
I am having the same issue with database calls. In this case I either resolve the whole database model with joins from top level (loosing the dynamic schema resolution in a nutshell) or wrap every function that has a database call with As a suggestion, maybe alongside |
@Aponace Can you use async resolvers instead? 🤔 |
No quite possible (for me) as the database handler is being used by hundreds of functions outside of strawberry. That would require me to write another handler in async, which would be a duplicate of the sync one, then adjust the code everywhere and make sure everyone use the correct resolver in the correct context (would be a nightmare). In terms of time and effort the easiest path is to annotate with If someone is writing the code from scratch and they are aware of this issue, they can write an async db handler. But, I think most of the people will write their code synchronous and then, when they hit performance issues, will discover this thread (same as me ~2 years into development). |
I've indeed had similar results in tests with
Same situation here. Due to the luxury of being able to scale out through k8s I hadn't really dived into our mediocre graphql query performance until last week. Large part of it is of our own doing - performing sync I/O within async resolvers. And indeed we also have sync resolvers, as mixing sync and async is documented without the caveat that sync resolvers block the event loop. While I can understand the reasoning behind it, it did surprise me. Of course the best solution is to go fully async, but we're in a similar place where the sync database handler is used in many other places and refactoring/separating that is going to take a lot of work. I'll probably look at a similar make_async workaround for the top-level Query resolvers. |
@Chris927 your make_async wrapper worked for my Strawberry FastAPI implementation. Thanks a mil |
Similar situation here as @Mark90. We've been scaling k8s pods and hadn't really noticed just how bad each pod was at handling concurrency. It was by digging into some issues with our healthchecks that eventually discovered that all of our sync GQL mutations were completely blocking and FastAPI couldn't even handle a concurrent health check whether we implemented the endpoint using Agreed some of the issues are on our side, but this really should, at the very least, be mentioned in the docs. I looked all over the Strawberry/FastAPI docs regarding how sync or async endpoints behave and found nothing. I had to spend hours adding Any FastAPI user would assume that the behavior is similar to FastAPI: if the function is |
@aiguofer would you mind sending a PR to explain this? 😊 |
In short, using the example on how to integrate Strawberry with FastAPI, you end up with a service that handles GraphQL requests sequentially. This defeats the purpose of FastAPI.
My request: Make this clear in the docs. Possibly by changing the example to async query functions, and adding a warning about using sync query functions.
I've documented my understanding and findings in this repo.
Am I missing anything? Let me know! I am happy to submit a CR to update the docs, if adequate.
Upvote & Fund
The text was updated successfully, but these errors were encountered: