Replies: 6 comments 18 replies
-
For early execution there are some precautions the implementing server will need to take. Example: query {
foo {
nonNullableFieldThatErrors
... @defer {
bar
}
} If a server implements "early execution" it may begin executing the [
{
"data": { "foo": "null" },
"hasNext": true
},
{
"incremental": [
{
// WRONG! invalid path, ["foo"] is null in previous response
"path": ["foo"],
"data": { "bar": "BAR" },
}
]
}
] (This was discussed in #45) There's non-obvious and non-trivial logic needed to cancel/filter these invalid results due to error bubbling with "early execution". If a server does "deferred execution" this logic is a no-op because its not possible for this scenario to be encountered. My current thinking is that we should write the spec with "early execution" if that's what we think most implementations will end up doing. In that case we can capture the logic needed for handling error bubbling in spec algorithms. "Deferred execution" would still be allowed due to the conformance clause, and those implementations can simply skip the filtering functions. If we only specify "deferred execution" we leave open the possibility of implementors missing this non-obvious and non-trivial logic and creating buggy implementations. |
Beta Was this translation helpful? Give feedback.
-
This is a great write-up of a potentially serious pitfall with early execution (or what I've been calling semi-concurrent execution)1. A solution hinted at above would be to open up separate connections to whatever resource is shared between the initial and deferred fields to avoid contention. Naively, this could be done for each deferred payload, whereas for a more complex solution, we could have a generic pool that ingests the overall priority of the current field (c.f. generic-pool) which would need to be passed down to resolvers -- and to the underlying business logic that resolvers call! In a limiting case, perhaps the pool could have a maximum of 2 connections, one for initial results, one for all deferred results. Just as setting up dataloaders on the However, the above fails on the theoretical level to solve the issue. At some point, we will inevitably "lose control" over the concurrency of the end-resources we are calling. In particular, those end-resources may call out to other even lower-level resources and either be unaware of the need to manage this concurrency with multiple connections and priority queues, or, perhaps more likely (?), run into a resource which cannot physically or otherwise multiplex. At that point, work for deferred fields which has begun semi-concurrently will have forced its way into a queue prior to later scheduled work for the initial result (or lower-order defers) and will begin blocking, with ripple effects all the way up to our nicely managed higher-level resources. Will all or any of the above be a common failure mode in practice? I would love to hear from real world users of Footnotes
|
Beta Was this translation helpful? Give feedback.
-
@benjie Great write-up! That said, can we change a question a bit? From the point of the particular field, there are three main stages:
So there are three options on how we can define what
@benjie As I understand your initial comment:
|
Beta Was this translation helpful? Give feedback.
-
My current thinking:
Question: Should we have the incremental delivery execution algorithm included within the spec demonstrate handling of early execution? Pros:
Cons:
My verdict: I think most implementors would include the option to at least SOMETIMES enable early execution, and so I think the con is limited. A thought: Another option to mitigate the main con is that we specify both algorithms within the spec, early execution and delayed execution, with one in the main text, and another in an appendix. |
Beta Was this translation helpful? Give feedback.
-
Also sharing some practical evidence that early execution of deferred is needed from Meta's usage of @defer -- what we found is that frequent incremental updates from the server lead to more battery consumption, more stalls on video play and reduced scroll performance and touch responsiveness (mostly likely due to increased sync and async update (consistency update, re-render, and re-mount etc). There is also some(although very limited) evidence to suggest that frequent flushes (i.e. breaking responses into too many pieces) result in memory fragmentation on the client side, hence increase the chances of foreground app death (FADs)/app crashes. So I agree with @yaacovCR that implementors should be allowed the option to enable early execution, as long as they can tell client in the response that they've fulfilled @defer eagerly so that client won't wait for the defer portion that will never come. |
Beta Was this translation helpful? Give feedback.
-
fyi for those interested, a follow-on PR at graphql/graphql-js#3895 does exactly that, performing in-place inlining of all of the "executed-early-and completed" children of a given incremental result as that result is sent. we do indeed have to keep track of the tree of all results in progress (although we were tracking all of those anyway) |
Beta Was this translation helpful? Give feedback.
-
Hi folks, I can't remember if I'd raised this anywhere else (and failed to track it down) so am starting a new discussion for it.
When executing a selection set, I see that we have three options for handling deferred fields within the selection set:
To me (1) and (2) are essentially the same thing, so I'll group them together as "early execution", and number (3) I'll call "deferred execution". In deferred execution, execution is split into layers at defer boundaries, and the next layer of defers do not start executing until the previous (or initial) layer is complete. In early execution, execution starts straight away (or very soon), but the results are not awaited until the previous layer has been sent.
Why allow early execution?
It seems fairly obvious: the earlier we start executing something, the sooner it can finish... right?
Michael laid out some really good points with early testing of stream/defer, indicating that early testers of the technology found that it was not useful to them if it deferred execution of the
@defer
'd fields because it increased latency of the entire request being completed significantly. (We should keep in mind this datapoint was with the early version of stream/defer that had significant result duplication.)Note that even in option (3), early execution is allowed, it's simply not specified - the observable result according to an external viewer should appear the same as if execution had been deferred.
Why not specify early execution?
Resolvers contain arbitrary logic, with arbitrary interactions. We cannot know what a resolver will try to do, we cannot figure out whether it's likely to hold up other resolvers or not.
Imagine you have GraphQL resolvers and a query like this:
Note that our resolvers use the
db
on context, which is a connection to our database. Imagine our database uses standard query-response protocol (no multiplexing).When executing this request with (1), the result might be the following queries being issued:
With (2), it might be:
Either way, the result of the
cheap
field cannot be returned until thesuperExpensive
field has completed - so the entire response would be delayed 5 seconds, and then thesuperExpensive
andcheap
fields would arrive at basically the same time - defer has served no purpose.With (3), however, the execution would look like:
Note that this is what a user would expect - the result of
cheap
comes near instantly to the user, and thensuperExpensive
arrives 5 seconds later after it has been calculated.Trade-offs of deferred execution
The main trade-off of deferred execution is that it can concretely increase the execution time. If you think of execution as a Gantt chart, deferred execution would make the critical path longer - it would add together the latency of each "defer layer" of execution, not allowing for concurrency (overlapping) between parent and child defers.
What I think we should do
Given we do not control or really have any say over the content of resolvers in a user's schema, I don't think we can safely specify that you should always use early execution - the result is that many user's schemas may fall into this trap where defer turns out to be useless noise.
Specifying both early execution and deferred execution would make the spec significantly more complex, and I am personally of the belief that adding both to the specification is undesirable.
What I propose is that we specify deferred execution only, and that we add a non-normative note to the spec indicating that should the implementer want to, they may start execution of deferred fields early, but they should be aware of the above issue. This non-normative note would be enabled by this part of the spec:
Other issues
Mutation
type fields) could have unpredictable results (no-one should be using these anyway, but people do and some even do conference talks on it 😬)Beta Was this translation helpful? Give feedback.
All reactions