|
|
- // Package queue implements a dynamic FIFO queue with a fixed upper bound
- // and a flexible quota mechanism to handle bursty load.
- package queue
-
- import (
- "context"
- "errors"
- "sync"
- )
-
- var (
- // ErrQueueFull is returned by the Add method of a queue when the queue has
- // reached its hard capacity limit.
- ErrQueueFull = errors.New("queue is full")
-
- // ErrNoCredit is returned by the Add method of a queue when the queue has
- // exceeded its soft quota and there is insufficient burst credit.
- ErrNoCredit = errors.New("insufficient burst credit")
-
- // ErrQueueClosed is returned by the Add method of a closed queue, and by
- // the Wait method of a closed empty queue.
- ErrQueueClosed = errors.New("queue is closed")
-
- // Sentinel errors reported by the New constructor.
- errHardLimit = errors.New("hard limit must be > 0 and ≥ soft quota")
- errBurstCredit = errors.New("burst credit must be non-negative")
- )
-
- // A Queue is a limited-capacity FIFO queue of arbitrary data items.
- //
- // A queue has a soft quota and a hard limit on the number of items that may be
- // contained in the queue. Adding items in excess of the hard limit will fail
- // unconditionally.
- //
- // For items in excess of the soft quota, a credit system applies: Each queue
- // maintains a burst credit score. Adding an item in excess of the soft quota
- // costs 1 unit of burst credit. If there is not enough burst credit, the add
- // will fail.
- //
- // The initial burst credit is assigned when the queue is constructed. Removing
- // items from the queue adds additional credit if the resulting queue length is
- // less than the current soft quota. Burst credit is capped by the hard limit.
- //
- // A Queue is safe for concurrent use by multiple goroutines.
- type Queue struct {
- mu sync.Mutex // protects the fields below
-
- softQuota int // adjusted dynamically (see Add, Remove)
- hardLimit int // fixed for the lifespan of the queue
- queueLen int // number of entries in the queue list
- credit float64 // current burst credit
-
- closed bool
- nempty *sync.Cond
- back *entry
- front *entry
-
- // The queue is singly-linked. Front points to the sentinel and back points
- // to the newest entry. The oldest entry is front.link if it exists.
- }
-
- // New constructs a new empty queue with the specified options. It reports an
- // error if any of the option values are invalid.
- func New(opts Options) (*Queue, error) {
- if opts.HardLimit <= 0 || opts.HardLimit < opts.SoftQuota {
- return nil, errHardLimit
- }
- if opts.BurstCredit < 0 {
- return nil, errBurstCredit
- }
- if opts.SoftQuota <= 0 {
- opts.SoftQuota = opts.HardLimit
- }
- if opts.BurstCredit == 0 {
- opts.BurstCredit = float64(opts.SoftQuota)
- }
- sentinel := new(entry)
- q := &Queue{
- softQuota: opts.SoftQuota,
- hardLimit: opts.HardLimit,
- credit: opts.BurstCredit,
- back: sentinel,
- front: sentinel,
- }
- q.nempty = sync.NewCond(&q.mu)
- return q, nil
- }
-
- // Add adds item to the back of the queue. It reports an error and does not
- // enqueue the item if the queue is full or closed, or if it exceeds its soft
- // quota and there is not enough burst credit.
- func (q *Queue) Add(item interface{}) error {
- q.mu.Lock()
- defer q.mu.Unlock()
-
- if q.closed {
- return ErrQueueClosed
- }
-
- if q.queueLen >= q.softQuota {
- if q.queueLen == q.hardLimit {
- return ErrQueueFull
- } else if q.credit < 1 {
- return ErrNoCredit
- }
-
- // Successfully exceeding the soft quota deducts burst credit and raises
- // the soft quota. This has the effect of reducing the credit cap and the
- // amount of credit given for removing items to better approximate the
- // rate at which the consumer is servicing the queue.
- q.credit--
- q.softQuota = q.queueLen + 1
- }
- e := &entry{item: item}
- q.back.link = e
- q.back = e
- q.queueLen++
- if q.queueLen == 1 { // was empty
- q.nempty.Signal()
- }
- return nil
- }
-
- // Remove removes and returns the frontmost (oldest) item in the queue and
- // reports whether an item was available. If the queue is empty, Remove
- // returns nil, false.
- func (q *Queue) Remove() (interface{}, bool) {
- q.mu.Lock()
- defer q.mu.Unlock()
-
- if q.queueLen == 0 {
- return nil, false
- }
- return q.popFront(), true
- }
-
- // Wait blocks until q is non-empty or closed, and then returns the frontmost
- // (oldest) item from the queue. If ctx ends before an item is available, Wait
- // returns a nil value and a context error. If the queue is closed while it is
- // still empty, Wait returns nil, ErrQueueClosed.
- func (q *Queue) Wait(ctx context.Context) (interface{}, error) {
- // If the context terminates, wake the waiter.
- ctx, cancel := context.WithCancel(ctx)
- defer cancel()
- go func() { <-ctx.Done(); q.nempty.Broadcast() }()
-
- q.mu.Lock()
- defer q.mu.Unlock()
-
- for q.queueLen == 0 {
- if q.closed {
- return nil, ErrQueueClosed
- }
- select {
- case <-ctx.Done():
- return nil, ctx.Err()
- default:
- q.nempty.Wait()
- }
- }
- return q.popFront(), nil
- }
-
- // Close closes the queue. After closing, any further Add calls will report an
- // error, but items that were added to the queue prior to closing will still be
- // available for Remove and Wait. Wait will report an error without blocking if
- // it is called on a closed, empty queue.
- func (q *Queue) Close() error {
- q.mu.Lock()
- defer q.mu.Unlock()
- q.closed = true
- q.nempty.Broadcast()
- return nil
- }
-
- // popFront removes the frontmost item of q and returns its value after
- // updating quota and credit settings.
- //
- // Preconditions: The caller holds q.mu and q is not empty.
- func (q *Queue) popFront() interface{} {
- e := q.front.link
- q.front.link = e.link
- if e == q.back {
- q.back = q.front
- }
- q.queueLen--
-
- if q.queueLen < q.softQuota {
- // Successfully removing items from the queue below half the soft quota
- // lowers the soft quota. This has the effect of increasing the credit cap
- // and the amount of credit given for removing items to better approximate
- // the rate at which the consumer is servicing the queue.
- if q.softQuota > 1 && q.queueLen < q.softQuota/2 {
- q.softQuota--
- }
-
- // Give credit for being below the soft quota. Note we do this after
- // adjusting the quota so the credit reflects the item we just removed.
- q.credit += float64(q.softQuota-q.queueLen) / float64(q.softQuota)
- if cap := float64(q.hardLimit - q.softQuota); q.credit > cap {
- q.credit = cap
- }
- }
-
- return e.item
- }
-
- // Options are the initial settings for a Queue.
- type Options struct {
- // The maximum number of items the queue will ever be permitted to hold.
- // This value must be positive, and greater than or equal to SoftQuota. The
- // hard limit is fixed and does not change as the queue is used.
- //
- // The hard limit should be chosen to exceed the largest burst size expected
- // under normal operating conditions.
- HardLimit int
-
- // The initial expected maximum number of items the queue should contain on
- // an average workload. If this value is zero, it is initialized to the hard
- // limit. The soft quota is adjusted from the initial value dynamically as
- // the queue is used.
- SoftQuota int
-
- // The initial burst credit score. This value must be greater than or equal
- // to zero. If it is zero, the soft quota is used.
- BurstCredit float64
- }
-
- type entry struct {
- item interface{}
- link *entry
- }
|