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

Breadth-first-traversal for PR #367 (allows for thunk-based dataloader batching) #388

Merged
merged 5 commits into from
Sep 10, 2018
Merged
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
142 changes: 128 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
}
dethunkMapDepthFirst(finalResults)

return &Result{
Data: finalResults,
Expand All @@ -254,6 +255,17 @@ 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)

dethunkMapWithBreadthFirstTraversal(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 +283,94 @@ func executeFields(p executeFieldsParams) *Result {
finalResults[responseName] = resolved
}

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

// dethunkQueue is a structure that allows us to execute a classic breadth-first traversal.
type dethunkQueue struct {
DethunkFuncs []func()
}

func (d *dethunkQueue) push(f func()) {
d.DethunkFuncs = append(d.DethunkFuncs, f)
}

func (d *dethunkQueue) shift() func() {
f := d.DethunkFuncs[0]
d.DethunkFuncs = d.DethunkFuncs[1:]
return f
}

// dethunkWithBreadthFirstTraversal 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. This parallels
// the reference graphql-js implementation, which calls Promise.all on thunks at each depth (which
// is an implicit parallel descent).
func dethunkMapWithBreadthFirstTraversal(finalResults map[string]interface{}) {
dethunkQueue := &dethunkQueue{DethunkFuncs: []func(){}}
dethunkMapBreadthFirst(finalResults, dethunkQueue)
for len(dethunkQueue.DethunkFuncs) > 0 {
f := dethunkQueue.shift()
f()
}
}

func dethunkMapBreadthFirst(m map[string]interface{}, dethunkQueue *dethunkQueue) {
for k, v := range m {
if f, ok := v.(func() interface{}); ok {
m[k] = f()
}
switch val := m[k].(type) {
case map[string]interface{}:
dethunkQueue.push(func() { dethunkMapBreadthFirst(val, dethunkQueue) })
case []interface{}:
dethunkQueue.push(func() { dethunkListBreadthFirst(val, dethunkQueue) })
}
}
}

func dethunkListBreadthFirst(list []interface{}, dethunkQueue *dethunkQueue) {
for i, v := range list {
if f, ok := v.(func() interface{}); ok {
list[i] = f()
}
switch val := list[i].(type) {
case map[string]interface{}:
dethunkQueue.push(func() { dethunkMapBreadthFirst(val, dethunkQueue) })
case []interface{}:
dethunkQueue.push(func() { dethunkListBreadthFirst(val, dethunkQueue) })
}
}
}

// dethunkMapDepthFirst performs a serial descent of the map, calling any thunks
// in the map values and replacing each thunk with that thunk's return value. This is needed
// to conform to the graphql-js reference implementation, which requires serial (depth-first)
// implementations for mutation selects.
func dethunkMapDepthFirst(m map[string]interface{}) {
for k, v := range m {
if f, ok := v.(func() interface{}); ok {
m[k] = f()
}
switch val := m[k].(type) {
case map[string]interface{}:
dethunkMapDepthFirst(val)
case []interface{}:
dethunkListDepthFirst(val)
}
}
}

func dethunkListDepthFirst(list []interface{}) {
for i, v := range list {
if f, ok := v.(func() interface{}); ok {
list[i] = f()
}
switch val := list[i].(type) {
case map[string]interface{}:
dethunkMapDepthFirst(val)
case []interface{}:
dethunkListDepthFirst(val)
}
}
}

Expand Down Expand Up @@ -558,13 +655,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 +719,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 +826,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