-
Notifications
You must be signed in to change notification settings - Fork 423
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
Interop with async? (Tokio/Futures) #2
Comments
The big thing here is that this would also enable an implementation of something like dataloader, which Facebook has deemed the best practice for handling remote data fetching for a GraphQL query. Based on my (limited) experience with GraphQL it's the only sane way to support large/complex backends and multiple queries per request in a sane way. |
I've made a few attempts locally at integrating So, these are some issues that have prevented me from building this. There are probably more :)
I've been dabbling with Despite all of this, I still think this a feature we want to have! :) However, there are some constraints on the implementation:
This became a long response with little amount of actual content :) I might open up a branch to do some work on this, but it's been hard trying work in incremental pieces since it's such a cross-cutting change. |
Juniper should only create a new heap-allocated future for each object to
resolve, not each field.
What about fields that require an expensive calculation that you'd rather
define with an async method? After all, fields are how you *get* objects in
the first place. :-)
Den ons 12 juli 2017 11:37Magnus Hallin <notifications@github.com> skrev:
… I've made a few attempts locally at integrating futures-rs into Juniper,
but there are a lot of stumbling blocks that need to be overcome. There are
also some nice properties the current synchronous implementation has that
we'll probably lose, particularly around memory usage and heap allocations.
This might not be a big issue, but I like the "you don't pay for what you
don't use" mentality of Rust and C++ in general.
So, these are some issues that have prevented me from building this. There
are probably more :)
- Many AST nodes, particularly identifiers, use references to the
original query string to avoid copying. Rust statically guarantees that no
AST nodes will outlive the query string, and it's easy to reason about when
execute just parses and executes the query synchronously.
- GraphQLType::resolve* would need to return a Box<Future>, which
means that *all* fields will cause extra heap allocations, even if
it's just field like_count() -> i32 { self.like_count }. Now that I
think about it, maybe you could change FieldResult to something like enum
{ Immediate(Value), Err(String), Deferred(Box<Future<Item=Value,
Error=String>>) }.
- The core execution logic
<https://github.com/mhallin/juniper/blob/master/src/types/base.rs#L291>
would need to be transformed into something that joins multiple futures
together and emits a result. No fundamental problem here, it was just a
very difficult programming task :)
- The futures-rs crate changing around a bit while working on this.
This shouldn't be the case anymore, but I would be weary releasing Juniper
1.0 while depending on a pre-1.0 release of futures-rs.
I've been dabbling with futures-rs and Tokio in another project that I'm
working on, and the general feeling I get is that's it's pretty unergonomic
to use. I've stumbled upon cases where I couldn't break out a big and_then
callback to a separate function because of various reasons: either the type
was "untypeable" - i.e. containing closures, BoxFuture requires Send
despite the name, Box<Future> did not work because of lifetime issues,
and even when switching to nightly and returning impl Future there have
been cases where returning lifetimes have been an issue.
Despite all of this, I still think this a feature we want to have! :)
However, there are some constraints on the implementation:
- Minimal impact on the non-async path. *Ideally*, Juniper should only
create a new heap-allocated future for each object to resolve, *not*
each field. Scalars should not cause any overhead at all.
- No reliance on unstable compiler features. Juniper should not
require a nightly compiler.
This became a long response with little amount of actual content :) I
might open up a branch to do some work on this, but it's been hard trying
work in incremental pieces since it's such a cross-cutting change.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#2 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAAGP7F4ZyFLEmO6FWhUxK2m_aCAzKzLks5sNJPqgaJpZM4K0V7G>
.
|
I maybe expressed myself a bit ambiguous there - if a user defines an async field, than it should obviously allocate a future for that field. I meant that no futures should be allocated for synchronous fields. |
@mhallin @theduke In your opinion, how feasible is it to ship this? Lack of async support is currently the thing that prevents me from using Juniper for more projects. I appreciate that all in all it would be a huge refactor, but maybe this is something that could be shipped in increments? I've been gathering a lot of futures/tokio experience lately; would you be motivated to help me get the PRs reviewed and merged if I got started on this? Or do you feel like it's not the right time for this feature? |
@srijs Just out of curiosity, do you already have a futures-based Rust codebase that you want to expose with Juniper, or are you looking at GraphQL servers with async support in general? To be honest, the more i work with Promises/ That said, I will of course help you out if you decide to tackle this! I'm not sure even where to begin, but keeping AST nodes alive for the duration of the entire execution without using references with lifetimes is something that needs to be solved first. That might require putting all nodes under |
@mhallin I do have existing Rust codebases (that use tokio extensively) where I would love to be able to use Juniper. It seems with generator functions/coroutines we're moving into the direction you're describing, but of course that will not be usable in stable Rust for quite a while. So while I agree that it would be simpler by leveraging coroutines, I don't think it's practical to wait. At least personally I'd like to see Juniper support async before coroutines land in Rust stable. I have started to play around with the AST/parser parts, to use reference-counted string objects as you suggested (although I'm not 100% they would need to be |
Hi, can someone give me an update on this, I recently started a project using Juniper but the data to fulfil the GraphQL request comes for an external web api so I need to take advantage of futures for performance. Is futures support being worked on, if so by when will it be ready? if not, how do people go about doing what I am trying to do without using async I/O ? Thanks |
Mostly in response to, or in addition to, @mhallin's comment above, here are some thoughts based on the recent advances on the rust futures & async front, especially drawing off of these sources:
heap allocationsWith conservative impl trait and, less importantly, universal impl trait slated for Per some comments above about reference issues even inside of a return AST sharingWith futures stability
So, once Thoughts? |
FYI, futures |
Yes! It would be so awesome to have async GraphQL in Rust! I’ve built GraphQL servers in Node, Python, Go & Rust; and Rust’s Juniper is by far the most powerful and ergonomic to work with. Adding async support will be the crown jewel, IMHO 🙌 I think now would be a great time for some futures integration, for sure. |
By the way, I'm not sure if this has been proposed/discussed, but other libraries implemented support for dataloader by using thunks and a breadth-first resolution, without the need to introduce async code into the graphql executor. The golang graphql library PR explains the approach and contains other references as well: graphql-go/graphql#388 Would this be worth pursuing? Should this be added as a separate issue? |
Will this async juniper comming soon? 😹 |
I looked into how this could be implemented a bit. My understanding is that we would need to return futures instead of results in a few places if we want to keep the changes minimal, for example With async/await and async methods in traits, it looks like it would be possible to generate code for async resolution without boxing a future per field, but it blocks this feature until at least a few months after async_await lands (existential types are not working at the moment, even on nightly, as far as I know). |
True, we will also need to figure out how to deal with the query AST. We will need to either put it behind an The boxing issue may not be that bad if we can come up with an API that's transparently upgradable to a non-boxing solution, which should be possible. Also, considering that async resolvers will only be necessary for fields that do DB or api requests the boxing should not be significant since the small allocation will be dwarfed by the actual workload. The bigger challenge may be
We should also be aiming at basing this on |
@theduke yea, I would imagine that cloning an Agreed on the boxing issue. Do you mind expounding a bit on As far as tokio runtime tuning, using
So, perhaps a nice balance may be to teach non-blocking in the docs fairly well, include examples of how to use the Also, +1 on |
I was hoping to wait with all this until the whole async ecosystem stabilizes and we see a new tokio release with std Future and async/await support but things are taking a long time sadly. |
Futures just stabilized in 1.36 - rust-lang/rust#59739 (as 0.3) Are we confident we now have an API to start building out juniper resolvers as Futures? |
@jkoudys & @theduke looks like with the 1.36 stabilization & the fact that tokio has interop and a strong compatibility layer between futures 01 and std futures, now might be a great time. For async/await, we will have to be on nightly ... but we could always ship a preview branch which requires nightly, and just not merge it until everything is on stable. Something of note on async/await, it seems that the async patterns are pretty well settled down, but the await syntax is still in the air. We could just use the nightly |
I don't think In addition to what I posted above, the big questions we need to answer are: the
|
The threadpool part could be hard to abstract, so it would make sense to leave setting up the runtime and handling blocking tasks to users of the library in my opinion. For example the tokio threadpool handles blocking tasks differently from most others (the blocking function). |
@theduke & @tomhoule yea, I definitely agree. As long as their blocking/background threadpool operation returns a future, then we should be good. We shouldn't have to do anything else to handle it. As far as Concurrency Limits:
Thoughts? |
For concurrency limits, does juniper already batch requests within a query? If not, it might be useful to see how the Sangria graphql server does it with what they call deferred values, deferred resolvers, and fetchers: https://sangria-graphql.org/learn/#deferred-value-resolution |
Is there anything I can do to help with this project? I have a personal and professional interest in moving this forward, and potentially some time to spend on it. |
Hello @bsmedberg-xometry , long time no see! Would love help as we have been a bit swamped lately. This work is happening on the async-await branch. The ideal short-term help would be to implement union and interface support there. That entails porting logic over from the macros into proc_macros. It's a bit hairy...not because the work is hard but because macros are annoying to work with. There is a WIP progress PR for async subscription support but it isn't touching the union and interface stuff so you shouldn't really conflict. |
Is there a need for more examples? I was able to successfully use the I'd be happy to add an example using that as the basis. |
Small update: I refactored the No async support yet, that's the next step, but a fairly small one. |
@theduke Will |
Yes, I'm replacing all the macros with proc macros. |
Just an additional update: I've been traveling a lot for the past few months, but starting this Saturday, I will have 2 weeks downtime. I will push hard to have a |
@dfrankland what would be extremely helpful is building out a full-featured benchmark with a complex schema in https://github.com/graphql-rust/juniper/tree/async-await/juniper_benchmarks . ( this is currently very rudimentary ) |
@theduke Seeing as we are telling folks to base their PRs on the aync-await branch, let's just get CI passing and merge that into master so we can jam on it. I don't think we'll do another sync-only release and if we need to we can re-branch at the pre-async point. |
👍 , that's what I wanted to suggest. |
This is landed on There is still some cleanup work to do (and the book tests don't pass) but all other tests seem to be working with both the |
Should this issue not be closed then? |
I believe that it wouldn't make sense until async support is in a release on crates.io |
Interfaces still don't work and we need to rip out the sync code. Keeping this open until those are done and a release is made. |
Hey! Thanks for the awesome work! I managed to integrate master with dataloader-rs! I noticed that the batch loading isn't working on deeper levels of the tree though. The following example will make it clear: query {
recipes {
name
ingredients {
ingredient { name }
amount
}
}
} This will result in the following db queries (all of which are done by dataloader batch loading functions). SELECT name FROM recipes
SELECT ingredient_id, amount FROM recipe_ingredients WHERE recipe_id IN ($1, $2, $3)
parameters: $1 = '3', $2 = '1', $3 = '2'
SELECT name FROM ingredients WHERE id IN ($1)
parameters: $1 = '1'
SELECT name FROM ingredients WHERE id IN ($1)
parameters: $1 = '2'
SELECT name FROM ingredients WHERE id IN ($1)
parameters: $1 = '4' So the the second level (recipe -> recipe_ingredients) of resolvers is batched but the third isn't (recipe_ingredient -> ingredient). This could just as well be a bug in dataloader-rs or even more likely just my incompetence, but i thought I'd post it here if someone has come across this and solved it already. EDIT: Ok it seems that if i set |
Do you have a link to code? |
With #682 landed, we're fully async compatible now! |
@tyranron Are there any examples? |
@wongjiahau check the book on |
I found the fix by using |
crates.io has been updated with juniper's async support, sorry for the delay. Any future bugs or api changes can get their own issues. Note that we still support synchronous execution via Thank you to all the contributors who made this possible, especially @nWacky, @tyranron , @theduke , and @davidpdrsn 🍻 🎉 🥇 |
Hi -
Have you put any thought into how this could be used with an async IO model, such as with Tokio? Right now it seems like it's all implicitly synchronous, but perhaps it's just a matter of making the various functions take/return
Future
s?Thanks
The text was updated successfully, but these errors were encountered: