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