Skip to content

Commit 0c65a91

Browse files
jbreeden-sfxBrian KruegerBrian Kruegeredwinmo-splunkeriiska
authored
Task/sync with splunk fork (#94)
* Added in returning 400's for plan failure, data set as nil when it doesn't exist * fixed go.mod * tidied mods * close channel being held open forever * break on close * Prioritize location selection for fragments When selecting a location for resolving Fields within Fragment Spreads, we should select the parent's location if possible to avoid executing federation unnecessarily. This fixes the problem where we used to simply select the first element in the array. The proposed fix will prioritize the parent's location. If no priority is matched, then the first location is still selected. Note that this creates uneven traffic for one location. * Query operation name (#4) adds operation name to queries tests that operation name is actually passed down to Query * fix query strings by putting operationName back into them (#5) * Broke out planning step for error handling; fixed memory leak (#81) * Added in returning 400's for plan failure, data set as nil when it doesn't exist * fixed go.mod * tidied mods * close channel being held open forever * break on close * changed installation type for run * fix query strings by putting operationName back into them * Fixed handling of multiple operations (#84) * added test to respect operation name * moved operation name to gateway execute * only the plan for the right operation name is executed * throw error if multiple plans with no operationName * no pointers to slices * ran gofmt * simpler fix * update unit tests Co-authored-by: Brian Krueger <bk@splunk.com> Co-authored-by: Alec Aivazis <alec@aivazis.com> * Add graphql codes inside the extensions tag for all errors * Add an option for setting location priorities Co-authored-by: Brian Krueger <bkrueger@splunk.com> Co-authored-by: Brian Krueger <bk@splunk.com> Co-authored-by: Edwin Mo Song <emsong@splunk.com> Co-authored-by: Erik Riiska <eriiska@splunk.com> Co-authored-by: Alec Aivazis <alec@aivazis.com> Co-authored-by: bardhi <bardho@gmail.com> Co-authored-by: Bardhi Shtylla <bshtylla@splunk.com> Co-authored-by: Jared Breeden <jbreeden@splunk.com>
1 parent 0ecae8b commit 0c65a91

File tree

6 files changed

+296
-41
lines changed

6 files changed

+296
-41
lines changed

execute.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,17 @@ func executeStep(
210210
}
211211
}
212212

213+
operationName := ""
214+
if plan != nil && plan.Operation != nil {
215+
operationName = plan.Operation.Name
216+
}
217+
213218
// fire the query
214219
err := queryer.Query(ctx.RequestContext, &graphql.QueryInput{
215220
Query: step.QueryString,
216221
QueryDocument: step.QueryDocument,
217222
Variables: variables,
223+
OperationName: operationName,
218224
}, &queryResult)
219225
if err != nil {
220226
log.Warn("Network Error: ", err)

execute_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ func TestExecutor_plansOfOne(t *testing.T) {
4747
"hello",
4848
"world",
4949
},
50-
}},
50+
},
51+
},
5152
},
5253
},
5354
},
@@ -1537,6 +1538,7 @@ func TestExecutor_threadsVariables(t *testing.T) {
15371538
Plan: &QueryPlan{
15381539
Operation: &ast.OperationDefinition{
15391540
Operation: ast.Query,
1541+
Name: "hoopla",
15401542
VariableDefinitions: fullVariableDefs,
15411543
},
15421544
RootStep: &QueryPlanStep{
@@ -1571,6 +1573,7 @@ func TestExecutor_threadsVariables(t *testing.T) {
15711573
// and definitions
15721574
assert.Equal(t, ast.VariableDefinitionList{fullVariableDefs[0]}, input.QueryDocument.Operations[0].VariableDefinitions)
15731575
assert.Equal(t, "hello", input.Query)
1576+
assert.Equal(t, "hoopla", input.OperationName)
15741577

15751578
return map[string]interface{}{"values": []string{"world"}}, nil
15761579
},

gateway.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ import (
1515
// remote schemas into one, generating a query plan to execute based on an incoming request, and following
1616
// that plan
1717
type Gateway struct {
18-
sources []*graphql.RemoteSchema
19-
schema *ast.Schema
20-
planner QueryPlanner
21-
executor Executor
22-
merger Merger
23-
middlewares MiddlewareList
24-
queryFields []*QueryField
25-
queryerFactory *QueryerFactory
26-
queryPlanCache QueryPlanCache
18+
sources []*graphql.RemoteSchema
19+
schema *ast.Schema
20+
planner QueryPlanner
21+
executor Executor
22+
merger Merger
23+
middlewares MiddlewareList
24+
queryFields []*QueryField
25+
queryerFactory *QueryerFactory
26+
queryPlanCache QueryPlanCache
27+
locationPriorities []string
2728

2829
// group up the list of middlewares at startup to avoid it during execution
2930
requestMiddlewares []graphql.NetworkMiddleware
@@ -149,6 +150,14 @@ func New(sources []*graphql.RemoteSchema, configs ...Option) (*Gateway, error) {
149150
}
150151
}
151152

153+
// if we have location priorities to assign
154+
if gateway.locationPriorities != nil {
155+
// if the planner can accept the priorities
156+
if planner, ok := gateway.planner.(PlannerWithLocationPriorities); ok {
157+
gateway.planner = planner.WithLocationPriorities(gateway.locationPriorities)
158+
}
159+
}
160+
152161
internal := gateway.internalSchema()
153162
// find the field URLs before we merge schemas. We need to make sure to include
154163
// the fields defined by the gateway's internal schema
@@ -256,6 +265,12 @@ func WithQueryerFactory(factory *QueryerFactory) Option {
256265
}
257266
}
258267

268+
func WithLocationPriorities(priorities []string) Option {
269+
return func(g *Gateway) {
270+
g.locationPriorities = priorities
271+
}
272+
}
273+
259274
var nodeField = &QueryField{
260275
Name: "node",
261276
Type: ast.NamedType("Node", &ast.Position{}),

gateway_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ func TestGateway(t *testing.T) {
127127
assert.Equal(t, &factory, gateway.planner.(*MinQueriesPlanner).QueryerFactory)
128128
})
129129

130+
t.Run("WithLocationPriorities", func(t *testing.T) {
131+
priorities := []string{"url1", "url2"}
132+
133+
gateway, err := New(sources, WithLocationPriorities(priorities))
134+
if err != nil {
135+
t.Error(err.Error())
136+
return
137+
}
138+
139+
assert.Equal(t, priorities, gateway.locationPriorities)
140+
})
141+
130142
t.Run("fieldURLs ignore introspection", func(t *testing.T) {
131143
locations := fieldURLs(sources, true)
132144

plan.go

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ type PlannerWithQueryerFactory interface {
6060
WithQueryerFactory(*QueryerFactory) QueryPlanner
6161
}
6262

63+
// PlannerWithLocationFactory is an interface for planners with configurable location priorities
64+
type PlannerWithLocationPriorities interface {
65+
WithLocationPriorities(priorities []string) QueryPlanner
66+
}
67+
6368
// QueryerFactory is a function that returns the queryer to use depending on the context
6469
type QueryerFactory func(ctx *PlanningContext, url string) graphql.Queryer
6570

@@ -72,6 +77,7 @@ type Planner struct {
7277
// MinQueriesPlanner does the most basic level of query planning
7378
type MinQueriesPlanner struct {
7479
Planner
80+
LocationPriorities []string
7581
}
7682

7783
// WithQueryerFactory returns a version of the planner with the factory set
@@ -80,6 +86,11 @@ func (p *MinQueriesPlanner) WithQueryerFactory(factory *QueryerFactory) QueryPla
8086
return p
8187
}
8288

89+
func (p *MinQueriesPlanner) WithLocationPriorities(priorities []string) QueryPlanner {
90+
p.LocationPriorities = priorities
91+
return p
92+
}
93+
8394
// PlanningContext is the input struct to the Plan method
8495
type PlanningContext struct {
8596
Query string
@@ -244,7 +255,7 @@ func (p *MinQueriesPlanner) generatePlans(ctx *PlanningContext, query *ast.Query
244255
}
245256

246257
// build up the query document
247-
step.QueryDocument = plannerBuildQuery(step.ParentType, variableDefs, step.SelectionSet, step.FragmentDefinitions)
258+
step.QueryDocument = plannerBuildQuery(plan.Operation.Name, step.ParentType, variableDefs, step.SelectionSet, step.FragmentDefinitions)
248259

249260
// we also need to turn the query into a string
250261
queryString, err := graphql.PrintQuery(step.QueryDocument)
@@ -599,13 +610,41 @@ func (p *MinQueriesPlanner) wrapSelectionSet(config *extractSelectionConfig, loc
599610
return ast.SelectionSet{selection}, nil
600611
}
601612

613+
// selects one location out of possibleLocations, prioritizing the parent's location and the internal schema
614+
func (p *MinQueriesPlanner) selectLocation(possibleLocations []string, config *extractSelectionConfig) string {
615+
// if this field can only be found in one location
616+
if len(possibleLocations) == 1 {
617+
return possibleLocations[0]
618+
// the field can be found in many locations
619+
} else {
620+
// locations to prioritize first
621+
priorities := make([]string, len(p.LocationPriorities), len(p.LocationPriorities)+2)
622+
copy(priorities, p.LocationPriorities)
623+
priorities = append(priorities, config.parentLocation, internalSchemaLocation)
624+
625+
for _, priority := range priorities {
626+
// look to see if the current location is one of the possible locations
627+
for _, location := range possibleLocations {
628+
// if the location is the same as the parent
629+
if location == priority {
630+
// assign this field to the parents entry
631+
return priority
632+
}
633+
}
634+
}
635+
636+
// if we got here then this field can be found in multiple services and none of the top priority locations.
637+
// for now, just use the first one
638+
return possibleLocations[0]
639+
}
640+
}
641+
602642
func (p *MinQueriesPlanner) groupSelectionSet(config *extractSelectionConfig) (map[string]ast.SelectionSet, map[string]ast.FragmentDefinitionList, error) {
603643

604644
locationFields := map[string]ast.SelectionSet{}
605645
locationFragments := map[string]ast.FragmentDefinitionList{}
606646

607647
// split each selection into groups of selection sets to be sent to a single service
608-
FieldLoop:
609648
for _, selection := range config.selection {
610649
// each kind of selection contributes differently to the final selection set
611650
switch selection := selection.(type) {
@@ -628,30 +667,8 @@ FieldLoop:
628667
return nil, nil, err
629668
}
630669

631-
// if this field can only be found in one location
632-
if len(possibleLocations) == 1 {
633-
locationFields[possibleLocations[0]] = append(locationFields[possibleLocations[0]], field)
634-
// the field can be found in many locations
635-
} else {
636-
// locations to prioritize first
637-
for _, priority := range []string{config.parentLocation, internalSchemaLocation} {
638-
// look to see if the current location is one of the possible locations
639-
for _, location := range possibleLocations {
640-
// if the location is the same as the parent
641-
if location == priority {
642-
// assign this field to the parents entry
643-
locationFields[priority] = append(locationFields[priority], field)
644-
// we're done with this field
645-
continue FieldLoop
646-
}
647-
}
648-
}
649-
650-
// if we got here then this field can be found in multiple services and none of the top priority locations.
651-
// for now, just use the first one
652-
locationFields[possibleLocations[0]] = append(locationFields[possibleLocations[0]], field)
653-
}
654-
670+
location := p.selectLocation(possibleLocations, config)
671+
locationFields[location] = append(locationFields[location], field)
655672
case *ast.FragmentSpread:
656673
log.Debug("Encountered fragment spread ", selection.Name)
657674

@@ -691,8 +708,8 @@ FieldLoop:
691708
return nil, nil, err
692709
}
693710

694-
// add the field to the location
695-
fragmentLocations[fieldLocations[0]] = append(fragmentLocations[fieldLocations[0]], field)
711+
fieldLocation := p.selectLocation(fieldLocations, config)
712+
fragmentLocations[fieldLocation] = append(fragmentLocations[fieldLocation], field)
696713

697714
case *ast.FragmentSpread, *ast.InlineFragment:
698715
// non-field selections will be handled in the next tick
@@ -890,11 +907,12 @@ func (p *Planner) GetQueryer(ctx *PlanningContext, url string) graphql.Queryer {
890907
return graphql.NewSingleRequestQueryer(url)
891908
}
892909

893-
func plannerBuildQuery(parentType string, variables ast.VariableDefinitionList, selectionSet ast.SelectionSet, fragmentDefinitions ast.FragmentDefinitionList) *ast.QueryDocument {
910+
func plannerBuildQuery(operationName, parentType string, variables ast.VariableDefinitionList, selectionSet ast.SelectionSet, fragmentDefinitions ast.FragmentDefinitionList) *ast.QueryDocument {
894911
log.Debug("Building Query: \n"+"\tParentType: ", parentType, " ")
895912
// build up an operation for the query
896913
operation := &ast.OperationDefinition{
897914
VariableDefinitions: variables,
915+
Name: operationName,
898916
}
899917

900918
// assign the right operation

0 commit comments

Comments
 (0)