Enforcing delivery order of payloads #17
Replies: 6 comments 12 replies
-
|
from @michaelstaib https://discord.com/channels/625400653321076807/862834364718514196/908330498441511033
|
Beta Was this translation helpful? Give feedback.
-
|
graphql-executor, the customizable graphql executor based on graphql-js, now supports incremental delivery with preservation of payload order for defer (a) and stream (d), allowing early send with nested non dependent defer (b) but not for resolution of field from another branch as in (c). |
Beta Was this translation helpful? Give feedback.
-
|
A related question: does this also mean that for Payload 1: {
"data": {
"id": "1"
},
"path": [
"products",
1
],
"hasNext": true
}Payload 2: {
"data": {
"id": "0"
},
"path": [
"products",
0
],
"hasNext": false
}(receiving |
Beta Was this translation helpful? Give feedback.
-
|
HI! I am experimenting with
|
Beta Was this translation helpful? Give feedback.
-
|
Thanks a lot! However with this I'm also getting |
Beta Was this translation helpful? Give feedback.
-
|
@robrichard great write up here. I think this completely makes sense for the defer and stream directives to ensure parents have been delivered before an incremental update is delivered. However, I have some questions on this part:
This helps to explain a "problem" I was running into. I was under the assumption that the In this example, the Im just curious on why the list order must be preserved when using Side note: Any tips on how to implement the desired functionality that I discussed (returning items to the client in resolve order and not the arbitrarily defined array order) would be greatly appreciated. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Context
A naive implementation of defer/stream could simply race all available upcoming payloads and return the first one that is ready. This could lead to results where a payload is sent with a path that references a field the client has not received yet.
Decision
It was discussed in the 2021-07-01 WG meeting and 2021-12-02 WG meeting and the group agreed that responses like this must be ordered to ensure payload paths do not reference fields that have not been sent yet. Similarly, a streamed field result must not be sent before a result with a lower index.
Discussed again in the 2022-02-03 WG meeting and there was general agreement on the approach outlined below. This feels like the most conservative approach. There may be scenarios where we may be trading performance for making client digestion of payloads easier, but we could potentially look at that later.
Implementation
tl;dr: When executing a deferred fragment or streamed field, a reference will be passed around. Any downstream deferred fragments or streamed fields must wait for the parent to complete its execution before it can be completed itself. Additionally, subsequent items in streamed fields must wait for the prior item to complete its execution.
The GraphQL spec defines execution of a selection set via the
ExecuteSelectionSetalgorithm. InExecuteSelectionSet, a group of fields are gathered using theCollectFieldsalgorithm, before executing each collected field using theExecuteFieldandCompleteValuealgorithms.Defer
Our proposal updates the
CollectFieldsalgorithm to detect fragments with@deferand separately execute their selection set by callingExecuteSelectionSetwith the selection set from the deferred fragment. Additionally a reference to the deferred fragment will now be passed toExecuteSelectionSet,ExecuteField, andCompleteValue.If a new deferred fragment is detected while executing the selection set from a previously detected deferred fragment, the execution of this new deferred fragment must not complete until the execution of the previous deferred fragment is complete.
This ensures results are delivered in a hierarchal order. It also ensures that it is not possible to receive a payload that contains a path reference to an object that has not yet been delivered.
Stream
Our proposal also updates the
CompleteValuealgorithm to detect list fields with the@streamdirective. Values yielded by the underlying iterator on a streamed field are resolved with an additional call toCompleteValue.If a deferred fragment reference is passed to
CompleteValue, the first streamed value must not complete its execution until the execution from the deferred fragment reference is complete. Similarly, all subsequent streamed values must not complete their execution until the prior streamed value is complete.This ensures the first streamed result is not delivered before the payload containing the list the streamed result belongs to. It also ensures that any subsequent streamed results are not delivered before prior streamed results.
Example A
Let's say that that the
person.homeworldfield take longer to resolve than theperson.speciesfield. The result would be:{ "data": { "person": { "name": "R2-D2" } }, "hasNext": true }, { "data": { "title": "Droid" }, "path": ["person", "species"], "label": "DeferNested", "hasNext": true }, { "data": { "homeworld": { "name": "Naboo" }, "species": {} }, "path": ["person"], "label": "DeferTop", "hasNext": false }This means the client would receive a payload with the path
["person", "species"]before the species field is returned to the client.In this case, a reference to the execution of
...TopFragment @defer(label: "DeferTop")will be passed toExecuteSelectionSetandCollectFieldswhen...NestedFragment @defer(label: "DeferNested")is encountered. The execution of...NestedFragment @defer(label: "DeferNested")will use this reference to ensure it is not resolved untilTopFragmentis complete.Example B: deferred fragments that do not have a direct tree dependency
In this case, if
NestedFragmentis ready beforeTopFragment, it will not wait forTopFragmentto be sent first. WhenCollectFieldsanalyzes the selection set ofperson(personID: "3"), it will recursively traverse the fragment spreads. Therefore...TopFragment @defer(label: "DeferTop")and...NestedFragment @defer(label: "DeferNested")can be resolved in parallel.Example C - parent dependency can be resolved via another fragment
What if
NestedFragmentis ready beforeTopFragment, but afterUnrelatedTopFragment? In this case,CollectFieldswill trigger execution of...TopFragment @defer(label: "DeferTop")and...UnrelatedTopFragment @defer(label: "DeferUnrelatedTop")in parallel, since they are siblings.CollectFieldswill encounter...NestedFragment @defer(label: "DeferNested")only while executing...TopFragment @defer(label: "DeferTop"), therefore the payload from this fragment will be its only dependency.Example D - Stream payloads should be sent after parent deferred fragment
In this case, the field
friends @stream(initialCount: 0)will be executed byCollectFieldsoriginating from a call toExecuteSelectionSetfor...DeferFragment @defer. The first result from this streamed field must be returned after the payload for...DeferFragment @defer.Example E - Stream payloads must not be sent out of order
Subsequent payloads from a
@streamdirective must not be sent before prior payloads from the same stream.Status
Beta Was this translation helpful? Give feedback.
All reactions