Skip to content

Commit f5b7433

Browse files
authored
Expose advance mechanism to timers (#3)
1 parent 14d2328 commit f5b7433

6 files changed

Lines changed: 290 additions & 280 deletions

File tree

README.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ clock.Advance(time.Day)
3636
clock.Now() // returns Feb 13, 1989
3737
```
3838

39-
The `Advance` method will also trigger a value on the channels created by the `After` and `Ticker` functions.
39+
The `Advance` method will also trigger a value on the channels created by the `After` and `Ticker` functions, if enough virtual time has elapsed for the events to fire.
4040

4141
```go
4242
clock := glock.NewMockClockAt(time.Unix(603288000, 0))
@@ -51,15 +51,60 @@ clock.Advance(time.Second * 30) // Fires c1
5151

5252
```go
5353
clock := glock.NewMockClock()
54+
5455
ticker := clock.NewTicker(time.Minute)
56+
defer ticker.Stop()
57+
58+
go func() {
59+
for range ticker.Chan() {
60+
// ...
61+
}
62+
}()
5563

56-
ch := ticker.Chan()
5764
clock.Advance(time.Second * 30)
5865
clock.Advance(time.Second * 30) // Fires ch
5966
clock.Advance(time.Second * 30)
6067
clock.Advance(time.Second * 30) // Fires ch
6168
```
6269

70+
The `Advance` method will send a value to any current listener registered to a channel on the clock. Timing these calls in relation with the clock consumer is not always an easy task. A variation of the advance method, `BlockingAdvance` can be used in its place when you want to first ensure that there is a listener on a channel returned by `After`.
71+
72+
73+
```go
74+
clock := glock.NewMockClock()
75+
76+
go func() {
77+
<-clock.After(time.Second * 30)
78+
}()
79+
80+
clock.BlockingAdvance(time.Second * 30) // blocks until the concurrent call to After
81+
clock.BlockingAdvance(time.Second * 30) // blocks indefinitely as there are no listeners
82+
```
83+
84+
Ticker instances themselves have the same time advancing mechanisms. Using `Advance` on a ticker (or using `Advance` on the clock from which a ticker was created) will cause the ticker to fire _once_ and then forward itself to the current time. This mimics the behavior of the Go runtime clock (see the test functions `^TestTickerOffset`).
85+
86+
Where the `Advance` method sends the ticker's time to the consumer in a background goroutine, the `BlockingAdvance` variant will send the value in the caller's goroutine.
87+
88+
```go
89+
ticker := clock.NewMockTicker(time.Second * 30)
90+
defer ticker.Stop()
91+
92+
go func() {
93+
<-ticker.Chan()
94+
<-ticker.Chan()
95+
<-ticker.Chan()
96+
}()
97+
98+
ticker.BlockingAdvance(time.Second * 15)
99+
ticker.BlockingAdvance(time.Second * 15) // Fires ch
100+
ticker.BlockingAdvance(time.Second * 15)
101+
ticker.BlockingAdvance(time.Second * 15) // Fires ch
102+
ticker.BlockingAdvance(time.Second * 60) // Fires ch _once_
103+
104+
ticker.Advance(time.Second * 30) // does not block; sent asynchronously
105+
ticker.BlockingAdvance(time.Second * 30) // blocks indefinitely as there are no listeners
106+
```
107+
63108
## Context Utilities
64109

65110
If you'd like to use a `context.Context` as a way to make a glock `Clock` available, this

advanceable.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package glock
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
type Advanceable interface {
9+
Advance(duration time.Duration)
10+
BlockingAdvance(duration time.Duration)
11+
SetCurrent(now time.Time)
12+
}
13+
14+
// advanceable is the base type for mock clocks and tickers. This struct is
15+
// written to be used as as a mixin, where the containing struct can mutate
16+
// its internals (assuming correct coordination is used).
17+
//
18+
// An "advanceable" struct has a current time set of subscribers that may
19+
// change over time. The current time can be moved explicitly by the user.
20+
type advanceable struct {
21+
now time.Time
22+
subscribers []subscriber
23+
m *sync.Mutex
24+
cond *sync.Cond
25+
}
26+
27+
type subscriber interface {
28+
// signal performs some behavior if the given current time is after a
29+
// deadline registered previously. This method should not block. If the
30+
// subscriber is still interested in being updated with the current
31+
// time, it should return true; the clock or timer instance will drop
32+
// a reference to this subscriber otherwise.
33+
signal(now time.Time) (requeue bool)
34+
}
35+
36+
// newAdvanceableAt returns a new advanceable struct with the given current time.
37+
func newAdvanceableAt(now time.Time) *advanceable {
38+
m := &sync.Mutex{}
39+
40+
return &advanceable{
41+
now: now,
42+
m: m,
43+
cond: sync.NewCond(m),
44+
}
45+
}
46+
47+
// Advance will advance the clock's internal time by the given duration.
48+
func (a *advanceable) Advance(duration time.Duration) {
49+
a.m.Lock()
50+
a.setCurrent(a.now.Add(duration))
51+
a.m.Unlock()
52+
}
53+
54+
// SetCurrent sets the clock's internal time to the given time.
55+
func (a *advanceable) SetCurrent(now time.Time) {
56+
a.m.Lock()
57+
a.setCurrent(now)
58+
a.m.Unlock()
59+
}
60+
61+
// setCurrent sets the new current time and invokes and filters the list of
62+
// subscribers.
63+
func (a *advanceable) setCurrent(now time.Time) {
64+
filtered := a.subscribers[:0]
65+
for _, e := range a.subscribers {
66+
if e.signal(now) {
67+
filtered = append(filtered, e)
68+
}
69+
}
70+
71+
a.now = now
72+
a.subscribers = filtered
73+
a.cond.Broadcast()
74+
}
75+
76+
// register marks a subscriber to be updated when the current time changes.
77+
func (a *advanceable) register(subscriber subscriber) {
78+
a.subscribers = append(a.subscribers, subscriber)
79+
a.cond.Broadcast()
80+
}

0 commit comments

Comments
 (0)