@@ -2,13 +2,17 @@ package clockwork
22
33import (
44 "context"
5+ "fmt"
6+ "sync"
7+ "time"
58)
69
710// contextKey is private to this package so we can ensure uniqueness here. This
811// type identifies context values provided by this package.
912type contextKey string
1013
11- // keyClock provides a clock for injecting during tests. If absent, a real clock should be used.
14+ // keyClock provides a clock for injecting during tests. If absent, a real clock
15+ // should be used.
1216var keyClock = contextKey ("clock" ) // clockwork.Clock
1317
1418// AddToContext creates a derived context that references the specified clock.
@@ -21,10 +25,145 @@ func AddToContext(ctx context.Context, clock Clock) context.Context {
2125 return context .WithValue (ctx , keyClock , clock )
2226}
2327
24- // FromContext extracts a clock from the context. If not present, a real clock is returned.
28+ // FromContext extracts a clock from the context. If not present, a real clock
29+ // is returned.
2530func FromContext (ctx context.Context ) Clock {
2631 if clock , ok := ctx .Value (keyClock ).(Clock ); ok {
2732 return clock
2833 }
2934 return NewRealClock ()
3035}
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)
51+ var 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.
62+ func 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].
72+ func 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.
85+ type 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+
101+ func 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+
119+ func (c * fakeClockContext ) Deadline () (time.Time , bool ) {
120+ return c .deadline , true
121+ }
122+
123+ func (c * fakeClockContext ) Done () <- chan struct {} {
124+ return c .ctxDone
125+ }
126+
127+ func (c * fakeClockContext ) Err () error {
128+ <- c .Done () // Don't return the error before it is ready.
129+ return c .err
130+ }
131+
132+ func (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.
145+ func (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+ }
0 commit comments