github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/internal/libs/queue/queue.go (about) 1 // Package queue implements a dynamic FIFO queue with a fixed upper bound 2 // and a flexible quota mechanism to handle bursty load. 3 package queue 4 5 import ( 6 "context" 7 "errors" 8 "sync" 9 ) 10 11 var ( 12 // ErrQueueFull is returned by the Add method of a queue when the queue has 13 // reached its hard capacity limit. 14 ErrQueueFull = errors.New("queue is full") 15 16 // ErrNoCredit is returned by the Add method of a queue when the queue has 17 // exceeded its soft quota and there is insufficient burst credit. 18 ErrNoCredit = errors.New("insufficient burst credit") 19 20 // ErrQueueClosed is returned by the Add method of a closed queue, and by 21 // the Wait method of a closed empty queue. 22 ErrQueueClosed = errors.New("queue is closed") 23 24 // Sentinel errors reported by the New constructor. 25 errHardLimit = errors.New("hard limit must be > 0 and ≥ soft quota") 26 errBurstCredit = errors.New("burst credit must be non-negative") 27 ) 28 29 // A Queue is a limited-capacity FIFO queue of arbitrary data items. 30 // 31 // A queue has a soft quota and a hard limit on the number of items that may be 32 // contained in the queue. Adding items in excess of the hard limit will fail 33 // unconditionally. 34 // 35 // For items in excess of the soft quota, a credit system applies: Each queue 36 // maintains a burst credit score. Adding an item in excess of the soft quota 37 // costs 1 unit of burst credit. If there is not enough burst credit, the add 38 // will fail. 39 // 40 // The initial burst credit is assigned when the queue is constructed. Removing 41 // items from the queue adds additional credit if the resulting queue length is 42 // less than the current soft quota. Burst credit is capped by the hard limit. 43 // 44 // A Queue is safe for concurrent use by multiple goroutines. 45 type Queue struct { 46 mu sync.Mutex // protects the fields below 47 48 softQuota int // adjusted dynamically (see Add, Remove) 49 hardLimit int // fixed for the lifespan of the queue 50 queueLen int // number of entries in the queue list 51 credit float64 // current burst credit 52 53 closed bool 54 nempty *sync.Cond 55 back *entry 56 front *entry 57 58 // The queue is singly-linked. Front points to the sentinel and back points 59 // to the newest entry. The oldest entry is front.link if it exists. 60 } 61 62 // New constructs a new empty queue with the specified options. It reports an 63 // error if any of the option values are invalid. 64 func New(opts Options) (*Queue, error) { 65 if opts.HardLimit <= 0 || opts.HardLimit < opts.SoftQuota { 66 return nil, errHardLimit 67 } 68 if opts.BurstCredit < 0 { 69 return nil, errBurstCredit 70 } 71 if opts.SoftQuota <= 0 { 72 opts.SoftQuota = opts.HardLimit 73 } 74 if opts.BurstCredit == 0 { 75 opts.BurstCredit = float64(opts.SoftQuota) 76 } 77 sentinel := new(entry) 78 q := &Queue{ 79 softQuota: opts.SoftQuota, 80 hardLimit: opts.HardLimit, 81 credit: opts.BurstCredit, 82 back: sentinel, 83 front: sentinel, 84 } 85 q.nempty = sync.NewCond(&q.mu) 86 return q, nil 87 } 88 89 // Add adds item to the back of the queue. It reports an error and does not 90 // enqueue the item if the queue is full or closed, or if it exceeds its soft 91 // quota and there is not enough burst credit. 92 func (q *Queue) Add(item interface{}) error { 93 q.mu.Lock() 94 defer q.mu.Unlock() 95 96 if q.closed { 97 return ErrQueueClosed 98 } 99 100 if q.queueLen >= q.softQuota { 101 if q.queueLen == q.hardLimit { 102 return ErrQueueFull 103 } else if q.credit < 1 { 104 return ErrNoCredit 105 } 106 107 // Successfully exceeding the soft quota deducts burst credit and raises 108 // the soft quota. This has the effect of reducing the credit cap and the 109 // amount of credit given for removing items to better approximate the 110 // rate at which the consumer is servicing the queue. 111 q.credit-- 112 q.softQuota = q.queueLen + 1 113 } 114 e := &entry{item: item} 115 q.back.link = e 116 q.back = e 117 q.queueLen++ 118 if q.queueLen == 1 { // was empty 119 q.nempty.Signal() 120 } 121 return nil 122 } 123 124 // Remove removes and returns the frontmost (oldest) item in the queue and 125 // reports whether an item was available. If the queue is empty, Remove 126 // returns nil, false. 127 func (q *Queue) Remove() (interface{}, bool) { 128 q.mu.Lock() 129 defer q.mu.Unlock() 130 131 if q.queueLen == 0 { 132 return nil, false 133 } 134 return q.popFront(), true 135 } 136 137 // Wait blocks until q is non-empty or closed, and then returns the frontmost 138 // (oldest) item from the queue. If ctx ends before an item is available, Wait 139 // returns a nil value and a context error. If the queue is closed while it is 140 // still empty, Wait returns nil, ErrQueueClosed. 141 func (q *Queue) Wait(ctx context.Context) (interface{}, error) { 142 // If the context terminates, wake the waiter. 143 ctx, cancel := context.WithCancel(ctx) 144 defer cancel() 145 go func() { <-ctx.Done(); q.nempty.Broadcast() }() 146 147 q.mu.Lock() 148 defer q.mu.Unlock() 149 150 for q.queueLen == 0 { 151 if q.closed { 152 return nil, ErrQueueClosed 153 } 154 select { 155 case <-ctx.Done(): 156 return nil, ctx.Err() 157 default: 158 q.nempty.Wait() 159 } 160 } 161 return q.popFront(), nil 162 } 163 164 // Close closes the queue. After closing, any further Add calls will report an 165 // error, but items that were added to the queue prior to closing will still be 166 // available for Remove and Wait. Wait will report an error without blocking if 167 // it is called on a closed, empty queue. 168 func (q *Queue) Close() error { 169 q.mu.Lock() 170 defer q.mu.Unlock() 171 q.closed = true 172 q.nempty.Broadcast() 173 return nil 174 } 175 176 // popFront removes the frontmost item of q and returns its value after 177 // updating quota and credit settings. 178 // 179 // Preconditions: The caller holds q.mu and q is not empty. 180 func (q *Queue) popFront() interface{} { 181 e := q.front.link 182 q.front.link = e.link 183 if e == q.back { 184 q.back = q.front 185 } 186 q.queueLen-- 187 188 if q.queueLen < q.softQuota { 189 // Successfully removing items from the queue below half the soft quota 190 // lowers the soft quota. This has the effect of increasing the credit cap 191 // and the amount of credit given for removing items to better approximate 192 // the rate at which the consumer is servicing the queue. 193 if q.softQuota > 1 && q.queueLen < q.softQuota/2 { 194 q.softQuota-- 195 } 196 197 // Give credit for being below the soft quota. Note we do this after 198 // adjusting the quota so the credit reflects the item we just removed. 199 q.credit += float64(q.softQuota-q.queueLen) / float64(q.softQuota) 200 if cap := float64(q.hardLimit - q.softQuota); q.credit > cap { 201 q.credit = cap 202 } 203 } 204 205 return e.item 206 } 207 208 // Options are the initial settings for a Queue. 209 type Options struct { 210 // The maximum number of items the queue will ever be permitted to hold. 211 // This value must be positive, and greater than or equal to SoftQuota. The 212 // hard limit is fixed and does not change as the queue is used. 213 // 214 // The hard limit should be chosen to exceed the largest burst size expected 215 // under normal operating conditions. 216 HardLimit int 217 218 // The initial expected maximum number of items the queue should contain on 219 // an average workload. If this value is zero, it is initialized to the hard 220 // limit. The soft quota is adjusted from the initial value dynamically as 221 // the queue is used. 222 SoftQuota int 223 224 // The initial burst credit score. This value must be greater than or equal 225 // to zero. If it is zero, the soft quota is used. 226 BurstCredit float64 227 } 228 229 type entry struct { 230 item interface{} 231 link *entry 232 }