From 89c4e8436357ffd8056eb1172d7535ab99f24d62 Mon Sep 17 00:00:00 2001 From: Shinra Yamakawa <13755428+OldBigBuddha@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:47:43 +0900 Subject: [PATCH] add `worker_limit` option for server code generation (#3376) * add property in exec * add semaphore for goroutine limitation in template * generation test with worker_limit option * add comment * update init-template * misc --- api/generate_test.go | 4 ++ api/testdata/workerlimit/gqlgen.yml | 70 +++++++++++++++++++ api/testdata/workerlimit/graph/model/doc.go | 1 + .../workerlimit/graph/schema.graphqls | 28 ++++++++ codegen/config/exec.go | 6 ++ codegen/generated!.gotpl | 1 + codegen/type.gotpl | 22 +++++- init-templates/gqlgen.yml.gotmpl | 2 + 8 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 api/testdata/workerlimit/gqlgen.yml create mode 100644 api/testdata/workerlimit/graph/model/doc.go create mode 100644 api/testdata/workerlimit/graph/schema.graphqls diff --git a/api/generate_test.go b/api/generate_test.go index 0572042ef2d..c98f91e27b4 100644 --- a/api/generate_test.go +++ b/api/generate_test.go @@ -34,6 +34,10 @@ func TestGenerate(t *testing.T) { name: "federation2", workDir: filepath.Join(wd, "testdata", "federation2"), }, + { + name: "worker_limit", + workDir: filepath.Join(wd, "testdata", "workerlimit"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/api/testdata/workerlimit/gqlgen.yml b/api/testdata/workerlimit/gqlgen.yml new file mode 100644 index 00000000000..0f0ff69c536 --- /dev/null +++ b/api/testdata/workerlimit/gqlgen.yml @@ -0,0 +1,70 @@ +# Where are all the schema files located? globs are supported eg src/**/*.graphqls +schema: + - graph/*.graphqls + +# Where should the generated server code go? +exec: + filename: graph/generated.go + package: graph + worker_limit: 1 + +# Uncomment to enable federation +# federation: +# filename: graph/federation.go +# package: graph + +# Where should any generated models go? +model: + filename: graph/model/models_gen.go + package: model + +# Where should the resolver implementations go? +resolver: + layout: follow-schema + dir: graph + package: graph + +# Optional: turn on use `gqlgen:"fieldName"` tags in your models +# struct_tag: json + +# Optional: turn on to use []Thing instead of []*Thing +# omit_slice_element_pointers: false + +# Optional: turn off to make struct-type struct fields not use pointers +# e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing } +# struct_fields_always_pointers: true + +# Optional: turn off to make resolvers return values instead of pointers for structs +# resolvers_always_return_pointers: true + +# Optional: turn on to return pointers instead of values in unmarshalInput +# return_pointers_in_unmarshalinput: false + +# Optional: wrap nullable input fields with Omittable +# nullable_input_omittable: true + +# Optional: set to speed up generation time by not performing a final validation pass. +# skip_validation: true + +# gqlgen will search for any type names in the schema in these go packages +# if they match it will use them, otherwise it will generate them. +autobind: + - "github.com/99designs/gqlgen/api/testdata/default/graph/model" + +# This section declares type mapping between the GraphQL and go type systems +# +# The first line in each type will be used as defaults for resolver arguments and +# modelgen, the others will be allowed when binding to fields. Configure them to +# your liking +models: + ID: + model: + - github.com/99designs/gqlgen/graphql.ID + - github.com/99designs/gqlgen/graphql.Int + - github.com/99designs/gqlgen/graphql.Int64 + - github.com/99designs/gqlgen/graphql.Int32 + Int: + model: + - github.com/99designs/gqlgen/graphql.Int + - github.com/99designs/gqlgen/graphql.Int64 + - github.com/99designs/gqlgen/graphql.Int32 diff --git a/api/testdata/workerlimit/graph/model/doc.go b/api/testdata/workerlimit/graph/model/doc.go new file mode 100644 index 00000000000..8b537907051 --- /dev/null +++ b/api/testdata/workerlimit/graph/model/doc.go @@ -0,0 +1 @@ +package model diff --git a/api/testdata/workerlimit/graph/schema.graphqls b/api/testdata/workerlimit/graph/schema.graphqls new file mode 100644 index 00000000000..c6a91bb4808 --- /dev/null +++ b/api/testdata/workerlimit/graph/schema.graphqls @@ -0,0 +1,28 @@ +# GraphQL schema example +# +# https://gqlgen.com/getting-started/ + +type Todo { + id: ID! + text: String! + done: Boolean! + user: User! +} + +type User { + id: ID! + name: String! +} + +type Query { + todos: [Todo!]! +} + +input NewTodo { + text: String! + userId: String! +} + +type Mutation { + createTodo(input: NewTodo!): Todo! +} diff --git a/codegen/config/exec.go b/codegen/config/exec.go index 838e17b2c22..37ec2c30b56 100644 --- a/codegen/config/exec.go +++ b/codegen/config/exec.go @@ -20,6 +20,12 @@ type ExecConfig struct { // Only for follow-schema layout: FilenameTemplate string `yaml:"filename_template,omitempty"` // String template with {name} as placeholder for base name. DirName string `yaml:"dir"` + + // Maximum number of goroutines in concurrency to use when running multiple child resolvers + // Suppressing the number of goroutines generated can reduce memory consumption per request, + // but processing time may increase due to the reduced number of concurrences + // Default: 0 (unlimited) + WorkerLimit uint `yaml:"worker_limit"` } type ExecLayout string diff --git a/codegen/generated!.gotpl b/codegen/generated!.gotpl index 7f526a2174b..8638b1cf9a9 100644 --- a/codegen/generated!.gotpl +++ b/codegen/generated!.gotpl @@ -10,6 +10,7 @@ {{ reserveImport "bytes" }} {{ reserveImport "embed" }} +{{ reserveImport "golang.org/x/sync/semaphore"}} {{ reserveImport "github.com/vektah/gqlparser/v2" "gqlparser" }} {{ reserveImport "github.com/vektah/gqlparser/v2/ast" }} {{ reserveImport "github.com/99designs/gqlgen/graphql" }} diff --git a/codegen/type.gotpl b/codegen/type.gotpl index 1898d44460f..aa143b1f22e 100644 --- a/codegen/type.gotpl +++ b/codegen/type.gotpl @@ -101,6 +101,9 @@ ret := make(graphql.Array, len(v)) {{- if not $type.IsScalar }} var wg sync.WaitGroup + {{- if gt $.Config.Exec.WorkerLimit 0 }} + sm := semaphore.NewWeighted({{ $.Config.Exec.WorkerLimit }}) + {{- end }} isLen1 := len(v) == 1 if !isLen1 { wg.Add(len(v)) @@ -124,14 +127,29 @@ }() {{- end }} if !isLen1 { - defer wg.Done() + {{- if gt $.Config.Exec.WorkerLimit 0 }} + defer func(){ + sm.Release(1) + wg.Done() + }() + {{- else }} + defer wg.Done() + {{- end }} } ret[i] = ec.{{ $type.Elem.MarshalFunc }}(ctx, sel, v[i]) } if isLen1 { f(i) } else { - go f(i) + {{- if gt $.Config.Exec.WorkerLimit 0 }} + if err := sm.Acquire(ctx, 1); err != nil { + ec.Error(ctx, ctx.Err()) + } else { + go f(i) + } + {{- else }} + go f(i) + {{- end }} } {{ else }} ret[i] = ec.{{ $type.Elem.MarshalFunc }}(ctx, sel, v[i]) diff --git a/init-templates/gqlgen.yml.gotmpl b/init-templates/gqlgen.yml.gotmpl index 648ec2b4430..6429c93a942 100644 --- a/init-templates/gqlgen.yml.gotmpl +++ b/init-templates/gqlgen.yml.gotmpl @@ -6,6 +6,8 @@ schema: exec: filename: graph/generated.go package: graph + # Optional: Maximum number of goroutines in concurrency to use per child resolvers(default: unlimited) + # worker_limit: 1000 # Uncomment to enable federation # federation: