github.com/benz9527/xboot@v0.0.0-20240504061247-c23f15593274/lib/queue/delay_queue.go (about) 1 package queue 2 3 import ( 4 "context" 5 "log/slog" 6 "sync" 7 "sync/atomic" 8 "time" 9 10 "github.com/benz9527/xboot/lib/infra" 11 ) 12 13 type dqItem[E comparable] struct { 14 PQItem[E] 15 } 16 17 func (item *dqItem[E]) Expiration() int64 { 18 return item.Priority() 19 } 20 21 func NewDelayQueueItem[E comparable](val E, exp int64) DQItem[E] { 22 return &dqItem[E]{ 23 PQItem: NewPriorityQueueItem[E](val, exp), 24 } 25 } 26 27 type sleepEnum = int32 28 29 const ( 30 wokeUp sleepEnum = iota 31 fallAsleep 32 ) 33 34 type ArrayDelayQueue[E comparable] struct { 35 pq PriorityQueue[E] 36 itemCounter atomic.Int64 37 workCtx context.Context 38 lock *sync.Mutex 39 exclusion *sync.Mutex // Avoid multiple poll 40 wakeUpC chan struct{} 41 waitForNextExpItemC chan struct{} 42 sleeping int32 43 } 44 45 func (dq *ArrayDelayQueue[E]) popIfExpired(expiredBoundary int64) (item ReadOnlyPQItem[E], deltaMs int64) { 46 if dq.pq.Len() == 0 { 47 return nil, 0 48 } 49 50 item = dq.pq.Peek() 51 // priority as expiration 52 exp := item.Priority() 53 if exp > expiredBoundary { 54 // not matched 55 return nil, exp - expiredBoundary 56 } 57 dq.lock.Lock() 58 item = dq.pq.Pop() 59 dq.lock.Unlock() 60 return item, 0 61 } 62 63 func (dq *ArrayDelayQueue[E]) Offer(item E, expiration int64) { 64 e := NewDelayQueueItem[E](item, expiration) 65 dq.lock.Lock() 66 dq.pq.Push(e) 67 dq.lock.Unlock() 68 if e.Index() == 0 { 69 // Highest priority item, wake up the consumer 70 if atomic.CompareAndSwapInt32(&dq.sleeping, fallAsleep, wokeUp) { 71 dq.wakeUpC <- struct{}{} 72 } 73 } 74 dq.itemCounter.Add(1) 75 } 76 77 func (dq *ArrayDelayQueue[E]) poll(nowFn func() int64, sender infra.SendOnlyChannel[E]) { 78 var timer *time.Timer 79 defer func() { 80 // FIXME recover defer execution order 81 if err := recover(); err != nil { 82 slog.Error("delay queue panic recover", "error", err) 83 } 84 // before exit 85 atomic.StoreInt32(&dq.sleeping, wokeUp) 86 87 if timer != nil { 88 timer.Stop() 89 timer = nil 90 } 91 }() 92 for { 93 now := nowFn() 94 item, deltaMs := dq.popIfExpired(now) 95 if item == nil { 96 // No expired item in the queue 97 // 1. without any item in the queue 98 // 2. all items in the queue are not expired 99 atomic.StoreInt32(&dq.sleeping, fallAsleep) 100 101 if deltaMs == 0 { 102 // Queue is empty, waiting for new item 103 select { 104 case <-dq.workCtx.Done(): 105 return 106 case <-dq.wakeUpC: 107 // Waiting for an immediately executed item 108 continue 109 } 110 } else if deltaMs > 0 { 111 if timer != nil { 112 timer.Stop() 113 } 114 // Avoid to use time.After(), it will create a new timer every time 115 // what's worse, the underlay timer will not be GC. 116 // Asynchronous timer. 117 timer = time.AfterFunc(time.Duration(deltaMs)*time.Millisecond, func() { 118 if atomic.SwapInt32(&dq.sleeping, wokeUp) == fallAsleep { 119 dq.waitForNextExpItemC <- struct{}{} 120 } 121 }) 122 123 select { 124 case <-dq.workCtx.Done(): 125 return 126 case <-dq.wakeUpC: 127 continue 128 case <-dq.waitForNextExpItemC: 129 // Waiting for this item to be expired 130 if timer != nil { 131 timer.Stop() 132 timer = nil 133 } 134 continue 135 } 136 } 137 } 138 139 // This is an expired item 140 if timer != nil { 141 // Woke up, stop the wait next expired timer 142 timer.Stop() 143 timer = nil 144 } 145 146 select { 147 case <-dq.workCtx.Done(): 148 return 149 default: 150 // Waiting for the consumer to consume this item 151 // If an external channel is closed, here will be panic 152 if !sender.IsClosed() { 153 if err := sender.Send(item.Value()); err != nil { 154 return 155 } 156 dq.itemCounter.Add(-1) 157 } else { 158 return 159 } 160 } 161 } 162 } 163 164 func (dq *ArrayDelayQueue[E]) Len() int64 { 165 if dq == nil { 166 return 0 167 } 168 return dq.itemCounter.Load() 169 } 170 171 func NewArrayDelayQueue[E comparable](ctx context.Context, capacity int) DelayQueue[E] { 172 dq := &ArrayDelayQueue[E]{ 173 pq: NewArrayPriorityQueue[E]( 174 WithArrayPriorityQueueEnableThreadSafe[E](), 175 WithArrayPriorityQueueCapacity[E](capacity), 176 ), 177 workCtx: ctx, 178 lock: &sync.Mutex{}, 179 exclusion: &sync.Mutex{}, 180 wakeUpC: make(chan struct{}), 181 waitForNextExpItemC: make(chan struct{}), 182 } 183 return dq 184 }