From 681f94b8b7832efc8ead765a87163cc270908771 Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Fri, 14 Aug 2020 16:17:38 -0700 Subject: [PATCH] Resolve requests for federation entities in parallel In apollo federation, we may be asked for data about a list of entities. These can typically be resolved in parallel, just as with sibling fields in ordinary GraphQL queries. Now we do! Note that I kept the behavior where if one lookup fails, we cancel the whole assembly. It's not clear to me if that's the ideal behavior -- there are arguments both ways -- but I figured it was easiest to retain it for now. (I used x/sync/errgroup to do so; it's already a transitive dep via x/tools but if the new direct dep is an issue I can do it with sync.Waitgroup instead, at the cost of some extra code.) The examples probably give the clearest picture of the changes. (And the clearest test; the changed functionality is already exercised by `integration-test.js` as watching the test server logs will attest.) Fixes #1278. --- .../accounts/graph/generated/federation.go | 38 ++++++++++++---- .../products/graph/generated/federation.go | 38 ++++++++++++---- .../reviews/graph/generated/federation.go | 45 ++++++++++++++----- go.mod | 1 + go.sum | 2 + plugin/federation/federation.gotpl | 40 +++++++++++++---- 6 files changed, 128 insertions(+), 36 deletions(-) diff --git a/example/federation/accounts/graph/generated/federation.go b/example/federation/accounts/graph/generated/federation.go index 6c574a8a863..d120773342f 100644 --- a/example/federation/accounts/graph/generated/federation.go +++ b/example/federation/accounts/graph/generated/federation.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/99designs/gqlgen/plugin/federation/fedruntime" + "golang.org/x/sync/errgroup" ) func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.Service, error) { @@ -31,31 +32,52 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. } func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) ([]fedruntime.Entity, error) { - list := []fedruntime.Entity{} - for _, rep := range representations { + list := make([]fedruntime.Entity, len(representations)) + resolveEntity := func(ctx context.Context, i int, rep map[string]interface{}) error { typeName, ok := rep["__typename"].(string) if !ok { - return nil, errors.New("__typename must be an existing string") + return errors.New("__typename must be an existing string") } switch typeName { case "User": id0, err := ec.unmarshalNID2string(ctx, rep["id"]) if err != nil { - return nil, errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) } entity, err := ec.resolvers.Entity().FindUserByID(ctx, id0) if err != nil { - return nil, err + return err } - list = append(list, entity) + list[i] = entity + return nil default: - return nil, errors.New("unknown type: " + typeName) + return errors.New("unknown type: " + typeName) } } - return list, nil + + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + switch len(representations) { + case 0: + return list, nil + case 1: + err := resolveEntity(ctx, 0, representations[0]) + return list, err + default: + eg, gCtx := errgroup.WithContext(ctx) + for i, rep := range representations { + i, rep := i, rep + eg.Go(func() error { return resolveEntity(gCtx, i, rep) }) + } + err := eg.Wait() + if err != nil { + return nil, err + } + return list, nil + } } diff --git a/example/federation/products/graph/generated/federation.go b/example/federation/products/graph/generated/federation.go index 6db7e70cd84..4ec15982ec8 100644 --- a/example/federation/products/graph/generated/federation.go +++ b/example/federation/products/graph/generated/federation.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/99designs/gqlgen/plugin/federation/fedruntime" + "golang.org/x/sync/errgroup" ) func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.Service, error) { @@ -31,31 +32,52 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. } func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) ([]fedruntime.Entity, error) { - list := []fedruntime.Entity{} - for _, rep := range representations { + list := make([]fedruntime.Entity, len(representations)) + resolveEntity := func(ctx context.Context, i int, rep map[string]interface{}) error { typeName, ok := rep["__typename"].(string) if !ok { - return nil, errors.New("__typename must be an existing string") + return errors.New("__typename must be an existing string") } switch typeName { case "Product": id0, err := ec.unmarshalNString2string(ctx, rep["upc"]) if err != nil { - return nil, errors.New(fmt.Sprintf("Field %s undefined in schema.", "upc")) + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "upc")) } entity, err := ec.resolvers.Entity().FindProductByUpc(ctx, id0) if err != nil { - return nil, err + return err } - list = append(list, entity) + list[i] = entity + return nil default: - return nil, errors.New("unknown type: " + typeName) + return errors.New("unknown type: " + typeName) } } - return list, nil + + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + switch len(representations) { + case 0: + return list, nil + case 1: + err := resolveEntity(ctx, 0, representations[0]) + return list, err + default: + eg, gCtx := errgroup.WithContext(ctx) + for i, rep := range representations { + i, rep := i, rep + eg.Go(func() error { return resolveEntity(gCtx, i, rep) }) + } + err := eg.Wait() + if err != nil { + return nil, err + } + return list, nil + } } diff --git a/example/federation/reviews/graph/generated/federation.go b/example/federation/reviews/graph/generated/federation.go index 60a34909578..eb7d0ab02bc 100644 --- a/example/federation/reviews/graph/generated/federation.go +++ b/example/federation/reviews/graph/generated/federation.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/99designs/gqlgen/plugin/federation/fedruntime" + "golang.org/x/sync/errgroup" ) func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.Service, error) { @@ -31,45 +32,67 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. } func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) ([]fedruntime.Entity, error) { - list := []fedruntime.Entity{} - for _, rep := range representations { + list := make([]fedruntime.Entity, len(representations)) + resolveEntity := func(ctx context.Context, i int, rep map[string]interface{}) error { typeName, ok := rep["__typename"].(string) if !ok { - return nil, errors.New("__typename must be an existing string") + return errors.New("__typename must be an existing string") } switch typeName { case "Product": id0, err := ec.unmarshalNString2string(ctx, rep["upc"]) if err != nil { - return nil, errors.New(fmt.Sprintf("Field %s undefined in schema.", "upc")) + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "upc")) } entity, err := ec.resolvers.Entity().FindProductByUpc(ctx, id0) if err != nil { - return nil, err + return err } - list = append(list, entity) + list[i] = entity + return nil case "User": id0, err := ec.unmarshalNID2string(ctx, rep["id"]) if err != nil { - return nil, errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) } entity, err := ec.resolvers.Entity().FindUserByID(ctx, id0) if err != nil { - return nil, err + return err } - list = append(list, entity) + list[i] = entity + return nil default: - return nil, errors.New("unknown type: " + typeName) + return errors.New("unknown type: " + typeName) } } - return list, nil + + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + switch len(representations) { + case 0: + return list, nil + case 1: + err := resolveEntity(ctx, 0, representations[0]) + return list, err + default: + eg, gCtx := errgroup.WithContext(ctx) + for i, rep := range representations { + i, rep := i, rep + eg.Go(func() error { return resolveEntity(gCtx, i, rep) }) + } + err := eg.Wait() + if err != nil { + return nil, err + } + return list, nil + } } diff --git a/go.mod b/go.mod index 13777ee3887..1219e364f73 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/urfave/cli/v2 v2.1.1 github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e github.com/vektah/gqlparser/v2 v2.1.0 + golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 gopkg.in/yaml.v2 v2.2.4 sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755 diff --git a/go.sum b/go.sum index 4c3da9b8125..72a4e3c6eca 100644 --- a/go.sum +++ b/go.sum @@ -82,7 +82,9 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/plugin/federation/federation.gotpl b/plugin/federation/federation.gotpl index 96c25e85723..27442a60fb4 100644 --- a/plugin/federation/federation.gotpl +++ b/plugin/federation/federation.gotpl @@ -3,6 +3,7 @@ {{ reserveImport "fmt" }} {{ reserveImport "strings" }} +{{ reserveImport "golang.org/x/sync/errgroup" }} {{ reserveImport "github.com/99designs/gqlgen/plugin/federation/fedruntime" }} func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.Service, error) { @@ -26,11 +27,11 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. {{if .Entities}} func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) ([]fedruntime.Entity, error) { - list := []fedruntime.Entity{} - for _, rep := range representations { + list := make([]fedruntime.Entity, len(representations)) + resolveEntity := func(ctx context.Context, i int, rep map[string]interface{}) error { typeName, ok := rep["__typename"].(string) if !ok { - return nil, errors.New("__typename must be an existing string") + return errors.New("__typename must be an existing string") } switch typeName { {{ range .Entities }} @@ -39,31 +40,52 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati {{ range $i, $keyField := .KeyFields -}} id{{$i}}, err := ec.{{.TypeReference.UnmarshalFunc}}(ctx, rep["{{$keyField.Field.Name}}"]) if err != nil { - return nil, errors.New(fmt.Sprintf("Field %s undefined in schema.", "{{$keyField.Field.Name}}")) + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "{{$keyField.Field.Name}}")) } {{end}} entity, err := ec.resolvers.Entity().{{.ResolverName | go}}(ctx, {{ range $i, $_ := .KeyFields -}} id{{$i}}, {{end}}) if err != nil { - return nil, err + return err } {{ range .Requires }} {{ range .Fields}} entity.{{.NameGo}}, err = ec.{{.TypeReference.UnmarshalFunc}}(ctx, rep["{{.Name}}"]) if err != nil { - return nil, err + return err } {{ end }} {{ end }} - list = append(list, entity) + list[i] = entity + return nil {{ end }} {{ end }} default: - return nil, errors.New("unknown type: "+typeName) + return errors.New("unknown type: "+typeName) } } - return list, nil + + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + switch len(representations) { + case 0: + return list, nil + case 1: + err := resolveEntity(ctx, 0, representations[0]) + return list, err + default: + eg, gCtx := errgroup.WithContext(ctx) + for i, rep := range representations { + i, rep := i, rep + eg.Go(func() error { return resolveEntity(gCtx, i, rep) }) + } + err := eg.Wait() + if err != nil { + return nil, err + } + return list, nil + } } {{end}}