gotimeoutgolangci-lint

Correct re-use of context.WithTimeout in golang?


First the code I have:

        for last < end {
            // HERE the linter is complaining with:
            // nested context in loop (fatcontext)
            ctx, cancel = context.WithTimeout(context.Background(), timeout)
            fetch := fmt.Sprintf("0x%x", last)
            resp, err := t.rpcClient.Call(ctx, endpoint, fetch)
            defer cancel()
            if err != nil {
                return err
            }
            // handle response and extract some data
            // based on this data, I need to do a SECOND CALL
            ctx, cancel = context.WithTimeout(context.Background(), timeout)
            resp2, err := t.rpcClient.Call(ctx, endpoint2, param, false)                
            if err != nil {
              return err
            }
            defer cancel()
            // more stuff happens
            last += 1
          }

OK so I have a loop which has to do repeated RPC calls. I want every call to recover gracefully in case of a timeout.

I THINK to assume that I can't just reuse the ctx for each call. Isn't it the case that the timeout time starts running when the context is created? Therefore, if the loop runs longer than the timeout, at some point the timeout will hit?

In other words, I want the timeout counter to start afresh before each call.

Can I reuse ctx and cancel at all? Should I just create a new one before each call? Or what is the best approach here.


Solution

  • Fat contexts are explained in https://gabnotes.org/fat-contexts/, and are generated when contexts are recursively wrapped in a loop, generating very deep context objects.

    In your code sample, the linter error is a false positive, since every assignment to ctx wraps a new empty context created with context.Background().

    You can scope the ctx variable to avoid avoid the linter error:

    for last < end {
                // HERE the linter is complaining with:
                // nested context in loop (fatcontext)
                ctx1, cancel := context.WithTimeout(context.Background(), timeout)
                fetch := fmt.Sprintf("0x%x", last)
                resp, err := t.rpcClient.Call(ctx1, endpoint, fetch)
                defer cancel()
                if err != nil {
                    return err
                }
                // handle response and extract some data
                // based on this data, I need to do a SECOND CALL
                ctx2, cancel := context.WithTimeout(context.Background(), timeout)
                resp2, err := t.rpcClient.Call(ctx2, endpoint2, param, false)                
                if err != nil {
                  return err
                }
                defer cancel()
                // more stuff happens
                last += 1
              }