-
Notifications
You must be signed in to change notification settings - Fork 18.3k
Description
This proposal addresses issue #26356 by extending package context
with new functions for writing and reading the "cause" of the context's cancelation. This approach is similar to what was proposed in #46273.
Design notes and alternatives
Naming
I chose the name Cause
because it's short. Alternatives include Reason
(as in #46273) and WhyDone
.
Cause is an error
Based on user feedback in #26356, we've made cause an error
so that the programmer can include structured data and introspect it with errors.Is/As
. Alternatives would be a string or a stack trace (captured automatically).
Compatibility
As package context
is covered by the Go 1 compatibility guarantee, we are unable to change any of the existing types or functions in the package. In particular
- we cannot add
Cause
as a method of theContext
interface - we cannot change the existing
CancelFunc
type to take a cause - we cannot change the existing
WithCancel
,WithDeadline
, orWithTimeout
function signatures - we cannot change
Err
to return any values other thannil
,Canceled
, orDeadlineExceeded
Performance considerations
Based on user feedback, our goal is to introduce no new performance overhead for existing uses of Context
(hence the rejection of the design that captured stack traces automatically for existing cancelations). We consider it acceptable to increase the size of some context implementation structs by a small amount (specifically, adding an error
field to cancelCtx
).
Cause(ctx) vs. ctx.Err()
@andreimatei pointed out that in the current prototype, it's possible for Cause(ctx)
to be nil when ctx.Err()
is non-nil. This is working as intended: we expect users to check ctx.Err()
before reading Cause(ctx)
. However, @andreimatei suggested users might want to check Cause(ctx)
instead of ctx.Err()
, in which case we'd need to guarantee that Cause(ctx)
is non-nil exactly when ctx.Err()
is non-nil. If we do this, I'd recommend Cause(ctx) == ctx.Err()
at all times except when the cause has been explicitly set to non-nil. This would complicate the handling of code that sets the cause to nil
.
Concerns for custom Context implementations
@andreimatei raised concerns about the fact that this design does not provide a way for custom Context
implementations to control the behavior of Cause(ctx)
. As a result, it's possible for Cause(ctx)
to return a non-nil error while the custom Context's Done
channel has not yet been closed. I pointed out that a custom Context
can control Cause(ctx)
by wrapping a standard Context
returned by WithCancelCause
; @andreimatei pointed out that this makes it difficult for custom Context
implementations to achieve their efficiency goals.
We could address this concern by exposing a key for Context.Value
that looks up the Cause
, which would allow custom Context
implementations to override the lookup for that key. This would require changing the current prototype that reuses the existing cancelCtxKey
to find the cause.
Proposed API changes
I've prototyped this change in CL https://go-review.googlesource.com/c/go/+/375977, including several tests to exercise various interleaving of cancelations with and without causes.
package context
// WithCancelCause behaves like WithCancel but returns a CancelCauseFunc instead of a CancelFunc.
// Calling cancel with a non-nil error (the "cause") records that error in ctx;
// it can then be retrieved using Cause(ctx).
//
// Example use:
// ctx, cancel := context.WithCancelCause(parent)
// cancel(myError)
// ctx.Err() // returns context.Canceled
// context.Cause(ctx) // returns myError
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)
// A CancelCauseFunc behaves like a CancelFunc but additionally sets the cancelation cause.
// This cause can be retrieved by calling Cause on the canceled Context or any of its derived Contexts.
// If the context has already been canceled, CancelCauseFunc does not set the cause.
type CancelCauseFunc func(cause error)
// Cause returns a non-nil error if a parent Context was canceled using a CancelCauseFunc that was passed that error.
// Otherwise Cause returns nil.
func Cause(c Context) error
// 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 WithDeadlineCause(parent, time.Now().Add(timeout), cause).
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)