github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/quotapool/quotapool.go (about)

     1  // Copyright 2019 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  // Package quotapool provides an abstract implementation of a pool of resources
    12  // to be distributed among concurrent clients.
    13  //
    14  // The library also offers a concrete implementation of such a quota pool for
    15  // single-dimension integer quota. This IntPool acts like a weighted semaphore
    16  // that additionally offers FIFO ordering for serving requests.
    17  package quotapool
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    26  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    27  )
    28  
    29  // TODO(ajwerner): provide option to limit the maximum queue size.
    30  // TODO(ajwerner): provide mechanism to collect metrics.
    31  
    32  // Resource is an interface that represents a quantity which is being
    33  // pooled and allocated. It is any quantity that can be subdivided and
    34  // combined.
    35  //
    36  // This library does not provide any concrete implementations of Resource but
    37  // internally the *IntAlloc is used as a resource.
    38  type Resource interface {
    39  
    40  	// Merge combines other into the current resource.
    41  	// After a Resource (other) is passed to Merge, the QuotaPool will never use
    42  	// that Resource again. This behavior allows clients to pool instances of
    43  	// Resources by creating Resource during Acquisition and destroying them in
    44  	// Merge.
    45  	Merge(other Resource)
    46  }
    47  
    48  // Request is an interface used to acquire quota from the pool.
    49  // Request is responsible for subdividing a resource into the portion which is
    50  // retained when the Request is fulfilled and the remainder.
    51  type Request interface {
    52  
    53  	// Acquire decides whether a Request can be fulfilled by a given quantity of
    54  	// Resource.
    55  	//
    56  	// If it is not fulfilled it must not modify or retain the passed alloc.
    57  	// If it is fulfilled, it should return any portion of the Alloc it does
    58  	// not intend to use.
    59  	//
    60  	// It is up to the implementer to decide if it makes sense to return a
    61  	// zero-valued, non-nil Resource or nil as unused when acquiring all of the
    62  	// passed Resource. If nil is returned and there is a notion of acquiring a
    63  	// zero-valued Resource unit from the pool then those acquisitions may need to
    64  	// wait until the pool is non-empty before proceeding. Those zero-valued
    65  	// acquisitions will still need to wait to be at the front of the queue. It
    66  	// may make sense for implementers to special case zero-valued acquisitions
    67  	// entirely as IntPool does.
    68  	Acquire(context.Context, Resource) (fulfilled bool, unused Resource)
    69  
    70  	// ShouldWait indicates whether this request should be queued. If this method
    71  	// returns false and there is insufficient capacity in the pool when the
    72  	// request is queued then ErrNotEnoughQuota will be returned from calls to
    73  	// Acquire.
    74  	ShouldWait() bool
    75  }
    76  
    77  // ErrClosed is returned from Acquire after Close has been called.
    78  type ErrClosed struct {
    79  	poolName string
    80  	reason   string
    81  }
    82  
    83  // Error implements error.
    84  func (ec *ErrClosed) Error() string {
    85  	return fmt.Sprintf("%s pool closed: %s", ec.poolName, ec.reason)
    86  }
    87  
    88  // QuotaPool is an abstract implementation of a pool that stores some unit of
    89  // Resource. The basic idea is that it allows requests to acquire a quantity of
    90  // Resource from the pool in FIFO order in a way that interacts well with
    91  // context cancelation.
    92  type QuotaPool struct {
    93  	config
    94  
    95  	// name is used for logging purposes and is passed to functions used to report
    96  	// acquistions or slow acqusitions.
    97  	name string
    98  
    99  	// Ongoing acquisitions listen on done which is closed when the quota
   100  	// pool is closed (see QuotaPool.Close).
   101  	done chan struct{}
   102  
   103  	// closeErr is populated with a non-nil error when Close is called.
   104  	closeErr *ErrClosed
   105  
   106  	mu struct {
   107  		syncutil.Mutex
   108  
   109  		// quota stores the current quantity of quota available in the pool.
   110  		quota Resource
   111  
   112  		// We service quota acquisitions in a first come, first serve basis. This
   113  		// is done in order to prevent starvations of large acquisitions by a
   114  		// continuous stream of smaller ones. Acquisitions 'register' themselves
   115  		// for a notification that indicates they're now first in line. This is
   116  		// done by appending to the queue the channel they will then wait
   117  		// on. If a goroutine no longer needs to be notified, i.e. their
   118  		// acquisition context has been canceled, the goroutine is responsible for
   119  		// blocking subsequent notifications to the channel by filling up the
   120  		// channel buffer.
   121  		q notifyQueue
   122  
   123  		// numCanceled is the number of members of q which have been canceled.
   124  		// It is used to determine the current number of active waiters in the queue
   125  		// which is q.len() less this value.
   126  		numCanceled int
   127  
   128  		// closed is set to true when the quota pool is closed (see
   129  		// QuotaPool.Close).
   130  		closed bool
   131  	}
   132  }
   133  
   134  // New returns a new quota pool initialized with a given quota. The quota
   135  // is capped at this amount, meaning that callers may return more quota than they
   136  // acquired without ever making more than the quota capacity available.
   137  func New(name string, initialResource Resource, options ...Option) *QuotaPool {
   138  	qp := &QuotaPool{
   139  		name: name,
   140  		done: make(chan struct{}),
   141  	}
   142  	for _, o := range options {
   143  		o.apply(&qp.config)
   144  	}
   145  	qp.mu.quota = initialResource
   146  	initializeNotifyQueue(&qp.mu.q)
   147  	return qp
   148  }
   149  
   150  // ApproximateQuota will report approximately the amount of quota available
   151  // in the pool to f. The provided Resource must not be mutated.
   152  func (qp *QuotaPool) ApproximateQuota(f func(Resource)) {
   153  	qp.mu.Lock()
   154  	defer qp.mu.Unlock()
   155  	f(qp.mu.quota)
   156  }
   157  
   158  // Len returns the current length of the queue for this QuotaPool.
   159  func (qp *QuotaPool) Len() int {
   160  	qp.mu.Lock()
   161  	defer qp.mu.Unlock()
   162  	return int(qp.mu.q.len) - qp.mu.numCanceled
   163  }
   164  
   165  // Close signals to all ongoing and subsequent acquisitions that they are
   166  // free to return to their callers. They will receive an *ErrClosed which
   167  // contains this reason.
   168  //
   169  // Safe for concurrent use.
   170  func (qp *QuotaPool) Close(reason string) {
   171  	qp.mu.Lock()
   172  	defer qp.mu.Unlock()
   173  	if qp.mu.closed {
   174  		return
   175  	}
   176  	qp.mu.closed = true
   177  	qp.closeErr = &ErrClosed{
   178  		poolName: qp.name,
   179  		reason:   reason,
   180  	}
   181  	close(qp.done)
   182  }
   183  
   184  // Add adds the provided Alloc back to the pool. The value will be merged with
   185  // the existing resources in the QuotaPool if there are any.
   186  //
   187  // Safe for concurrent use.
   188  func (qp *QuotaPool) Add(v Resource) {
   189  	qp.mu.Lock()
   190  	defer qp.mu.Unlock()
   191  	qp.addLocked(v)
   192  }
   193  
   194  func (qp *QuotaPool) addLocked(r Resource) {
   195  	if qp.mu.quota != nil {
   196  		r.Merge(qp.mu.quota)
   197  	}
   198  	qp.mu.quota = r
   199  	// Notify the head of the queue if there is one waiting.
   200  	if n := qp.mu.q.peek(); n != nil && n.c != nil {
   201  		select {
   202  		case n.c <- struct{}{}:
   203  		default:
   204  		}
   205  	}
   206  }
   207  
   208  // chanSyncPool is used to pool allocations of the channels used to notify
   209  // goroutines waiting in Acquire.
   210  var chanSyncPool = sync.Pool{
   211  	New: func() interface{} { return make(chan struct{}, 1) },
   212  }
   213  
   214  // Acquire attempts to fulfill the Request with Resource from the qp.
   215  // Requests are serviced in a FIFO order; only a single request is ever
   216  // being offered resources at a time. A Request will be offered the pool's
   217  // current quantity of Resource until it is fulfilled or its context is
   218  // canceled.
   219  //
   220  // Safe for concurrent use.
   221  func (qp *QuotaPool) Acquire(ctx context.Context, r Request) (err error) {
   222  	// Set up onAcquisition if we have one.
   223  	start := timeutil.Now()
   224  	if qp.config.onAcquisition != nil {
   225  		defer func() {
   226  			if err == nil {
   227  				qp.config.onAcquisition(ctx, qp.name, r, start)
   228  			}
   229  		}()
   230  	}
   231  	// Attempt to acquire quota on the fast path.
   232  	fulfilled, n, err := qp.acquireFastPath(ctx, r)
   233  	if fulfilled || err != nil {
   234  		return err
   235  	}
   236  	// Set up the infrastructure to report slow requests.
   237  	var slowTimer *timeutil.Timer
   238  	var slowTimerC <-chan time.Time
   239  	if qp.onSlowAcquisition != nil {
   240  		slowTimer = timeutil.NewTimer()
   241  		defer slowTimer.Stop()
   242  		// Intentionally reset only once, for we care more about the select duration in
   243  		// goroutine profiles than periodic logging.
   244  		slowTimer.Reset(qp.slowAcquisitionThreshold)
   245  		slowTimerC = slowTimer.C
   246  	}
   247  	for {
   248  		select {
   249  		case <-slowTimerC:
   250  			slowTimer.Read = true
   251  			slowTimerC = nil
   252  			defer qp.onSlowAcquisition(ctx, qp.name, r, start)()
   253  			continue
   254  		case <-n.c:
   255  			if fulfilled := qp.tryAcquireOnNotify(ctx, r, n); fulfilled {
   256  				return nil
   257  			}
   258  		case <-ctx.Done():
   259  			qp.cleanupOnCancel(n)
   260  			return ctx.Err()
   261  		case <-qp.done:
   262  			// We don't need to 'unregister' ourselves as in the case when the
   263  			// context is canceled. In fact, we want others waiters to only
   264  			// receive on qp.done and signaling them would work against that.
   265  			return qp.closeErr // always non-nil when qp.done is closed
   266  		}
   267  	}
   268  }
   269  
   270  // acquireFastPath attempts to acquire quota if nobody is waiting and returns a
   271  // notifyee if the request is not immediately fulfilled.
   272  func (qp *QuotaPool) acquireFastPath(
   273  	ctx context.Context, r Request,
   274  ) (fulfilled bool, _ *notifyee, _ error) {
   275  	qp.mu.Lock()
   276  	defer qp.mu.Unlock()
   277  	if qp.mu.closed {
   278  		return false, nil, qp.closeErr
   279  	}
   280  	if qp.mu.q.len == 0 {
   281  		if fulfilled, unused := r.Acquire(ctx, qp.mu.quota); fulfilled {
   282  			qp.mu.quota = unused
   283  			return true, nil, nil
   284  		}
   285  	}
   286  	if !r.ShouldWait() {
   287  		return false, nil, ErrNotEnoughQuota
   288  	}
   289  	c := chanSyncPool.Get().(chan struct{})
   290  	return false, qp.mu.q.enqueue(c), nil
   291  }
   292  
   293  func (qp *QuotaPool) tryAcquireOnNotify(
   294  	ctx context.Context, r Request, n *notifyee,
   295  ) (fulfilled bool) {
   296  	// Release the notify channel back into the sync pool if we're fulfilled.
   297  	// Capture nc's value because it's not safe to avoid a race accessing n after
   298  	// it has been released back to the notifyQueue.
   299  	defer func(nc chan struct{}) {
   300  		if fulfilled {
   301  			chanSyncPool.Put(nc)
   302  		}
   303  	}(n.c)
   304  
   305  	qp.mu.Lock()
   306  	defer qp.mu.Unlock()
   307  	// Make sure nobody already notified us again between the last receive and grabbing
   308  	// the mutex.
   309  	if len(n.c) > 0 {
   310  		<-n.c
   311  	}
   312  	var unused Resource
   313  	if fulfilled, unused = r.Acquire(ctx, qp.mu.quota); fulfilled {
   314  		n.c = nil
   315  		qp.mu.quota = unused
   316  		qp.notifyNextLocked()
   317  	}
   318  	return fulfilled
   319  }
   320  
   321  func (qp *QuotaPool) cleanupOnCancel(n *notifyee) {
   322  	// No matter what, we're going to want to put our notify channel back in to
   323  	// the sync pool. Note that this defer call evaluates n.c here and is not
   324  	// affected by later code that sets n.c to nil.
   325  	defer chanSyncPool.Put(n.c)
   326  
   327  	qp.mu.Lock()
   328  	defer qp.mu.Unlock()
   329  
   330  	// It we're not the head, prevent ourselves from being notified and move
   331  	// along.
   332  	if n != qp.mu.q.peek() {
   333  		n.c = nil
   334  		qp.mu.numCanceled++
   335  		return
   336  	}
   337  
   338  	// If we're the head, make sure nobody already notified us before we notify the
   339  	// next waiting notifyee.
   340  	if len(n.c) > 0 {
   341  		<-n.c
   342  	}
   343  	qp.notifyNextLocked()
   344  }
   345  
   346  // notifyNextLocked notifies the waiting acquisition goroutine next in line (if
   347  // any). It requires that qp.mu.Mutex is held.
   348  func (qp *QuotaPool) notifyNextLocked() {
   349  	// Pop ourselves off the front of the queue.
   350  	qp.mu.q.dequeue()
   351  	// We traverse until we find a goroutine waiting to be notified, notify the
   352  	// goroutine and truncate our queue to ensure the said goroutine is at the
   353  	// head of the queue. Normally the next lined up waiter is the one waiting for
   354  	// notification, but if others behind us have also gotten their context
   355  	// canceled, they will leave behind notifyees with nil channels that we skip
   356  	// below.
   357  	//
   358  	// If we determine there are no goroutines waiting, we simply truncate the
   359  	// queue to reflect this.
   360  	for n := qp.mu.q.peek(); n != nil; n = qp.mu.q.peek() {
   361  		if n.c == nil {
   362  			qp.mu.numCanceled--
   363  			qp.mu.q.dequeue()
   364  			continue
   365  		}
   366  		n.c <- struct{}{}
   367  		break
   368  	}
   369  }