gogo-context

Golang copy all values of context


I have a HTTP server application which serves async jobs.

-> Request
   --> Do async job with goroutine
<- Response
    -------start goroutine------
       -> Job1
          -> Job1A
          -> Job1B
       -> Job2
       -> Job3

User can request for the long running async job and the application responses to the request immediately after make goroutine.

I put Request ID, authenticated token, and user information in context.Context of the request. And, I want to bring it under the goroutines. But, using the same context with the request context will cause unexpected cancel after the response, which is not my intended behavior.

How can I generate new context with all values, independent from the parent request context? Or, any other way to guarantee the context bring into the goroutines is not dead after response?

One more extra question:

Job1 ~ Job3 should be serialized, i.e., Job2 should wait for Job1 and Job3 wait for Job2. And, Job1A and Job1B can be run simultaneously. If I want to propagate cancelation of given context, how can I make cancel path(?) of them? Should I check with select statement of all functions?

I understood concepts of context propagating cancelation and early exit without doing meaningless tasks. However, I didn't catch how to deal within the code yet. I'll be happy if someone can help understanding.


Solution

  • There is no way to discover the values in a context. They are not stored as a map, they are stored as layers of contexts, and each level may provide a different implementation of how values are stored.

    However, if you know what values you need to propagate, you can query them and create a new context using those values.

    That said, you can implement a new context type that uses the values in another context:

    type newContext struct {
      context.Context
      values context.Context
    }
    
    func (c newContext) Value(key any) any {
      return c.values.Value(key)
    }
    
    ...
    newCtx:=newContext{
      Context: context.Background(),
      values: ctx,
    }
    

    This uses an existing context for values and a new context for everything else.

    Then, start a new goroutine to continue processing the request using that new context.

    If you want to create multiple concurrent jobs, you can do that in that goroutine:

    go func(ctx context.Context) {
       withCancel, cancel:=context.WithCancel(ctx)
       defer cancel()
    
       wg:=sync.WaitGroup{}
       wg.Add(2)
       go job1(withCancel,&wg)
       go job2(withCancel,&wg)
       wg.Wait()
    }(newCtx)
    

    This way, when the context is canceled, both jobs will get the cancellation notification. If you want to control cancellation of job1 and job2 separately:

    go func(ctx context.Context) {
       withCancel1, cancel1:=context.WithCancel(ctx)
       defer cancel1()
       withCancel2, cancel2:=context.WithCancel(ctx)
       defer cancel2()
    
       wg:=sync.WaitGroup{}
       wg.Add(2)
       go job1(withCancel1,&wg)
       go job2(withCancel2,&wg)
       wg.Wait()
    }(newCtx)
    

    For sequential jobs (i.e. job3 finishes after job1), simply combine them so they appear like a single job.

    To check if a context is canceled, you can do a select on the Done channel of the context, or simply check:

    if ctx.Err()!=nil {
       // Context canceled
    }