| // Copyright 2016 Google LLC. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // Package bundler supports bundling (batching) of items. Bundling amortizes an | |
| // action with fixed costs over multiple items. For example, if an API provides | |
| // an RPC that accepts a list of items as input, but clients would prefer | |
| // adding items one at a time, then a Bundler can accept individual items from | |
| // the client and bundle many of them into a single RPC. | |
| // | |
| // This package is experimental and subject to change without notice. | |
| package bundler | |
| import ( | |
| "context" | |
| "errors" | |
| "reflect" | |
| "sync" | |
| "time" | |
| "golang.org/x/sync/semaphore" | |
| ) | |
| type mode int | |
| const ( | |
| DefaultDelayThreshold = time.Second | |
| DefaultBundleCountThreshold = 10 | |
| DefaultBundleByteThreshold = 1e6 // 1M | |
| DefaultBufferedByteLimit = 1e9 // 1G | |
| ) | |
| const ( | |
| none mode = iota | |
| add | |
| addWait | |
| ) | |
| var ( | |
| // ErrOverflow indicates that Bundler's stored bytes exceeds its BufferedByteLimit. | |
| ErrOverflow = errors.New("bundler reached buffered byte limit") | |
| // ErrOversizedItem indicates that an item's size exceeds the maximum bundle size. | |
| ErrOversizedItem = errors.New("item size exceeds bundle byte limit") | |
| // errMixedMethods indicates that mutually exclusive methods has been | |
| // called subsequently. | |
| errMixedMethods = errors.New("calls to Add and AddWait cannot be mixed") | |
| ) | |
| // A Bundler collects items added to it into a bundle until the bundle | |
| // exceeds a given size, then calls a user-provided function to handle the | |
| // bundle. | |
| // | |
| // The exported fields are only safe to modify prior to the first call to Add | |
| // or AddWait. | |
| type Bundler struct { | |
| // Starting from the time that the first message is added to a bundle, once | |
| // this delay has passed, handle the bundle. The default is DefaultDelayThreshold. | |
| DelayThreshold time.Duration | |
| // Once a bundle has this many items, handle the bundle. Since only one | |
| // item at a time is added to a bundle, no bundle will exceed this | |
| // threshold, so it also serves as a limit. The default is | |
| // DefaultBundleCountThreshold. | |
| BundleCountThreshold int | |
| // Once the number of bytes in current bundle reaches this threshold, handle | |
| // the bundle. The default is DefaultBundleByteThreshold. This triggers handling, | |
| // but does not cap the total size of a bundle. | |
| BundleByteThreshold int | |
| // The maximum size of a bundle, in bytes. Zero means unlimited. | |
| BundleByteLimit int | |
| // The maximum number of bytes that the Bundler will keep in memory before | |
| // returning ErrOverflow. The default is DefaultBufferedByteLimit. | |
| BufferedByteLimit int | |
| // The maximum number of handler invocations that can be running at once. | |
| // The default is 1. | |
| HandlerLimit int | |
| handler func(interface{}) // called to handle a bundle | |
| itemSliceZero reflect.Value // nil (zero value) for slice of items | |
| mu sync.Mutex // guards access to fields below | |
| flushTimer *time.Timer // implements DelayThreshold | |
| handlerCount int // # of bundles currently being handled (i.e. handler is invoked on them) | |
| sem *semaphore.Weighted // enforces BufferedByteLimit | |
| semOnce sync.Once // guards semaphore initialization | |
| // The current bundle we're adding items to. Not yet in the queue. | |
| // Appended to the queue once the flushTimer fires or the bundle | |
| // thresholds/limits are reached. If curBundle is nil and tail is | |
| // not, we first try to add items to tail. Once tail is full or handled, | |
| // we create a new curBundle for the incoming item. | |
| curBundle *bundle | |
| // The next bundle in the queue to be handled. Nil if the queue is | |
| // empty. | |
| head *bundle | |
| // The last bundle in the queue to be handled. Nil if the queue is | |
| // empty. If curBundle is nil and tail isn't, we attempt to add new | |
| // items to the tail until if becomes full or has been passed to the | |
| // handler. | |
| tail *bundle | |
| curFlush *sync.WaitGroup // counts outstanding bundles since last flush | |
| prevFlush chan bool // signal used to wait for prior flush | |
| // The first call to Add or AddWait, mode will be add or addWait respectively. | |
| // If there wasn't call yet then mode is none. | |
| mode mode | |
| // TODO: consider alternative queue implementation for head/tail bundle. see: | |
| // https://code-review.googlesource.com/c/google-api-go-client/+/47991/4/support/bundler/bundler.go#74 | |
| } | |
| // A bundle is a group of items that were added individually and will be passed | |
| // to a handler as a slice. | |
| type bundle struct { | |
| items reflect.Value // slice of T | |
| size int // size in bytes of all items | |
| next *bundle // bundles are handled in order as a linked list queue | |
| flush *sync.WaitGroup // the counter that tracks flush completion | |
| } | |
| // add appends item to this bundle and increments the total size. It requires | |
| // that b.mu is locked. | |
| func (bu *bundle) add(item interface{}, size int) { | |
| bu.items = reflect.Append(bu.items, reflect.ValueOf(item)) | |
| bu.size += size | |
| } | |
| // NewBundler creates a new Bundler. | |
| // | |
| // itemExample is a value of the type that will be bundled. For example, if you | |
| // want to create bundles of *Entry, you could pass &Entry{} for itemExample. | |
| // | |
| // handler is a function that will be called on each bundle. If itemExample is | |
| // of type T, the argument to handler is of type []T. handler is always called | |
| // sequentially for each bundle, and never in parallel. | |
| // | |
| // Configure the Bundler by setting its thresholds and limits before calling | |
| // any of its methods. | |
| func NewBundler(itemExample interface{}, handler func(interface{})) *Bundler { | |
| b := &Bundler{ | |
| DelayThreshold: DefaultDelayThreshold, | |
| BundleCountThreshold: DefaultBundleCountThreshold, | |
| BundleByteThreshold: DefaultBundleByteThreshold, | |
| BufferedByteLimit: DefaultBufferedByteLimit, | |
| HandlerLimit: 1, | |
| handler: handler, | |
| itemSliceZero: reflect.Zero(reflect.SliceOf(reflect.TypeOf(itemExample))), | |
| curFlush: &sync.WaitGroup{}, | |
| } | |
| return b | |
| } | |
| func (b *Bundler) initSemaphores() { | |
| // Create the semaphores lazily, because the user may set limits | |
| // after NewBundler. | |
| b.semOnce.Do(func() { | |
| b.sem = semaphore.NewWeighted(int64(b.BufferedByteLimit)) | |
| }) | |
| } | |
| // enqueueCurBundle moves curBundle to the end of the queue. The bundle may be | |
| // handled immediately if we are below HandlerLimit. It requires that b.mu is | |
| // locked. | |
| func (b *Bundler) enqueueCurBundle() { | |
| // We don't require callers to check if there is a pending bundle. It | |
| // may have already been appended to the queue. If so, return early. | |
| if b.curBundle == nil { | |
| return | |
| } | |
| // If we are below the HandlerLimit, the queue must be empty. Handle | |
| // immediately with a new goroutine. | |
| if b.handlerCount < b.HandlerLimit { | |
| b.handlerCount++ | |
| go b.handle(b.curBundle) | |
| } else if b.tail != nil { | |
| // There are bundles on the queue, so append to the end | |
| b.tail.next = b.curBundle | |
| b.tail = b.curBundle | |
| } else { | |
| // The queue is empty, so initialize the queue | |
| b.head = b.curBundle | |
| b.tail = b.curBundle | |
| } | |
| b.curBundle = nil | |
| if b.flushTimer != nil { | |
| b.flushTimer.Stop() | |
| b.flushTimer = nil | |
| } | |
| } | |
| // setMode sets the state of Bundler's mode. If mode was defined before | |
| // and passed state is different from it then return an error. | |
| func (b *Bundler) setMode(m mode) error { | |
| b.mu.Lock() | |
| defer b.mu.Unlock() | |
| if b.mode == m || b.mode == none { | |
| b.mode = m | |
| return nil | |
| } | |
| return errMixedMethods | |
| } | |
| // canFit returns true if bu can fit an additional item of size bytes based | |
| // on the limits of Bundler b. | |
| func (b *Bundler) canFit(bu *bundle, size int) bool { | |
| return (b.BundleByteLimit <= 0 || bu.size+size <= b.BundleByteLimit) && | |
| (b.BundleCountThreshold <= 0 || bu.items.Len() < b.BundleCountThreshold) | |
| } | |
| // Add adds item to the current bundle. It marks the bundle for handling and | |
| // starts a new one if any of the thresholds or limits are exceeded. | |
| // The type of item must be assignable to the itemExample parameter of the NewBundler | |
| // method, otherwise there will be a panic. | |
| // | |
| // If the item's size exceeds the maximum bundle size (Bundler.BundleByteLimit), then | |
| // the item can never be handled. Add returns ErrOversizedItem in this case. | |
| // | |
| // If adding the item would exceed the maximum memory allowed | |
| // (Bundler.BufferedByteLimit) or an AddWait call is blocked waiting for | |
| // memory, Add returns ErrOverflow. | |
| // | |
| // Add never blocks. | |
| func (b *Bundler) Add(item interface{}, size int) error { | |
| if err := b.setMode(add); err != nil { | |
| return err | |
| } | |
| // If this item exceeds the maximum size of a bundle, | |
| // we can never send it. | |
| if b.BundleByteLimit > 0 && size > b.BundleByteLimit { | |
| return ErrOversizedItem | |
| } | |
| // If adding this item would exceed our allotted memory | |
| // footprint, we can't accept it. | |
| // (TryAcquire also returns false if anything is waiting on the semaphore, | |
| // so calls to Add and AddWait shouldn't be mixed.) | |
| b.initSemaphores() | |
| if !b.sem.TryAcquire(int64(size)) { | |
| return ErrOverflow | |
| } | |
| b.mu.Lock() | |
| defer b.mu.Unlock() | |
| return b.add(item, size) | |
| } | |
| // add adds item to the tail of the bundle queue or curBundle depending on space | |
| // and nil-ness (see inline comments). It marks curBundle for handling (by | |
| // appending it to the queue) if any of the thresholds or limits are exceeded. | |
| // curBundle is lazily initialized. It requires that b.mu is locked. | |
| func (b *Bundler) add(item interface{}, size int) error { | |
| // If we don't have a curBundle, see if we can add to the queue tail. | |
| if b.tail != nil && b.curBundle == nil && b.canFit(b.tail, size) { | |
| b.tail.add(item, size) | |
| return nil | |
| } | |
| // If we can't fit in the existing curBundle, move it onto the queue. | |
| if b.curBundle != nil && !b.canFit(b.curBundle, size) { | |
| b.enqueueCurBundle() | |
| } | |
| // Create a curBundle if we don't have one. | |
| if b.curBundle == nil { | |
| b.curFlush.Add(1) | |
| b.curBundle = &bundle{ | |
| items: b.itemSliceZero, | |
| flush: b.curFlush, | |
| } | |
| } | |
| // Add the item. | |
| b.curBundle.add(item, size) | |
| // If curBundle is ready for handling, move it to the queue. | |
| if b.curBundle.size >= b.BundleByteThreshold || | |
| b.curBundle.items.Len() == b.BundleCountThreshold { | |
| b.enqueueCurBundle() | |
| } | |
| // If we created a new bundle and it wasn't immediately handled, set a timer | |
| if b.curBundle != nil && b.flushTimer == nil { | |
| b.flushTimer = time.AfterFunc(b.DelayThreshold, b.tryHandleBundles) | |
| } | |
| return nil | |
| } | |
| // tryHandleBundles is the timer callback that handles or queues any current | |
| // bundle after DelayThreshold time, even if the bundle isn't completely full. | |
| func (b *Bundler) tryHandleBundles() { | |
| b.mu.Lock() | |
| b.enqueueCurBundle() | |
| b.mu.Unlock() | |
| } | |
| // next returns the next bundle that is ready for handling and removes it from | |
| // the internal queue. It requires that b.mu is locked. | |
| func (b *Bundler) next() *bundle { | |
| if b.head == nil { | |
| return nil | |
| } | |
| out := b.head | |
| b.head = b.head.next | |
| if b.head == nil { | |
| b.tail = nil | |
| } | |
| out.next = nil | |
| return out | |
| } | |
| // handle calls the user-specified handler on the given bundle. handle is | |
| // intended to be run as a goroutine. After the handler returns, we update the | |
| // byte total. handle continues processing additional bundles that are ready. | |
| // If no more bundles are ready, the handler count is decremented and the | |
| // goroutine ends. | |
| func (b *Bundler) handle(bu *bundle) { | |
| for bu != nil { | |
| b.handler(bu.items.Interface()) | |
| bu = b.postHandle(bu) | |
| } | |
| } | |
| func (b *Bundler) postHandle(bu *bundle) *bundle { | |
| b.mu.Lock() | |
| defer b.mu.Unlock() | |
| b.sem.Release(int64(bu.size)) | |
| bu.flush.Done() | |
| bu = b.next() | |
| if bu == nil { | |
| b.handlerCount-- | |
| } | |
| return bu | |
| } | |
| // AddWait adds item to the current bundle. It marks the bundle for handling and | |
| // starts a new one if any of the thresholds or limits are exceeded. | |
| // | |
| // If the item's size exceeds the maximum bundle size (Bundler.BundleByteLimit), then | |
| // the item can never be handled. AddWait returns ErrOversizedItem in this case. | |
| // | |
| // If adding the item would exceed the maximum memory allowed (Bundler.BufferedByteLimit), | |
| // AddWait blocks until space is available or ctx is done. | |
| // | |
| // Calls to Add and AddWait should not be mixed on the same Bundler. | |
| func (b *Bundler) AddWait(ctx context.Context, item interface{}, size int) error { | |
| if err := b.setMode(addWait); err != nil { | |
| return err | |
| } | |
| // If this item exceeds the maximum size of a bundle, | |
| // we can never send it. | |
| if b.BundleByteLimit > 0 && size > b.BundleByteLimit { | |
| return ErrOversizedItem | |
| } | |
| // If adding this item would exceed our allotted memory footprint, block | |
| // until space is available. The semaphore is FIFO, so there will be no | |
| // starvation. | |
| b.initSemaphores() | |
| if err := b.sem.Acquire(ctx, int64(size)); err != nil { | |
| return err | |
| } | |
| b.mu.Lock() | |
| defer b.mu.Unlock() | |
| return b.add(item, size) | |
| } | |
| // Flush invokes the handler for all remaining items in the Bundler and waits | |
| // for it to return. | |
| func (b *Bundler) Flush() { | |
| b.mu.Lock() | |
| // If a curBundle is pending, move it to the queue. | |
| b.enqueueCurBundle() | |
| // Store a pointer to the WaitGroup that counts outstanding bundles | |
| // in the current flush and create a new one to track the next flush. | |
| wg := b.curFlush | |
| b.curFlush = &sync.WaitGroup{} | |
| // Flush must wait for all prior, outstanding flushes to complete. | |
| // We use a channel to communicate completion between each flush in | |
| // the sequence. | |
| prev := b.prevFlush | |
| next := make(chan bool) | |
| b.prevFlush = next | |
| b.mu.Unlock() | |
| // Wait until the previous flush is finished. | |
| if prev != nil { | |
| <-prev | |
| } | |
| // Wait until this flush is finished. | |
| wg.Wait() | |
| // Allow the next flush to finish. | |
| close(next) | |
| } |