Given an API setup with GraphQL and Echo, a colleague ran into a race condition situation. There was a concurrent read/write issue on Echo’s context. GraphQL runs its resolvers in parallel if set so, and when context is shared between resolvers, things can go wrong.
I took a look into Echo’s context implementation and I saw a simple map is used for Get/Set.
For every API call, a handle functions is given an Echo context and executes the GraphQL schema with the specified context.
func handle(c echo.Context) error { schema, err := gqlgo.ParseSchema( ... gqlgo.MaxParallelism(10), ) schema.Exec( c, ... ) }
My solution was to use a custom context which embeds the original one and uses a concurrent map instead of Echo’s.
type Context struct { echo.Context store sync.Map } func (c *Context) Set(key string, val interface{}) { c.store.Store(key, val) } func (c *Context) Get(key string) interface{} { val, _ := c.store.Load(key) return val } func (c *Context) Deadline() (deadline time.Time, ok bool) { return c.Request().Context().Deadline() } func (c *Context) Done() <-chan struct{} { return c.Request().Context().Done() } func (c *Context) Err() error { return c.Request().Context().Err() } func (c *Context) Value(key interface{}) interface{} { return c.Get(fmt.Sprintf("%v", key)) } func handle(c echo.Context) error { ctx := &Context{Context: c} schema.Exec( ctx, ... ) }
While this fixes the issue, a colleague suggested another approach. Using a new map like I did will ignore any possible values already existing on the original context. So if you want to preserve the original context you can use a Mutex to prevent concurrent read/write issues.
type Context struct { echo.Context sync.RWMutex } func (c *Context) Set(key string, val interface{}) { c.Lock() c.Context.Set(key, val) c.Unlock() } func (c *Context) Get(key string) interface{} { c.RLock() val := c.Context.Get(key) c.RUnlock() return val }
You should in fact try to fix this in Echo. I think they welcome contributions. I do not see why they made that context non concurrency safe.
On a sidenote, cool that there is a graphql implementation in go. I was not aware of that.
I think that in a normal server situation you don’t get to this issue, while GraphQL is based on concurrent handling. And it also depends on your GraphQL resolvers design.
As I was suspecting, this isn’t a new situation and it should be handled outside of Echo: https://github.com/labstack/echo/issues/273