blob: 59242619e74e4048ebec9a03b8525fab7218b772 [file] [log] [blame]
Abhay Kumara2ae5992025-11-10 14:02:24 +00001package clockwork
2
3import (
4 "context"
5 "fmt"
6 "sync"
7 "time"
8)
9
10// contextKey is private to this package so we can ensure uniqueness here. This
11// type identifies context values provided by this package.
12type contextKey string
13
14// keyClock provides a clock for injecting during tests. If absent, a real clock
15// should be used.
16var keyClock = contextKey("clock") // clockwork.Clock
17
18// AddToContext creates a derived context that references the specified clock.
19//
20// Be aware this doesn't change the behavior of standard library functions, such
21// as [context.WithTimeout] or [context.WithDeadline]. For this reason, users
22// should prefer passing explicit [clockwork.Clock] variables rather can passing
23// the clock via the context.
24func AddToContext(ctx context.Context, clock Clock) context.Context {
25 return context.WithValue(ctx, keyClock, clock)
26}
27
28// FromContext extracts a clock from the context. If not present, a real clock
29// is returned.
30func FromContext(ctx context.Context) Clock {
31 if clock, ok := ctx.Value(keyClock).(Clock); ok {
32 return clock
33 }
34 return NewRealClock()
35}
36
37// ErrFakeClockDeadlineExceeded is the error returned by [context.Context] when
38// the deadline passes on a context which uses a [FakeClock].
39//
40// It wraps a [context.DeadlineExceeded] error, i.e.:
41//
42// // The following is true for any Context whose deadline has been exceeded,
43// // including contexts made with clockwork.WithDeadline or clockwork.WithTimeout.
44//
45// errors.Is(ctx.Err(), context.DeadlineExceeded)
46//
47// // The following can only be true for contexts made
48// // with clockwork.WithDeadline or clockwork.WithTimeout.
49//
50// errors.Is(ctx.Err(), clockwork.ErrFakeClockDeadlineExceeded)
51var ErrFakeClockDeadlineExceeded error = fmt.Errorf("clockwork.FakeClock: %w", context.DeadlineExceeded)
52
53// WithDeadline returns a context with a deadline based on a [FakeClock].
54//
55// The returned context ignores parent cancelation if the parent was cancelled
56// with a [context.DeadlineExceeded] error. Any other error returned by the
57// parent is treated normally, cancelling the returned context.
58//
59// If the parent is cancelled with a [context.DeadlineExceeded] error, the only
60// way to then cancel the returned context is by calling the returned
61// context.CancelFunc.
62func WithDeadline(parent context.Context, clock Clock, t time.Time) (context.Context, context.CancelFunc) {
63 if fc, ok := clock.(*FakeClock); ok {
64 return newFakeClockContext(parent, t, fc.newTimerAtTime(t, nil).Chan())
65 }
66 return context.WithDeadline(parent, t)
67}
68
69// WithTimeout returns a context with a timeout based on a [FakeClock].
70//
71// The returned context follows the same behaviors as [WithDeadline].
72func WithTimeout(parent context.Context, clock Clock, d time.Duration) (context.Context, context.CancelFunc) {
73 if fc, ok := clock.(*FakeClock); ok {
74 t, deadline := fc.newTimer(d, nil)
75 return newFakeClockContext(parent, deadline, t.Chan())
76 }
77 return context.WithTimeout(parent, d)
78}
79
80// fakeClockContext implements context.Context, using a fake clock for its
81// deadline.
82//
83// It ignores parent cancellation if the parent is cancelled with
84// context.DeadlineExceeded.
85type fakeClockContext struct {
86 parent context.Context
87 deadline time.Time // The user-facing deadline based on the fake clock's time.
88
89 // Tracks timeout/deadline cancellation.
90 timerDone <-chan time.Time
91
92 // Tracks manual calls to the cancel function.
93 cancel func() // Closes cancelCalled wrapped in a sync.Once.
94 cancelCalled chan struct{}
95
96 // The user-facing data from the context.Context interface.
97 ctxDone chan struct{} // Returned by Done().
98 err error // nil until ctxDone is ready to be closed.
99}
100
101func newFakeClockContext(parent context.Context, deadline time.Time, timer <-chan time.Time) (context.Context, context.CancelFunc) {
102 cancelCalled := make(chan struct{})
103 ctx := &fakeClockContext{
104 parent: parent,
105 deadline: deadline,
106 timerDone: timer,
107 cancelCalled: cancelCalled,
108 ctxDone: make(chan struct{}),
109 cancel: sync.OnceFunc(func() {
110 close(cancelCalled)
111 }),
112 }
113 ready := make(chan struct{}, 1)
114 go ctx.runCancel(ready)
115 <-ready // Wait until the cancellation goroutine is running.
116 return ctx, ctx.cancel
117}
118
119func (c *fakeClockContext) Deadline() (time.Time, bool) {
120 return c.deadline, true
121}
122
123func (c *fakeClockContext) Done() <-chan struct{} {
124 return c.ctxDone
125}
126
127func (c *fakeClockContext) Err() error {
128 <-c.Done() // Don't return the error before it is ready.
129 return c.err
130}
131
132func (c *fakeClockContext) Value(key any) any {
133 return c.parent.Value(key)
134}
135
136// runCancel runs the fakeClockContext's cancel goroutine and returns the
137// fakeClockContext's cancel function.
138//
139// fakeClockContext is then cancelled when any of the following occur:
140//
141// - The fakeClockContext.done channel is closed by its timer.
142// - The returned CancelFunc is executed.
143// - The fakeClockContext's parent context is cancelled with an error other
144// than context.DeadlineExceeded.
145func (c *fakeClockContext) runCancel(ready chan struct{}) {
146 parentDone := c.parent.Done()
147
148 // Close ready when done, just in case the ready signal races with other
149 // branches of our select statement below.
150 defer close(ready)
151
152 for c.err == nil {
153 select {
154 case <-c.timerDone:
155 c.err = ErrFakeClockDeadlineExceeded
156 case <-c.cancelCalled:
157 c.err = context.Canceled
158 case <-parentDone:
159 c.err = c.parent.Err()
160
161 case ready <- struct{}{}:
162 // Signals the cancellation goroutine has begun, in an attempt to minimize
163 // race conditions related to goroutine startup time.
164 ready = nil // This case statement can only fire once.
165 }
166 }
167 close(c.ctxDone)
168 return
169}