Skip to content

Commit 4c9e1e8

Browse files
all: add cancellation causes to timeouts
The new WithTimeoutCause and WithDeadlineCause functions allow us to decorate contexts with metadata surrounding a specific timeout or deadline. Combined with the automatic discovery of the context cause in the errors and event packages, we should get much more information about context cancellations.
1 parent 55a1d77 commit 4c9e1e8

File tree

20 files changed

+146
-32
lines changed

20 files changed

+146
-32
lines changed

api/client.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,7 @@ func (c *Client) NewRequest(ctx context.Context, method, requestPath string, bod
736736
// Do takes a properly configured request and applies client configuration to
737737
// it, returning the response.
738738
func (c *Client) Do(r *retryablehttp.Request, opt ...Option) (*Response, error) {
739+
const op = "api.(Client).Do"
739740
opts := getOpts(opt...)
740741
c.modifyLock.RLock()
741742
limiter := c.config.Limiter
@@ -772,7 +773,11 @@ func (c *Client) Do(r *retryablehttp.Request, opt ...Option) (*Response, error)
772773

773774
if timeout != 0 {
774775
var cancel context.CancelFunc
775-
ctx, cancel = context.WithTimeout(ctx, timeout)
776+
ctx, cancel = context.WithTimeoutCause(
777+
ctx,
778+
timeout,
779+
fmt.Errorf("%s: client configured timeout exceeded", op),
780+
)
776781
// This dance is just to ignore vet warnings; we don't want to cancel
777782
// this as it will make reading the response body impossible
778783
_ = cancel
@@ -841,6 +846,9 @@ func (c *Client) Do(r *retryablehttp.Request, opt ...Option) (*Response, error)
841846
}
842847

843848
if err != nil {
849+
if ctxCause := context.Cause(ctx); ctxCause != nil {
850+
return nil, fmt.Errorf("%w (%w)", err, ctxCause)
851+
}
844852
if strings.Contains(err.Error(), "tls: oversized") {
845853
err = fmt.Errorf(
846854
"%w\n\n"+

api/proxy/proxy.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type ClientProxy struct {
8080
// EXPERIMENTAL: While this API is not expected to change, it is new and
8181
// feedback from users may necessitate changes.
8282
func New(ctx context.Context, authzToken string, opt ...Option) (*ClientProxy, error) {
83+
const op = "proxy.New"
8384
opts, err := getOpts(opt...)
8485
if err != nil {
8586
return nil, fmt.Errorf("could not parse options: %w", err)
@@ -142,7 +143,7 @@ func New(ctx context.Context, authzToken string, opt ...Option) (*ClientProxy, e
142143
// We don't _rely_ on client-side timeout verification but this prevents us
143144
// seeming to be ready for a connection that will immediately fail when we
144145
// try to actually make it
145-
p.ctx, p.cancel = context.WithDeadline(ctx, p.expiration)
146+
p.ctx, p.cancel = context.WithDeadlineCause(ctx, p.expiration, fmt.Errorf("%s: session expiration exceeded", op))
146147

147148
transport := cleanhttp.DefaultTransport()
148149
transport.DisableKeepAlives = false
@@ -173,6 +174,7 @@ func New(ctx context.Context, authzToken string, opt ...Option) (*ClientProxy, e
173174
// EXPERIMENTAL: While this API is not expected to change, it is new and
174175
// feedback from users may necessitate changes.
175176
func (p *ClientProxy) Start(opt ...Option) (retErr error) {
177+
const op = "proxy.(ClientProxy).Start"
176178
opts, err := getOpts(opt...)
177179
if err != nil {
178180
return fmt.Errorf("could not parse options: %w", err)
@@ -350,9 +352,16 @@ func (p *ClientProxy) Start(opt ...Option) (retErr error) {
350352
return nil
351353
}
352354

353-
ctx, cancel := context.WithTimeout(context.Background(), opts.withSessionTeardownTimeout)
355+
ctx, cancel := context.WithTimeoutCause(
356+
context.Background(),
357+
opts.withSessionTeardownTimeout,
358+
fmt.Errorf("%s: session teardown timeout exceeded", op),
359+
)
354360
defer cancel()
355361
if err := p.sendSessionTeardown(ctx); err != nil {
362+
if ctxCause := ctx.Err(); ctxCause != nil {
363+
return fmt.Errorf("error sending session teardown request to worker: %w (%w)", err, ctxCause)
364+
}
356365
return fmt.Errorf("error sending session teardown request to worker: %w", err)
357366
}
358367

internal/clientcache/cmd/cache/wrapper_register.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func silentUi() *cli.BasicUi {
8080
// addTokenToCache runs AddTokenCommand with the token used in, or retrieved by
8181
// the wrapped command.
8282
func addTokenToCache(ctx context.Context, baseCmd *base.Command, token string) bool {
83+
const op = "cache.addTokenToCache"
8384
com := AddTokenCommand{Command: base.NewCommand(baseCmd.UI)}
8485
client, err := baseCmd.Client()
8586
if err != nil {
@@ -95,7 +96,11 @@ func addTokenToCache(ctx context.Context, baseCmd *base.Command, token string) b
9596

9697
// Since the daemon might have just started, we need to wait until it can
9798
// respond to our requests
98-
waitCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
99+
waitCtx, cancel := context.WithTimeoutCause(
100+
ctx,
101+
3*time.Second,
102+
fmt.Errorf("%s: daemon startup timeout exceeded", op),
103+
)
99104
defer cancel()
100105
if err := waitForDaemon(waitCtx); err != nil {
101106
// TODO: Print the result of this out into a log in the dot directory

internal/clientcache/internal/cache/refresh.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ func (r *RefreshService) RefreshForSearch(ctx context.Context, authTokenid strin
154154
const op = "cache.(RefreshService).RefreshForSearch"
155155
if r.maxSearchRefreshTimeout > 0 {
156156
var cancel context.CancelFunc
157-
ctx, cancel = context.WithTimeout(ctx, r.maxSearchRefreshTimeout)
157+
ctx, cancel = context.WithTimeoutCause(
158+
ctx,
159+
r.maxSearchRefreshTimeout,
160+
fmt.Errorf("%s: search refresh timeout exceeded", op),
161+
)
158162
defer cancel()
159163
}
160164
at, err := r.repo.LookupToken(ctx, authTokenid)

internal/clientcache/internal/daemon/server.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,15 @@ func (s *CacheServer) Shutdown(ctx context.Context) error {
139139
if s.conf.ContextCancel != nil {
140140
s.conf.ContextCancel()
141141
}
142-
srvCtx, srvCancel := context.WithTimeout(context.Background(), 5*time.Second)
142+
srvCtx, srvCancel := context.WithTimeoutCause(
143+
context.Background(),
144+
5*time.Second,
145+
fmt.Errorf("%s: http server shutdown timeout exceeded", op),
146+
)
143147
defer srvCancel()
144148
err := s.httpSrv.Shutdown(srvCtx)
145149
if err != nil {
146-
shutdownErr = fmt.Errorf("error shutting down server: %w", err)
150+
shutdownErr = errors.Wrap(ctx, err, op, errors.WithMsg("error shutting down server"), errors.WithoutEvent())
147151
return
148152
}
149153
s.tickerWg.Wait()

internal/cmd/commands/server/server.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ func (c *Command) AutocompleteFlags() complete.Flags {
172172
}
173173

174174
func (c *Command) Run(args []string) int {
175+
const op = "server.(Command).Run"
175176
c.CombineLogs = c.flagCombineLogs
176177

177178
defer func() {
@@ -479,12 +480,23 @@ func (c *Command) Run(args []string) int {
479480
// 1 second is chosen so the shutdown is still responsive and this is a mostly
480481
// non critical step since the lock should be released when the session with the
481482
// database is closed.
482-
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
483+
ctx, cancel := context.WithTimeoutCause(
484+
context.Background(),
485+
1*time.Second,
486+
fmt.Errorf("%s: database lock release timeout exceeded", op),
487+
)
483488
defer cancel()
484489

485490
err := c.schemaManager.Close(ctx)
486491
if err != nil {
487-
c.UI.Error(fmt.Errorf("Unable to release shared lock to the database: %w", err).Error())
492+
// Use errors.E to capture the context cause if there is one
493+
c.UI.Error(errors.Wrap(
494+
ctx,
495+
err,
496+
op,
497+
errors.WithMsg("Unable to release shared lock to the database"),
498+
errors.WithoutEvent(),
499+
).Error())
488500
}
489501
}()
490502

internal/cmd/ops/server.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package ops
77

88
import (
99
"context"
10-
"errors"
10+
stderrors "errors"
1111
"fmt"
1212
"net"
1313
"net/http"
@@ -17,6 +17,7 @@ import (
1717
"github.com/hashicorp/boundary/internal/cmd/base"
1818
"github.com/hashicorp/boundary/internal/daemon/controller"
1919
"github.com/hashicorp/boundary/internal/daemon/worker"
20+
"github.com/hashicorp/boundary/internal/errors"
2021
"github.com/hashicorp/go-cleanhttp"
2122
"github.com/hashicorp/go-hclog"
2223
"github.com/hashicorp/go-secure-stdlib/listenerutil"
@@ -94,18 +95,22 @@ func (s *Server) Shutdown() error {
9495
return fmt.Errorf("%s: missing bundle, listener or its fields", op)
9596
}
9697

97-
ctx, cancel := context.WithTimeout(context.Background(), b.ln.Config.MaxRequestDuration)
98+
ctx, cancel := context.WithTimeoutCause(
99+
context.Background(),
100+
b.ln.Config.MaxRequestDuration,
101+
fmt.Errorf("%s: max request duration exceeded", op),
102+
)
98103
defer cancel()
99104

100105
err := b.ln.HTTPServer.Shutdown(ctx)
101106
if err != nil {
102-
errors.Join(closeErrors, fmt.Errorf("%s: failed to shutdown http server: %w", op, err))
107+
closeErrors = stderrors.Join(closeErrors, errors.Wrap(ctx, err, op, errors.WithMsg("failed to shutdown http server")))
103108
}
104109

105110
err = b.ln.OpsListener.Close()
106111
err = listenerCloseErrorCheck(b.ln.Config.Type, err)
107112
if err != nil {
108-
errors.Join(closeErrors, fmt.Errorf("%s: failed to close listener mux: %w", op, err))
113+
closeErrors = stderrors.Join(closeErrors, fmt.Errorf("%s: failed to close listener mux: %w", op, err))
109114
}
110115
}
111116

internal/daemon/controller/handler.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,11 @@ func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProp
476476
w.Header().Set("Cache-Control", "no-store")
477477

478478
// Start with the request context and our timeout
479-
ctx, cancelFunc := context.WithTimeout(r.Context(), maxRequestDuration)
479+
ctx, cancelFunc := context.WithTimeoutCause(
480+
r.Context(),
481+
maxRequestDuration,
482+
fmt.Errorf("%s: max request duration exceeded", op),
483+
)
480484
defer cancelFunc()
481485

482486
// Add a size limiter if desired

internal/daemon/controller/handlers/targets/target_service.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,7 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession
10641064
if retErr != nil {
10651065
// Delete created session in case of errors.
10661066
// Use new context for deletion in case error is because of context cancellation.
1067-
deleteCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
1067+
deleteCtx, cancel := context.WithTimeoutCause(context.Background(), 5*time.Second, stderrors.New("session deletion timeout exceeded"))
10681068
defer cancel()
10691069
_, err := sessionRepo.DeleteSession(deleteCtx, sess.PublicId)
10701070
retErr = stderrors.Join(retErr, err)
@@ -1095,7 +1095,11 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession
10951095
if retErr != nil {
10961096
// Revoke issued credentials in case of errors.
10971097
// Use new context for deletion in case error is because of context cancellation.
1098-
deleteCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
1098+
deleteCtx, cancel := context.WithTimeoutCause(
1099+
context.Background(),
1100+
time.Minute,
1101+
fmt.Errorf("%s: credential revocation timeout exceeded", op),
1102+
)
10991103
defer cancel()
11001104
err := credRepo.Revoke(deleteCtx, sess.PublicId)
11011105
retErr = stderrors.Join(retErr, err)

internal/daemon/controller/interceptor.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,11 @@ func eventsResponseInterceptor(
495495
func requestMaxDurationInterceptor(_ context.Context, maxRequestDuration time.Duration) grpc.UnaryServerInterceptor {
496496
const op = "controller.requestMaxDurationInterceptor"
497497
return func(interceptorCtx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
498-
withTimeout, cancel := context.WithTimeout(interceptorCtx, maxRequestDuration)
498+
withTimeout, cancel := context.WithTimeoutCause(
499+
interceptorCtx,
500+
maxRequestDuration,
501+
fmt.Errorf("%s: max request duration exceeded", op),
502+
)
499503
defer cancel()
500504
return handler(withTimeout, req)
501505
}

0 commit comments

Comments
 (0)