go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/clock/testclock/fastclock.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package testclock 16 17 import ( 18 "container/heap" 19 "context" 20 "fmt" 21 "sync" 22 "time" 23 24 "go.chromium.org/luci/common/clock" 25 ) 26 27 // FastClock mimics faster physical clock in tests. 28 // 29 // Its API is exactly the same as that of TestClock and can be used in-place. 30 // However, unlike TestClock, the time in this clock moves forward as 31 // a normal wall clock would, but at an arbitrarily faster rate. 32 // 33 // It's useful for integration tests simulating a large system over long period 34 // of (wall clock) time where adjusting each indiviudal timeout/delay/timer via 35 // testclock API isn't feasible. 36 type FastClock struct { 37 mutex sync.Mutex 38 initSysTime time.Time 39 initFastTime time.Time 40 fastToSysRatio int 41 pendingTimers pendingTimerHeap 42 timerCallback TimerCallback 43 // pendingTimersChanged pipes "true" to the worker 44 // whenever pending timers change. 45 // 46 // In Close(), this channel is closed and thus pipes "false" indefinitely. 47 pendingTimersChanged chan bool 48 systemNow func() time.Time // mocked in tests. 49 } 50 51 // NewFastClock creates a new FastClock running faster than a system clock. 52 // 53 // You SHOULD call .Close() on the returned object after use to avoid leaks. 54 func NewFastClock(now time.Time, ratio int) *FastClock { 55 f := &FastClock{ 56 initFastTime: now, 57 initSysTime: time.Now(), 58 fastToSysRatio: ratio, 59 pendingTimersChanged: make(chan bool, 1), // holds at most one "poke" 60 systemNow: time.Now, 61 } 62 go f.worker() 63 return f 64 } 65 66 // onTimersChangedLocked wakes up the worker() func if it hasn't been poked 67 // already. 68 func (f *FastClock) onTimersChangedLocked() { 69 select { 70 case f.pendingTimersChanged <- true: 71 default: 72 // Already notified. 73 } 74 } 75 76 // periodicTimerNotify follows system (wall) clock and wakes us timers as 77 // necessary. 78 func (f *FastClock) worker() { 79 const maxSysWait = time.Hour 80 // Create system timer to wait on. 81 sysTimer := time.NewTimer(maxSysWait) 82 // Make the timer ready for Reset. 83 if !sysTimer.Stop() { 84 <-sysTimer.C 85 } 86 87 notifyAndResetSysTimer := func() { 88 f.mutex.Lock() 89 defer f.mutex.Unlock() 90 fNow := f.Now() 91 triggerTimersLocked(fNow, &f.pendingTimers) 92 wait := maxSysWait 93 if len(f.pendingTimers) > 0 { 94 // Due to triggerTimersLocked() before, `wait` must be >0. 95 wait = f.pendingTimers[0].deadline.Sub(fNow) / time.Duration(f.fastToSysRatio) 96 } 97 sysTimer.Reset(wait) 98 } 99 100 for { 101 notifyAndResetSysTimer() 102 select { 103 case <-sysTimer.C: 104 case changed := <-f.pendingTimersChanged: 105 if !sysTimer.Stop() { 106 <-sysTimer.C 107 } 108 if !changed { 109 // The pendingTimersChanged channel was closed by Close(). 110 return 111 } 112 } 113 } 114 } 115 116 // Close frees clock resources. 117 func (f *FastClock) Close() { 118 close(f.pendingTimersChanged) 119 } 120 121 // Now returns the current time (see time.Now). 122 func (f *FastClock) Now() time.Time { 123 _, fNow := f.now() 124 return fNow 125 } 126 127 // now returns system (wall) clock time and this clock's time. 128 func (f *FastClock) now() (time.Time, time.Time) { 129 sNow := f.systemNow() 130 fNow := f.initFastTime.Add(sNow.Sub(f.initSysTime) * time.Duration(f.fastToSysRatio)) 131 return sNow, fNow 132 } 133 134 // Sleep sleeps the current goroutine (see time.Sleep). 135 // 136 // Sleep will return a TimerResult containing the time when it was awakened 137 // and detailing its execution. If the sleep terminated prematurely from 138 // cancellation, the TimerResult's Incomplete() method will return true. 139 func (f *FastClock) Sleep(ctx context.Context, d time.Duration) clock.TimerResult { 140 t := f.NewTimer(ctx) 141 t.Reset(d) 142 return <-t.GetC() 143 } 144 145 // NewTimer creates a new Timer instance, bound to this Clock. 146 // 147 // If the supplied Context is canceled, the timer will expire immediately. 148 func (f *FastClock) NewTimer(ctx context.Context) clock.Timer { 149 return newTimer(ctx, f) 150 } 151 152 // Set sets the test clock's time to at least the given time. 153 // 154 // Noop if Now() is already after the given time. 155 func (f *FastClock) Set(fNew time.Time) { 156 f.mutex.Lock() 157 defer f.mutex.Unlock() 158 159 sNow, fBefore := f.now() 160 if fBefore.After(fNew) { 161 // fNew is already in the past. 162 return 163 } 164 f.initSysTime = sNow 165 f.initFastTime = fNew 166 167 triggerTimersLocked(fNew, &f.pendingTimers) 168 f.onTimersChangedLocked() 169 } 170 171 // Add advances the test clock's time. 172 func (f *FastClock) Add(d time.Duration) { 173 if d < 0 { 174 panic(fmt.Errorf("cannot go backwards in time. You're not Doc Brown.\nDelta: %s", d)) 175 } 176 177 f.mutex.Lock() 178 defer f.mutex.Unlock() 179 180 sNow, fBefore := f.now() 181 f.initSysTime = sNow 182 f.initFastTime = fBefore.Add(d) 183 triggerTimersLocked(f.initFastTime, &f.pendingTimers) 184 f.onTimersChangedLocked() 185 } 186 187 // SetTimerCallback is a goroutine-safe method to set an instance-wide 188 // callback that is invoked when any timer begins. 189 func (f *FastClock) SetTimerCallback(clbk TimerCallback) { 190 f.mutex.Lock() 191 f.timerCallback = clbk 192 f.mutex.Unlock() 193 } 194 195 func (f *FastClock) addPendingTimer(t *timer, d time.Duration, triggerC chan<- time.Time) { 196 deadline := f.Now().Add(d) 197 if callback := f.timerCallback; callback != nil { 198 callback(d, t) 199 } 200 201 f.mutex.Lock() 202 defer f.mutex.Unlock() 203 204 heap.Push(&f.pendingTimers, &pendingTimer{ 205 timer: t, 206 deadline: deadline, 207 triggerC: triggerC, 208 }) 209 _, now := f.now() 210 triggerTimersLocked(now, &f.pendingTimers) 211 f.onTimersChangedLocked() 212 } 213 214 func (f *FastClock) clearPendingTimer(t *timer) { 215 f.mutex.Lock() 216 defer f.mutex.Unlock() 217 218 for i := 0; i < len(f.pendingTimers); { 219 if e := f.pendingTimers[0]; e.timer == t { 220 heap.Remove(&f.pendingTimers, i) 221 close(e.triggerC) 222 } else { 223 i++ 224 } 225 } 226 f.onTimersChangedLocked() 227 }