gqlgen-goroutine-restriction-workaround
Dirty restriction for goroutines generated by gqlgen.
Thank you @lmt-swallow for giving me the approach.
Background
In the code generated by gqlgen, goroutine is generated for the execution of the child resolver as follows:
https://github.com/99designs/gqlgen/blob/f02c5b4b554b8f60c6d4d8bea0b7299610001d38/codegen/type.gotpl#L134
If there are 10,000 objects, this code will require at least 20 MiB of memory.
Of course, pagination is the proper handling, but in our use case, we needed to control the generation of goroutines somehow, since it is a requirement to retrieve all data.
Workaround
Fortunately, there is only one place where goroutine is generated, so I decided to apply the plugin feature provided by gqlgen to directly overwrite the generated code.
Here you can see the actual logic of replacement.
Generated code diffs
before:
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNTodo2ᚖgithubᚗcomᚋOldBigBuddhaᚋgqlgenᚑgoroutineᚑrestrictionᚑworkaroundᚋgraphᚋmodelᚐTodo(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
for _, e := range ret {
if e == graphql.Null {
return graphql.Null
}
}
return ret
after:
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
sm := semaphore.NewWeighted(1000)
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer func() {
sm.Release(1)
wg.Done()
}()
}
ret[i] = ec.marshalNTodo2ᚖgithubᚗcomᚋOldBigBuddhaᚋgqlgenᚑgoroutineᚑrestrictionᚑworkaroundᚋgraphᚋmodelᚐTodo(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
if err := sm.Acquire(ctx, 1); err != nil {
ec.Error(ctx, ctx.Err())
} else {
go f(i)
}
}
}
wg.Wait()
for _, e := range ret {
if e == graphql.Null {
return graphql.Null
}
}
return ret
Looking forward to the future
It would be nice to be able to replace the template used for code generation by configuration.
Like:
schema:
- graph/*.graphqls
exec:
filename: graph/generated.go
package: generated
templates:
type: <file path to the custom template>
# ...