I am developing a layered backend API with controllers, services, repositories, etc.
Each method in these layers takes context.Context
as its first parameter containing the request context. This is convenient because any method can access various request-related data (such as correlationID, etc.)
Each request context has a timeout set by the following TimeoutMiddleware
:
func TimeoutMiddleware(timeoutFn func(*gin.Context) time.Duration) gin.HandlerFunc { return func(c *gin.Context) { timeout := timeoutFn(c) ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() c.Request = c.Request.WithContext(ctx) c.Next() } } func TimeoutFn(c *gin.Context) time.Duration { return conf.HTTPRouter.DefaultContextTimeout }
The idea is to gracefully stop any ongoing operations when the request context times out. Based on my understanding of context and concurrency (very little), I built this helper function:
package helpers import "context" // Checks for context cancellation and returns ctx.Err() if canceled. func HandleContextCancel(ctx context.Context) error { select { case <-ctx.Done(): // If the context is canceled return ctx.Err() // return immediately with the canceled error. default: return nil // Continue with the normal processing. } }
In theory, if I wanted to stop any operation as quickly as possible, I would need to call this function at the beginning of every method in the application, like this: < /p>
func DoSomething(ctx context.Context, ...) resterrors.RestErr { if err := helpers.HandleContextCancel(ctx); err != nil { return resterrors.NewRequestTimeoutError( fmt.Errorf("DoSomething: %w", err), ) } // ...
Also, I know that in the repository that accesses the database, most functions require a Context, such as Query
, QueryRow
, Exec
...,As follows:
rows, err := pgclient.GetSession().Query(ctx, query, param1, ...)
So every time an error occurs in the line above, I have to check if the error is not due to context cancellation instead of just returning internal_server_error
and the error message like this:
rows, err := pgclient.GetSession().Query(ctx, query, param1, ...) if err != nil { return helpers.MapRepoError("DoSomething: Query Error:", err) }
func MapRepoError(location string, err error) resterrors.RestErr { if errors.Is(err, context.DeadlineExceeded) { return resterrors.NewRequestTimeoutError( fmt.Errorf("%s request_timeout", location), ) } return resterrors.NewInternalServerError( fmt.Errorf("%s %w", location, err), ) }
Using the HandleContextCancel
function looks a bit redundant, what do you think?
You don't need the HandleContextCancel
function, you can simply do the following:
if ctx.Err()!=nil { // Context timed out or canceled. Return return ctx.Err() }
If your other error handling functions wrap this error correctly (i.e. they implement the Unwrap() error
method, then you can check at the top level whether the error contains a timeout/cancellation error and decide if you To handle what type of errors like returns. You don't have to do this for every layer.
The above is the detailed content of Handling cross-layer context cancellation. For more information, please follow other related articles on the PHP Chinese website!