gogo-gingo-context

Gin - Gonic context getting cancelled in child go routines


I'm using Gin web framework for a Go API back-end service. Inside the gin handler function of an API endpoint, I'm starting a Go routine to run some background tasks. These tasks are supposed to run without any dependency on the response sent to the client. But, often I face "context cancelled" error during database operations in the child Go routine and the background operations fail.

I cannot create a new context and pass it around as the gin context some data I need and there are some existing functions that I need to reuse, which expect *gin.context as arguments.

I did some debugging and found that the error occur due to the child routines using the same *gin.context as the parent routine, which is the handler itself. The context gets cancelled after sending the response to the client which lead to error in db operations.

I read a solution that said to create a new context using context.Background() and pass it around. But I need to use *gin.context itself or else I'll have to rewrite a lot of functions and copy some data too.

Another promising solution I saw was the suggestion to use *gin.context.Copy(). Inside the gin documentation for Copy(), it explicitly says:

Copy returns a copy of the current context that can be safely used outside the request's scope.
This has to be used when the context has to be passed to a goroutine.

I was expecting Copy() to work properly. But even if I pass the copied context, I get the same context cancelled error.

Following is what I did:

func SendApiResponseToClient(c *gin.Context) {
  response := doSomeOperations(c)
  
  newContext := c.Copy()
  go doSomeBackGroundOperations(newContext, response)

  sendResponse(response)
}

The doSomeBackGroundOperations() functions have some DB operations and that's what fails.

Just to confirm everything else other than context is fine, I added a one second timeout right before sending the API response to client to wait for the background operations to finish. In that case, I faced no error and everything worked as expected.

I've also found some github open issues in the gin-gonic/gin repository like the following:

I believe that these issues are in someway related to the one I'm facing. But, I still can't find a definitive solution.

Any suggestion that might help me would be really appreciated. Thanks


Solution

  • The *gin.Context is a type that has a field called Request. This Request field is of the type *http.Request and this contains the standard library context.Context. It is this context.Context that is responsible for propagating cancellations through your code.

    Therefore, if we want to change how these cancellations propagate, we need to modify this context.Context.

    The following snippet would work to copy the Gin context, and attach the background context:

    func detachContext(c *gin.Context) *gin.Context {
      // First call .Copy on the gin.Context. This doesn't behave quite as
      // we'd originally expect, and references the c.Request rather than
      // actually copying it. Hence, we'll need to follow up with a proper
      // clone of the underlying Request.
      copy := c.Copy()
      // As we don't want to modify the existing http.Request,
      // we want to use Clone, which handily allows us to specify
      // a new context.Context.
      // We use context.WithoutCancel to take a copy of the values of
      // a context without inheriting it's cancellation propagation.
      copy.Request = copy.Request.Clone(context.WithoutCancel(copy.Request.Context))
      return copy
    }
    

    Keep in mind the newly created context has no cancellation and this could lead to an accumulation of Go routines. You may want to use context.WithTimeout or similar to control the lifetime of the child Goroutine.

    Finally, with all this having been said, I would recommend avoiding *gin.Context penetrating your code too deeply. It's specific to Gin. Instead, I would suggest that you extract information that you need from it at the HTTP handler layer and then pass this information to other units of code directly. This will reduce the amount to which your entire project is coupled with Gin.