Skip to content

context: add APIs for setting a cancelation cause when deadline or timer expires #56661

Closed
@Sajmani

Description

@Sajmani

This is a follow on to proposal #51365 to add WithDeadlineCause and WithTimeoutCause to the context package. These functions enable the user to arrange a cancelation cause to be set when a deadline or timer expires. This is not possible to do efficiently with the APIs introduced in https://go-review.git.corp.google.com/c/go/+/375977.

The proposed API additions are:

// WithDeadlineCause behaves like WithDeadline but also sets the cause of the
// returned Context when the deadline is exceeded. The returned CancelFunc does
// not set the cause.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc)

// WithTimeoutCause behaves like WithTimeout but also sets the cause of the
// returned Context when the timout expires. The returned CancelFunc does
// not set the cause.
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)

Example:

tooSlow := fmt.Errorf("too slow!")
ctx, cancel := context.WithTimeoutCause(context.Background(), 1*time.Second, tooSlow)
time.Sleep(2*time.Second) // timer fires, setting the cause
cancel() // no effect as ctx has already been canceled
// ctx.Err() == context.DeadlineExceeded && Cause(ctx) == tooSlow

The cause passed to these functions is set only when the deadline or timer expires. The common case is that the context is canceled before that happens via the returned CancelFunc. In this case, the cause will be context.Canceled:

tooSlow := fmt.Errorf("too slow!")
ctx, cancel := context.WithTimeoutCause(context.Background(), 1*time.Second, tooSlow)
time.Sleep(500*time.Millisecond) // timer hasn't expired yet
cancel() // cancels the timer and sets ctx.Err()
// ctx.Err() == Cause(ctx) == context.Canceled

If users need to set the cause on both the timer and cancel() paths, they can stack contexts:

Example where the timer fires first:

finishedEarly := fmt.Errorf("finished early")
tooSlow := fmt.Errorf("too slow!")
ctx, cancel := context.WithCancelCause(context.Background())
ctx, _ = context.WithTimeoutCause(ctx, 1*time.Second, tooSlow)
time.Sleep(2*time.Second) // timer fires, setting the cause
cancel(finishedEarly) // no effect as ctx has already been canceled
// ctx.Err() == context.DeadlineExceeded && Cause(ctx) == tooSlow

Example where cancel happens first:

finishedEarly := fmt.Errorf("finished early")
tooSlow := fmt.Errorf("too slow!")
ctx, cancel := context.WithCancelCause(context.Background())
ctx, _ = context.WithTimeoutCause(ctx, 1*time.Second, tooSlow)
time.Sleep(500*time.Millisecond) // timer hasn't expired yet
cancel(finishedEarly) // cancels the timer and sets ctx.Err()
// ctx.Err() == context.Canceled && Cause(ctx) == finishedEarly

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions