gqlgen-goroutine-restriction-workaround

command module
v0.0.0-...-b2d1882 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 8, 2024 License: MIT Imports: 7 Imported by: 0

README

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>

# ...

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL