Skip to content
This repository was archived by the owner on Feb 27, 2023. It is now read-only.

Commit bc84afa

Browse files
authored
Merge pull request graphql-java#967 from bbakerman/defer-support-exploration
Experimental support for @defer
2 parents 7e06f57 + 562fa3a commit bc84afa

21 files changed

+1399
-5
lines changed

docs/defer.rst

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
Deferred Execution
2+
==================
3+
4+
Often when executing a query you have two classes of data. The data you need immediately and the data that could arrive little bit later.
5+
6+
For example imagine this query that gets data on a ``post` and its ``comments`` and ``reviews``.
7+
8+
9+
.. code-block:: graphql
10+
11+
query {
12+
post {
13+
postText
14+
comments {
15+
commentText
16+
}
17+
reviews {
18+
reviewText {
19+
}
20+
}
21+
22+
In this form, you *must* wait for the ``comments`` and ``reviews`` data to be retrieved before you can send the ``post`` data back
23+
to the client. All three data elements are bound to the one query
24+
25+
A naive approach would be to make two queries to gett he most important data first but there is now a better way.
26+
27+
There is ``experimental`` support for deferred execution in graphql-java.
28+
29+
.. code-block:: graphql
30+
31+
query {
32+
post {
33+
postText
34+
comments @defer {
35+
commentText
36+
}
37+
reviews @defer {
38+
reviewText {
39+
}
40+
}
41+
42+
The ``@defer`` directive tells the engine to defer execution of those fields and deliver them later. The rest of the query is executed as
43+
usual. There will be the usual ``ExecutionResult`` of initial data and then a ``org.reactivestreams.Publisher`` of the deferred data.
44+
45+
In the query above, the ``post`` data will be send out in the initial result and then the comments and review data will be sent (in query order)
46+
down a ``Publisher`` later.
47+
48+
The first thing you need to put in place is including the ``defer`` directive into your schema. It wont work without it and graphql-java will
49+
give you an error if you don't.
50+
51+
52+
.. code-block:: java
53+
54+
GraphQLSchema buildSchemaWithDirective() {
55+
56+
GraphQLSchema schema = buildSchema();
57+
schema = schema.transform(builder ->
58+
builder.additionalDirective(Directives.DeferDirective)
59+
);
60+
return schema;
61+
}
62+
63+
64+
Then you execute your query as you would any other graphql query. The deferred results ``Publisher`` will be given to you via
65+
the ``extensions`` map
66+
67+
68+
.. code-block:: java
69+
70+
GraphQLSchema schema = buildSchemaWithDirective();
71+
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
72+
73+
//
74+
// deferredQuery contains the query with @defer directives in it
75+
//
76+
ExecutionResult initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(deferredQuery).build());
77+
78+
//
79+
// then initial results happen first, the deferred ones will begin AFTER these initial
80+
// results have completed
81+
//
82+
sendResult(httpServletResponse, initialResult);
83+
84+
Map<Object, Object> extensions = initialResult.getExtensions();
85+
Publisher<ExecutionResult> deferredResults = (Publisher<ExecutionResult>) extensions.get(GraphQL.DEFERRED_RESULTS);
86+
87+
//
88+
// you subscribe to the deferred results like any other reactive stream
89+
//
90+
deferredResults.subscribe(new Subscriber<ExecutionResult>() {
91+
92+
Subscription subscription;
93+
94+
@Override
95+
public void onSubscribe(Subscription s) {
96+
subscription = s;
97+
//
98+
// how many you request is up to you
99+
subscription.request(10);
100+
}
101+
102+
@Override
103+
public void onNext(ExecutionResult executionResult) {
104+
//
105+
// as each deferred result arrives, send it to where it needs to go
106+
//
107+
sendResult(httpServletResponse, executionResult);
108+
subscription.request(10);
109+
}
110+
111+
@Override
112+
public void onError(Throwable t) {
113+
handleError(httpServletResponse, t);
114+
}
115+
116+
@Override
117+
public void onComplete() {
118+
completeResponse(httpServletResponse);
119+
}
120+
});
121+
122+
The above code subscribes to the deferred results and when each one arrives, sends it down to the client.
123+
124+
You can see more details on reactive-streams code here http://www.reactive-streams.org/
125+
126+
``RxJava`` is a popular implementation of reactive-streams. Check out http://reactivex.io/intro.html to find out more
127+
about creating Subscriptions.
128+
129+
graphql-java only produces a stream of deferred results. It does not concern itself with sending these over the network on things
130+
like web sockets and so on. That is important but not a concern of the base graphql-java library. Its up to you
131+
to use whatever network mechanism (websockets / long poll / ....) to get results back to you clients.
132+
133+
Also note that this capability is currently ``experimental`` and not defined by the official ``graphql`` specification. We reserve the
134+
right to change it in a future release or if it enters the official specification. The graphql-java project
135+
is keen to get feedback on this capability.
136+
137+

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ graphql-java is licensed under the MIT License.
5252
mapping
5353
scalars
5454
subscriptions
55+
defer
5556
exceptions
5657
batching
5758
instrumentation

src/main/java/graphql/Directives.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graphql;
22

33

4+
import graphql.introspection.Introspection;
45
import graphql.schema.GraphQLDirective;
56
import graphql.schema.GraphQLNonNull;
67

@@ -47,5 +48,11 @@ public class Directives {
4748
.validLocations(FIELD_DEFINITION)
4849
.build();
4950

51+
@ExperimentalApi
52+
public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective()
53+
.name("defer")
54+
.description("This experimental directive allows results to be deferred during execution")
55+
.validLocations(Introspection.DirectiveLocation.FIELD)
56+
.build();
5057

5158
}

src/main/java/graphql/GraphQL.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@
7777
@PublicApi
7878
public class GraphQL {
7979

80+
/**
81+
* When @defer directives are used, this is the extension key name used to contain the {@link org.reactivestreams.Publisher}
82+
* of deferred results
83+
*/
84+
public static final String DEFERRED_RESULTS = "deferredResults";
85+
8086
private static final Logger log = LoggerFactory.getLogger(GraphQL.class);
8187

8288
private static final ExecutionIdProvider DEFAULT_EXECUTION_ID_PROVIDER = (query, operationName, context) -> ExecutionId.generate();

src/main/java/graphql/execution/AsyncExecutionStrategy.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package graphql.execution;
22

33
import graphql.ExecutionResult;
4+
import graphql.execution.defer.DeferSupport;
5+
import graphql.execution.defer.DeferredCall;
6+
import graphql.execution.defer.DeferredErrorSupport;
47
import graphql.execution.instrumentation.Instrumentation;
58
import graphql.execution.instrumentation.InstrumentationContext;
69
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
@@ -50,6 +53,9 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
5053
ExecutionStrategyParameters newParameters = parameters
5154
.transform(builder -> builder.field(currentField).path(fieldPath));
5255

56+
if (isDeferred(executionContext, newParameters, currentField)) {
57+
continue;
58+
}
5359
CompletableFuture<ExecutionResult> future = resolveField(executionContext, newParameters);
5460
futures.add(future);
5561
}
@@ -63,4 +69,16 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
6369
return overallResult;
6470
}
6571

72+
private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyParameters newParameters, List<Field> currentField) {
73+
DeferSupport deferSupport = executionContext.getDeferSupport();
74+
if (deferSupport.checkForDeferDirective(currentField)) {
75+
DeferredErrorSupport errorSupport = new DeferredErrorSupport();
76+
ExecutionStrategyParameters callParameters = newParameters.transform(builder -> builder.deferredErrorSupport(errorSupport));
77+
78+
DeferredCall call = new DeferredCall(() -> resolveField(executionContext, callParameters), errorSupport);
79+
deferSupport.enqueue(call);
80+
return true;
81+
}
82+
return false;
83+
}
6684
}

src/main/java/graphql/execution/Execution.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import graphql.ExecutionInput;
55
import graphql.ExecutionResult;
66
import graphql.ExecutionResultImpl;
7+
import graphql.GraphQL;
78
import graphql.GraphQLError;
89
import graphql.Internal;
10+
import graphql.execution.defer.DeferSupport;
911
import graphql.execution.instrumentation.Instrumentation;
1012
import graphql.execution.instrumentation.InstrumentationContext;
1113
import graphql.execution.instrumentation.InstrumentationState;
@@ -19,6 +21,7 @@
1921
import graphql.language.VariableDefinition;
2022
import graphql.schema.GraphQLObjectType;
2123
import graphql.schema.GraphQLSchema;
24+
import org.reactivestreams.Publisher;
2225
import org.slf4j.Logger;
2326
import org.slf4j.LoggerFactory;
2427

@@ -169,7 +172,26 @@ private CompletableFuture<ExecutionResult> executeOperation(ExecutionContext exe
169172

170173
result = result.whenComplete(executeOperationCtx::onCompleted);
171174

172-
return result;
175+
return deferSupport(executionContext, result);
176+
}
177+
178+
/*
179+
* Adds the deferred publisher if its needed at the end of the query. This is also a good time for the deferred code to start running
180+
*/
181+
private CompletableFuture<ExecutionResult> deferSupport(ExecutionContext executionContext, CompletableFuture<ExecutionResult> result) {
182+
return result.thenApply(er -> {
183+
DeferSupport deferSupport = executionContext.getDeferSupport();
184+
if (deferSupport.isDeferDetected()) {
185+
// we start the rest of the query now to maximize throughput. We have the initial important results
186+
// and now we can start the rest of the calls as early as possible (even before some one subscribes)
187+
Publisher<ExecutionResult> publisher = deferSupport.startDeferredCalls();
188+
return ExecutionResultImpl.newExecutionResult().from((ExecutionResultImpl) er)
189+
.addExtension(GraphQL.DEFERRED_RESULTS, publisher)
190+
.build();
191+
}
192+
return er;
193+
});
194+
173195
}
174196

175197
private GraphQLObjectType getOperationRootType(GraphQLSchema graphQLSchema, OperationDefinition operationDefinition) {

src/main/java/graphql/execution/ExecutionContext.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33

44
import graphql.GraphQLError;
55
import graphql.PublicApi;
6+
import graphql.execution.defer.DeferSupport;
67
import graphql.execution.instrumentation.Instrumentation;
78
import graphql.execution.instrumentation.InstrumentationState;
89
import graphql.language.Document;
910
import graphql.language.FragmentDefinition;
1011
import graphql.language.OperationDefinition;
1112
import graphql.schema.GraphQLSchema;
1213

13-
import java.util.ArrayList;
1414
import java.util.Collections;
1515
import java.util.List;
1616
import java.util.Map;
@@ -34,6 +34,7 @@ public class ExecutionContext {
3434
private final Object context;
3535
private final Instrumentation instrumentation;
3636
private final List<GraphQLError> errors = new CopyOnWriteArrayList<>();
37+
private final DeferSupport deferSupport = new DeferSupport();
3738

3839
public ExecutionContext(Instrumentation instrumentation, ExecutionId executionId, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy, Map<String, FragmentDefinition> fragmentsByName, Document document, OperationDefinition operationDefinition, Map<String, Object> variables, Object context, Object root) {
3940
this(instrumentation, executionId, graphQLSchema, instrumentationState, queryStrategy, mutationStrategy, subscriptionStrategy, fragmentsByName, document, operationDefinition, variables, context, root, Collections.emptyList());
@@ -157,6 +158,9 @@ public ExecutionStrategy getSubscriptionStrategy() {
157158
return subscriptionStrategy;
158159
}
159160

161+
public DeferSupport getDeferSupport() {
162+
return deferSupport;
163+
}
160164

161165
/**
162166
* This helps you transform the current ExecutionContext object into another one by starting a builder with all

src/main/java/graphql/execution/ExecutionStrategy.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ private void handleFetchingException(ExecutionContext executionContext,
282282
.build();
283283

284284
dataFetcherExceptionHandler.accept(handlerParameters);
285+
286+
parameters.deferredErrorSupport().onFetchingException(parameters, e);
285287
}
286288

287289
/**
@@ -554,6 +556,9 @@ private Object handleCoercionProblem(ExecutionContext context, ExecutionStrategy
554556
SerializationError error = new SerializationError(parameters.getPath(), e);
555557
log.warn(error.getMessage(), e);
556558
context.addError(error);
559+
560+
parameters.deferredErrorSupport().onError(error);
561+
557562
return null;
558563
}
559564

@@ -691,6 +696,8 @@ private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrate
691696
TypeMismatchError error = new TypeMismatchError(parameters.getPath(), parameters.getTypeInfo().getType());
692697
log.warn("{} got {}", error.getMessage(), result.getClass());
693698
context.addError(error);
699+
700+
parameters.deferredErrorSupport().onError(error);
694701
}
695702

696703

src/main/java/graphql/execution/ExecutionStrategyParameters.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import graphql.Assert;
44
import graphql.PublicApi;
5+
import graphql.execution.defer.DeferredErrorSupport;
56
import graphql.language.Field;
67

78
import java.util.List;
@@ -22,15 +23,17 @@ public class ExecutionStrategyParameters {
2223
private final NonNullableFieldValidator nonNullableFieldValidator;
2324
private final ExecutionPath path;
2425
private final List<Field> currentField;
26+
private final DeferredErrorSupport deferredErrorSupport;
2527

26-
private ExecutionStrategyParameters(ExecutionTypeInfo typeInfo, Object source, Map<String, List<Field>> fields, Map<String, Object> arguments, NonNullableFieldValidator nonNullableFieldValidator, ExecutionPath path, List<Field> currentField) {
28+
private ExecutionStrategyParameters(ExecutionTypeInfo typeInfo, Object source, Map<String, List<Field>> fields, Map<String, Object> arguments, NonNullableFieldValidator nonNullableFieldValidator, ExecutionPath path, List<Field> currentField, DeferredErrorSupport deferredErrorSupport) {
2729
this.typeInfo = assertNotNull(typeInfo, "typeInfo is null");
2830
this.fields = assertNotNull(fields, "fields is null");
2931
this.source = source;
3032
this.arguments = arguments;
3133
this.nonNullableFieldValidator = nonNullableFieldValidator;
3234
this.path = path;
3335
this.currentField = currentField;
36+
this.deferredErrorSupport = deferredErrorSupport;
3437
}
3538

3639
public ExecutionTypeInfo getTypeInfo() {
@@ -57,6 +60,10 @@ public ExecutionPath getPath() {
5760
return path;
5861
}
5962

63+
public DeferredErrorSupport deferredErrorSupport() {
64+
return deferredErrorSupport;
65+
}
66+
6067
/**
6168
* This returns the current field in its query representations. Global fragments mean that
6269
* a single named field can have multiple representations and different field subselections
@@ -96,6 +103,7 @@ public static class Builder {
96103
NonNullableFieldValidator nonNullableFieldValidator;
97104
ExecutionPath path = ExecutionPath.rootPath();
98105
List<Field> currentField;
106+
DeferredErrorSupport deferredErrorSupport = new DeferredErrorSupport();
99107

100108
/**
101109
* @see ExecutionStrategyParameters#newParameters()
@@ -112,8 +120,9 @@ private Builder(ExecutionStrategyParameters oldParameters) {
112120
this.fields = oldParameters.fields;
113121
this.arguments = oldParameters.arguments;
114122
this.nonNullableFieldValidator = oldParameters.nonNullableFieldValidator;
115-
this.path = oldParameters.path;
116123
this.currentField = oldParameters.currentField;
124+
this.deferredErrorSupport = oldParameters.deferredErrorSupport;
125+
this.path = oldParameters.path;
117126
}
118127

119128
public Builder typeInfo(ExecutionTypeInfo type) {
@@ -156,8 +165,13 @@ public Builder path(ExecutionPath path) {
156165
return this;
157166
}
158167

168+
public Builder deferredErrorSupport(DeferredErrorSupport deferredErrorSupport) {
169+
this.deferredErrorSupport = deferredErrorSupport;
170+
return this;
171+
}
172+
159173
public ExecutionStrategyParameters build() {
160-
return new ExecutionStrategyParameters(typeInfo, source, fields, arguments, nonNullableFieldValidator, path, currentField);
174+
return new ExecutionStrategyParameters(typeInfo, source, fields, arguments, nonNullableFieldValidator, path, currentField, deferredErrorSupport);
161175
}
162176
}
163177
}

0 commit comments

Comments
 (0)