// fires. The go vet tool checks that CancelFuncs are used on all
// control-flow paths.
//
+// The WithCancelCause function returns a CancelCauseFunc, which
+// takes an error and records it as the cancelation cause. Calling
+// Cause on the canceled context or any of its children retrieves
+// the cause. If no cause is specified, Cause(ctx) returns the same
+// value as ctx.Err().
+//
// Programs that use Contexts should follow these rules to keep interfaces
// consistent across packages and enable static analysis tools to check context
// propagation:
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
+ c := withCancel(parent)
+ return c, func() { c.cancel(true, Canceled, nil) }
+}
+
+// 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 on
+// any of its derived Contexts.
+//
+// If the context has already been canceled, CancelCauseFunc does not set the cause.
+// For example, if childContext is derived from parentContext:
+// - if parentContext is canceled with cause1 before childContext is canceled with cause2,
+// then Cause(parentContext) == Cause(childContext) == cause1
+// - if childContext is canceled with cause2 before parentContext is canceled with cause1,
+// then Cause(parentContext) == cause1 and Cause(childContext) == cause2
+type CancelCauseFunc func(cause error)
+
+// 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).
+// Calling cancel with nil sets the cause to Canceled.
+//
+// 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) {
+ c := withCancel(parent)
+ return c, func(cause error) { c.cancel(true, Canceled, cause) }
+}
+
+func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
- propagateCancel(parent, &c)
- return &c, func() { c.cancel(true, Canceled) }
+ propagateCancel(parent, c)
+ return c
+}
+
+// Cause returns a non-nil error explaining why c was canceled.
+// The first cancelation of c or one of its parents sets the cause.
+// If that cancelation happened via a call to CancelCauseFunc(err),
+// then Cause returns err.
+// Otherwise Cause(c) returns the same value as c.Err().
+// Cause returns nil if c has not been canceled yet.
+func Cause(c Context) error {
+ if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok {
+ return cc.cause
+ }
+ return nil
}
// newCancelCtx returns an initialized cancelCtx.
-func newCancelCtx(parent Context) cancelCtx {
- return cancelCtx{Context: parent}
+func newCancelCtx(parent Context) *cancelCtx {
+ return &cancelCtx{Context: parent}
}
// goroutines counts the number of goroutines ever created; for testing.
select {
case <-done:
// parent is already canceled
- child.cancel(false, parent.Err())
+ child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
- child.cancel(false, p.err)
+ child.cancel(false, p.err, p.cause)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
go func() {
select {
case <-parent.Done():
- child.cancel(false, parent.Err())
+ child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
- cancel(removeFromParent bool, err error)
+ cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
+ cause error // set to non-nil by the first cancel call
}
func (c *cancelCtx) Value(key any) any {
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
-func (c *cancelCtx) cancel(removeFromParent bool, err error) {
+// cancel sets c.cause to cause if this is the first time c is canceled.
+func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
+ if cause == nil {
+ cause = err
+ }
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
+ c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
- child.cancel(false, err)
+ child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
- c.cancel(true, DeadlineExceeded) // deadline has already passed
- return c, func() { c.cancel(false, Canceled) }
+ c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
+ return c, func() { c.cancel(false, Canceled, nil) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
- c.cancel(true, DeadlineExceeded)
+ c.cancel(true, DeadlineExceeded, nil)
})
}
- return c, func() { c.cancel(true, Canceled) }
+ return c, func() { c.cancel(true, Canceled, nil) }
}
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
- cancelCtx
+ *cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
time.Until(c.deadline).String() + "])"
}
-func (c *timerCtx) cancel(removeFromParent bool, err error) {
- c.cancelCtx.cancel(false, err)
+func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
+ c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
c = ctx.Context
case *timerCtx:
if key == &cancelCtxKey {
- return &ctx.cancelCtx
+ return ctx.cancelCtx
}
c = ctx.Context
case *emptyCtx:
}
func XTestWithCancelCanceledParent(t testingT) {
- parent, pcancel := WithCancel(Background())
- pcancel()
+ parent, pcancel := WithCancelCause(Background())
+ cause := fmt.Errorf("Because!")
+ pcancel(cause)
c, _ := WithCancel(parent)
select {
if got, want := c.Err(), Canceled; got != want {
t.Errorf("child not canceled; got = %v, want = %v", got, want)
}
+ if got, want := Cause(c), cause; got != want {
+ t.Errorf("child has wrong cause; got = %v, want = %v", got, want)
+ }
}
func XTestWithValueChecksKey(t testingT) {
defer cancel7()
checkNoGoroutine()
}
+
+func XTestCause(t testingT) {
+ var (
+ parentCause = fmt.Errorf("parentCause")
+ childCause = fmt.Errorf("childCause")
+ )
+ for _, test := range []struct {
+ name string
+ ctx Context
+ err error
+ cause error
+ }{
+ {
+ name: "Background",
+ ctx: Background(),
+ err: nil,
+ cause: nil,
+ },
+ {
+ name: "TODO",
+ ctx: TODO(),
+ err: nil,
+ cause: nil,
+ },
+ {
+ name: "WithCancel",
+ ctx: func() Context {
+ ctx, cancel := WithCancel(Background())
+ cancel()
+ return ctx
+ }(),
+ err: Canceled,
+ cause: Canceled,
+ },
+ {
+ name: "WithCancelCause",
+ ctx: func() Context {
+ ctx, cancel := WithCancelCause(Background())
+ cancel(parentCause)
+ return ctx
+ }(),
+ err: Canceled,
+ cause: parentCause,
+ },
+ {
+ name: "WithCancelCause nil",
+ ctx: func() Context {
+ ctx, cancel := WithCancelCause(Background())
+ cancel(nil)
+ return ctx
+ }(),
+ err: Canceled,
+ cause: Canceled,
+ },
+ {
+ name: "WithCancelCause: parent cause before child",
+ ctx: func() Context {
+ ctx, cancelParent := WithCancelCause(Background())
+ ctx, cancelChild := WithCancelCause(ctx)
+ cancelParent(parentCause)
+ cancelChild(childCause)
+ return ctx
+ }(),
+ err: Canceled,
+ cause: parentCause,
+ },
+ {
+ name: "WithCancelCause: parent cause after child",
+ ctx: func() Context {
+ ctx, cancelParent := WithCancelCause(Background())
+ ctx, cancelChild := WithCancelCause(ctx)
+ cancelChild(childCause)
+ cancelParent(parentCause)
+ return ctx
+ }(),
+ err: Canceled,
+ cause: childCause,
+ },
+ {
+ name: "WithCancelCause: parent cause before nil",
+ ctx: func() Context {
+ ctx, cancelParent := WithCancelCause(Background())
+ ctx, cancelChild := WithCancel(ctx)
+ cancelParent(parentCause)
+ cancelChild()
+ return ctx
+ }(),
+ err: Canceled,
+ cause: parentCause,
+ },
+ {
+ name: "WithCancelCause: parent cause after nil",
+ ctx: func() Context {
+ ctx, cancelParent := WithCancelCause(Background())
+ ctx, cancelChild := WithCancel(ctx)
+ cancelChild()
+ cancelParent(parentCause)
+ return ctx
+ }(),
+ err: Canceled,
+ cause: Canceled,
+ },
+ {
+ name: "WithCancelCause: child cause after nil",
+ ctx: func() Context {
+ ctx, cancelParent := WithCancel(Background())
+ ctx, cancelChild := WithCancelCause(ctx)
+ cancelParent()
+ cancelChild(childCause)
+ return ctx
+ }(),
+ err: Canceled,
+ cause: Canceled,
+ },
+ {
+ name: "WithCancelCause: child cause before nil",
+ ctx: func() Context {
+ ctx, cancelParent := WithCancel(Background())
+ ctx, cancelChild := WithCancelCause(ctx)
+ cancelChild(childCause)
+ cancelParent()
+ return ctx
+ }(),
+ err: Canceled,
+ cause: childCause,
+ },
+ {
+ name: "WithTimeout",
+ ctx: func() Context {
+ ctx, cancel := WithTimeout(Background(), 0)
+ cancel()
+ return ctx
+ }(),
+ err: DeadlineExceeded,
+ cause: DeadlineExceeded,
+ },
+ } {
+ if got, want := test.ctx.Err(), test.err; want != got {
+ t.Errorf("%s: ctx.Err() = %v want %v", test.name, got, want)
+ }
+ if got, want := Cause(test.ctx), test.cause; want != got {
+ t.Errorf("%s: Cause(ctx) = %v want %v", test.name, got, want)
+ }
+ }
+}