Skip to content

Commit ab894db

Browse files
LarsKrogJensenbbakerman
authored andcommitted
Preparsed document provider/cache to avoid costly parse and validation
1 parent 7740ebe commit ab894db

9 files changed

Lines changed: 342 additions & 83 deletions

File tree

src/main/java/graphql/GraphQL.java

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import graphql.execution.instrumentation.NoOpInstrumentation;
1111
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
1212
import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters;
13+
import graphql.execution.preparsed.NoOpPreparsedDocumentProvider;
14+
import graphql.execution.preparsed.PreparsedDocumentEntry;
15+
import graphql.execution.preparsed.PreparsedDocumentProvider;
1316
import graphql.language.Document;
1417
import graphql.language.SourceLocation;
1518
import graphql.parser.Parser;
@@ -39,6 +42,7 @@ public class GraphQL {
3942
private final ExecutionStrategy subscriptionStrategy;
4043
private final ExecutionIdProvider idProvider;
4144
private final Instrumentation instrumentation;
45+
private final PreparsedDocumentProvider preparsedDocumentProvider;
4246

4347

4448
/**
@@ -79,7 +83,7 @@ public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy) {
7983
*/
8084
@Internal
8185
public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) {
82-
this(graphQLSchema, queryStrategy, mutationStrategy, null, DEFAULT_EXECUTION_ID_PROVIDER, NoOpInstrumentation.INSTANCE);
86+
this(graphQLSchema, queryStrategy, mutationStrategy, null, DEFAULT_EXECUTION_ID_PROVIDER, NoOpInstrumentation.INSTANCE, NoOpPreparsedDocumentProvider.INSTANCE);
8387
}
8488

8589
/**
@@ -94,16 +98,17 @@ public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, Exe
9498
*/
9599
@Internal
96100
public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy) {
97-
this(graphQLSchema, queryStrategy, mutationStrategy, subscriptionStrategy, DEFAULT_EXECUTION_ID_PROVIDER, NoOpInstrumentation.INSTANCE);
101+
this(graphQLSchema, queryStrategy, mutationStrategy, subscriptionStrategy, DEFAULT_EXECUTION_ID_PROVIDER, NoOpInstrumentation.INSTANCE, NoOpPreparsedDocumentProvider.INSTANCE);
98102
}
99103

100-
private GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy, ExecutionIdProvider idProvider, Instrumentation instrumentation) {
104+
private GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy, ExecutionIdProvider idProvider, Instrumentation instrumentation, PreparsedDocumentProvider preparsedDocumentProvider) {
101105
this.graphQLSchema = assertNotNull(graphQLSchema, "queryStrategy must be non null");
102106
this.queryStrategy = queryStrategy != null ? queryStrategy : new SimpleExecutionStrategy();
103107
this.mutationStrategy = mutationStrategy != null ? mutationStrategy : new SimpleExecutionStrategy();
104108
this.subscriptionStrategy = subscriptionStrategy != null ? subscriptionStrategy : new SimpleExecutionStrategy();
105109
this.idProvider = assertNotNull(idProvider, "idProvider must be non null");
106110
this.instrumentation = instrumentation;
111+
this.preparsedDocumentProvider = assertNotNull(preparsedDocumentProvider, "preparsedDocumentProvider must be non null");
107112
}
108113

