Skip to content

Commit 6496791

Browse files
authored
Include data alongside errors for a service's partial success (#213)
* Bump nautilus/graphql to pick up partial success changes * Add failing tests for partial success with one service * Fix tests - Remove unused field - Extract channel sending from execution of a single step - Extract wait group handling from single step execution - Remove impossible condition with nil result Fixes #212
1 parent fb2d006 commit 6496791

File tree

6 files changed

+136
-60
lines changed

6 files changed

+136
-60
lines changed

cmd/gateway/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ go 1.16
44

55
require (
66
github.com/nautilus/gateway v0.3.17
7-
github.com/nautilus/graphql v0.0.25
7+
github.com/nautilus/graphql v0.0.26
88
github.com/spf13/cobra v0.0.5
99
)

cmd/gateway/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ github.com/nautilus/graphql v0.0.23 h1:uiQV2SxOfl63xNr4E5Qk9IxecD2llM/gVqKbM2lEH
7878
github.com/nautilus/graphql v0.0.23/go.mod h1:eoqPxo/+IdRSfLQKZ2V0al2vYeoMJKO72XpTsUYOmFI=
7979
github.com/nautilus/graphql v0.0.25 h1:N+wwpxnlob3nQK/+HTUPeGfSR0ayehhNqeJq1cdcqSU=
8080
github.com/nautilus/graphql v0.0.25/go.mod h1:ex8FB+RiAJoWGlL9iUcCWvetFOICVpvYP2jWi0cxp9E=
81+
github.com/nautilus/graphql v0.0.26 h1:Mfv0Eazj+xKWlSSIcXgJN1zFWhbSQkPsz4IWSk0G5EQ=
82+
github.com/nautilus/graphql v0.0.26/go.mod h1:ex8FB+RiAJoWGlL9iUcCWvetFOICVpvYP2jWi0cxp9E=
8183
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
8284
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
8385
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=

execute.go

Lines changed: 57 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type ParallelExecutor struct{}
3232
type queryExecutionResult struct {
3333
InsertionPoint []string
3434
Result map[string]interface{}
35-
StripNode bool
35+
Err error
3636
}
3737

3838
// execution is broken up into two phases:
@@ -83,7 +83,7 @@ func (executor *ParallelExecutor) Execute(ctx *ExecutionContext) (map[string]int
8383
// the root step could have multiple steps that have to happen
8484
for _, step := range ctx.Plan.RootStep.Then {
8585
stepWg.Add(1)
86-
go executeStep(ctx, ctx.Plan, step, []string{}, resultLock, ctx.Variables, resultCh, errCh, stepWg)
86+
go executeStep(ctx, ctx.Plan, step, []string{}, resultLock, ctx.Variables, resultCh, stepWg)
8787
}
8888

8989
// the list of errors we have encountered while executing the plan
@@ -95,24 +95,23 @@ func (executor *ParallelExecutor) Execute(ctx *ExecutionContext) (map[string]int
9595
select {
9696
// we have a new result
9797
case payload := <-resultCh:
98-
if payload == nil {
99-
continue
100-
}
10198
ctx.logger.Debug("Inserting result into ", payload.InsertionPoint)
10299
ctx.logger.Debug("Result: ", payload.Result)
103100

104101
// we have to grab the value in the result and write it to the appropriate spot in the
105102
// acumulator.
106-
err := executorInsertObject(ctx, result, resultLock, payload.InsertionPoint, payload.Result)
107-
if err != nil {
108-
errCh <- err
109-
continue
103+
insertErr := executorInsertObject(ctx, result, resultLock, payload.InsertionPoint, payload.Result)
104+
105+
switch {
106+
case payload.Err != nil: // response errors are the highest priority to return
107+
errCh <- payload.Err
108+
case insertErr != nil:
109+
errCh <- insertErr
110+
default:
111+
ctx.logger.Debug("Done. ", result)
112+
// one of the queries is done
113+
stepWg.Done()
110114
}
111-
112-
ctx.logger.Debug("Done. ", result)
113-
// one of the queries is done
114-
stepWg.Done()
115-
116115
case err := <-errCh:
117116
if err != nil {
118117
errMutex.Lock()
@@ -158,9 +157,41 @@ func executeStep(
158157
resultLock *sync.Mutex,
159158
queryVariables map[string]interface{},
160159
resultCh chan *queryExecutionResult,
161-
errCh chan error,
162160
stepWg *sync.WaitGroup,
163161
) {
162+
queryResult, dependentSteps, queryErr := executeOneStep(ctx, plan, step, insertionPoint, resultLock, queryVariables)
163+
// before publishing the current result, tell the wait-group about the dependent steps to wait for
164+
stepWg.Add(len(dependentSteps))
165+
ctx.logger.Debug("Pushing Result. Insertion point: ", insertionPoint, ". Value: ", queryResult)
166+
// send the result to be stitched in with our accumulator
167+
resultCh <- &queryExecutionResult{
168+
InsertionPoint: insertionPoint,
169+
Result: queryResult,
170+
Err: queryErr,
171+
}
172+
// We need to collect all the dependent steps and execute them after emitting the parent result in this function.
173+
// This avoids a race condition, where the result of a dependent request is published to the
174+
// result channel even before the result created in this iteration.
175+
// Execute dependent steps after the main step has been published.
176+
for _, sr := range dependentSteps {
177+
ctx.logger.Info("Spawn ", sr.insertionPoint)
178+
go executeStep(ctx, plan, sr.step, sr.insertionPoint, resultLock, queryVariables, resultCh, stepWg)
179+
}
180+
}
181+
182+
type dependentStepArgs struct {
183+
step *QueryPlanStep
184+
insertionPoint []string
185+
}
186+
187+
func executeOneStep(
188+
ctx *ExecutionContext,
189+
plan *QueryPlan,
190+
step *QueryPlanStep,
191+
insertionPoint []string,
192+
resultLock *sync.Mutex,
193+
queryVariables map[string]interface{},
194+
) (map[string]interface{}, []dependentStepArgs, error) {
164195
ctx.logger.Debug("Executing step to be inserted in ", step.ParentType, ". Insertion point: ", insertionPoint)
165196

166197
ctx.logger.Debug(step.SelectionSet)
@@ -186,14 +217,12 @@ func executeStep(
186217
// get the data of the point
187218
pointData, err := executorGetPointData(head)
188219
if err != nil {
189-
errCh <- err
190-
return
220+
return nil, nil, err
191221
}
192222

193223
// if we dont have an id
194224
if pointData.ID == "" {
195-
errCh <- fmt.Errorf("Could not find id in path")
196-
return
225+
return nil, nil, fmt.Errorf("Could not find id in path")
197226
}
198227

199228
// save the id as a variable to the query
@@ -202,8 +231,7 @@ func executeStep(
202231

203232
// if there is no queryer
204233
if step.Queryer == nil {
205-
errCh <- errors.New(" could not find queryer for step")
206-
return
234+
return nil, nil, errors.New(" could not find queryer for step")
207235
}
208236

209237
// the query we will use
@@ -233,8 +261,7 @@ func executeStep(
233261
}, &queryResult)
234262
if err != nil {
235263
ctx.logger.Warn("Network Error: ", err)
236-
errCh <- err
237-
return
264+
return queryResult, nil, err
238265
}
239266

240267
// NOTE: this insertion point could point to a list of values. If it did, we have to have
@@ -249,36 +276,19 @@ func executeStep(
249276
// get the result from the response that we have to stitch there
250277
extractedResult, err := executorExtractValue(ctx, queryResult, resultLock, []string{"node"})
251278
if err != nil {
252-
errCh <- err
253-
return
279+
return nil, nil, err
254280
}
255281

256282
resultObj, ok := extractedResult.(map[string]interface{})
257283
if !ok {
258-
errCh <- fmt.Errorf("Query result of node query was not an object: %v", queryResult)
259-
return
284+
return nil, nil, fmt.Errorf("Query result of node query was not an object: %v", queryResult)
260285
}
261286

262287
queryResult = resultObj
263288
}
264289

265-
// we need to collect all the dependent steps and execute them at last in this function
266-
// to avoid a race condition, where the result of a dependent request is published to the
267-
// result channel even before the result created in this iteration
268-
type stepArgs struct {
269-
step *QueryPlanStep
270-
insertionPoint []string
271-
}
272-
var dependentSteps []stepArgs
273-
// defer the execution of the dependent steps after the main step has been published
274-
defer func() {
275-
for _, sr := range dependentSteps {
276-
ctx.logger.Info("Spawn ", sr.insertionPoint)
277-
go executeStep(ctx, plan, sr.step, sr.insertionPoint, resultLock, queryVariables, resultCh, errCh, stepWg)
278-
}
279-
}()
280-
281290
// if there are next steps
291+
var dependentSteps []dependentStepArgs
282292
if len(step.Then) > 0 {
283293
ctx.logger.Debug("Kicking off child queries")
284294
// we need to find the ids of the objects we are inserting into and then kick of the worker with the right
@@ -288,30 +298,19 @@ func executeStep(
288298
copy(copiedInsertionPoint, insertionPoint)
289299
insertPoints, err := executorFindInsertionPoints(ctx, resultLock, dependent.InsertionPoint, step.SelectionSet, queryResult, [][]string{copiedInsertionPoint}, step.FragmentDefinitions)
290300
if err != nil {
291-
// reset dependent steps - result would be discarded anyways
292-
dependentSteps = nil
293-
errCh <- err
294-
return
301+
return nil, nil, err
295302
}
296303

297304
// this dependent needs to fire for every object that the insertion point references
298305
for _, insertionPoint := range insertPoints {
299-
dependentSteps = append(dependentSteps, stepArgs{
306+
dependentSteps = append(dependentSteps, dependentStepArgs{
300307
step: dependent,
301308
insertionPoint: insertionPoint,
302309
})
303310
}
304311
}
305312
}
306-
307-
// before publishing the current result, tell the wait-group about the dependent steps to wait for
308-
stepWg.Add(len(dependentSteps))
309-
ctx.logger.Debug("Pushing Result. Insertion point: ", insertionPoint, ". Value: ", queryResult)
310-
// send the result to be stitched in with our accumulator
311-
resultCh <- &queryExecutionResult{
312-
InsertionPoint: insertionPoint,
313-
Result: queryResult,
314-
}
313+
return queryResult, dependentSteps, nil
315314
}
316315

317316
func max(a, b int) int {
@@ -386,7 +385,7 @@ func executorFindInsertionPoints(ctx *ExecutionContext, resultLock *sync.Mutex,
386385
// make sure we are looking at the top of the selection set next time
387386
selectionSetRoot = foundSelection.SelectionSet
388387

389-
var value = resultChunk
388+
value := resultChunk
390389

391390
// the bit of result chunk with the appropriate key should be a list
392391
rootValue, ok := value[point]

gateway_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,3 +753,51 @@ type User {
753753
}
754754
`, resp.Body.String())
755755
}
756+
757+
func TestDataAndErrorsBothReturnFromOneServicePartialSuccess(t *testing.T) {
758+
t.Parallel()
759+
schema, err := graphql.LoadSchema(`
760+
type Query {
761+
foo: String
762+
bar: String
763+
}
764+
`)
765+
require.NoError(t, err)
766+
queryerFactory := QueryerFactory(func(ctx *PlanningContext, url string) graphql.Queryer {
767+
return graphql.QueryerFunc(func(input *graphql.QueryInput) (interface{}, error) {
768+
return map[string]interface{}{
769+
"foo": "foo",
770+
"bar": nil,
771+
}, graphql.ErrorList{
772+
&graphql.Error{
773+
Message: "bar is broken",
774+
Path: []interface{}{"bar"},
775+
},
776+
}
777+
})
778+
})
779+
gateway, err := New([]*graphql.RemoteSchema{
780+
{Schema: schema, URL: "boo"},
781+
}, WithQueryerFactory(&queryerFactory))
782+
require.NoError(t, err)
783+
784+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"query": "query { foo bar }"}`))
785+
resp := httptest.NewRecorder()
786+
gateway.GraphQLHandler(resp, req)
787+
assert.Equal(t, http.StatusOK, resp.Code)
788+
assert.JSONEq(t, `
789+
{
790+
"data": {
791+
"foo": "foo",
792+
"bar": null
793+
},
794+
"errors": [
795+
{
796+
"message": "bar is broken",
797+
"path": ["bar"],
798+
"extensions": null
799+
}
800+
]
801+
}
802+
`, resp.Body.String())
803+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.17
55
require (
66
github.com/99designs/gqlgen v0.17.15
77
github.com/mitchellh/mapstructure v1.4.1
8-
github.com/nautilus/graphql v0.0.25
8+
github.com/nautilus/graphql v0.0.26
99
github.com/sirupsen/logrus v1.9.3
1010
github.com/stretchr/testify v1.9.0
1111
github.com/vektah/gqlparser/v2 v2.5.16

0 commit comments

Comments
 (0)