go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/sync/dispatcher/buffer/full_behavior.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package buffer
    16  
    17  import (
    18  	"go.chromium.org/luci/common/errors"
    19  )
    20  
    21  // FullBehavior allows you to customize the Buffer's behavior when it gets too
    22  // full.
    23  //
    24  // Generally you'll pick one of DropOldestBatch or BlockNewItems.
    25  type FullBehavior interface {
    26  	// Check inspects Options to see if it's compatible with this FullBehavior
    27  	// implementation.
    28  	//
    29  	// Called exactly once during Buffer creation before any other methods are
    30  	// used.
    31  	Check(Options) error
    32  
    33  	// ComputeState evaluates the state of the Buffer (via Stats) and returns:
    34  	//
    35  	//   * okToInsert - User can add an item without blocking.
    36  	//   * dropBatch - Buffer needs to drop the oldest batch if the user does
    37  	//     insert data.
    38  	//
    39  	// Called after Check.
    40  	ComputeState(stats Stats) (okToInsert, dropBatch bool)
    41  
    42  	// NeedsItemSize should return true iff this FullBehavior requires item sizes
    43  	// to effectively apply its policy.
    44  	//
    45  	// Called after Check.
    46  	NeedsItemSize() bool
    47  }
    48  
    49  // BlockNewItems prevents the Buffer from accepting any new items as long as it
    50  // it has MaxItems worth of items.
    51  //
    52  // This will never drop batches, but will block inserts.
    53  type BlockNewItems struct {
    54  	// The maximum number of items that this Buffer is allowed to house (including
    55  	// both leased and unleased items).
    56  	//
    57  	// Default: -1 if BatchItemsMax != -1 else max(1000, BatchItemsMax)
    58  	// Required: Must be >= BatchItemsMax or -1 (only MaxSize applies)
    59  	MaxItems int
    60  
    61  	// The maximum* number of 'size units' that this Buffer is allowed to house
    62  	// (including both leased and unleased items).
    63  	//
    64  	// NOTE(*): This only blocks addition of new items once the buffer is
    65  	// at-or-past capacitiy. e.g. if the buffer has a MaxSize of 1000 and
    66  	// a BatchSizeMax of 100, and you insert items worth 999 units, this will
    67  	// still allow the addition of another item (of up to 100 units) before
    68  	// claiming the buffer is full.
    69  	//
    70  	// NOTE: This may only be set if BatchSizeMax is > 0; Otherwise buffer will
    71  	// not enforce item sizes, and so BlockNewItems will not be able to enforce
    72  	// this policy.
    73  	//
    74  	// Default: -1 if BatchSizeMax != -1 else BatchSizeMax * 5
    75  	// Required: Must be >= BatchSizeMax or -1 (only MaxItems applies)
    76  	MaxSize int
    77  }
    78  
    79  var _ FullBehavior = (*BlockNewItems)(nil)
    80  
    81  // ComputeState implements FullBehavior.ComputeState.
    82  func (b *BlockNewItems) ComputeState(stats Stats) (okToInsert, dropBatch bool) {
    83  	itemsOK := b.MaxItems == -1 || stats.Total() < b.MaxItems
    84  	sizeOK := b.MaxSize == -1 || stats.TotalSize() < b.MaxSize
    85  	okToInsert = itemsOK && sizeOK
    86  	return
    87  }
    88  
    89  // Check implements FullBehavior.Check.
    90  func (b *BlockNewItems) Check(opts Options) (err error) {
    91  	if b.MaxItems == 0 {
    92  		b.MaxItems = negOneOrMax(opts.BatchItemsMax, 1000)
    93  	}
    94  	if b.MaxSize == 0 {
    95  		b.MaxSize = negOneOrMult(opts.BatchSizeMax, 5)
    96  	}
    97  
    98  	switch {
    99  	case b.MaxItems == -1:
   100  	case b.MaxItems < opts.BatchItemsMax:
   101  		return errors.Reason("BlockNewItems.MaxItems must be >= BatchItemsMax[%d]: got %d",
   102  			opts.BatchItemsMax, b.MaxItems).Err()
   103  	}
   104  
   105  	switch {
   106  	case b.MaxSize == -1:
   107  	case b.MaxSize < opts.BatchSizeMax:
   108  		return errors.Reason("BlockNewItems.MaxSize must be >= BatchSizeMax[%d]: got %d",
   109  			opts.BatchSizeMax, b.MaxSize).Err()
   110  	case opts.BatchSizeMax == -1:
   111  		return errors.Reason("BlockNewItems.MaxSize may only be set with BatchSizeMax[%d] > 0",
   112  			opts.BatchSizeMax).Err()
   113  	}
   114  
   115  	if b.MaxItems == -1 && b.MaxSize == -1 {
   116  		return errors.Reason("BlockNewItems must have one of MaxItems or MaxSize > 0").Err()
   117  	}
   118  
   119  	return
   120  }
   121  
   122  // NeedsItemSize implements FullBehavior.NeedsItemSize.
   123  func (b *BlockNewItems) NeedsItemSize() bool {
   124  	return b.MaxSize > 0
   125  }
   126  
   127  // DropOldestBatch will drop buffered data whenever the number of unleased items
   128  // plus leased items would grow beyond MaxLiveItems.
   129  //
   130  // This will never block inserts, but will drop batches.
   131  type DropOldestBatch struct {
   132  	// The maximum number of leased and unleased items that the Buffer may have
   133  	// before dropping data.
   134  	//
   135  	// Once a batch is dropped, it no longer counts against MaxLiveItems, but it
   136  	// may still be in memory if the dropped batch was currently leased.
   137  	//
   138  	// NOTE: The maximum Stats.Total number of items the Buffer could have at
   139  	// a given time is:
   140  	//
   141  	//     MaxLiveItems + (BatchItemsMax * MaxLeases)
   142  	//
   143  	// Default: -1 if BatchItemsMax == -1 else max(1000, BatchItemsMax)
   144  	// Required: Must be >= BatchItemsMax or -1 (only MaxLiveSize applies)
   145  	MaxLiveItems int
   146  
   147  	// The maximum number of leased and unleased size units that the Buffer may
   148  	// have before dropping data.
   149  	//
   150  	// Once a batch is dropped, it no longer counts against MaxLiveSize, but it
   151  	// may still be in memory if the dropped batch was currently leased at the
   152  	// time it was dropped.
   153  	//
   154  	// NOTE: The maximum Stats.TotalSize the Buffer could have at a given
   155  	// time is:
   156  	//
   157  	//     MaxLiveSize + (BatchSizeMax * MaxLeases)
   158  	//
   159  	// NOTE: This may only be set if BatchSizeMax is > 0; Otherwise buffer will
   160  	// not size inserted items, and so DropOldestBatch will not be able to enforce
   161  	// this policy.
   162  	//
   163  	// Default: -1 if BatchSizeMax == -1 else BatchSizeMax * 5
   164  	// Required: Must be >= BatchSizeMax or -1 (only MaxLiveItems applies)
   165  	MaxLiveSize int
   166  }
   167  
   168  var _ FullBehavior = (*DropOldestBatch)(nil)
   169  
   170  // ComputeState implements FullBehavior.ComputeState.
   171  func (d *DropOldestBatch) ComputeState(stats Stats) (okToInsert, dropBatch bool) {
   172  	okToInsert = true
   173  	itemsDrop := d.MaxLiveItems > -1 && stats.Total() >= d.MaxLiveItems
   174  	sizeDrop := d.MaxLiveSize > -1 && stats.TotalSize() >= d.MaxLiveSize
   175  	dropBatch = itemsDrop || sizeDrop
   176  	return
   177  }
   178  
   179  // Check implements FullBehavior.Check.
   180  func (d *DropOldestBatch) Check(opts Options) (err error) {
   181  	if d.MaxLiveItems == 0 {
   182  		d.MaxLiveItems = negOneOrMax(opts.BatchItemsMax, 1000)
   183  	}
   184  	if d.MaxLiveSize == 0 {
   185  		d.MaxLiveSize = negOneOrMult(opts.BatchSizeMax, 5)
   186  	}
   187  
   188  	switch {
   189  	case d.MaxLiveItems == -1:
   190  	case d.MaxLiveItems < opts.BatchItemsMax:
   191  		return errors.Reason("DropOldestBatch.MaxLiveItems must be >= BatchItemsMax[%d]: got %d",
   192  			opts.BatchItemsMax, d.MaxLiveItems).Err()
   193  	}
   194  
   195  	switch {
   196  	case d.MaxLiveSize == -1:
   197  	case d.MaxLiveSize < opts.BatchSizeMax:
   198  		return errors.Reason("DropOldestBatch.MaxLiveSize must be >= BatchSizeMax[%d]: got %d",
   199  			opts.BatchSizeMax, d.MaxLiveSize).Err()
   200  	case opts.BatchSizeMax == -1:
   201  		return errors.Reason("DropOldestBatch.MaxLiveSize may only be set with BatchSizeMax[%d] > 0",
   202  			opts.BatchSizeMax).Err()
   203  	}
   204  
   205  	if d.MaxLiveItems == -1 && d.MaxLiveSize == -1 {
   206  		return errors.Reason("DropOldestBatch must have one of MaxLiveItems or MaxLiveSize > 0").Err()
   207  	}
   208  
   209  	return
   210  }
   211  
   212  // NeedsItemSize implements FullBehavior.NeedsItemSize.
   213  func (d *DropOldestBatch) NeedsItemSize() bool {
   214  	return d.MaxLiveSize > 0
   215  }
   216  
   217  // InfiniteGrowth will not drop data or block new items. It just grows until
   218  // your computer runs out of memory.
   219  //
   220  // This will never block inserts, and will not drop batches.
   221  type InfiniteGrowth struct{}
   222  
   223  var _ FullBehavior = InfiniteGrowth{}
   224  
   225  // ComputeState implements FullBehavior.ComputeState.
   226  func (i InfiniteGrowth) ComputeState(Stats) (okToInsert, dropBatch bool) { return true, false }
   227  
   228  // Check implements FullBehavior.Check.
   229  func (i InfiniteGrowth) Check(opts Options) (err error) { return nil }
   230  
   231  // NeedsItemSize implements FullBehavior.NeedsItemSize.
   232  func (i InfiniteGrowth) NeedsItemSize() bool { return false }
   233  
   234  func negOneOrMult(a, b int) int {
   235  	if a == -1 {
   236  		return -1
   237  	}
   238  	return a * b
   239  }
   240  
   241  func negOneOrMax(a, b int) int {
   242  	if a == -1 {
   243  		return -1
   244  	}
   245  	return intMax(a, b)
   246  }