1
0

timeout.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package nebula
  2. import (
  3. "sync"
  4. "time"
  5. )
  6. // How many timer objects should be cached
  7. const timerCacheMax = 50000
  8. // TimerWheel is a hashed timing wheel: a fixed slot array indexed by (now + delay) % wheelLen,
  9. // with each slot a singly linked list of items due in that bucket.
  10. // Adds are O(1), Purges return items in arrival-within-slot order, and an internal cache of TimeoutItems
  11. // keeps steady-state inserts allocation-free.
  12. //
  13. // The TimerWheel does not handle concurrency or lifecycle on its own.
  14. // Callers drive Advance/Purge from their own ticker loop, take their own locks (or use LockingTimerWheel),
  15. // and decide whether to keep ticking when the wheel is empty.
  16. //
  17. // Pick a TimerWheel when scheduling is high-rate and uniform: line-rate conntrack inserts,
  18. // per-tunnel traffic checks at fixed intervals. O(1) insert plus the item cache means the hot path doesn't allocate.
  19. // Items added in the same tick are dispatched together when that slot rotates current,
  20. // which amortizes the cost of waking the worker.
  21. //
  22. // Pick a Scheduler when delay precision matters or scheduling is sparse or uneven.
  23. // The wheel rounds requested timeouts up to its tick resolution and clamps anything beyond its wheel duration;
  24. // both are silent in this implementation.
  25. type TimerWheel[T any] struct {
  26. // Current tick
  27. current int
  28. // Cheat on finding the length of the wheel
  29. wheelLen int
  30. // Last time we ticked, since we are lazy ticking
  31. lastTick *time.Time
  32. // Durations of a tick and the entire wheel
  33. tickDuration time.Duration
  34. wheelDuration time.Duration
  35. // The actual wheel which is just a set of singly linked lists, head/tail pointers
  36. wheel []*TimeoutList[T]
  37. // Singly linked list of items that have timed out of the wheel
  38. expired *TimeoutList[T]
  39. // Item cache to avoid garbage collect
  40. itemCache *TimeoutItem[T]
  41. itemsCached int
  42. }
  43. type LockingTimerWheel[T any] struct {
  44. m sync.Mutex
  45. t *TimerWheel[T]
  46. }
  47. // TimeoutList Represents a tick in the wheel
  48. type TimeoutList[T any] struct {
  49. Head *TimeoutItem[T]
  50. Tail *TimeoutItem[T]
  51. }
  52. // TimeoutItem Represents an item within a tick
  53. type TimeoutItem[T any] struct {
  54. Item T
  55. Next *TimeoutItem[T]
  56. }
  57. // NewTimerWheel Builds a timer wheel and identifies the tick duration and wheel duration from the provided values
  58. // Purge must be called once per entry to actually remove anything
  59. // The TimerWheel does not handle concurrency on its own.
  60. // Locks around access to it must be used if multiple routines are manipulating it.
  61. func NewTimerWheel[T any](min, max time.Duration) *TimerWheel[T] {
  62. //TODO provide an error
  63. //if min >= max {
  64. // return nil
  65. //}
  66. // Round down and add 2 so we can have the smallest # of ticks in the wheel and still account for a full
  67. // max duration, even if our current tick is at the maximum position and the next item to be added is at maximum
  68. // timeout
  69. wLen := int((max / min) + 2)
  70. tw := TimerWheel[T]{
  71. wheelLen: wLen,
  72. wheel: make([]*TimeoutList[T], wLen),
  73. tickDuration: min,
  74. wheelDuration: max,
  75. expired: &TimeoutList[T]{},
  76. }
  77. for i := range tw.wheel {
  78. tw.wheel[i] = &TimeoutList[T]{}
  79. }
  80. return &tw
  81. }
  82. // NewLockingTimerWheel is version of TimerWheel that is safe for concurrent use with a small performance penalty
  83. func NewLockingTimerWheel[T any](min, max time.Duration) *LockingTimerWheel[T] {
  84. return &LockingTimerWheel[T]{
  85. t: NewTimerWheel[T](min, max),
  86. }
  87. }
  88. // Add will add an item to the wheel in its proper timeout.
  89. // Caller should Advance the wheel prior to ensure the proper slot is used.
  90. func (tw *TimerWheel[T]) Add(v T, timeout time.Duration) *TimeoutItem[T] {
  91. i := tw.findWheel(timeout)
  92. // Try to fetch off the cache
  93. ti := tw.itemCache
  94. if ti != nil {
  95. tw.itemCache = ti.Next
  96. tw.itemsCached--
  97. ti.Next = nil
  98. } else {
  99. ti = &TimeoutItem[T]{}
  100. }
  101. // Relink and return
  102. ti.Item = v
  103. if tw.wheel[i].Tail == nil {
  104. tw.wheel[i].Head = ti
  105. tw.wheel[i].Tail = ti
  106. } else {
  107. tw.wheel[i].Tail.Next = ti
  108. tw.wheel[i].Tail = ti
  109. }
  110. return ti
  111. }
  112. // Purge removes and returns the first available expired item from the wheel and the 2nd argument is true.
  113. // If no item is available then an empty T is returned and the 2nd argument is false.
  114. func (tw *TimerWheel[T]) Purge() (T, bool) {
  115. if tw.expired.Head == nil {
  116. var na T
  117. return na, false
  118. }
  119. ti := tw.expired.Head
  120. tw.expired.Head = ti.Next
  121. if tw.expired.Head == nil {
  122. tw.expired.Tail = nil
  123. }
  124. // Clear out the items references
  125. ti.Next = nil
  126. // Maybe cache it for later
  127. if tw.itemsCached < timerCacheMax {
  128. ti.Next = tw.itemCache
  129. tw.itemCache = ti
  130. tw.itemsCached++
  131. }
  132. return ti.Item, true
  133. }
  134. // findWheel find the next position in the wheel for the provided timeout given the current tick
  135. func (tw *TimerWheel[T]) findWheel(timeout time.Duration) (i int) {
  136. if timeout < tw.tickDuration {
  137. // Can't track anything below the set resolution
  138. timeout = tw.tickDuration
  139. } else if timeout > tw.wheelDuration {
  140. // We aren't handling timeouts greater than the wheels duration
  141. timeout = tw.wheelDuration
  142. }
  143. // Find the next highest, rounding up
  144. tick := int(((timeout - 1) / tw.tickDuration) + 1)
  145. // Add another tick since the current tick may almost be over then map it to the wheel from our
  146. // current position
  147. tick += tw.current + 1
  148. if tick >= tw.wheelLen {
  149. tick -= tw.wheelLen
  150. }
  151. return tick
  152. }
  153. // Advance will move the wheel forward by the appropriate number of ticks for the provided time and all items
  154. // passed over will be moved to the expired list. Calling Purge is necessary to remove them entirely.
  155. func (tw *TimerWheel[T]) Advance(now time.Time) {
  156. if tw.lastTick == nil {
  157. tw.lastTick = &now
  158. }
  159. // We want to round down
  160. ticks := int(now.Sub(*tw.lastTick) / tw.tickDuration)
  161. adv := ticks
  162. if ticks > tw.wheelLen {
  163. ticks = tw.wheelLen
  164. }
  165. for i := 0; i < ticks; i++ {
  166. tw.current++
  167. if tw.current >= tw.wheelLen {
  168. tw.current = 0
  169. }
  170. if tw.wheel[tw.current].Head != nil {
  171. // We need to append the expired items as to not starve evicting the oldest ones
  172. if tw.expired.Tail == nil {
  173. tw.expired.Head = tw.wheel[tw.current].Head
  174. tw.expired.Tail = tw.wheel[tw.current].Tail
  175. } else {
  176. tw.expired.Tail.Next = tw.wheel[tw.current].Head
  177. tw.expired.Tail = tw.wheel[tw.current].Tail
  178. }
  179. tw.wheel[tw.current].Head = nil
  180. tw.wheel[tw.current].Tail = nil
  181. }
  182. }
  183. // Advance the tick based on duration to avoid losing some accuracy
  184. newTick := tw.lastTick.Add(tw.tickDuration * time.Duration(adv))
  185. tw.lastTick = &newTick
  186. }
  187. func (lw *LockingTimerWheel[T]) Add(v T, timeout time.Duration) *TimeoutItem[T] {
  188. lw.m.Lock()
  189. defer lw.m.Unlock()
  190. return lw.t.Add(v, timeout)
  191. }
  192. func (lw *LockingTimerWheel[T]) Purge() (T, bool) {
  193. lw.m.Lock()
  194. defer lw.m.Unlock()
  195. return lw.t.Purge()
  196. }
  197. func (lw *LockingTimerWheel[T]) Advance(now time.Time) {
  198. lw.m.Lock()
  199. defer lw.m.Unlock()
  200. lw.t.Advance(now)
  201. }