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  }