109114
/**
@@ -126,6 +131,7 @@ public static class Builder {
126131
private ExecutionStrategy subscriptionExecutionStrategy = new SimpleExecutionStrategy();
127132
private ExecutionIdProvider idProvider = DEFAULT_EXECUTION_ID_PROVIDER;
128133
private Instrumentation instrumentation = NoOpInstrumentation.INSTANCE;
134+
private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE;
129135

130136

131137
public Builder(GraphQLSchema graphQLSchema) {
@@ -157,6 +163,11 @@ public Builder instrumentation(Instrumentation instrumentation) {
157163
return this;
158164
}
159165

166+
public Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) {
167+
this.preparsedDocumentProvider = assertNotNull(preparsedDocumentProvider, "PreparsedDocumentProvider must be non null");
168+
return this;
169+
}
170+
160171
public Builder executionIdProvider(ExecutionIdProvider executionIdProvider) {
161172
this.idProvider = assertNotNull(executionIdProvider, "ExecutionIdProvider must be non null");
162173
return this;
@@ -166,7 +177,7 @@ public GraphQL build() {
166177
assertNotNull(graphQLSchema, "queryStrategy must be non null");
167178
assertNotNull(queryExecutionStrategy, "queryStrategy must be non null");
168179
assertNotNull(idProvider, "idProvider must be non null");
169-
return new GraphQL(graphQLSchema, queryExecutionStrategy, mutationExecutionStrategy, subscriptionExecutionStrategy, idProvider, instrumentation);
180+
return new GraphQL(graphQLSchema, queryExecutionStrategy, mutationExecutionStrategy, subscriptionExecutionStrategy, idProvider, instrumentation, preparsedDocumentProvider);
170181
}
171182
}
172183

@@ -289,18 +300,29 @@ public ExecutionResult execute(ExecutionInput executionInput) {
289300
}
290301

291302
private ExecutionResult parseValidateAndExecute(ExecutionInput executionInput) {
292-
ParseResult parseResult = parse(executionInput);
293-
if (parseResult.isFailure()) {
294-
return toParseFailureExecutionResult(parseResult.getException());
303+
PreparsedDocumentEntry preparsedDocumentEntry = preparsedDocumentProvider.get(executionInput.getQuery());
304+
if (preparsedDocumentEntry == null) {
305+
ParseResult parseResult = parse(executionInput);
306+
if (parseResult.isFailure()) {
307+
preparsedDocumentEntry = new PreparsedDocumentEntry(toInvalidSyntaxError(parseResult.getException()));
308+
} else {
309+
final Document document = parseResult.getDocument();
310+
311+
final List<ValidationError> errors = validate(executionInput, document);
312+
if (!errors.isEmpty()) {
313+
preparsedDocumentEntry = new PreparsedDocumentEntry(errors);
314+
} else {
315+
preparsedDocumentEntry = new PreparsedDocumentEntry(document);
316+
}
317+
}
318+
preparsedDocumentProvider.put(executionInput.getQuery(), preparsedDocumentEntry);
295319
}
296-
final Document document = parseResult.getDocument();
297320

298-
final List<ValidationError> errors = validate(executionInput, document);
299-
if (!errors.isEmpty()) {
300-
return new ExecutionResultImpl(errors);
321+
if (preparsedDocumentEntry.hasErrors()) {
322+
return new ExecutionResultImpl(preparsedDocumentEntry.getErrors());
301323
}
302324

303-
return execute(executionInput, document);
325+
return execute(executionInput, preparsedDocumentEntry.getDocument());
304326
}
305327

306328
private ParseResult parse(ExecutionInput executionInput) {
@@ -339,11 +361,6 @@ private ExecutionResult execute(ExecutionInput executionInput, Document document
339361
return execution.execute(document, graphQLSchema, executionId, executionInput);
340362
}
341363

342-
private ExecutionResult toParseFailureExecutionResult(RecognitionException exception) {
343-
InvalidSyntaxError invalidSyntaxError = toInvalidSyntaxError(exception);
344-
return new ExecutionResultImpl(invalidSyntaxError);
345-
}
346-
347364
private InvalidSyntaxError toInvalidSyntaxError(RecognitionException recognitionException) {
348365
SourceLocation sourceLocation = null;
349366
if (recognitionException != null) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package graphql.execution.preparsed;
2+
3+
4+
public class NoOpPreparsedDocumentProvider implements PreparsedDocumentProvider {
5+
public static final NoOpPreparsedDocumentProvider INSTANCE = new NoOpPreparsedDocumentProvider();
6+
7+
@Override
8+
public PreparsedDocumentEntry get(String query) {
9+
return null;
10+
}
11+
12+
@Override
13+
public void put(String query, PreparsedDocumentEntry entry) {
14+
15+
}
16+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package graphql.execution.preparsed;
2+
3+
import graphql.GraphQLError;
4+
import graphql.language.Document;
5+
6+
import java.util.Collections;
7+
import java.util.List;
8+
9+
/**
10+
* An instance of a preparsed doucument entry represents the result of a query parse and validation, like
11+
* an either implementation it contains either the correct result in th document property or the errors.
12+
*/
13+
public class PreparsedDocumentEntry {
14+
private Document document;
15+
private List<? extends GraphQLError> errors;
16+
17+
public PreparsedDocumentEntry(Document document) {
18+
this.document = document;
19+
}
20+
21+
public PreparsedDocumentEntry(List<? extends GraphQLError> errors) {
22+
this.errors = errors;
23+
}
24+
25+
public PreparsedDocumentEntry(GraphQLError error) {
26+
this.errors = Collections.singletonList(error);
27+
}
28+
29+
public Document getDocument() {
30+
return document;
31+
}
32+
33+
public List<? extends GraphQLError> getErrors() {
34+
return errors;
35+
}
36+
37+
38+
public boolean hasErrors() {
39+
return errors != null && !errors.isEmpty();
40+
}
41+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package graphql.execution.preparsed;
2+
3+
4+
/**
5+
* Interface that allows clients to hook in Document caching and/or whitelisting of queries
6+
*/
7+
public interface PreparsedDocumentProvider {
8+
/**
9+
* Get existing instance of a preparsed query
10+
*
11+
* @param query The graphql query
12+
* @return Null of missing or an instance of {@link PreparsedDocumentEntry}
13+
*/
14+
PreparsedDocumentEntry get(String query);
15+
16+
/**
17+
* Put the parse and validate result into the provider
18+
*
19+
* @param query The graphql query
20+
* @param entry The result of parse and validation of the query
21+
*/
22+
void put(String query, PreparsedDocumentEntry entry);
23+
}
24+
25+

src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy

Lines changed: 1 addition & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,12 @@
11
package graphql.execution.instrumentation
22

3-
import graphql.ExecutionResult
43
import graphql.GraphQL
54
import graphql.StarWarsSchema
65
import graphql.execution.SimpleExecutionStrategy
7-
import graphql.execution.instrumentation.parameters.*
8-
import graphql.language.Document
9-
import graphql.validation.ValidationError
106
import spock.lang.Specification
117

128
class InstrumentationTest extends Specification {
139

14-
class Timer<T> implements InstrumentationContext<T> {
15-
def op
16-
def start = System.currentTimeMillis()
17-
def executionList = []
18-
19-
Timer(op, executionList) {
20-
this.op = op
21-
this.executionList = executionList
22-
executionList << "start:$op"
23-
println("Started $op...")
24-
}
25-
26-
def end() {
27-
this.executionList << "end:$op"
28-
def ms = System.currentTimeMillis() - start
29-
println("\tEnded $op in ${ms}ms")
30-
}
31-
32-
@Override
33-
void onEnd(T result) {
34-
end()
35-
}
36-
37-
@Override
38-
void onEnd(Exception e) {
39-
end()
40-
}
41-
}
4210

4311

4412
def 'Instrumentation of simple serial execution'() {
@@ -85,40 +53,7 @@ class InstrumentationTest extends Specification {
8553

8654
when:
8755

88-
def instrumentation = new Instrumentation() {
89-
90-
def executionList = []
91-
92-
@Override
93-
InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
94-
new Timer("execution", executionList)
95-
}
96-
97-
@Override
98-
InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters) {
99-
return new Timer("parse", executionList)
100-
}
101-
102-
@Override
103-
InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters) {
104-
return new Timer("validation", executionList)
105-
}
106-
107-
@Override
108-
InstrumentationContext<ExecutionResult> beginDataFetch(InstrumentationDataFetchParameters parameters) {
109-
return new Timer("data-fetch", executionList)
110-
}
111-
112-
@Override
113-
InstrumentationContext<ExecutionResult> beginField(InstrumentationFieldParameters parameters) {
114-
return new Timer("field-$parameters.field.name", executionList)
115-
}
116-
117-
@Override
118-
InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters) {
119-
return new Timer("fetch-$parameters.field.name", executionList)
120-
}
121-
}
56+
def instrumentation = new TestingInstrumentation()
12257

