Skip to content

Commit 0c9c67d

Browse files
authored
Merge pull request #330 from cschleiden/context-improvements
Context and channel improvements
2 parents b40e6ea + 7810cc0 commit 0c9c67d

File tree

5 files changed

+145
-13
lines changed

5 files changed

+145
-13
lines changed

internal/sync/channel.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ type Channel[T any] interface {
1010
ReceiveNonBlocking() (v T, ok bool)
1111

1212
Close()
13+
14+
Len() int
1315
}
1416

1517
type Receiver[T any] struct {
@@ -52,6 +54,10 @@ type channel[T any] struct {
5254
size int
5355
}
5456

57+
func (c *channel[T]) Len() int {
58+
return len(c.c)
59+
}
60+
5561
func (c *channel[T]) Close() {
5662
c.closed = true
5763

internal/sync/channel_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,3 +470,47 @@ func Test_CancellationHandler_Remove(t *testing.T) {
470470

471471
require.Equal(t, 1, f)
472472
}
473+
474+
func Test_Channel_Len(t *testing.T) {
475+
tests := []struct {
476+
name string
477+
setup func(ctx Context) Channel[int]
478+
expected int
479+
}{
480+
{
481+
name: "EmptyChannel",
482+
setup: func(ctx Context) Channel[int] {
483+
return NewChannel[int]()
484+
},
485+
expected: 0,
486+
},
487+
{
488+
name: "NonEmptyBufferedChannel",
489+
setup: func(ctx Context) Channel[int] {
490+
c := NewBufferedChannel[int](4)
491+
c.Send(ctx, 42)
492+
c.Send(ctx, 23)
493+
return c
494+
},
495+
expected: 2,
496+
},
497+
}
498+
499+
for _, tt := range tests {
500+
t.Run(tt.name, func(t *testing.T) {
501+
var actual int
502+
ctx := Background()
503+
504+
cr := NewCoroutine(ctx, func(ctx Context) error {
505+
c := tt.setup(ctx)
506+
actual = c.Len()
507+
508+
return nil
509+
})
510+
511+
cr.Execute()
512+
513+
require.Equal(t, tt.expected, actual)
514+
})
515+
}
516+
}

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/channel.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ type Channel[T any] interface {
1919

2020
// Close closes the channel. This will cause all future send operations to panic.
2121
Close()
22+
23+
// Len returns the number of elements currently in the channel.
24+
Len() int
2225
}
2326

2427
// NewChannel creates a new channel.

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)