Skip to content

context: add APIs for writing and reading cancelation cause #51365

@Sajmani

Description

@Sajmani

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 the Context interface
  • we cannot change the existing CancelFunc type to take a cause
  • we cannot change the existing WithCancel, WithDeadline, or WithTimeout function signatures
  • we cannot change Err to return any values other than nil, Canceled, or DeadlineExceeded

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)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions