github.com/juju/clock@v1.0.3/testclock/dilated.go (about) 1 // Copyright 2022 Canonical Ltd. 2 // Licensed under the LGPLv3, see LICENCE file for details. 3 4 package testclock 5 6 import ( 7 "sync" 8 "sync/atomic" 9 "time" 10 11 "github.com/juju/clock" 12 ) 13 14 // NewDilatedWallClock returns a clock that can be sped up or slowed down. 15 // realSecondDuration is the real duration of a second. 16 func NewDilatedWallClock(realSecondDuration time.Duration) AdvanceableClock { 17 dc := &dilationClock{ 18 epoch: time.Now(), 19 realSecondDuration: realSecondDuration, 20 offsetChanged: make(chan any), 21 } 22 dc.offsetChangedCond = sync.NewCond(dc.offsetChangedMutex.RLocker()) 23 return dc 24 } 25 26 type dilationClock struct { 27 epoch time.Time 28 realSecondDuration time.Duration 29 30 // offsetAtomic is the current dilated offset to allow for time jumps/advances. 31 offsetAtomic int64 32 // offsetChanged is a channel that is closed when timers need to be signaled 33 // that there is a offset change coming. 34 offsetChanged chan any 35 // offsetChangedMutex is a mutex protecting the offsetChanged and is used by 36 // the offsetChangedCond. 37 offsetChangedMutex sync.RWMutex 38 // offsetChangedCond is used to signal timers that they may try to pull the new 39 // offset. 40 offsetChangedCond *sync.Cond 41 } 42 43 // Now is part of the Clock interface. 44 func (dc *dilationClock) Now() time.Time { 45 dt, _ := dc.nowWithOffset() 46 return dt 47 } 48 49 func (dc *dilationClock) nowWithOffset() (time.Time, time.Duration) { 50 offset := time.Duration(atomic.LoadInt64(&dc.offsetAtomic)) 51 realNow := time.Now() 52 dt := dilateTime(dc.epoch, realNow, dc.realSecondDuration, offset) 53 return dt, offset 54 } 55 56 // After implements Clock.After 57 func (dc *dilationClock) After(d time.Duration) <-chan time.Time { 58 t := newDilatedWallTimer(dc, d, nil) 59 return t.c 60 } 61 62 // AfterFunc implements Clock.AfterFunc 63 func (dc *dilationClock) AfterFunc(d time.Duration, f func()) clock.Timer { 64 return newDilatedWallTimer(dc, d, f) 65 } 66 67 // NewTimer implements Clock.NewTimer 68 func (dc *dilationClock) NewTimer(d time.Duration) clock.Timer { 69 return newDilatedWallTimer(dc, d, nil) 70 } 71 72 // Advance implements AdvanceableClock.Advance 73 func (dc *dilationClock) Advance(d time.Duration) { 74 close(dc.offsetChanged) 75 dc.offsetChangedMutex.Lock() 76 dc.offsetChanged = make(chan any) 77 atomic.AddInt64(&dc.offsetAtomic, int64(d)) 78 dc.offsetChangedCond.Broadcast() 79 dc.offsetChangedMutex.Unlock() 80 } 81 82 // dilatedWallTimer implements the Timer interface. 83 type dilatedWallTimer struct { 84 timer *time.Timer 85 dc *dilationClock 86 c chan time.Time 87 target time.Time 88 offset time.Duration 89 after func() 90 done chan any 91 resetChan chan resetReq 92 resetMutex sync.Mutex 93 stopChan chan chan bool 94 } 95 96 type resetReq struct { 97 d time.Duration 98 r chan bool 99 } 100 101 func newDilatedWallTimer(dc *dilationClock, d time.Duration, after func()) *dilatedWallTimer { 102 t := &dilatedWallTimer{ 103 dc: dc, 104 c: make(chan time.Time), 105 resetChan: make(chan resetReq), 106 stopChan: make(chan chan bool), 107 } 108 t.start(d, after) 109 return t 110 } 111 112 func (t *dilatedWallTimer) start(d time.Duration, after func()) { 113 t.dc.offsetChangedMutex.RLock() 114 dialatedNow, offset := t.dc.nowWithOffset() 115 realDuration := time.Duration(float64(d) * t.dc.realSecondDuration.Seconds()) 116 t.target = dialatedNow.Add(d) 117 t.timer = time.NewTimer(realDuration) 118 t.offset = offset 119 t.after = after 120 t.done = make(chan any) 121 go t.run() 122 } 123 124 func (t *dilatedWallTimer) run() { 125 defer t.dc.offsetChangedMutex.RUnlock() 126 defer close(t.done) 127 var sendChan chan time.Time 128 var sendTime time.Time 129 for { 130 select { 131 case reset := <-t.resetChan: 132 realNow := time.Now() 133 dialatedNow := dilateTime(t.dc.epoch, realNow, t.dc.realSecondDuration, t.offset) 134 realDuration := time.Duration(float64(reset.d) * t.dc.realSecondDuration.Seconds()) 135 t.target = dialatedNow.Add(reset.d) 136 sendChan = nil 137 sendTime = time.Time{} 138 reset.r <- t.timer.Reset(realDuration) 139 case stop := <-t.stopChan: 140 stop <- t.timer.Stop() 141 return 142 case tt := <-t.timer.C: 143 if t.after != nil { 144 t.after() 145 return 146 } 147 if sendChan != nil { 148 panic("reset should have been called") 149 } 150 sendChan = t.c 151 sendTime = dilateTime(t.dc.epoch, tt, t.dc.realSecondDuration, t.offset) 152 case sendChan <- sendTime: 153 sendChan = nil 154 sendTime = time.Time{} 155 return 156 case <-t.dc.offsetChanged: 157 t.dc.offsetChangedCond.Wait() 158 newOffset := time.Duration(atomic.LoadInt64(&t.dc.offsetAtomic)) 159 if newOffset == t.offset { 160 continue 161 } 162 t.offset = newOffset 163 stopped := t.timer.Stop() 164 if !stopped { 165 continue 166 } 167 realNow := time.Now() 168 dialatedNow := dilateTime(t.dc.epoch, realNow, t.dc.realSecondDuration, t.offset) 169 dialatedDuration := t.target.Sub(dialatedNow) 170 if dialatedDuration <= 0 { 171 sendChan = t.c 172 sendTime = dialatedNow 173 continue 174 } 175 realDuration := time.Duration(float64(dialatedDuration) * t.dc.realSecondDuration.Seconds()) 176 t.timer.Reset(realDuration) 177 } 178 } 179 } 180 181 // Chan implements Timer.Chan 182 func (t *dilatedWallTimer) Chan() <-chan time.Time { 183 return t.c 184 } 185 186 // Chan implements Timer.Reset 187 func (t *dilatedWallTimer) Reset(d time.Duration) bool { 188 t.resetMutex.Lock() 189 defer t.resetMutex.Unlock() 190 reset := resetReq{ 191 d: d, 192 r: make(chan bool), 193 } 194 select { 195 case <-t.done: 196 t.start(d, nil) 197 return true 198 case t.resetChan <- reset: 199 return <-reset.r 200 } 201 } 202 203 // Chan implements Timer.Stop 204 func (t *dilatedWallTimer) Stop() bool { 205 stop := make(chan bool) 206 select { 207 case <-t.done: 208 return false 209 case t.stopChan <- stop: 210 return <-stop 211 } 212 } 213 214 func dilateTime(epoch, realNow time.Time, 215 realSecondDuration, dilatedOffset time.Duration) time.Time { 216 delta := realNow.Sub(epoch) 217 if delta < 0 { 218 delta = time.Duration(0) 219 } 220 return epoch.Add(dilatedOffset).Add(time.Duration(float64(delta) / realSecondDuration.Seconds())) 221 }