Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yet another PR for async execution and batching. Defer calling thunks until as late as possible during execution. #367

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 75 additions & 14 deletions executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ func executeFieldsSerially(p executeFieldsParams) *Result {
}
finalResults[responseName] = resolved
}
dethunkMap(finalResults)

return &Result{
Data: finalResults,
Expand All @@ -254,6 +255,16 @@ func executeFieldsSerially(p executeFieldsParams) *Result {

// Implements the "Evaluating selection sets" section of the spec for "read" mode.
func executeFields(p executeFieldsParams) *Result {
finalResults := executeSubFields(p)
dethunkMap(finalResults)

return &Result{
Data: finalResults,
Errors: p.ExecutionContext.Errors,
}
}

func executeSubFields(p executeFieldsParams) map[string]interface{} {
if p.Source == nil {
p.Source = map[string]interface{}{}
}
Expand All @@ -271,9 +282,42 @@ func executeFields(p executeFieldsParams) *Result {
finalResults[responseName] = resolved
}

return &Result{
Data: finalResults,
Errors: p.ExecutionContext.Errors,
return finalResults
}

// dethunkMap performs a breadth-first descent of the map, calling any thunks
// in the map values and replacing each thunk with that thunk's return value.
func dethunkMap(m map[string]interface{}) {
for k, v := range m {
if f, ok := v.(func() interface{}); ok {
m[k] = f()
}
}
for _, v := range m {
switch val := v.(type) {
case map[string]interface{}:
dethunkMap(val)
case []interface{}:
dethunkList(val)
}
}
}

// dethunkList iterates through the list, calling any thunks in the list
// and replacing each thunk with that thunk's return value.
func dethunkList(list []interface{}) {
for i, v := range list {
if f, ok := v.(func() interface{}); ok {
list[i] = f()
}
}
for _, v := range list {
switch val := v.(type) {
case map[string]interface{}:
dethunkMap(val)
case []interface{}:
dethunkList(val)
}
}
}

Expand Down Expand Up @@ -558,13 +602,9 @@ func completeValueCatchingError(eCtx *executionContext, returnType Type, fieldAS
func completeValue(eCtx *executionContext, returnType Type, fieldASTs []*ast.Field, info ResolveInfo, path *responsePath, result interface{}) interface{} {

resultVal := reflect.ValueOf(result)
for resultVal.IsValid() && resultVal.Type().Kind() == reflect.Func {
if propertyFn, ok := result.(func() interface{}); ok {
result = propertyFn()
resultVal = reflect.ValueOf(result)
} else {
err := gqlerrors.NewFormattedError("Error resolving func. Expected `func() interface{}` signature")
panic(gqlerrors.FormatError(err))
if resultVal.IsValid() && resultVal.Kind() == reflect.Func {
return func() interface{} {
return completeThunkValueCatchingError(eCtx, returnType, fieldASTs, info, path, result)
}
}

Expand Down Expand Up @@ -626,6 +666,30 @@ func completeValue(eCtx *executionContext, returnType Type, fieldASTs []*ast.Fie
return nil
}

func completeThunkValueCatchingError(eCtx *executionContext, returnType Type, fieldASTs []*ast.Field, info ResolveInfo, path *responsePath, result interface{}) (completed interface{}) {

// catch any panic invoked from the propertyFn (thunk)
defer func() {
if r := recover(); r != nil {
handleFieldError(r, FieldASTsToNodeASTs(fieldASTs), path, returnType, eCtx)
}
}()

propertyFn, ok := result.(func() interface{})
if !ok {
err := gqlerrors.NewFormattedError("Error resolving func. Expected `func() interface{}` signature")
panic(gqlerrors.FormatError(err))
}
result = propertyFn()

if returnType, ok := returnType.(*NonNull); ok {
completed := completeValue(eCtx, returnType, fieldASTs, info, path, result)
return completed
}
completed = completeValue(eCtx, returnType, fieldASTs, info, path, result)
return completed
}

// completeAbstractValue completes value of an Abstract type (Union / Interface) by determining the runtime type
// of that value, then completing based on that type.
func completeAbstractValue(eCtx *executionContext, returnType Abstract, fieldASTs []*ast.Field, info ResolveInfo, path *responsePath, result interface{}) interface{} {
Expand Down Expand Up @@ -709,10 +773,7 @@ func completeObjectValue(eCtx *executionContext, returnType *Object, fieldASTs [
Fields: subFieldASTs,
Path: path,
}
results := executeFields(executeFieldsParams)

return results.Data

return executeSubFields(executeFieldsParams)
}

// completeLeafValue complete a leaf value (Scalar / Enum) by serializing to a valid value, returning nil if serialization is not possible.
Expand Down
30 changes: 22 additions & 8 deletions lists_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,11 +579,18 @@ func TestLists_NullableListOfNonNullArrayOfFunc_ContainsNulls(t *testing.T) {
},
}
expected := &graphql.Result{
Data: map[string]interface{}{
"nest": map[string]interface{}{
"test": nil,
},
},
/*
// TODO: Because thunks are called after the result map has been assembled,
// we are not able to traverse up the tree until we find a nullable type,
// so in this case the entire data is nil. Will need some significant code
// restructure to restore this.
Data: map[string]interface{}{
"nest": map[string]interface{}{
"test": nil,
},
},
*/
Data: nil,
Errors: []gqlerrors.FormattedError{
{
Message: "Cannot return null for non-nullable field DataType.test.",
Expand Down Expand Up @@ -803,9 +810,16 @@ func TestLists_NonNullListOfNonNullArrayOfFunc_ContainsNulls(t *testing.T) {
},
}
expected := &graphql.Result{
Data: map[string]interface{}{
"nest": nil,
},
/*
// TODO: Because thunks are called after the result map has been assembled,
// we are not able to traverse up the tree until we find a nullable type,
// so in this case the entire data is nil. Will need some significant code
// restructure to restore this.
Data: map[string]interface{}{
"nest": nil,
},
*/
Data: nil,
Errors: []gqlerrors.FormattedError{
{
Message: "Cannot return null for non-nullable field DataType.test.",
Expand Down