Skip to content

Commit 1435aeb

Browse files
committed
Resolve fields concurrently
Using "deferred resolve functions"
1 parent 105a6c2 commit 1435aeb

File tree

3 files changed

+168
-19
lines changed

3 files changed

+168
-19
lines changed

executor.go

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/graphql-go/graphql/gqlerrors"
1111
"github.com/graphql-go/graphql/language/ast"
12+
"sync"
1213
)
1314

1415
type ExecuteParams struct {
@@ -110,6 +111,15 @@ type ExecutionContext struct {
110111
VariableValues map[string]interface{}
111112
Errors []gqlerrors.FormattedError
112113
Context context.Context
114+
115+
errorsMutex sync.Mutex
116+
resolveWaitGroup sync.WaitGroup
117+
}
118+
119+
func (eCtx *ExecutionContext) addError(err gqlerrors.FormattedError) {
120+
eCtx.errorsMutex.Lock()
121+
defer eCtx.errorsMutex.Unlock()
122+
eCtx.Errors = append(eCtx.Errors, err)
113123
}
114124

115125
func buildExecutionContext(p BuildExecutionCtxParams) (*ExecutionContext, error) {
@@ -278,13 +288,38 @@ func executeFields(p ExecuteFieldsParams) *Result {
278288
p.Fields = map[string][]*ast.Field{}
279289
}
280290

291+
var numberOfDeferredFunctions int
292+
recoverChan := make(chan interface{}, len(p.Fields))
293+
294+
var resultsMutex sync.Mutex
281295
finalResults := map[string]interface{}{}
282296
for responseName, fieldASTs := range p.Fields {
283297
resolved, state := resolveField(p.ExecutionContext, p.ParentType, p.Source, fieldASTs)
284298
if state.hasNoFieldDefs {
285299
continue
286300
}
287-
finalResults[responseName] = resolved
301+
if resolve, ok := resolved.(deferredResolveFunction); ok {
302+
numberOfDeferredFunctions += 1
303+
go func() {
304+
defer func() {
305+
recoverChan <- recover()
306+
}()
307+
308+
resultsMutex.Lock()
309+
defer resultsMutex.Unlock()
310+
finalResults[responseName] = resolve()
311+
}()
312+
} else {
313+
resultsMutex.Lock()
314+
finalResults[responseName] = resolved
315+
resultsMutex.Unlock()
316+
}
317+
}
318+
319+
for i := 0; i < numberOfDeferredFunctions; i++ {
320+
if r := <-recoverChan; r != nil {
321+
panic(r)
322+
}
288323
}
289324

290325
return &Result{
@@ -503,32 +538,36 @@ type resolveFieldResultState struct {
503538
hasNoFieldDefs bool
504539
}
505540

541+
type deferredResolveFunction func() interface{}
542+
506543
// Resolves the field on the given source object. In particular, this
507544
// figures out the value that the field returns by calling its resolve function,
508545
// then calls completeValue to complete promises, serialize scalars, or execute
509546
// the sub-selection-set for objects.
510547
func resolveField(eCtx *ExecutionContext, parentType *Object, source interface{}, fieldASTs []*ast.Field) (result interface{}, resultState resolveFieldResultState) {
511548
// catch panic from resolveFn
512549
var returnType Output
550+
handleRecover := func(r interface{}) {
551+
var err error
552+
if r, ok := r.(string); ok {
553+
err = NewLocatedError(
554+
fmt.Sprintf("%v", r),
555+
FieldASTsToNodeASTs(fieldASTs),
556+
)
557+
}
558+
if r, ok := r.(error); ok {
559+
err = gqlerrors.FormatError(r)
560+
}
561+
// send panic upstream
562+
if _, ok := returnType.(*NonNull); ok {
563+
panic(gqlerrors.FormatError(err))
564+
}
565+
eCtx.addError(gqlerrors.FormatError(err))
566+
}
567+
513568
defer func() (interface{}, resolveFieldResultState) {
514569
if r := recover(); r != nil {
515-
516-
var err error
517-
if r, ok := r.(string); ok {
518-
err = NewLocatedError(
519-
fmt.Sprintf("%v", r),
520-
FieldASTsToNodeASTs(fieldASTs),
521-
)
522-
}
523-
if r, ok := r.(error); ok {
524-
err = gqlerrors.FormatError(r)
525-
}
526-
// send panic upstream
527-
if _, ok := returnType.(*NonNull); ok {
528-
panic(gqlerrors.FormatError(err))
529-
}
530-
eCtx.Errors = append(eCtx.Errors, gqlerrors.FormatError(err))
531-
return result, resultState
570+
handleRecover(r)
532571
}
533572
return result, resultState
534573
}()
@@ -580,6 +619,25 @@ func resolveField(eCtx *ExecutionContext, parentType *Object, source interface{}
580619
panic(gqlerrors.FormatError(resolveFnError))
581620
}
582621

622+
if deferredResolveFn, ok := result.(func() (interface{}, error)); ok {
623+
return deferredResolveFunction(func() (result interface{}) {
624+
defer func() interface{} {
625+
if r := recover(); r != nil {
626+
handleRecover(r)
627+
}
628+
629+
return result
630+
}()
631+
632+
result, resolveFnError = deferredResolveFn()
633+
if resolveFnError != nil {
634+
panic(gqlerrors.FormatError(resolveFnError))
635+
}
636+
637+
return completeValueCatchingError(eCtx, returnType, fieldASTs, info, result)
638+
}), resultState
639+
}
640+
583641
completed := completeValueCatchingError(eCtx, returnType, fieldASTs, info, result)
584642
return completed, resultState
585643
}
@@ -593,7 +651,7 @@ func completeValueCatchingError(eCtx *ExecutionContext, returnType Type, fieldAS
593651
panic(r)
594652
}
595653
if err, ok := r.(gqlerrors.FormattedError); ok {
596-
eCtx.Errors = append(eCtx.Errors, err)
654+
eCtx.addError(err)
597655
}
598656
return completed
599657
}

executor_resolve_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,55 @@ func TestExecutesResolveFunction_UsesProvidedResolveFunction(t *testing.T) {
114114
}
115115
}
116116

117+
func TestExecutesResolveFunction_UsesProvidedResolveFunction_ResolveFunctionIsDeferred(t *testing.T) {
118+
schema := testSchema(t, &graphql.Field{
119+
Type: graphql.String,
120+
Args: graphql.FieldConfigArgument{
121+
"aStr": &graphql.ArgumentConfig{Type: graphql.String},
122+
"aInt": &graphql.ArgumentConfig{Type: graphql.Int},
123+
},
124+
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
125+
return func() (interface{}, error) {
126+
b, err := json.Marshal(p.Args)
127+
return string(b), err
128+
}, nil
129+
},
130+
})
131+
132+
expected := map[string]interface{}{
133+
"test": "{}",
134+
}
135+
result := graphql.Do(graphql.Params{
136+
Schema: schema,
137+
RequestString: `{ test }`,
138+
})
139+
if !reflect.DeepEqual(expected, result.Data) {
140+
t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result.Data))
141+
}
142+
143+
expected = map[string]interface{}{
144+
"test": `{"aStr":"String!"}`,
145+
}
146+
result = graphql.Do(graphql.Params{
147+
Schema: schema,
148+
RequestString: `{ test(aStr: "String!") }`,
149+
})
150+
if !reflect.DeepEqual(expected, result.Data) {
151+
t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result.Data))
152+
}
153+
154+
expected = map[string]interface{}{
155+
"test": `{"aInt":-123,"aStr":"String!"}`,
156+
}
157+
result = graphql.Do(graphql.Params{
158+
Schema: schema,
159+
RequestString: `{ test(aInt: -123, aStr: "String!") }`,
160+
})
161+
if !reflect.DeepEqual(expected, result.Data) {
162+
t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result.Data))
163+
}
164+
}
165+
117166
func TestExecutesResolveFunction_UsesProvidedResolveFunction_SourceIsStruct_WithoutJSONTags(t *testing.T) {
118167

119168
// For structs without JSON tags, it will map to upper-cased exported field names

executor_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,6 +1483,48 @@ func TestQuery_ExecutionDoesNotAddErrorsFromFieldResolveFn(t *testing.T) {
14831483
}
14841484
}
14851485

