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  }