mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Unified Storage: added pkg/util/ring package to handle queueing of notifications (#84657)
* added pkg/util/rinq package to handle queueing of notifications * fix linters * Fix typo in comment Co-authored-by: Dan Cech <dcech@grafana.com> * improve allocation strategy for Enqueue; remove unnecessary clearing of slice * Update pkg/util/ringq/dyn_chan_bench_test.go Co-authored-by: Dan Cech <dcech@grafana.com> * Update pkg/util/ringq/ringq.go Co-authored-by: Dan Cech <dcech@grafana.com> * refactor to move stats and shrinking into Ring * add missing error assertions in tests * add missing error assertions in tests and linting issues * simplify controller closed check * improve encapsulation of internal state in Ring * use (*Ring).Len for clarity instead of stats --------- Co-authored-by: Dan Cech <dcech@grafana.com>
This commit is contained in:
parent
eb86fd867f
commit
e6ead667b3
285
pkg/util/ring/adaptive_chan.go
Normal file
285
pkg/util/ring/adaptive_chan.go
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
package ring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Package level named errors.
|
||||||
|
var (
|
||||||
|
ErrAdaptiveChanClosed = errors.New("closed AdaptiveChan")
|
||||||
|
ErrAdaptiveChanControllerClosed = errors.New("closed AdaptiveChanController")
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdaptiveChan provides a queueing system based on a send-only, a receive-only,
|
||||||
|
// and an internal ring buffer queue backed by a *Ring. It also provides an
|
||||||
|
// AdaptiveChanController to provide stats and some control on the internal
|
||||||
|
// *Ring. Termination is controlled by closing the returned send-only channel.
|
||||||
|
// After doing so, the receive-only channel will have the chance to receive all
|
||||||
|
// the items still in the queue and will be immediately closed afterwards. Once
|
||||||
|
// both channels are closed, the AdaptiveChanController will no longer be usable
|
||||||
|
// and will only return ErrAdaptiveChanClosed for all its methods. Leaving the
|
||||||
|
// growth and shrinkage of the internal *Ring apart, which can be controlled
|
||||||
|
// with the AdaptiveChanController, the implementation is allocation free.
|
||||||
|
//
|
||||||
|
// The implementation explicitly returns two channels and a struct, instead of
|
||||||
|
// just one struct that has the channels, to make a clear statement about the
|
||||||
|
// intended usage pattern:
|
||||||
|
//
|
||||||
|
// 1. Create an adaptive channel.
|
||||||
|
// 2. Provide the send-only channel to your producer(s). They are responsible
|
||||||
|
// for closing this channel when they're done. If more than one goroutine
|
||||||
|
// will have access to this channel, then it's the producer's responsibility
|
||||||
|
// to coordinate the channel close operation.
|
||||||
|
// 3. Provide the receive-only channel to your consumer(s), and let them
|
||||||
|
// receive from it with the two return value syntax for channels in order to
|
||||||
|
// check for termination from the sending side.
|
||||||
|
// 4. Use the AdaptiveChanController to control the internal buffer behaviour
|
||||||
|
// and to monitor stats. This should typically be held by the creator of the
|
||||||
|
// adaptive channel. Refrain from holding a reference to the send-only
|
||||||
|
// channel to force termination of the producing side. Instead, provide a
|
||||||
|
// side mechanism to communicate the intention of terminating the sending
|
||||||
|
// side, e.g. providing your producer(s) with a context as well as the
|
||||||
|
// send-only channel. An adaptive channel is meant as a queueing system, not
|
||||||
|
// as a coordination mechanism for producer(s), consumer(s) and
|
||||||
|
// controller(s).
|
||||||
|
//
|
||||||
|
// This pattern is designed to maximize decoupling while providing insights and
|
||||||
|
// granular control on memory usage. While the controller is not meant to make
|
||||||
|
// any direct changes to the queued data, the Clear method provides the
|
||||||
|
// opportunity to discard all queued items as an administrative measure. This
|
||||||
|
// doesn't terminate the queue, though, i.e. it doesn't close the send-only
|
||||||
|
// channel.
|
||||||
|
func AdaptiveChan[T any]() (send chan<- T, recv <-chan T, ctrl *AdaptiveChanController) {
|
||||||
|
internalSend := make(chan T)
|
||||||
|
internalRecv := make(chan T)
|
||||||
|
statsChan := make(chan AdaptiveChanStats)
|
||||||
|
cmdChan := make(chan acCmd)
|
||||||
|
ctrl = &AdaptiveChanController{
|
||||||
|
statsChan: statsChan,
|
||||||
|
cmdChan: cmdChan,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(internalRecv)
|
||||||
|
defer close(statsChan)
|
||||||
|
|
||||||
|
var q Ring[T]
|
||||||
|
var stats AdaptiveChanStats
|
||||||
|
|
||||||
|
// the loop condition is that we either have items to dequeue or that we
|
||||||
|
// have the possibility to receive new items to be queued
|
||||||
|
for q.Len() > 0 || internalSend != nil {
|
||||||
|
// NOTE: the overhead of writing stats in each iteration is
|
||||||
|
// negligible. I tried a two phase stats writing with a chan
|
||||||
|
// struct{} to get notified that the controller wanted stats, then
|
||||||
|
// updating the stats and finally writing to statsChan. There was no
|
||||||
|
// observable difference for just enqueueing and dequeueing after
|
||||||
|
// running the benchmarks several times, and reading stats got worse
|
||||||
|
// by ~22%
|
||||||
|
q.WriteStats(&stats.RingStats)
|
||||||
|
|
||||||
|
// if we don't have anything in the queue, then make the dequeueing
|
||||||
|
// branch of the select block indefinitely by providing a nil
|
||||||
|
// channel, leaving only the queueing branch available
|
||||||
|
dequeueChan := internalRecv
|
||||||
|
if q.Len() == 0 {
|
||||||
|
dequeueChan = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case v, ok := <-internalSend: // blocks until something is queued
|
||||||
|
if !ok {
|
||||||
|
// in was closed, so if we leave it like that the next
|
||||||
|
// iteration will keep receiving zero values with ok=false
|
||||||
|
// without any blocking. So we set in to nil, so that
|
||||||
|
// the next iteration the select will block indefinitely on
|
||||||
|
// this branch of the select and leave only the dequeing
|
||||||
|
// branch active until all items have been dequeued
|
||||||
|
internalSend = nil
|
||||||
|
} else {
|
||||||
|
q.Enqueue(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
case dequeueChan <- q.Peek(): // blocks if nothing to dequeue
|
||||||
|
// we don't want to call Dequeue in the `case` above since that
|
||||||
|
// would consume the item and it would be lost if the queueing
|
||||||
|
// branch was selected, so instead we Peek in the `case` and we
|
||||||
|
// do the actual dequeueing here once we know this branch was
|
||||||
|
// selected
|
||||||
|
q.Dequeue()
|
||||||
|
|
||||||
|
case statsChan <- stats:
|
||||||
|
// stats reading
|
||||||
|
stats.StatsRead++
|
||||||
|
|
||||||
|
case cmd, ok := <-cmdChan:
|
||||||
|
if !ok {
|
||||||
|
// AdaptiveChanController was closed. Set cmdChan to nil so
|
||||||
|
// this branch blocks in the next iteration
|
||||||
|
cmdChan = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute a command on the internal *Ring
|
||||||
|
switch cmd.acCmdType {
|
||||||
|
case acCmdMin:
|
||||||
|
q.Min = cmd.intValue
|
||||||
|
stats.Min = cmd.intValue
|
||||||
|
case acCmdMax:
|
||||||
|
q.Max = cmd.intValue
|
||||||
|
stats.Max = cmd.intValue
|
||||||
|
case acCmdGrow:
|
||||||
|
q.Grow(cmd.intValue)
|
||||||
|
case acCmdShrink:
|
||||||
|
q.Shrink(cmd.intValue)
|
||||||
|
case acCmdClear:
|
||||||
|
q.Clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return internalSend, internalRecv, ctrl
|
||||||
|
}
|
||||||
|
|
||||||
|
type acCmdType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
acCmdMin = iota
|
||||||
|
acCmdMax
|
||||||
|
acCmdClear
|
||||||
|
acCmdGrow
|
||||||
|
acCmdShrink
|
||||||
|
)
|
||||||
|
|
||||||
|
type acCmd struct {
|
||||||
|
acCmdType
|
||||||
|
intValue int
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdaptiveChanController provides access to an AdaptiveChan's internal *Ring.
|
||||||
|
type AdaptiveChanController struct {
|
||||||
|
statsChan <-chan AdaptiveChanStats
|
||||||
|
cmdChan chan<- acCmd
|
||||||
|
cmdChanMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases resources associated with this controller. After calling this
|
||||||
|
// method, all other methods will return ErrAdaptiveChanControllerClosed. It is
|
||||||
|
// idempotent. This doesn't affect the queue itself, but rather prevents further
|
||||||
|
// administrative tasks to be performed through the AdaptiveChanController.
|
||||||
|
func (r *AdaptiveChanController) Close() {
|
||||||
|
r.cmdChanMu.Lock()
|
||||||
|
defer r.cmdChanMu.Unlock()
|
||||||
|
|
||||||
|
if r.cmdChan != nil {
|
||||||
|
close(r.cmdChan)
|
||||||
|
r.cmdChan = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min sets the value of Min in the internal *Ring.
|
||||||
|
func (r *AdaptiveChanController) Min(ctx context.Context, n int) error {
|
||||||
|
r.cmdChanMu.Lock()
|
||||||
|
defer r.cmdChanMu.Unlock()
|
||||||
|
|
||||||
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
||||||
|
acCmdType: acCmdMin,
|
||||||
|
intValue: n,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max sets the value of Max in the internal *Ring.
|
||||||
|
func (r *AdaptiveChanController) Max(ctx context.Context, n int) error {
|
||||||
|
r.cmdChanMu.Lock()
|
||||||
|
defer r.cmdChanMu.Unlock()
|
||||||
|
|
||||||
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
||||||
|
acCmdType: acCmdMax,
|
||||||
|
intValue: n,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow calls Grow on the internal *Ring.
|
||||||
|
func (r *AdaptiveChanController) Grow(ctx context.Context, n int) error {
|
||||||
|
r.cmdChanMu.Lock()
|
||||||
|
defer r.cmdChanMu.Unlock()
|
||||||
|
|
||||||
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
||||||
|
acCmdType: acCmdGrow,
|
||||||
|
intValue: n,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shrink calls Shrink on the internal *Ring.
|
||||||
|
func (r *AdaptiveChanController) Shrink(ctx context.Context, n int) error {
|
||||||
|
r.cmdChanMu.Lock()
|
||||||
|
defer r.cmdChanMu.Unlock()
|
||||||
|
|
||||||
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
||||||
|
acCmdType: acCmdShrink,
|
||||||
|
intValue: n,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear calls Clear on the internal *Ring.
|
||||||
|
func (r *AdaptiveChanController) Clear(ctx context.Context) error {
|
||||||
|
r.cmdChanMu.Lock()
|
||||||
|
defer r.cmdChanMu.Unlock()
|
||||||
|
|
||||||
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
||||||
|
acCmdType: acCmdClear,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteStats writes a snapshot of general stats about the associated
|
||||||
|
// AdaptiveChan to the given *AdaptiveChanStats.
|
||||||
|
func (r *AdaptiveChanController) WriteStats(ctx context.Context, s *AdaptiveChanStats) error {
|
||||||
|
r.cmdChanMu.Lock()
|
||||||
|
defer r.cmdChanMu.Unlock()
|
||||||
|
if r.cmdChan == nil {
|
||||||
|
return ErrAdaptiveChanControllerClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
return recvOrErr(ctx, r.statsChan, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdaptiveChanStats is a snapshot of general stats for an AdaptiveChan.
|
||||||
|
type AdaptiveChanStats struct {
|
||||||
|
RingStats
|
||||||
|
// Min is the value of Min in the internal *Ring.
|
||||||
|
Min int
|
||||||
|
// Max value of Max in the internal *Ring.
|
||||||
|
Max int
|
||||||
|
// StatsRead is the total number of stats read before this snapshot. If it
|
||||||
|
// is zero, it means this snapshot is the first reading.
|
||||||
|
StatsRead uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendOrErr[T any](ctx context.Context, c chan<- T, v T) error {
|
||||||
|
if c == nil {
|
||||||
|
return ErrAdaptiveChanControllerClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case c <- v:
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recvOrErr[T any](ctx context.Context, c <-chan T, tptr *T) error {
|
||||||
|
select {
|
||||||
|
case t, ok := <-c:
|
||||||
|
if !ok {
|
||||||
|
return ErrAdaptiveChanClosed
|
||||||
|
}
|
||||||
|
*tptr = t
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
73
pkg/util/ring/adaptive_chan_bench_test.go
Normal file
73
pkg/util/ring/adaptive_chan_bench_test.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package ring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkAdaptiveChanBaseline(b *testing.B) {
|
||||||
|
in, out, _ := AdaptiveChan[int]()
|
||||||
|
in <- 1
|
||||||
|
<-out
|
||||||
|
b.Cleanup(func() {
|
||||||
|
close(in)
|
||||||
|
})
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
in <- i
|
||||||
|
val := <-out
|
||||||
|
if val != i {
|
||||||
|
b.Fatalf("expected 1, got %d", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAdaptiveChanWithStatsRead(b *testing.B) {
|
||||||
|
var stats AdaptiveChanStats
|
||||||
|
in, out, sr := AdaptiveChan[int]()
|
||||||
|
in <- 1
|
||||||
|
<-out
|
||||||
|
ctx := context.Background()
|
||||||
|
b.Cleanup(func() {
|
||||||
|
close(in)
|
||||||
|
})
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
in <- 1
|
||||||
|
val := <-out
|
||||||
|
if val != 1 {
|
||||||
|
b.Fatalf("expected 1, got %d", val)
|
||||||
|
}
|
||||||
|
err := sr.WriteStats(ctx, &stats)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if stats.Enqueued == 0 {
|
||||||
|
b.Fatalf("unexpected stats: %v", stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGoChanBaseline(b *testing.B) {
|
||||||
|
c := make(chan int, 1)
|
||||||
|
b.Cleanup(func() {
|
||||||
|
close(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c <- 1
|
||||||
|
val := <-c
|
||||||
|
if val != 1 {
|
||||||
|
b.Fatalf("expected 1, got %d", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
272
pkg/util/ring/adaptive_chan_test.go
Normal file
272
pkg/util/ring/adaptive_chan_test.go
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
package ring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/goleak"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// make sure we don't leak goroutines after tests in this package have
|
||||||
|
// finished. This is especially important as AdaptiveChan uses a different
|
||||||
|
// goroutine to coordinate work
|
||||||
|
goleak.VerifyTestMain(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdaptiveChan(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("edge case - close send and controller after creation", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
send, recv, ctrl := AdaptiveChan[int]()
|
||||||
|
cleanupAC(t, send, recv, ctrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("basic operation", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
var stats, expectedStats AdaptiveChanStats
|
||||||
|
|
||||||
|
send, recv, ctrl := AdaptiveChan[int]()
|
||||||
|
cleanupAC(t, send, recv, ctrl)
|
||||||
|
|
||||||
|
sendNonBlock(t, send, ints(10)...)
|
||||||
|
err := ctrl.WriteStats(ctxFromTest(t), &stats)
|
||||||
|
require.NoError(t, err)
|
||||||
|
removeAllocStats(&stats.RingStats)
|
||||||
|
expectedStats.Len = 10
|
||||||
|
expectedStats.Enqueued = 10
|
||||||
|
require.Equal(t, expectedStats, stats)
|
||||||
|
|
||||||
|
recvNonBlock(t, recv, ints(10)...)
|
||||||
|
err = ctrl.WriteStats(ctxFromTest(t), &stats)
|
||||||
|
require.NoError(t, err)
|
||||||
|
removeAllocStats(&stats.RingStats)
|
||||||
|
expectedStats.Len = 0
|
||||||
|
expectedStats.Dequeued = 10
|
||||||
|
expectedStats.StatsRead = 1
|
||||||
|
require.Equal(t, expectedStats, stats)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("using commands to control the ring", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
send, recv, ctrl := AdaptiveChan[int]()
|
||||||
|
cleanupAC(t, send, recv, ctrl)
|
||||||
|
|
||||||
|
var stats, expectedStats AdaptiveChanStats
|
||||||
|
expectedStats.Min = 10
|
||||||
|
expectedStats.Max = 20
|
||||||
|
|
||||||
|
err := ctrl.Min(ctxFromTest(t), expectedStats.Min)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ctrl.Max(ctxFromTest(t), expectedStats.Max)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sendNonBlock(t, send, 1)
|
||||||
|
err = ctrl.WriteStats(ctxFromTest(t), &stats)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectedStats.Min, stats.Cap, "failed to allocate Min")
|
||||||
|
removeAllocStats(&stats.RingStats)
|
||||||
|
expectedStats.Len = 1
|
||||||
|
expectedStats.Enqueued = 1
|
||||||
|
require.Equal(t, expectedStats, stats)
|
||||||
|
|
||||||
|
err = ctrl.Grow(ctxFromTest(t), (expectedStats.Max+expectedStats.Min)/2-1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ctrl.WriteStats(ctxFromTest(t), &stats)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, (expectedStats.Max+expectedStats.Min)/2, stats.Cap, "failed to Grow")
|
||||||
|
|
||||||
|
err = ctrl.Shrink(ctxFromTest(t), expectedStats.Min)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ctrl.WriteStats(ctxFromTest(t), &stats)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectedStats.Min+1, stats.Cap, "failed to Shrink")
|
||||||
|
|
||||||
|
err = ctrl.Clear(ctxFromTest(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ctrl.WriteStats(ctxFromTest(t), &stats)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectedStats.Min, stats.Cap, "failed to Clear")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("use of send and recv channels with a closed controller", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
send, recv, ctrl := AdaptiveChan[int]()
|
||||||
|
|
||||||
|
ctrl.Close()
|
||||||
|
assertCtrlWriteErr(t, ctrl, ctxFromTest(t), ErrAdaptiveChanControllerClosed)
|
||||||
|
|
||||||
|
sendNonBlock(t, send, ints(10)...)
|
||||||
|
recvNonBlock(t, recv, ints(10)...)
|
||||||
|
close(send)
|
||||||
|
shouldBeClosed(t, recv)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendOrErr(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const val = 44203
|
||||||
|
var c chan int
|
||||||
|
|
||||||
|
err := sendOrErr(ctxFromTest(t), c, val)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, ErrAdaptiveChanControllerClosed)
|
||||||
|
|
||||||
|
c = make(chan int, 1)
|
||||||
|
err = sendOrErr(ctxFromTest(t), c, val)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err = sendOrErr(canceledCtx, c, val)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, context.Canceled)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case v, ok := <-c:
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, val, v)
|
||||||
|
default:
|
||||||
|
t.Fatalf("value not sent to channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecvOrErr(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const (
|
||||||
|
val = 44203
|
||||||
|
witness = -1
|
||||||
|
)
|
||||||
|
var c chan int
|
||||||
|
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
got := witness
|
||||||
|
err := recvOrErr(canceledCtx, c, &got)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, context.Canceled)
|
||||||
|
require.Equal(t, witness, got)
|
||||||
|
|
||||||
|
c = make(chan int, 1)
|
||||||
|
c <- val
|
||||||
|
|
||||||
|
err = recvOrErr(ctxFromTest(t), c, &got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, val, got)
|
||||||
|
|
||||||
|
close(c)
|
||||||
|
got = witness
|
||||||
|
err = recvOrErr(ctxFromTest(t), c, &got)
|
||||||
|
require.ErrorIs(t, err, ErrAdaptiveChanClosed)
|
||||||
|
require.Equal(t, witness, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupAC closes the send channel and the controller, and perform a series of
|
||||||
|
// rutinary assertions.
|
||||||
|
func cleanupAC[T any](t *testing.T, send chan<- T, recv <-chan T, ctrl *AdaptiveChanController) {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
close(send)
|
||||||
|
shouldBeClosed(t, recv)
|
||||||
|
|
||||||
|
var stats AdaptiveChanStats
|
||||||
|
err := ctrl.WriteStats(ctxFromTest(t), &stats)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, ErrAdaptiveChanClosed)
|
||||||
|
require.Equal(t, AdaptiveChanStats{}, stats)
|
||||||
|
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
assertCtrlWriteErr(t, ctrl, canceledCtx, context.Canceled)
|
||||||
|
|
||||||
|
ctrl.Close()
|
||||||
|
|
||||||
|
err = ctrl.WriteStats(ctxFromTest(t), &stats)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, ErrAdaptiveChanControllerClosed)
|
||||||
|
require.Equal(t, AdaptiveChanStats{}, stats)
|
||||||
|
|
||||||
|
assertCtrlWriteErr(t, ctrl, ctxFromTest(t), ErrAdaptiveChanControllerClosed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertCtrlWriteErr(t *testing.T, ctrl *AdaptiveChanController, ctx context.Context, expectedErr error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
err := ctrl.Min(ctx, 1)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, expectedErr)
|
||||||
|
|
||||||
|
err = ctrl.Max(ctx, 1)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, expectedErr)
|
||||||
|
|
||||||
|
err = ctrl.Grow(ctx, 1)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, expectedErr)
|
||||||
|
|
||||||
|
err = ctrl.Shrink(ctx, 1)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, expectedErr)
|
||||||
|
|
||||||
|
err = ctrl.Clear(ctx)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, expectedErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldBeClosed[T any](t *testing.T, recv <-chan T) {
|
||||||
|
t.Helper()
|
||||||
|
select {
|
||||||
|
case v, ok := <-recv:
|
||||||
|
require.False(t, ok, "unexpected value %q received", v)
|
||||||
|
case <-ctxFromTest(t).Done():
|
||||||
|
t.Fatalf("context canceled where recv chan should be closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendNonBlock[T any](t *testing.T, send chan<- T, s ...T) {
|
||||||
|
t.Helper()
|
||||||
|
var canceled bool
|
||||||
|
for i, v := range s {
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
select {
|
||||||
|
case send <- v:
|
||||||
|
case <-ctxFromTest(t).Done():
|
||||||
|
canceled = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
require.False(t, canceled, "context canceled while sending item %d/%d", i+1, len(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recvNonBlock[T any](t *testing.T, recv <-chan T, s ...T) {
|
||||||
|
t.Helper()
|
||||||
|
var canceled bool
|
||||||
|
for i := range s {
|
||||||
|
select {
|
||||||
|
case s[i] = <-recv:
|
||||||
|
case <-ctxFromTest(t).Done():
|
||||||
|
canceled = true
|
||||||
|
}
|
||||||
|
require.False(t, canceled, "context canceled while receiving item %d/%d", i+1, len(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctxFromTest(t *testing.T) context.Context {
|
||||||
|
return ctxFromTestWithDefault(t, time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctxFromTestWithDefault(t *testing.T, d time.Duration) context.Context {
|
||||||
|
require.Greater(t, d, 0*time.Second)
|
||||||
|
deadline, ok := t.Deadline()
|
||||||
|
if !ok {
|
||||||
|
deadline = time.Now().Add(d)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
return ctx
|
||||||
|
}
|
254
pkg/util/ring/ring.go
Normal file
254
pkg/util/ring/ring.go
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
package ring
|
||||||
|
|
||||||
|
// Ring is a ring buffer backed by a slice that rearranges itself to grow and
|
||||||
|
// shrink as needed. It can also be grown and shrunk manually. It is not safe
|
||||||
|
// for concurrent use, and the zero value is ready for use. Dequeued and cleared
|
||||||
|
// items are zeroed in the underlying slice to release references and allow
|
||||||
|
// garbage collection. Leaving growth and shrinkage of the internal slice apart,
|
||||||
|
// which can be directly controlled, all operations are allocation free.
|
||||||
|
type Ring[T any] struct {
|
||||||
|
buf []T
|
||||||
|
stats RingStats
|
||||||
|
back, len int
|
||||||
|
|
||||||
|
// Min sets the minimum capacity that the Ring can have, and takes effect
|
||||||
|
// in the next write operation. The Ring will naturally tend to shrink
|
||||||
|
// towards Min when free capacity allows it. Setting this value has no
|
||||||
|
// immediate effect, but instead affects future writing operations. Min is
|
||||||
|
// valid only if:
|
||||||
|
// 0 < Min && ( Max <= 0 || Min <= Max )
|
||||||
|
// Note that this allows setting Min but not Max, or setting both as well.
|
||||||
|
Min int
|
||||||
|
|
||||||
|
// Max sets the maximum capacity that the Ring can grow to store new items.
|
||||||
|
// Setting this value has no immediate effect, but instead affects future
|
||||||
|
// writing operations. Max is valid only if:
|
||||||
|
// 0 < Max && Min <= Max
|
||||||
|
// Note that this allows setting Max but not Min, or setting both as well.
|
||||||
|
Max int
|
||||||
|
}
|
||||||
|
|
||||||
|
// RingStats provides general stats for a Ring.
|
||||||
|
type RingStats struct {
|
||||||
|
// Len is the used capacity.
|
||||||
|
Len int
|
||||||
|
// Cap is the current total capacity.
|
||||||
|
Cap int
|
||||||
|
// Grown is the number of times a larger buffer was allocated.
|
||||||
|
Grown uint64
|
||||||
|
// Shrunk is the number of times a smaller buffer was allocated.
|
||||||
|
Shrunk uint64
|
||||||
|
// Allocs is Grown + Shrunk.
|
||||||
|
Allocs uint64
|
||||||
|
// Enqueued is the total number of items entered into the Ring, including
|
||||||
|
// those which caused other items to be dropped.
|
||||||
|
Enqueued uint64
|
||||||
|
// Dequeued is the total number of items removed from the Ring, including
|
||||||
|
// items removed with Dequeue and with Clear.
|
||||||
|
Dequeued uint64
|
||||||
|
// Dropped is the number of items lost due to the Ring being at capacity.
|
||||||
|
Dropped uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the used capacity.
|
||||||
|
func (rq *Ring[T]) Len() int {
|
||||||
|
return rq.len
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap returns the current total capacity.
|
||||||
|
func (rq *Ring[T]) Cap() int {
|
||||||
|
return len(rq.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteStats writes general stats about this Ring to the given *RingStats, if
|
||||||
|
// it's non-nil.
|
||||||
|
func (rq *Ring[T]) WriteStats(s *RingStats) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rq.stats.Len = rq.len
|
||||||
|
rq.stats.Cap = len(rq.buf)
|
||||||
|
*s = rq.stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes all items from the Ring and returns the number of items
|
||||||
|
// removed. If Min is valid and Cap() > Min, it will also shrink the capacity to
|
||||||
|
// Min. Stats are not cleared, but instead Dequeued is increased by the number
|
||||||
|
// of removed items.
|
||||||
|
func (rq *Ring[T]) Clear() int {
|
||||||
|
cleared := rq.len
|
||||||
|
rq.stats.Dequeued += uint64(cleared)
|
||||||
|
shouldMigrate := clearShouldMigrate(len(rq.buf), rq.Min, rq.Max)
|
||||||
|
|
||||||
|
if rq.len > 0 && !shouldMigrate {
|
||||||
|
// if we migrate we don't need to clear items, since moving to the new
|
||||||
|
// slice will just have the old slice garbage collected
|
||||||
|
chunk := min(rq.back+rq.len, len(rq.buf))
|
||||||
|
clear(rq.buf[rq.back:chunk])
|
||||||
|
clear(rq.buf[:rq.len-chunk])
|
||||||
|
}
|
||||||
|
rq.back = 0
|
||||||
|
rq.len = 0
|
||||||
|
|
||||||
|
if shouldMigrate {
|
||||||
|
rq.migrate(rq.Min)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleared
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shrink makes sure free capacity is not greater than n, shrinking if
|
||||||
|
// necessary. If a new allocation is needed then it will be capped to Min, given
|
||||||
|
// than Min is valid.
|
||||||
|
func (rq *Ring[T]) Shrink(n int) {
|
||||||
|
if n < 0 || rq.len+n >= len(rq.buf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rq.migrate(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow makes sure free capacity is at least n, growing if necessary. If a new
|
||||||
|
// allocation is needed then it will be capped to Max, given that Max is valid.
|
||||||
|
func (rq *Ring[T]) Grow(n int) {
|
||||||
|
if n < 1 || rq.len+n <= len(rq.buf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rq.migrate(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rq *Ring[T]) migrate(newFreeCap int) {
|
||||||
|
newCap := rq.len + newFreeCap
|
||||||
|
newCap = fixAllocSize(rq.len, rq.Min, rq.Max, newCap)
|
||||||
|
if newCap == len(rq.buf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var s []T
|
||||||
|
if newCap > 0 {
|
||||||
|
// if newCap == 0 then just set rq.s to nil
|
||||||
|
s = make([]T, newCap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) > len(rq.buf) {
|
||||||
|
rq.stats.Grown++
|
||||||
|
} else {
|
||||||
|
rq.stats.Shrunk++
|
||||||
|
}
|
||||||
|
|
||||||
|
if rq.len > 0 {
|
||||||
|
chunk1 := min(rq.back+rq.len, len(rq.buf))
|
||||||
|
copied := copy(s, rq.buf[rq.back:chunk1])
|
||||||
|
|
||||||
|
if copied < rq.len {
|
||||||
|
// wrapped the slice
|
||||||
|
chunk2 := rq.len - copied
|
||||||
|
copy(s[copied:], rq.buf[:chunk2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rq.back = 0
|
||||||
|
rq.buf = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue adds the given item to the Ring, growing the capacity if needed. If
|
||||||
|
// the Ring is at capacity (0 < Max && Min <= Max && rq.Len() == rq.Cap()),
|
||||||
|
// then the new item will overwrite the oldest enqueued item.
|
||||||
|
func (rq *Ring[T]) Enqueue(v T) {
|
||||||
|
// try to add space if we're at capacity or fix min allocation
|
||||||
|
if rq.len == len(rq.buf) || (minIsValid(rq.Min, rq.Max) && len(rq.buf) < rq.Min) {
|
||||||
|
newFreeCap := rq.len + 1
|
||||||
|
newFreeCap = newFreeCap*3/2 + 1 // classic append: https://go.dev/blog/slices
|
||||||
|
newFreeCap -= rq.len // migrate only takes free capacity
|
||||||
|
rq.migrate(newFreeCap)
|
||||||
|
|
||||||
|
// if growing was capped at max, then overwrite the first item to be
|
||||||
|
// dequeued
|
||||||
|
if rq.len == len(rq.buf) {
|
||||||
|
rq.stats.Dropped++
|
||||||
|
rq.len--
|
||||||
|
if rq.back++; rq.back >= len(rq.buf) {
|
||||||
|
rq.back = 0 // wrap the slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writePos := rq.back + rq.len
|
||||||
|
if writePos >= len(rq.buf) {
|
||||||
|
writePos -= len(rq.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
rq.buf[writePos] = v
|
||||||
|
rq.len++
|
||||||
|
rq.stats.Enqueued++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek is like Dequeue, but it doesn't remove the item.
|
||||||
|
func (rq *Ring[T]) Peek() (v T) {
|
||||||
|
if rq.len == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return rq.buf[rq.back]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dequeue removes the oldest enqueued item and returns it. If the Ring is
|
||||||
|
// empty, it returns the zero value.
|
||||||
|
func (rq *Ring[T]) Dequeue() (v T) {
|
||||||
|
if rq.len == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the value into v, and also zero out the slice item to release
|
||||||
|
// references so they can be gc'd
|
||||||
|
v, rq.buf[rq.back] = rq.buf[rq.back], v
|
||||||
|
rq.len--
|
||||||
|
if rq.back++; rq.back >= len(rq.buf) {
|
||||||
|
rq.back = 0 // wrap the slice
|
||||||
|
}
|
||||||
|
|
||||||
|
if minIsValid(rq.Min, rq.Max) && rq.len < len(rq.buf)/2+1 {
|
||||||
|
newFreeCap := len(rq.buf)*2/3 + 1 // opposite of growing arithmetic
|
||||||
|
newFreeCap -= rq.len // migrate only takes free capacity
|
||||||
|
rq.migrate(newFreeCap)
|
||||||
|
}
|
||||||
|
rq.stats.Dequeued++
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// the following functions provide small checks and arithmetics that are far
|
||||||
|
// easier to test separately than creating big and more complex tests covering a
|
||||||
|
// huge amount of combinatory options. This reduces the complexity of higher
|
||||||
|
// level tests and leaves only higher level logic, but also allows us to provide
|
||||||
|
// high coverage for even the most rare edge and boundary cases by adding a new
|
||||||
|
// line to the test cases table. They're also inlineable, so no penalty in
|
||||||
|
// calling them.
|
||||||
|
|
||||||
|
func minIsValid(Min, Max int) bool {
|
||||||
|
return 0 < Min && (Max <= 0 || Min <= Max)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxIsValid(Min, Max int) bool {
|
||||||
|
return 0 < Max && Min <= Max
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearShouldMigrate(CurCap, Min, Max int) bool {
|
||||||
|
return minIsValid(Min, Max) && CurCap > Min
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixAllocSize is a helper to determine what should be the new size to be
|
||||||
|
// allocated for a new slice, given the intended NewCap and the current relevant
|
||||||
|
// state of Ring. This is expected to be called inside (*Ring).migrate.
|
||||||
|
func fixAllocSize(CurLen, Min, Max, NewCap int) int {
|
||||||
|
if minIsValid(Min, Max) { // Min is valid
|
||||||
|
NewCap = max(NewCap, CurLen, Min)
|
||||||
|
} else {
|
||||||
|
NewCap = max(CurLen, NewCap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxIsValid(Min, Max) { // Max is valid
|
||||||
|
NewCap = min(NewCap, Max)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewCap
|
||||||
|
}
|
573
pkg/util/ring/ring_test.go
Normal file
573
pkg/util/ring/ring_test.go
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
package ring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ints(n int) []int {
|
||||||
|
ret := make([]int, n)
|
||||||
|
for i := range ret {
|
||||||
|
ret[i] = i + 1
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRing(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const (
|
||||||
|
dLen = 10
|
||||||
|
dHalfLen = dLen / 2
|
||||||
|
)
|
||||||
|
data := ints(dLen)
|
||||||
|
lData := slices.Clone(data[:dHalfLen])
|
||||||
|
rData := slices.Clone(data[dHalfLen:])
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
new(Ring[int]).WriteStats(nil)
|
||||||
|
}, "WriteStats should be panic free")
|
||||||
|
|
||||||
|
t.Run("basic enqueue and dequeue - no min, no max", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
q, expected := new(Ring[int]), new(Ring[int])
|
||||||
|
|
||||||
|
enq(t, q, data...)
|
||||||
|
expected.len = dLen
|
||||||
|
expected.stats.Enqueued = dLen
|
||||||
|
expected.buf = data
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
deq(t, q, lData...)
|
||||||
|
expected.back = dHalfLen
|
||||||
|
expected.len = dHalfLen
|
||||||
|
expected.stats.Dequeued = dHalfLen
|
||||||
|
expected.buf = append(make([]int, dHalfLen), rData...)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
enq(t, q, data...)
|
||||||
|
expected.back = 0
|
||||||
|
expected.len = dLen + dHalfLen
|
||||||
|
expected.stats.Enqueued += dLen
|
||||||
|
expected.buf = append(rData, data...)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
deqAll(t, q, append(rData, data...)...)
|
||||||
|
expected.len = 0
|
||||||
|
expected.stats.Dequeued += dLen + dHalfLen
|
||||||
|
expected.buf = []int{}
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
enq(t, q, data...)
|
||||||
|
expected.len = dLen
|
||||||
|
expected.stats.Enqueued += dLen
|
||||||
|
expected.buf = data
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
clearRing(t, q)
|
||||||
|
expected.len = 0
|
||||||
|
expected.stats.Dequeued += dLen
|
||||||
|
expected.buf = []int{}
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("enqueue, dequeue, grow and shrink - no min, yes max", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
q, expected := new(Ring[int]), new(Ring[int])
|
||||||
|
q.Max = dLen
|
||||||
|
|
||||||
|
// basic wrap and overwrite
|
||||||
|
enq(t, q, lData...)
|
||||||
|
enq(t, q, data...)
|
||||||
|
enq(t, q, data...)
|
||||||
|
expected.back = dHalfLen
|
||||||
|
expected.buf = append(rData, lData...)
|
||||||
|
expected.len = dLen
|
||||||
|
expected.stats.Enqueued = 2*dLen + dHalfLen
|
||||||
|
expected.stats.Dropped = dLen + dHalfLen
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dLen, q.Cap())
|
||||||
|
|
||||||
|
// can't allocate past max and cannot shrink because we're at capacity
|
||||||
|
q.Grow(3 * dLen)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dLen, q.Cap())
|
||||||
|
|
||||||
|
q.Shrink(2 * dLen)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dLen, q.Cap())
|
||||||
|
|
||||||
|
// remove some items and play with extra space
|
||||||
|
deq(t, q, lData...)
|
||||||
|
expected.back = 0
|
||||||
|
expected.buf = rData
|
||||||
|
expected.len -= dHalfLen
|
||||||
|
expected.stats.Dequeued = dHalfLen
|
||||||
|
require.Equal(t, dLen, q.Cap())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
q.Shrink(1)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen+1, q.Cap())
|
||||||
|
|
||||||
|
q.Grow(2)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen+2, q.Cap())
|
||||||
|
|
||||||
|
q.Grow(dLen)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dLen, q.Cap())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("enqueue, dequeue, grow and shrink - yes min, no max", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
q, expected := new(Ring[int]), new(Ring[int])
|
||||||
|
q.Min = dHalfLen
|
||||||
|
|
||||||
|
// enqueueing one item should allocate Min
|
||||||
|
enq(t, q, 1)
|
||||||
|
expected.buf = []int{1}
|
||||||
|
expected.len = 1
|
||||||
|
expected.stats.Enqueued = 1
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
|
||||||
|
// clearing should not migrate now
|
||||||
|
clearRing(t, q)
|
||||||
|
expected.buf = []int{}
|
||||||
|
expected.len = 0
|
||||||
|
expected.stats.Dequeued = 1
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
|
||||||
|
// enqueue some data
|
||||||
|
enq(t, q, data...)
|
||||||
|
expected.buf = data
|
||||||
|
expected.len = dLen
|
||||||
|
expected.stats.Enqueued += dLen
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.GreaterOrEqual(t, q.Cap(), dLen)
|
||||||
|
|
||||||
|
// now clearing should migrate and move to a slice of Min length
|
||||||
|
clearRing(t, q)
|
||||||
|
expected.buf = []int{}
|
||||||
|
expected.len = 0
|
||||||
|
expected.stats.Dequeued += dLen
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
|
||||||
|
// we shouldn't be able to shrink past Min, but it shouldn't allocate a
|
||||||
|
// greater slice either because it's purpose is to reduce allocated
|
||||||
|
// memory if possible
|
||||||
|
q.Min = dLen
|
||||||
|
q.Shrink(dHalfLen)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
|
||||||
|
// dequeueing shouldn't allocate either, just in case
|
||||||
|
require.Zero(t, q.Dequeue())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
|
||||||
|
// enqueueing one item allocates again to Min, which is now greater than
|
||||||
|
// before
|
||||||
|
enq(t, q, 1)
|
||||||
|
expected.buf = []int{1}
|
||||||
|
expected.len = 1
|
||||||
|
expected.stats.Enqueued += 1
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dLen, q.Cap())
|
||||||
|
|
||||||
|
// we reduce Min again, then we should be able to shrink as well
|
||||||
|
q.Min = dHalfLen
|
||||||
|
q.Shrink(dHalfLen)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen+1, q.Cap())
|
||||||
|
q.Shrink(1)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
q.Shrink(0)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
|
||||||
|
// enqueue a lot and then dequeue all, we should still see Min cap
|
||||||
|
enq(t, q, data...)
|
||||||
|
expected.buf = append(expected.buf, data...)
|
||||||
|
expected.len += dLen
|
||||||
|
expected.stats.Enqueued += dLen
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.GreaterOrEqual(t, q.Cap(), dLen+1)
|
||||||
|
|
||||||
|
deqAll(t, q, expected.buf...)
|
||||||
|
expected.buf = []int{}
|
||||||
|
expected.len = 0
|
||||||
|
expected.stats.Dequeued += dLen + 1
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("enqueue, dequeue, grow and shrink - yes min, yes max", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
q, expected := new(Ring[int]), new(Ring[int])
|
||||||
|
q.Min, q.Max = dHalfLen, dLen
|
||||||
|
|
||||||
|
// single enqueueing should allocate for Min
|
||||||
|
enq(t, q, 1)
|
||||||
|
expected.buf = []int{1}
|
||||||
|
expected.len = 1
|
||||||
|
expected.stats.Enqueued = 1
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
|
||||||
|
// enqueue a lot until we overwrite the first item
|
||||||
|
enq(t, q, data...)
|
||||||
|
expected.back = 1
|
||||||
|
expected.buf = append(data[dLen-1:], data[:dLen-1]...)
|
||||||
|
expected.len = dLen
|
||||||
|
expected.stats.Enqueued += dLen
|
||||||
|
expected.stats.Dropped = 1
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dLen, q.Cap())
|
||||||
|
|
||||||
|
// clearing should bring us back to Min alloc
|
||||||
|
clearRing(t, q)
|
||||||
|
expected.back = 0
|
||||||
|
expected.buf = expected.buf[:0]
|
||||||
|
expected.len = 0
|
||||||
|
expected.stats.Dequeued += dLen
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
require.Equal(t, dHalfLen, q.Cap())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("growing and shrinking invariants - no min, no max", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
q, expected := new(Ring[int]), new(Ring[int])
|
||||||
|
|
||||||
|
// dummy grow and shrink
|
||||||
|
q.Grow(0)
|
||||||
|
require.Equal(t, 0, q.Cap())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
q.Shrink(0)
|
||||||
|
require.Equal(t, 0, q.Cap())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
// add 3*dLen and leave 2*dLen
|
||||||
|
q.Grow(3 * dLen)
|
||||||
|
expected.buf = []int{}
|
||||||
|
require.Equal(t, 3*dLen, q.Cap())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
q.Shrink(2 * dLen)
|
||||||
|
require.Equal(t, 2*dLen, q.Cap())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
// add dLen items and play with cap
|
||||||
|
enq(t, q, data...)
|
||||||
|
expected.buf = data
|
||||||
|
expected.len = dLen
|
||||||
|
expected.stats.Enqueued = dLen
|
||||||
|
require.Equal(t, 2*dLen, q.Cap())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
q.Grow(2 * dLen)
|
||||||
|
require.GreaterOrEqual(t, q.Cap(), 3*dLen)
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
q.Shrink(0)
|
||||||
|
require.Equal(t, dLen, q.Cap())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
// remove all items and shrink to zero
|
||||||
|
deqAll(t, q, data...)
|
||||||
|
expected.buf = []int{}
|
||||||
|
expected.len = 0
|
||||||
|
expected.stats.Dequeued = dLen
|
||||||
|
require.Equal(t, dLen, q.Cap())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
|
||||||
|
q.Shrink(0)
|
||||||
|
expected.buf = nil
|
||||||
|
require.Equal(t, 0, q.Cap())
|
||||||
|
ringEq(t, expected, q)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// enq enqueues the given items into the given Ring.
|
||||||
|
func enq[T any](t *testing.T, q *Ring[T], s ...T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
initLen := q.Len()
|
||||||
|
initCap := q.Cap()
|
||||||
|
for _, v := range s {
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
q.Enqueue(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedLen := initLen + len(s)
|
||||||
|
if q.Max > 0 {
|
||||||
|
expectedMax := max(initCap, q.Max)
|
||||||
|
expectedLen = min(expectedLen, expectedMax)
|
||||||
|
}
|
||||||
|
require.Equal(t, expectedLen, q.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// deq dequeues len(expected) items from the given Ring and compares them to
|
||||||
|
// expected. Ring should have at least len(expected) items.
|
||||||
|
func deq[T any](t *testing.T, q *Ring[T], expected ...T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if q.Cap() == 0 {
|
||||||
|
require.Nil(t, q.buf) // internal state
|
||||||
|
require.Equal(t, 0, q.back) // internal state
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldLen := q.Len()
|
||||||
|
require.True(t, oldLen >= len(expected))
|
||||||
|
got := make([]T, len(expected))
|
||||||
|
for i := range got {
|
||||||
|
var val T
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
prePeekLen := q.Len()
|
||||||
|
val = q.Peek()
|
||||||
|
require.Equal(t, prePeekLen, q.Len())
|
||||||
|
got[i] = q.Dequeue()
|
||||||
|
})
|
||||||
|
require.Equal(t, val, got[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, expected, got)
|
||||||
|
require.Equal(t, oldLen-len(expected), q.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearRing calls Clear on the given Ring and performs a set of assertions that
|
||||||
|
// should be satisfied afterwards.
|
||||||
|
func clearRing[T any](t *testing.T, q *Ring[T]) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var expectedBuf []T
|
||||||
|
if clearShouldMigrate(q.Cap(), q.Min, q.Max) {
|
||||||
|
expectedBuf = make([]T, q.Min)
|
||||||
|
} else {
|
||||||
|
expectedBuf = make([]T, q.Cap())
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
q.Clear()
|
||||||
|
})
|
||||||
|
require.Equal(t, expectedBuf, q.buf) // internal state
|
||||||
|
require.Equal(t, 0, q.Len())
|
||||||
|
require.Equal(t, 0, q.back) // internal state
|
||||||
|
|
||||||
|
// dequeueing should yield zero values
|
||||||
|
var zero T
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
var val1, val2 T
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
val1 = q.Peek()
|
||||||
|
val1 = q.Dequeue()
|
||||||
|
})
|
||||||
|
require.Equal(t, zero, val1)
|
||||||
|
require.Equal(t, zero, val2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deqAll depletes the given Ring and compares the dequeued items to those
|
||||||
|
// provided.
|
||||||
|
func deqAll[T any](t *testing.T, q *Ring[T], expected ...T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
deq[T](t, q, expected...)
|
||||||
|
|
||||||
|
zeroS := make([]T, q.Cap())
|
||||||
|
require.Equal(t, zeroS, q.buf) // internal state
|
||||||
|
|
||||||
|
require.Equal(t, 0, q.Len())
|
||||||
|
|
||||||
|
// dequeueing further should yield zero values when empty
|
||||||
|
var zero T
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
var val1, val2 T
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
val1 = q.Peek()
|
||||||
|
val2 = q.Dequeue()
|
||||||
|
})
|
||||||
|
require.Equal(t, zero, val1)
|
||||||
|
require.Equal(t, zero, val2)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearRing(t, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ringEq tests that the given Rings are the same in many aspects. The following
|
||||||
|
// are the things that are not checked:
|
||||||
|
// - The values of Min and Max, since the code does not programmatically
|
||||||
|
// channge them
|
||||||
|
// - Allocation numbers (Cap, Grown, Shrunk, Allocs)
|
||||||
|
// - The free capacity to the right of `got`
|
||||||
|
func ringEq[T any](t *testing.T, expected, got *Ring[T]) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var expStats, gotStats RingStats
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
expected.WriteStats(&expStats)
|
||||||
|
got.WriteStats(&gotStats)
|
||||||
|
})
|
||||||
|
|
||||||
|
// capacity and allocations are to be tested separately
|
||||||
|
removeAllocStats(&expStats)
|
||||||
|
removeAllocStats(&gotStats)
|
||||||
|
|
||||||
|
require.Equal(t, expStats, gotStats, "expStats == gotStats")
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
require.Equal(t, expected.back, got.back, "expected.back == got.back")
|
||||||
|
// only check for used capacity
|
||||||
|
require.Equal(t, expected.buf, got.buf[:min(got.back+got.len, len(got.buf))],
|
||||||
|
"expected.buf == got.buf[:min(got.back+got.len, len(got.s))]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeAllocStats(s *RingStats) {
|
||||||
|
s.Cap = 0
|
||||||
|
s.Grown = 0
|
||||||
|
s.Shrunk = 0
|
||||||
|
s.Allocs = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinMaxValidity(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
Min, Max int
|
||||||
|
minIsValid, maxIsValid bool
|
||||||
|
}{
|
||||||
|
{Min: 0, Max: 0, minIsValid: false, maxIsValid: false},
|
||||||
|
{Min: 0, Max: 1, minIsValid: false, maxIsValid: true},
|
||||||
|
{Min: 0, Max: 2, minIsValid: false, maxIsValid: true},
|
||||||
|
{Min: 1, Max: 0, minIsValid: true, maxIsValid: false},
|
||||||
|
{Min: 1, Max: 1, minIsValid: true, maxIsValid: true},
|
||||||
|
{Min: 1, Max: 2, minIsValid: true, maxIsValid: true},
|
||||||
|
{Min: 2, Max: 0, minIsValid: true, maxIsValid: false},
|
||||||
|
{Min: 2, Max: 1, minIsValid: false, maxIsValid: false},
|
||||||
|
{Min: 2, Max: 2, minIsValid: true, maxIsValid: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testCases {
|
||||||
|
gotMinIsValid := minIsValid(tc.Min, tc.Max)
|
||||||
|
require.Equal(t, tc.minIsValid, gotMinIsValid,
|
||||||
|
"test index %d; test data: %#v", i, tc)
|
||||||
|
|
||||||
|
gotMaxIsValid := maxIsValid(tc.Min, tc.Max)
|
||||||
|
require.Equal(t, tc.maxIsValid, gotMaxIsValid,
|
||||||
|
"test index %d; test data: %#v", i, tc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClearShouldMigrate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
// we don't need to include Max in the test, we just disable it by
|
||||||
|
// passing zero because Max is only needed to establish the validity of
|
||||||
|
// Min. The validity of Min wrt Max is already covered in the test for
|
||||||
|
// minIsValid, and once Min is valid Max has no impact on the outcome of
|
||||||
|
// clearShouldMigrate.
|
||||||
|
CurCap, Min int
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{CurCap: 0, Min: 0, expected: false},
|
||||||
|
{CurCap: 0, Min: 9, expected: false},
|
||||||
|
{CurCap: 0, Min: 10, expected: false},
|
||||||
|
{CurCap: 0, Min: 11, expected: false},
|
||||||
|
{CurCap: 10, Min: 0, expected: false},
|
||||||
|
{CurCap: 10, Min: 9, expected: true},
|
||||||
|
{CurCap: 10, Min: 10, expected: false},
|
||||||
|
{CurCap: 10, Min: 11, expected: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testCases {
|
||||||
|
got := clearShouldMigrate(tc.CurCap, tc.Min, 0)
|
||||||
|
require.Equal(t, tc.expected, got,
|
||||||
|
"test index %d; test data: %#v", i, tc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixAllocSize(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
CurLen, Min, Max, NewCap, expected int
|
||||||
|
}{
|
||||||
|
// we don't need to add test cases for odd configurations of Min and Max
|
||||||
|
// not being valid for different reasons because that is already covered
|
||||||
|
// in the unit tests for minIsValid and maxIsValid. It suffices to
|
||||||
|
// provide a zero for Min or Max to disable their respective behaviour
|
||||||
|
|
||||||
|
{CurLen: 0, Min: 0, Max: 0, NewCap: 0, expected: 0},
|
||||||
|
{CurLen: 0, Min: 0, Max: 0, NewCap: 5, expected: 5},
|
||||||
|
|
||||||
|
{CurLen: 0, Min: 0, Max: 10, NewCap: 0, expected: 0},
|
||||||
|
{CurLen: 0, Min: 0, Max: 10, NewCap: 9, expected: 9},
|
||||||
|
{CurLen: 0, Min: 0, Max: 10, NewCap: 10, expected: 10},
|
||||||
|
{CurLen: 0, Min: 0, Max: 10, NewCap: 11, expected: 10},
|
||||||
|
|
||||||
|
{CurLen: 0, Min: 10, Max: 0, NewCap: 0, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 0, NewCap: 5, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 0, NewCap: 9, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 0, NewCap: 10, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 0, NewCap: 11, expected: 11},
|
||||||
|
|
||||||
|
{CurLen: 0, Min: 10, Max: 10, NewCap: 0, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 10, NewCap: 5, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 10, NewCap: 9, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 10, NewCap: 10, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 10, NewCap: 11, expected: 10},
|
||||||
|
|
||||||
|
{CurLen: 0, Min: 10, Max: 20, NewCap: 0, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 20, NewCap: 5, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 20, NewCap: 9, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 20, NewCap: 10, expected: 10},
|
||||||
|
{CurLen: 0, Min: 10, Max: 20, NewCap: 19, expected: 19},
|
||||||
|
{CurLen: 0, Min: 10, Max: 20, NewCap: 20, expected: 20},
|
||||||
|
{CurLen: 0, Min: 10, Max: 20, NewCap: 21, expected: 20},
|
||||||
|
|
||||||
|
{CurLen: 5, Min: 0, Max: 0, NewCap: 0, expected: 5},
|
||||||
|
{CurLen: 5, Min: 0, Max: 0, NewCap: 5, expected: 5},
|
||||||
|
{CurLen: 5, Min: 0, Max: 0, NewCap: 10, expected: 10},
|
||||||
|
|
||||||
|
{CurLen: 5, Min: 0, Max: 10, NewCap: 0, expected: 5},
|
||||||
|
{CurLen: 5, Min: 0, Max: 10, NewCap: 5, expected: 5},
|
||||||
|
{CurLen: 5, Min: 0, Max: 10, NewCap: 9, expected: 9},
|
||||||
|
{CurLen: 5, Min: 0, Max: 10, NewCap: 10, expected: 10},
|
||||||
|
{CurLen: 5, Min: 0, Max: 10, NewCap: 11, expected: 10},
|
||||||
|
|
||||||
|
{CurLen: 5, Min: 10, Max: 0, NewCap: 0, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 0, NewCap: 5, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 0, NewCap: 9, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 0, NewCap: 10, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 0, NewCap: 11, expected: 11},
|
||||||
|
|
||||||
|
{CurLen: 5, Min: 10, Max: 10, NewCap: 0, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 10, NewCap: 5, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 10, NewCap: 9, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 10, NewCap: 10, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 10, NewCap: 11, expected: 10},
|
||||||
|
|
||||||
|
{CurLen: 5, Min: 10, Max: 20, NewCap: 0, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 20, NewCap: 5, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 20, NewCap: 9, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 20, NewCap: 10, expected: 10},
|
||||||
|
{CurLen: 5, Min: 10, Max: 20, NewCap: 19, expected: 19},
|
||||||
|
{CurLen: 5, Min: 10, Max: 20, NewCap: 20, expected: 20},
|
||||||
|
{CurLen: 5, Min: 10, Max: 20, NewCap: 21, expected: 20},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testCases {
|
||||||
|
got := fixAllocSize(tc.CurLen, tc.Min, tc.Max, tc.NewCap)
|
||||||
|
require.Equal(t, tc.expected, got,
|
||||||
|
"test index %d; test data %#v", i, tc)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user