github.com/weedge/lib@v0.0.0-20230424045628-a36dcc1d90e4/timingwheel/timingwheel.go (about) 1 package timingwheel 2 3 import ( 4 "errors" 5 "sync/atomic" 6 "time" 7 "unsafe" 8 9 "github.com/weedge/lib/container/queue" 10 ) 11 12 // TimingWheel is an implementation of Hierarchical Timing Wheels. 13 type TimingWheel struct { 14 tick int64 // in milliseconds 15 wheelSize int64 16 17 interval int64 // in milliseconds 18 currentTime int64 // in milliseconds 19 buckets []*bucket 20 queue *queue.DelayQueue 21 22 // The higher-level overflow wheel. 23 // 24 // NOTE: This field may be updated and read concurrently, through Add(). 25 overflowWheel unsafe.Pointer // type: *TimingWheel 26 27 exitC chan struct{} 28 waitGroup waitGroupWrapper 29 } 30 31 // NewTimingWheel creates an instance of TimingWheel with the given tick and wheelSize. 32 func NewTimingWheel(tick time.Duration, wheelSize int64) *TimingWheel { 33 tickMs := int64(tick / time.Millisecond) 34 if tickMs <= 0 { 35 panic(errors.New("tick must be greater than or equal to 1ms")) 36 } 37 38 startMs := timeToMs(time.Now().UTC()) 39 40 return newTimingWheel( 41 tickMs, 42 wheelSize, 43 startMs, 44 queue.NewDelayQueue(int(wheelSize)), 45 ) 46 } 47 48 // newTimingWheel is an internal helper function that really creates an instance of TimingWheel. 49 func newTimingWheel(tickMs int64, wheelSize int64, startMs int64, queue *queue.DelayQueue) *TimingWheel { 50 buckets := make([]*bucket, wheelSize) 51 for i := range buckets { 52 buckets[i] = newBucket() 53 } 54 return &TimingWheel{ 55 tick: tickMs, 56 wheelSize: wheelSize, 57 currentTime: truncate(startMs, tickMs), 58 interval: tickMs * wheelSize, 59 buckets: buckets, 60 queue: queue, 61 exitC: make(chan struct{}), 62 } 63 } 64 65 // add inserts the timer t into the current timing wheel. 66 func (tw *TimingWheel) add(t *Timer) bool { 67 currentTime := atomic.LoadInt64(&tw.currentTime) 68 if t.expiration < currentTime+tw.tick { 69 // Already expired 70 return false 71 } else if t.expiration < currentTime+tw.interval { 72 // Put it into its own bucket 73 virtualID := t.expiration / tw.tick 74 b := tw.buckets[virtualID%tw.wheelSize] 75 b.Add(t) 76 77 // Set the bucket expiration time 78 if b.SetExpiration(virtualID * tw.tick) { 79 // The bucket needs to be enqueued since it was an expired bucket. 80 // We only need to enqueue the bucket when its expiration time has changed, 81 // i.e. the wheel has advanced and this bucket get reused with a new expiration. 82 // Any further calls to set the expiration within the same wheel cycle will 83 // pass in the same value and hence return false, thus the bucket with the 84 // same expiration will not be enqueued multiple times. 85 tw.queue.Offer(b, b.Expiration()) 86 } 87 88 return true 89 } else { 90 // Out of the interval. Put it into the overflow wheel 91 overflowWheel := atomic.LoadPointer(&tw.overflowWheel) 92 if overflowWheel == nil { 93 atomic.CompareAndSwapPointer( 94 &tw.overflowWheel, 95 nil, 96 unsafe.Pointer(newTimingWheel( 97 tw.interval, 98 tw.wheelSize, 99 currentTime, 100 tw.queue, 101 )), 102 ) 103 overflowWheel = atomic.LoadPointer(&tw.overflowWheel) 104 } 105 return (*TimingWheel)(overflowWheel).add(t) 106 } 107 } 108 109 // addOrRun inserts the timer t into the current timing wheel, or run the 110 // timer's task if it has already expired. 111 func (tw *TimingWheel) addOrRun(t *Timer) { 112 if !tw.add(t) { 113 // Already expired 114 115 // Like the standard time.AfterFunc (https://golang.org/pkg/time/#AfterFunc), 116 // always execute the timer's task in its own goroutine. 117 go t.task() 118 } 119 } 120 121 func (tw *TimingWheel) advanceClock(expiration int64) { 122 currentTime := atomic.LoadInt64(&tw.currentTime) 123 if expiration >= currentTime+tw.tick { 124 currentTime = truncate(expiration, tw.tick) 125 atomic.StoreInt64(&tw.currentTime, currentTime) 126 127 // Try to advance the clock of the overflow wheel if present 128 overflowWheel := atomic.LoadPointer(&tw.overflowWheel) 129 if overflowWheel != nil { 130 (*TimingWheel)(overflowWheel).advanceClock(currentTime) 131 } 132 } 133 } 134 135 // Start starts the current timing wheel. 136 func (tw *TimingWheel) Start() { 137 tw.waitGroup.Wrap(func() { 138 tw.queue.Poll(tw.exitC, func() int64 { 139 return timeToMs(time.Now().UTC()) 140 }) 141 }) 142 143 tw.waitGroup.Wrap(func() { 144 tw.queue.Do(tw.exitC, func(elem interface{}) { 145 b := elem.(*bucket) 146 tw.advanceClock(b.Expiration()) 147 b.Flush(tw.addOrRun) 148 }) 149 }) 150 } 151 152 // Stop stops the current timing wheel. 153 // 154 // If there is any timer's task being running in its own goroutine, Stop does 155 // not wait for the task to complete before returning. If the caller needs to 156 // know whether the task is completed, it must coordinate with the task explicitly. 157 func (tw *TimingWheel) Stop() { 158 close(tw.exitC) 159 tw.waitGroup.Wait() 160 } 161 162 // AfterFunc waits for the duration to elapse and then calls f in its own goroutine. 163 // It returns a Timer that can be used to cancel the call using its Stop method. 164 func (tw *TimingWheel) AfterFunc(d time.Duration, f func()) *Timer { 165 t := &Timer{ 166 expiration: timeToMs(time.Now().UTC().Add(d)), 167 task: f, 168 } 169 tw.addOrRun(t) 170 return t 171 } 172 173 // Scheduler determines the execution plan of a task. 174 type Scheduler interface { 175 // Next returns the next execution time after the given (previous) time. 176 // It will return a zero time if no next time is scheduled. 177 // 178 // All times must be UTC. 179 Next(time.Time) time.Time 180 } 181 182 // ScheduleFunc calls f (in its own goroutine) according to the execution 183 // plan scheduled by s. It returns a Timer that can be used to cancel the 184 // call using its Stop method. 185 // 186 // If the caller want to terminate the execution plan halfway, it must 187 // stop the timer and ensure that the timer is stopped actually, since in 188 // the current implementation, there is a gap between the expiring and the 189 // restarting of the timer. The wait time for ensuring is short since the 190 // gap is very small. 191 // 192 // Internally, ScheduleFunc will ask the first execution time (by calling 193 // s.Next()) initially, and create a timer if the execution time is non-zero. 194 // Afterwards, it will ask the next execution time each time f is about to 195 // be executed, and f will be called at the next execution time if the time 196 // is non-zero. 197 func (tw *TimingWheel) ScheduleFunc(s Scheduler, f func()) (t *Timer) { 198 expiration := s.Next(time.Now().UTC()) 199 if expiration.IsZero() { 200 // No time is scheduled, return nil. 201 return 202 } 203 204 t = &Timer{ 205 expiration: timeToMs(expiration), 206 task: func() { 207 // Schedule the task to execute at the next time if possible. 208 expiration := s.Next(msToTime(t.expiration)) 209 if !expiration.IsZero() { 210 t.expiration = timeToMs(expiration) 211 tw.addOrRun(t) 212 } 213 214 // Actually execute the task. 215 f() 216 }, 217 } 218 tw.addOrRun(t) 219 220 return 221 }