github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/internal/client/requestbatcher/batcher.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 requestbatcher is a library to enable easy batching of roachpb 12 // requests. 13 // 14 // Batching in general represents a tradeoff between throughput and latency. The 15 // underlying assumption being that batched operations are cheaper than an 16 // individual operation. If this is not the case for your workload, don't use 17 // this library. 18 // 19 // Batching assumes that data with the same key can be sent in a single batch. 20 // The initial implementation uses rangeID as the key explicitly to avoid 21 // creating an overly general solution without motivation but interested readers 22 // should recognize that it would be easy to extend this package to accept an 23 // arbitrary comparable key. 24 package requestbatcher 25 26 import ( 27 "container/heap" 28 "context" 29 "sync" 30 "time" 31 32 "github.com/cockroachdb/cockroach/pkg/kv" 33 "github.com/cockroachdb/cockroach/pkg/roachpb" 34 "github.com/cockroachdb/cockroach/pkg/util/contextutil" 35 "github.com/cockroachdb/cockroach/pkg/util/log" 36 "github.com/cockroachdb/cockroach/pkg/util/stop" 37 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 38 ) 39 40 // The motivating use case for this package are opportunities to perform cleanup 41 // operations in a single raft transaction rather than several. Three main 42 // opportunities are known: 43 // 44 // 1) Intent resolution 45 // 2) Txn heartbeating 46 // 3) Txn record garbage collection 47 // 48 // The first two have a relatively tight time bound expectations. In other words 49 // it would be surprising and negative for a client if operations were not sent 50 // soon after they were queued. The transaction record GC workload can be rather 51 // asynchronous. This motivates the need for some knobs to control the maximum 52 // acceptable amount of time to buffer operations before sending. 53 // Another wrinkle is dealing with the different ways in which sending a batch 54 // may fail. A batch may fail in an ambiguous way (RPC/network errors), it may 55 // fail completely (which is likely indistinguishable from the ambiguous 56 // failure) and lastly it may fail partially. Today's Sender contract is fairly 57 // ambiguous about the contract between BatchResponse inner responses and errors 58 // returned from a batch request. 59 60 // TODO(ajwerner): Do we need to consider ordering dependencies between 61 // operations? For the initial motivating use cases for this library there are 62 // no data dependencies between operations and the only key will be the guess 63 // for where an operation should go. 64 65 // TODO(ajwerner): Consider a more sophisticated mechanism to limit on maximum 66 // number of requests in flight at a time. This may ultimately lead to a need 67 // for queuing. Furthermore consider using batch time to dynamically tune the 68 // amount of time we wait. 69 70 // TODO(ajwerner): Consider filtering requests which might have been canceled 71 // before sending a batch. 72 73 // TODO(ajwerner): Consider more dynamic policies with regards to deadlines. 74 // Perhaps we want to wait no more than some percentile of the duration of 75 // historical operations and stay idle only some other percentile. For example 76 // imagine if the max delay was the 50th and the max idle was the 10th? This 77 // has a problem when much of the prior workload was say local operations and 78 // happened very rapidly. Perhaps we need to provide some bounding envelope? 79 80 // TODO(ajwerner): Consider a more general purpose interface for this package. 81 // While several interface-oriented interfaces have been explored they all felt 82 // heavy and allocation intensive. 83 84 // TODO(ajwerner): Consider providing an interface which enables a single 85 // goroutine to dispatch a number of requests destined for different ranges to 86 // the RequestBatcher which may then wait for completion of all of the requests. 87 // What is the right contract for error handling? Imagine a situation where a 88 // client has dispatched N requests and one has been sent and returns with an 89 // error while others are queued. What should happen? Should the client receive 90 // the error rapidly? Should the other requests be sent at all? Should they be 91 // filtered before sending? 92 93 // Config contains the dependencies and configuration for a Batcher. 94 type Config struct { 95 96 // Name of the batcher, used for logging, timeout errors, and the stopper. 97 Name string 98 99 // Sender can round-trip a batch. Sender must not be nil. 100 Sender kv.Sender 101 102 // Stopper controls the lifecycle of the Batcher. Stopper must not be nil. 103 Stopper *stop.Stopper 104 105 // MaxSizePerBatch is the maximum number of bytes in individual requests in a 106 // batch. If MaxSizePerBatch <= 0 then no limit is enforced. 107 MaxSizePerBatch int 108 109 // MaxMsgsPerBatch is the maximum number of messages. 110 // If MaxMsgsPerBatch <= 0 then no limit is enforced. 111 MaxMsgsPerBatch int 112 113 // MaxKeysPerBatchReq is the maximum number of keys that each batch is 114 // allowed to touch during one of its requests. If the limit is exceeded, 115 // the batch is paginated over a series of individual requests. This limit 116 // corresponds to the MaxSpanRequestKeys assigned to the Header of each 117 // request. If MaxKeysPerBatchReq <= 0 then no limit is enforced. 118 MaxKeysPerBatchReq int 119 120 // MaxWait is the maximum amount of time a message should wait in a batch 121 // before being sent. If MaxWait is <= 0 then no wait timeout is enforced. 122 // It is inadvisable to disable both MaxIdle and MaxWait. 123 MaxWait time.Duration 124 125 // MaxIdle is the amount of time a batch should wait between message additions 126 // before being sent. The idle timer allows clients to observe low latencies 127 // when throughput is low. If MaxWait is <= 0 then no wait timeout is 128 // enforced. It is inadvisable to disable both MaxIdle and MaxWait. 129 MaxIdle time.Duration 130 131 // InFlightBackpressureLimit is the number of batches in flight above which 132 // sending clients should experience backpressure. If the batcher has more 133 // requests than this in flight it will not accept new requests until the 134 // number of in flight batches is again below this threshold. This value does 135 // not limit the number of batches which may ultimately be in flight as 136 // batches which are queued to send but not yet in flight will still send. 137 // Note that values less than or equal to zero will result in the use of 138 // DefaultInFlightBackpressureLimit. 139 InFlightBackpressureLimit int 140 141 // NowFunc is used to determine the current time. It defaults to timeutil.Now. 142 NowFunc func() time.Time 143 } 144 145 const ( 146 // DefaultInFlightBackpressureLimit is the InFlightBackpressureLimit used if 147 // a zero value for that setting is passed in a Config to New. 148 // TODO(ajwerner): Justify this number. 149 DefaultInFlightBackpressureLimit = 1000 150 151 // BackpressureRecoveryFraction is the fraction of InFlightBackpressureLimit 152 // used to detect when enough in flight requests have completed such that more 153 // requests should now be accepted. A value less than 1 is chosen in order to 154 // avoid thrashing on backpressure which might ultimately defeat the purpose 155 // of the RequestBatcher. 156 backpressureRecoveryFraction = .8 157 ) 158 159 func backpressureRecoveryThreshold(limit int) int { 160 if l := int(float64(limit) * backpressureRecoveryFraction); l > 0 { 161 return l 162 } 163 return 1 // don't allow the recovery threshold to be 0 164 } 165 166 // RequestBatcher batches requests destined for a single range based on 167 // a configured batching policy. 168 type RequestBatcher struct { 169 pool pool 170 cfg Config 171 172 // sendBatchOpName is the string passed to contextutil.RunWithTimeout when 173 // sending a batch. 174 sendBatchOpName string 175 176 batches batchQueue 177 178 requestChan chan *request 179 sendDoneChan chan struct{} 180 } 181 182 // Response is exported for use with the channel-oriented SendWithChan method. 183 // At least one of Resp or Err will be populated for every sent Response. 184 type Response struct { 185 Resp roachpb.Response 186 Err error 187 } 188 189 // New creates a new RequestBatcher. 190 func New(cfg Config) *RequestBatcher { 191 validateConfig(&cfg) 192 b := &RequestBatcher{ 193 cfg: cfg, 194 pool: makePool(), 195 batches: makeBatchQueue(), 196 requestChan: make(chan *request), 197 sendDoneChan: make(chan struct{}), 198 } 199 b.sendBatchOpName = b.cfg.Name + ".sendBatch" 200 if err := cfg.Stopper.RunAsyncTask(context.Background(), b.cfg.Name, b.run); err != nil { 201 panic(err) 202 } 203 return b 204 } 205 206 func validateConfig(cfg *Config) { 207 if cfg.Stopper == nil { 208 panic("cannot construct a Batcher with a nil Stopper") 209 } else if cfg.Sender == nil { 210 panic("cannot construct a Batcher with a nil Sender") 211 } 212 if cfg.InFlightBackpressureLimit <= 0 { 213 cfg.InFlightBackpressureLimit = DefaultInFlightBackpressureLimit 214 } 215 if cfg.NowFunc == nil { 216 cfg.NowFunc = timeutil.Now 217 } 218 } 219 220 // SendWithChan sends a request with a client provided response channel. The 221 // client is responsible for ensuring that the passed respChan has a buffer at 222 // least as large as the number of responses it expects to receive. Using an 223 // insufficiently buffered channel can lead to deadlocks and unintended delays 224 // processing requests inside the RequestBatcher. 225 func (b *RequestBatcher) SendWithChan( 226 ctx context.Context, respChan chan<- Response, rangeID roachpb.RangeID, req roachpb.Request, 227 ) error { 228 select { 229 case b.requestChan <- b.pool.newRequest(ctx, rangeID, req, respChan): 230 return nil 231 case <-b.cfg.Stopper.ShouldQuiesce(): 232 return stop.ErrUnavailable 233 case <-ctx.Done(): 234 return ctx.Err() 235 } 236 } 237 238 // Send sends req as a part of a batch. An error is returned if the context 239 // is canceled before the sending of the request completes. The context with 240 // the latest deadline for a batch is used to send the underlying batch request. 241 func (b *RequestBatcher) Send( 242 ctx context.Context, rangeID roachpb.RangeID, req roachpb.Request, 243 ) (roachpb.Response, error) { 244 responseChan := b.pool.getResponseChan() 245 if err := b.SendWithChan(ctx, responseChan, rangeID, req); err != nil { 246 return nil, err 247 } 248 select { 249 case resp := <-responseChan: 250 // It's only safe to put responseChan back in the pool if it has been 251 // received from. 252 b.pool.putResponseChan(responseChan) 253 return resp.Resp, resp.Err 254 case <-b.cfg.Stopper.ShouldQuiesce(): 255 return nil, stop.ErrUnavailable 256 case <-ctx.Done(): 257 return nil, ctx.Err() 258 } 259 } 260 261 func (b *RequestBatcher) sendDone(ba *batch) { 262 b.pool.putBatch(ba) 263 select { 264 case b.sendDoneChan <- struct{}{}: 265 case <-b.cfg.Stopper.ShouldQuiesce(): 266 } 267 } 268 269 func (b *RequestBatcher) sendBatch(ctx context.Context, ba *batch) { 270 b.cfg.Stopper.RunWorker(ctx, func(ctx context.Context) { 271 defer b.sendDone(ba) 272 var br *roachpb.BatchResponse 273 send := func(ctx context.Context) error { 274 var pErr *roachpb.Error 275 if br, pErr = b.cfg.Sender.Send(ctx, ba.batchRequest(&b.cfg)); pErr != nil { 276 return pErr.GoError() 277 } 278 return nil 279 } 280 if !ba.sendDeadline.IsZero() { 281 actualSend := send 282 send = func(context.Context) error { 283 return contextutil.RunWithTimeout( 284 ctx, b.sendBatchOpName, timeutil.Until(ba.sendDeadline), actualSend) 285 } 286 } 287 // Send requests in a loop to support pagination, which may be necessary 288 // if MaxKeysPerBatchReq is set. If so, partial responses with resume 289 // spans may be returned for requests, indicating that the limit was hit 290 // before they could complete and that they should be resumed over the 291 // specified key span. Requests in the batch are neither guaranteed to 292 // be ordered nor guaranteed to be non-overlapping, so we can make no 293 // assumptions about the requests that will result in full responses 294 // (with no resume spans) vs. partial responses vs. empty responses (see 295 // the comment on roachpb.Header.MaxSpanRequestKeys). 296 // 297 // To accommodate this, we keep track of all partial responses from 298 // previous iterations. After receiving a batch of responses during an 299 // iteration, the responses are each combined with the previous response 300 // for their corresponding requests. From there, responses that have no 301 // resume spans are removed. Responses that have resume spans are 302 // updated appropriately and sent again in the next iteration. The loop 303 // proceeds until all requests have been run to completion. 304 var prevResps []roachpb.Response 305 for len(ba.reqs) > 0 { 306 err := send(ctx) 307 nextReqs, nextPrevResps := ba.reqs[:0], prevResps[:0] 308 for i, r := range ba.reqs { 309 var res Response 310 if br != nil { 311 resp := br.Responses[i].GetInner() 312 if prevResps != nil { 313 prevResp := prevResps[i] 314 if cErr := roachpb.CombineResponses(prevResp, resp); cErr != nil { 315 log.Fatalf(ctx, "%v", cErr) 316 } 317 resp = prevResp 318 } 319 if resume := resp.Header().ResumeSpan; resume != nil { 320 // Add a trimmed request to the next batch. 321 h := r.req.Header() 322 h.SetSpan(*resume) 323 r.req = r.req.ShallowCopy() 324 r.req.SetHeader(h) 325 nextReqs = append(nextReqs, r) 326 // Strip resume span from previous response and record. 327 prevH := resp.Header() 328 prevH.ResumeSpan = nil 329 prevResp := resp 330 prevResp.SetHeader(prevH) 331 nextPrevResps = append(nextPrevResps, prevResp) 332 continue 333 } 334 res.Resp = resp 335 } 336 if err != nil { 337 res.Err = err 338 } 339 b.sendResponse(r, res) 340 } 341 ba.reqs, prevResps = nextReqs, nextPrevResps 342 } 343 }) 344 } 345 346 func (b *RequestBatcher) sendResponse(req *request, resp Response) { 347 // This send should never block because responseChan is buffered. 348 req.responseChan <- resp 349 b.pool.putRequest(req) 350 } 351 352 func addRequestToBatch(cfg *Config, now time.Time, ba *batch, r *request) (shouldSend bool) { 353 // Update the deadline for the batch if this requests's deadline is later 354 // than the current latest. 355 rDeadline, rHasDeadline := r.ctx.Deadline() 356 // If this is the first request or 357 if len(ba.reqs) == 0 || 358 // there are already requests and there is a deadline and 359 (len(ba.reqs) > 0 && !ba.sendDeadline.IsZero() && 360 // this request either doesn't have a deadline or has a later deadline, 361 (!rHasDeadline || rDeadline.After(ba.sendDeadline))) { 362 // set the deadline to this request's deadline. 363 ba.sendDeadline = rDeadline 364 } 365 366 ba.reqs = append(ba.reqs, r) 367 ba.size += r.req.Size() 368 ba.lastUpdated = now 369 370 if cfg.MaxIdle > 0 { 371 ba.deadline = ba.lastUpdated.Add(cfg.MaxIdle) 372 } 373 if cfg.MaxWait > 0 { 374 waitDeadline := ba.startTime.Add(cfg.MaxWait) 375 if cfg.MaxIdle <= 0 || waitDeadline.Before(ba.deadline) { 376 ba.deadline = waitDeadline 377 } 378 } 379 return (cfg.MaxMsgsPerBatch > 0 && len(ba.reqs) >= cfg.MaxMsgsPerBatch) || 380 (cfg.MaxSizePerBatch > 0 && ba.size >= cfg.MaxSizePerBatch) 381 } 382 383 func (b *RequestBatcher) cleanup(err error) { 384 for ba := b.batches.popFront(); ba != nil; ba = b.batches.popFront() { 385 for _, r := range ba.reqs { 386 b.sendResponse(r, Response{Err: err}) 387 } 388 } 389 } 390 391 func (b *RequestBatcher) run(ctx context.Context) { 392 // Create a context to be used in sendBatch to cancel in-flight batches when 393 // this function exits. If we did not cancel in-flight requests then the 394 // Stopper might get stuck waiting for those requests to complete. 395 sendCtx, cancel := context.WithCancel(ctx) 396 defer cancel() 397 var ( 398 // inFlight tracks the number of batches currently being sent. 399 // true. 400 inFlight = 0 401 // inBackPressure indicates whether the reqChan is enabled. 402 // It becomes true when inFlight exceeds b.cfg.InFlightBackpressureLimit. 403 inBackPressure = false 404 // recoveryThreshold is the number of in flight requests below which the 405 // the inBackPressure state should exit. 406 recoveryThreshold = backpressureRecoveryThreshold(b.cfg.InFlightBackpressureLimit) 407 // reqChan consults inBackPressure to determine whether the goroutine is 408 // accepting new requests. 409 reqChan = func() <-chan *request { 410 if inBackPressure { 411 return nil 412 } 413 return b.requestChan 414 } 415 sendBatch = func(ba *batch) { 416 inFlight++ 417 if inFlight >= b.cfg.InFlightBackpressureLimit { 418 inBackPressure = true 419 } 420 b.sendBatch(sendCtx, ba) 421 } 422 handleSendDone = func() { 423 inFlight-- 424 if inFlight < recoveryThreshold { 425 inBackPressure = false 426 } 427 } 428 handleRequest = func(req *request) { 429 now := b.cfg.NowFunc() 430 ba, existsInQueue := b.batches.get(req.rangeID) 431 if !existsInQueue { 432 ba = b.pool.newBatch(now) 433 } 434 if shouldSend := addRequestToBatch(&b.cfg, now, ba, req); shouldSend { 435 if existsInQueue { 436 b.batches.remove(ba) 437 } 438 sendBatch(ba) 439 } else { 440 b.batches.upsert(ba) 441 } 442 } 443 deadline time.Time 444 timer = timeutil.NewTimer() 445 maybeSetTimer = func() { 446 var nextDeadline time.Time 447 if next := b.batches.peekFront(); next != nil { 448 nextDeadline = next.deadline 449 } 450 if !deadline.Equal(nextDeadline) || timer.Read { 451 deadline = nextDeadline 452 if !deadline.IsZero() { 453 timer.Reset(timeutil.Until(deadline)) 454 } else { 455 // Clear the current timer due to a sole batch already sent before 456 // the timer fired. 457 timer.Stop() 458 timer = timeutil.NewTimer() 459 } 460 } 461 } 462 ) 463 for { 464 select { 465 case req := <-reqChan(): 466 handleRequest(req) 467 maybeSetTimer() 468 case <-timer.C: 469 timer.Read = true 470 sendBatch(b.batches.popFront()) 471 maybeSetTimer() 472 case <-b.sendDoneChan: 473 handleSendDone() 474 case <-b.cfg.Stopper.ShouldQuiesce(): 475 b.cleanup(stop.ErrUnavailable) 476 return 477 case <-ctx.Done(): 478 b.cleanup(ctx.Err()) 479 return 480 } 481 } 482 } 483 484 type request struct { 485 ctx context.Context 486 req roachpb.Request 487 rangeID roachpb.RangeID 488 responseChan chan<- Response 489 } 490 491 type batch struct { 492 reqs []*request 493 size int // bytes 494 495 // sendDeadline is the latest deadline reported by a request's context. 496 // It will be zero valued if any request does not contain a deadline. 497 sendDeadline time.Time 498 499 // idx is the batch's index in the batchQueue. 500 idx int 501 502 // deadline is the time at which this batch should be sent according to the 503 // Batcher's configuration. 504 deadline time.Time 505 // startTime is the time at which the first request was added to the batch. 506 startTime time.Time 507 // lastUpdated is the latest time when a request was added to the batch. 508 lastUpdated time.Time 509 } 510 511 func (b *batch) rangeID() roachpb.RangeID { 512 if len(b.reqs) == 0 { 513 panic("rangeID cannot be called on an empty batch") 514 } 515 return b.reqs[0].rangeID 516 } 517 518 func (b *batch) batchRequest(cfg *Config) roachpb.BatchRequest { 519 req := roachpb.BatchRequest{ 520 // Preallocate the Requests slice. 521 Requests: make([]roachpb.RequestUnion, 0, len(b.reqs)), 522 } 523 for _, r := range b.reqs { 524 req.Add(r.req) 525 } 526 if cfg.MaxKeysPerBatchReq > 0 { 527 req.MaxSpanRequestKeys = int64(cfg.MaxKeysPerBatchReq) 528 } 529 return req 530 } 531 532 // pool stores object pools for the various commonly reused objects of the 533 // batcher 534 type pool struct { 535 responseChanPool sync.Pool 536 batchPool sync.Pool 537 requestPool sync.Pool 538 } 539 540 func makePool() pool { 541 return pool{ 542 responseChanPool: sync.Pool{ 543 New: func() interface{} { return make(chan Response, 1) }, 544 }, 545 batchPool: sync.Pool{ 546 New: func() interface{} { return &batch{} }, 547 }, 548 requestPool: sync.Pool{ 549 New: func() interface{} { return &request{} }, 550 }, 551 } 552 } 553 554 func (p *pool) getResponseChan() chan Response { 555 return p.responseChanPool.Get().(chan Response) 556 } 557 558 func (p *pool) putResponseChan(r chan Response) { 559 p.responseChanPool.Put(r) 560 } 561 562 func (p *pool) newRequest( 563 ctx context.Context, rangeID roachpb.RangeID, req roachpb.Request, responseChan chan<- Response, 564 ) *request { 565 r := p.requestPool.Get().(*request) 566 *r = request{ 567 ctx: ctx, 568 rangeID: rangeID, 569 req: req, 570 responseChan: responseChan, 571 } 572 return r 573 } 574 575 func (p *pool) putRequest(r *request) { 576 *r = request{} 577 p.requestPool.Put(r) 578 } 579 580 func (p *pool) newBatch(now time.Time) *batch { 581 ba := p.batchPool.Get().(*batch) 582 *ba = batch{ 583 startTime: now, 584 idx: -1, 585 } 586 return ba 587 } 588 589 func (p *pool) putBatch(b *batch) { 590 *b = batch{} 591 p.batchPool.Put(b) 592 } 593 594 // batchQueue is a container for batch objects which offers O(1) get based on 595 // rangeID and peekFront as well as O(log(n)) upsert, removal, popFront. 596 // Batch structs are heap ordered inside of the batches slice based on their 597 // deadline with the earliest deadline at the front. 598 // 599 // Note that the batch struct stores its index in the batches slice and is -1 600 // when not part of the queue. The heap methods update the batch indices when 601 // updating the heap. Take care not to ever put a batch in to multiple 602 // batchQueues. At time of writing this package only ever used one batchQueue 603 // per RequestBatcher. 604 type batchQueue struct { 605 batches []*batch 606 byRange map[roachpb.RangeID]*batch 607 } 608 609 var _ heap.Interface = (*batchQueue)(nil) 610 611 func makeBatchQueue() batchQueue { 612 return batchQueue{ 613 byRange: map[roachpb.RangeID]*batch{}, 614 } 615 } 616 617 func (q *batchQueue) peekFront() *batch { 618 if q.Len() == 0 { 619 return nil 620 } 621 return q.batches[0] 622 } 623 624 func (q *batchQueue) popFront() *batch { 625 if q.Len() == 0 { 626 return nil 627 } 628 return heap.Pop(q).(*batch) 629 } 630 631 func (q *batchQueue) get(id roachpb.RangeID) (*batch, bool) { 632 b, exists := q.byRange[id] 633 return b, exists 634 } 635 636 func (q *batchQueue) remove(ba *batch) { 637 delete(q.byRange, ba.rangeID()) 638 heap.Remove(q, ba.idx) 639 } 640 641 func (q *batchQueue) upsert(ba *batch) { 642 if ba.idx >= 0 { 643 heap.Fix(q, ba.idx) 644 } else { 645 heap.Push(q, ba) 646 } 647 } 648 649 func (q *batchQueue) Len() int { 650 return len(q.batches) 651 } 652 653 func (q *batchQueue) Swap(i, j int) { 654 q.batches[i], q.batches[j] = q.batches[j], q.batches[i] 655 q.batches[i].idx = i 656 q.batches[j].idx = j 657 } 658 659 func (q *batchQueue) Less(i, j int) bool { 660 idl, jdl := q.batches[i].deadline, q.batches[j].deadline 661 if before := idl.Before(jdl); before || !idl.Equal(jdl) { 662 return before 663 } 664 return q.batches[i].rangeID() < q.batches[j].rangeID() 665 } 666 667 func (q *batchQueue) Push(v interface{}) { 668 ba := v.(*batch) 669 ba.idx = len(q.batches) 670 q.byRange[ba.rangeID()] = ba 671 q.batches = append(q.batches, ba) 672 } 673 674 func (q *batchQueue) Pop() interface{} { 675 ba := q.batches[len(q.batches)-1] 676 q.batches = q.batches[:len(q.batches)-1] 677 delete(q.byRange, ba.rangeID()) 678 ba.idx = -1 679 return ba 680 }