Skip to content

Commit 7810cc0

Browse files
committed
Add WithCancelCause
1 parent 7852759 commit 7810cc0

File tree

2 files changed

+92
-13
lines changed

2 files changed

+92
-13
lines changed

internal/sync/context.go

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,24 +162,71 @@ type CancelFunc func()
162162
// Canceling this context releases resources associated with it, so code should
163163
// call cancel as soon as the operations running in this Context complete.
164164
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
165+
c := withCancel(parent)
166+
return c, func() { c.cancel(true, Canceled, nil) }
167+
}
168+
169+
// A CancelCauseFunc behaves like a [CancelFunc] but additionally sets the cancellation cause.
170+
// This cause can be retrieved by calling [Cause] on the canceled Context or on
171+
// any of its derived Contexts.
172+
//
173+
// If the context has already been canceled, CancelCauseFunc does not set the cause.
174+
// For example, if childContext is derived from parentContext:
175+
// - if parentContext is canceled with cause1 before childContext is canceled with cause2,
176+
// then Cause(parentContext) == Cause(childContext) == cause1
177+
// - if childContext is canceled with cause2 before parentContext is canceled with cause1,
178+
// then Cause(parentContext) == cause1 and Cause(childContext) == cause2
179+
type CancelCauseFunc func(cause error)
180+
181+
// WithCancelCause behaves like [WithCancel] but returns a [CancelCauseFunc] instead of a [CancelFunc].
182+
// Calling cancel with a non-nil error (the "cause") records that error in ctx;
183+
// it can then be retrieved using Cause(ctx).
184+
// Calling cancel with nil sets the cause to Canceled.
185+
//
186+
// Example use:
187+
//
188+
// ctx, cancel := context.WithCancelCause(parent)
189+
// cancel(myError)
190+
// ctx.Err() // returns context.Canceled
191+
// context.Cause(ctx) // returns myError
192+
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
193+
c := withCancel(parent)
194+
return c, func(cause error) { c.cancel(true, Canceled, cause) }
195+
}
196+
197+
func withCancel(parent Context) *cancelCtx {
165198
if parent == nil {
166199
panic("cannot create context from nil parent")
167200
}
168-
c := newCancelCtx(parent)
169-
propagateCancel(parent, &c)
170-
return &c, func() { c.cancel(true, Canceled) }
201+
c := newCancelCtx()
202+
c.propagateCancel(parent, c)
203+
return c
204+
}
205+
206+
// Cause returns a non-nil error explaining why c was canceled.
207+
// The first cancellation of c or one of its parents sets the cause.
208+
// If that cancellation happened via a call to CancelCauseFunc(err),
209+
// then [Cause] returns err.
210+
// Otherwise Cause(c) returns the same value as c.Err().
211+
// Cause returns nil if c has not been canceled yet.
212+
func Cause(c Context) error {
213+
if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok {
214+
return cc.cause
215+
}
216+
return nil
171217
}
172218

173219
// newCancelCtx returns an initialized cancelCtx.
174-
func newCancelCtx(parent Context) cancelCtx {
175-
return cancelCtx{
176-
Context: parent,
177-
done: NewChannel[struct{}](),
220+
func newCancelCtx() *cancelCtx {
221+
return &cancelCtx{
222+
done: NewChannel[struct{}](),
178223
}
179224
}
180225

181226
// propagateCancel arranges for child to be canceled when parent is.
182-
func propagateCancel(parent Context, child canceler) {
227+
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
228+
c.Context = parent
229+
183230
done := parent.Done()
184231
if done == nil {
185232
return // parent is never canceled
@@ -189,7 +236,7 @@ func propagateCancel(parent Context, child canceler) {
189236
parent,
190237
Receive(done, func(ctx Context, _ struct{}, _ bool) {
191238
// Parent is already canceled
192-
child.cancel(false, parent.Err())
239+
child.cancel(false, parent.Err(), Cause(parent))
193240
}),
194241
Default(func(_ Context) {
195242
// Ignore
@@ -199,7 +246,7 @@ func propagateCancel(parent Context, child canceler) {
199246
if p, ok := parentCancelCtx(parent); ok {
200247
if p.err != nil {
201248
// parent has already been canceled
202-
child.cancel(false, p.err)
249+
child.cancel(false, p.err, p.cause)
203250
} else {
204251
if p.children == nil {
205252
p.children = make(map[canceler]struct{})
@@ -257,7 +304,7 @@ func removeChild(parent Context, child canceler) {
257304
// A canceler is a context type that can be canceled directly. The
258305
// implementations are *cancelCtx and *timerCtx.
259306
type canceler interface {
260-
cancel(removeFromParent bool, err error)
307+
cancel(removeFromParent bool, err, cause error)
261308
Done() Channel[struct{}]
262309
}
263310

@@ -276,6 +323,7 @@ type cancelCtx struct {
276323
done Channel[struct{}]
277324
children map[canceler]struct{} // set to nil by the first cancel call
278325
err error // set to non-nil by the first cancel call
326+
cause error // set to non-nil by the first cancel call
279327
}
280328

281329
func (c *cancelCtx) Value(key interface{}) interface{} {
@@ -296,21 +344,25 @@ func (c *cancelCtx) Err() error {
296344

297345
// cancel closes c.done, cancels each of c's children, and, if
298346
// removeFromParent is true, removes c from its parent's children.
299-
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
347+
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
300348
if err == nil {
301349
panic("context: internal error: missing cancel error")
302350
}
351+
if cause == nil {
352+
cause = err
353+
}
303354
if c.err != nil {
304355
return // already canceled
305356
}
306357
c.err = err
358+
c.cause = cause
307359
if c.done == nil {
308360
c.done = closedchan
309361
} else {
310362
c.done.Close()
311363
}
312364
for child := range c.children {
313-
child.cancel(false, err)
365+
child.cancel(false, err, cause)
314366
}
315367
c.children = nil
316368

workflow/context.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,33 @@ func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
1414
return sync.WithCancel(parent)
1515
}
1616

17+
// A CancelCauseFunc behaves like a [CancelFunc] but additionally sets the cancellation cause.
18+
// This cause can be retrieved by calling [Cause] on the canceled Context or on
19+
// any of its derived Contexts.
20+
//
21+
// If the context has already been canceled, CancelCauseFunc does not set the cause.
22+
// For example, if childContext is derived from parentContext:
23+
// - if parentContext is canceled with cause1 before childContext is canceled with cause2,
24+
// then Cause(parentContext) == Cause(childContext) == cause1
25+
// - if childContext is canceled with cause2 before parentContext is canceled with cause1,
26+
// then Cause(parentContext) == cause1 and Cause(childContext) == cause2
27+
type CancelCauseFunc = sync.CancelCauseFunc
28+
29+
// WithCancelCause behaves like [WithCancel] but returns a [CancelCauseFunc] instead of a [CancelFunc].
30+
// Calling cancel with a non-nil error (the "cause") records that error in ctx;
31+
// it can then be retrieved using Cause(ctx).
32+
// Calling cancel with nil sets the cause to Canceled.
33+
//
34+
// Example use:
35+
//
36+
// ctx, cancel := context.WithCancelCause(parent)
37+
// cancel(myError)
38+
// ctx.Err() // returns context.Canceled
39+
// context.Cause(ctx) // returns myError
40+
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
41+
return sync.WithCancelCause(parent)
42+
}
43+
1744
// WithValue returns a copy of parent in which the value associated with key is
1845
// val.
1946
//

0 commit comments

Comments
 (0)