1486+
func TestQuery_DeferredResolveFn_ExecutionAddsErrorsFromFieldResolveFn(t *testing.T) {
1487+
qError := errors.New("queryError")
1488+
q := graphql.NewObject(graphql.ObjectConfig{
1489+
Name: "Query",
1490+
Fields: graphql.Fields{
1491+
"a": &graphql.Field{
1492+
Type: graphql.String,
1493+
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
1494+
return func() (interface{}, error) {
1495+
return nil, qError
1496+
}, nil
1497+
},
1498+
},
1499+
"b": &graphql.Field{
1500+
Type: graphql.String,
1501+
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
1502+
return func() (interface{}, error) {
1503+
return "ok", nil
1504+
}, nil
1505+
},
1506+
},
1507+
},
1508+
})
1509+
blogSchema, err := graphql.NewSchema(graphql.SchemaConfig{
1510+
Query: q,
1511+
})
1512+
if err != nil {
1513+
t.Fatalf("unexpected error, got: %v", err)
1514+
}
1515+
query := "{ a }"
1516+
result := graphql.Do(graphql.Params{
1517+
Schema: blogSchema,
1518+
RequestString: query,
1519+
})
1520+
if len(result.Errors) == 0 {
1521+
t.Fatal("wrong result, expected errors, got no errors")
1522+
}
1523+
if result.Errors[0].Error() != qError.Error() {
1524+
t.Fatalf("wrong result, unexpected error, got: %v, expected: %v", result.Errors[0], qError)
1525+
}
1526+
}
1527+
14861528
func TestQuery_InputObjectUsesFieldDefaultValueFn(t *testing.T) {
14871529
inputType := graphql.NewInputObject(graphql.InputObjectConfig{
14881530
Name: "Input",

0 commit comments

Comments
 (0)