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 }