12358
def strategy = new SimpleExecutionStrategy()
12459
def graphQL = GraphQL
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package graphql.execution.instrumentation
2+
3+
class TestingInstrumentContext<T> implements InstrumentationContext<T> {
4+
def op
5+
def start = System.currentTimeMillis()
6+
def executionList = []
7+
8+
TestingInstrumentContext(op, executionList) {
9+
this.op = op
10+
this.executionList = executionList
11+
executionList << "start:$op"
12+
println("Started $op...")
13+
}
14+
15+
def end() {
16+
this.executionList << "end:$op"
17+
def ms = System.currentTimeMillis() - start
18+
println("\tEnded $op in ${ms}ms")
19+
}
20+
21+
@Override
22+
void onEnd(T result) {
23+
end()
24+
}
25+
26+
@Override
27+
void onEnd(Exception e) {
28+
end()
29+
}
30+
}
31+
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package graphql.execution.instrumentation
2+
3+
import graphql.ExecutionResult
4+
import graphql.execution.instrumentation.parameters.*
5+
import graphql.language.Document
6+
import graphql.validation.ValidationError
7+
8+
class TestingInstrumentation implements Instrumentation {
9+
10+
def executionList = []
11+
12+
@Override
13+
InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
14+
new TestingInstrumentContext("execution", executionList)
15+
}
16+
17+
@Override
18+
InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters) {
19+
return new TestingInstrumentContext("parse", executionList)
20+
}
21+
22+
@Override
23+
InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters) {
24+
return new TestingInstrumentContext("validation", executionList)
25+
}
26+
27+
@Override
28+
InstrumentationContext<ExecutionResult> beginDataFetch(InstrumentationDataFetchParameters parameters) {
29+
return new TestingInstrumentContext("data-fetch", executionList)
30+
}
31+
32+
@Override
33+
InstrumentationContext<ExecutionResult> beginField(InstrumentationFieldParameters parameters) {
34+
return new TestingInstrumentContext("field-$parameters.field.name", executionList)
35+
}
36+
37+
@Override
38+
InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters) {
39+
return new TestingInstrumentContext("fetch-$parameters.field.name", executionList)
40+
}
41+
}
42+

0 commit comments

Comments
 (0)