From fcded864375c5aeec15c30dc0fae8bc5c429c060 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Tue, 20 Oct 2026 19:29:49 +0200 Subject: [PATCH] support keyed pool items --- adaptive_test.go | 10 +- event_test.go | 8 +- lib.go | 107 ++++++++++--- maxto_test.go | 8 +- pool.go | 225 ++++++++++++++++++++------- pool_test.go | 398 ++++++++++++++++++++++++++++++++++++++++++++++- scenario_test.go | 4 +- 7 files changed, 659 insertions(+), 101 deletions(-) diff --git a/adaptive_test.go b/adaptive_test.go index f9ee7b0..e0d48c6 100644 --- a/adaptive_test.go +++ b/adaptive_test.go @@ -54,7 +54,7 @@ func TestAdaptive(t *testing.T) { var o pool.Options o.Testing.Clock = clock o.Testing.Bus = bus - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, o) var active [][]byte @@ -127,7 +127,7 @@ func TestAdaptive(t *testing.T) { steadyUseCycles = 8 ) - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{}) var active [][]byte @@ -174,7 +174,7 @@ func TestAdaptive(t *testing.T) { highLoadCycles = 8 ) - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{}) var active [][]byte @@ -231,7 +231,7 @@ func TestAdaptive(t *testing.T) { steadyUseCycles = 8 ) - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{}) var active [][]byte @@ -280,7 +280,7 @@ func TestAdaptive(t *testing.T) { highLoadCycles = 8 ) - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{}) var active [][]byte diff --git a/event_test.go b/event_test.go index 2f7881e..ba08447 100644 --- a/event_test.go +++ b/event_test.go @@ -8,7 +8,7 @@ import ( func TestEvent(t *testing.T) { t.Run("get put allocate free", func(t *testing.T) { - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } e := make(chan pool.Event, 7) o := pool.Options{ Algo: pool.Max(2), @@ -53,7 +53,7 @@ func TestEvent(t *testing.T) { }) t.Run("allocate error", func(t *testing.T) { - alloc := func() ([]byte, error) { return nil, errTest } + alloc := func(string) ([]byte, error) { return nil, errTest } e := make(chan pool.Event, 1) o := pool.Options{ Algo: pool.NoShrink(), @@ -74,7 +74,7 @@ func TestEvent(t *testing.T) { }) t.Run("drop", func(t *testing.T) { - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } e := make(chan pool.Event) o := pool.Options{ Algo: pool.NoShrink(), @@ -96,7 +96,7 @@ func TestEvent(t *testing.T) { }) t.Run("mask", func(t *testing.T) { - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } e := make(chan pool.Event, 1) o := pool.Options{ Algo: pool.NoShrink(), diff --git a/lib.go b/lib.go index 85c386e..597ec36 100644 --- a/lib.go +++ b/lib.go @@ -73,7 +73,7 @@ const ( FreeOperation // AllocateError is the type of events sent after a failed allocation. The error will not be included - // with the event, but it will be returned by the failed Get function call. + // with the event, but it will be returned by the failed Get* function call. AllocateError // AllEvents can be used as a mask that includes all the event types. @@ -101,7 +101,7 @@ type Event struct { // the pool guarantees that the algo instance is accessed only from one goroutine at a time. type Algo interface { - // Target is called on every Put operation with the current pool state as the input. It is expected to + // Target is called on every Put* operation with the current pool state as the input. It is expected to // return the target idle count, as dictated by the implementing algorithm. Optionally, a timeout value // can be returned (nextCheck), and if it is a positive value, the pool will call Target again after the // defined time expires to fetch the next target idle count. In each case, when Target returns a smaller @@ -110,8 +110,8 @@ type Algo interface { // When using nextCheck, not every returned value results in calling Target by the pool, only the ones // that were set after the previous check expired. // - // Implementations should consider that while the recommended way of using the pool is to only call Put - // with items that were received by calling Get, the pool itself doesn't prohibit calling Put with + // Implementations should consider that while the recommended way of using the pool is to only call Put* + // with items that were received by calling Get*, the pool itself doesn't prohibit calling Put* with // 'foreign' items. Target(Stats) (target int, nextCheck time.Duration) @@ -141,12 +141,12 @@ type Options struct { Testing struct { - // Clock is an optional clock meant to be used with testing. The main purpose is to avoid time sensitive - // tests running for a too long time. + // Clock is an optional clock meant to be used with testing. The main purpose is to avoid time + // sensitive tests running for a too long time. Clock times.Clock - // Bus is an optional signal bus meant to be used with testing. The main purpose is to ensure that - // specific blocks of code are executed in a predefined order during concurrent tests. + // Bus is an optional signal bus meant to be used with testing. The main purpose is to ensure + // that specific blocks of code are executed in a predefined order during concurrent tests. Bus *syncbus.SyncBus } } @@ -158,8 +158,8 @@ type Pool[R any] struct { pool pool[R] } -// ErrNoItems is returned on Get calls when the pool does not have enough items and it was initialized without an -// allocate function. +// ErrNoItems is returned on Get* calls when the pool does not have enough items and it was initialized without +// an allocate function. var ErrNoItems = errors.New("not enough items in the pool") // ErrUninitialized is returned when the pool was not initialized. @@ -230,7 +230,7 @@ func (s Stats) String() string { // on very sudden drops in traffic, it ensures the eventual release of all pooled items with an internal // background job. This job is timed based on the duration of the last active usage session (the period during // which there were active items). Together with the pool implementation, it always reuses the most recent -// items, as in LIFO for Get and FIFO for Free. +// items, as in LIFO for Get* and FIFO for Free. // // We need to be aware of some potential caveats due to its zero-config nature. Because it relies on the // sequence of operations rather than wall-clock thresholds for its core logic, the algorithm treats rapid @@ -243,7 +243,7 @@ func Adaptive() Algo { // MaxTimeout creates a pool shrink algorithm instance, that releases items whenever the number of idle items // exceeds max, and it also releases those items that were idle for too long. Together with the pool, it ensures -// that the Get operation is LIFO and the Free operation is FIFO. +// that the Get* operation is LIFO and the Free operation is FIFO. // // If max <= 0, the max pool size is not enforced. If to <= 0, the timeout is not enforced. func MaxTimeout(max int, to time.Duration) Algo { @@ -268,23 +268,55 @@ func NoShrink() Algo { // Make initializes a Pool instance. // -// The parameter alloc is used on Get operations when the pool is empty. If alloc is nil, and the pool is empty -// at the time of calling Get, Get will return ErrEmpty. If alloc returns an error, the same error is returned -// by Get. If events were configured, alloc triggers an AllocateOperation event. This event is typically the -// same as the GetOperation event. +// The parameter alloc is used on Get* operations when the pool is empty. For unkeyed Get* operations, the input +// key will be the empty string. If alloc is nil, and the pool is empty at the time of calling Get*, Get* will +// return ErrEmpty. If alloc returns an error, the same error is returned by Get*. If events were configured, +// alloc triggers an AllocateOperation event. This event is typically the same as the GetOperation event. // // The parameter free is called when an item is released from the pool, with the item being released as the // argument. It can be nil for resource types that don't need explicit deallocation. If events were configured, // releasing an item triggers a FreeOperation event, regardless if the free parameter is nil. -func Make[R any](alloc func() (R, error), free func(R), o Options) Pool[R] { +func Make[R any](alloc func(string) (R, error), free func(R), o Options) Pool[R] { return Pool[R]{pool: makePool(alloc, free, o)} } -// GetN returns n items from the pool. If the pool is empty and no allocation function was configured, it -// returns ErrEmpty. If the pool has less than n items and no allocation function was configured, it returns the -// available items and ErrEtmpy. If the pool is empty or has less than the requested items, and the allocation -// function returns an error, it returns the available items and that error. If events were configured, GetN +// GetKeyN returns n items with the provided key from the pool. If the pool has no items with the provided key +// and no allocation function was configured, it returns ErrEmpty. If the pool has less than n items with the +// provided key and no allocation function was configured, it returns ErrEmpty. If the pool has no items with +// the provided key or has less than the requested items, and the allocation function returns an error, it +// returns the available items and that error. If events were configured, GetKeyN triggers a GetOperation event. +func (p Pool[R]) GetKeyN(k string, n int) ([]R, error) { + if p.pool.state == nil { + return nil, ErrUninitialized + } + + return p.pool.getKey(key{value: k}, n) +} + +// GetKey returns an item from the pool with the provided key. If the pool has no items with the provided key +// and no allocation function was configured, it returns ErrEmpty. If the pool has no items with the provided +// key, and the allocation function returns an error, it returns that error. If events were configured, GetKey // triggers a GetOperation event. +func (p Pool[R]) GetKey(k string) (R, error) { + if p.pool.state == nil { + var r R + return r, ErrUninitialized + } + + r, err := p.pool.getKey(key{value: k}, 1) + if err != nil { + var r R + return r, err + } + + return r[0], nil +} + +// GetN returns n items from the pool. If the pool is empty and no allocation function was configured, it +// returns ErrEmpty. If the pool has less than n items and no allocation function was configured, it returns +// ErrEmpty. If the pool is empty or has less than the requested items, and the allocation function returns an +// error, it returns the available items and that error. If events were configured, GetN triggers a GetOperation +// event. func (p Pool[R]) GetN(n int) ([]R, error) { if p.pool.state == nil { return nil, ErrUninitialized @@ -304,13 +336,28 @@ func (p Pool[R]) Get() (R, error) { r, err := p.pool.get(1) if err != nil { - var rr R - return rr, err + var r R + return r, err } return r[0], nil } +// PutKey stores one or more items in the pool with the provided key. If events were configured, it triggers a +// PutOperation event. +// +// It is recommended to use it only with items that were received by the GetKey method. While it is allowed to +// put other items in the pool, it may change the way the shrinking algorithm works. E.g. it can be considered +// as a sudden drop in the number of active items. If the pool needs to be prewarmed, or prepared for an +// expected spike of traffic, consider using the Load method. +func (p Pool[R]) PutKey(k string, i ...R) { + if p.pool.state == nil { + return + } + + p.pool.putKey(key{value: k}, i...) +} + // Put stores one or more items in the pool. If events were configured, it triggers a PutOperation event. // // It is recommended to use it only with items that were received by the Get method. While it is allowed to put @@ -325,7 +372,8 @@ func (p Pool[R]) Put(i ...R) { p.pool.put(i...) } -// DropN indicates to the pool that N items received by Get were dropped and will not be put back into the pool. +// DropN indicates to the pool that N items received by Get* were dropped and will not be put back into the +// pool. // // While it's not mandatory call it when some items were dropped, it has a twofold purpose. Depending on the // shrinking algorithm, it may work more consistently, if the algorithm is notified about the change in the @@ -347,6 +395,17 @@ func (p Pool[R]) Drop() { p.pool.drop(1) } +// LoadKey can be used to populate the pool with items for the provided key that were not allocated as the +// result of the GetKey operation. It can be useful in scenarios where prewarming or preparing for a sudden +// traffic spike is necessary. If events were configured, it triggers a LoadOperation event. +func (p Pool[R]) LoadKey(k string, i []R) { + if p.pool.state == nil { + return + } + + p.pool.loadKey(key{value: k}, i) +} + // Load can be used to populate the pool with items that were not allocated as the result of the Get operation. // It can be useful in scenarios where prewarming or preparing for a sudden traffic spike is necessary. If // events were configured, it triggers a LoadOperation event. diff --git a/maxto_test.go b/maxto_test.go index 46b4a3d..c18a460 100644 --- a/maxto_test.go +++ b/maxto_test.go @@ -225,7 +225,7 @@ func TestMaxTO(t *testing.T) { var o pool.Options o.Algo = pool.MaxTimeout(15, 300*time.Millisecond) o.Testing.Clock = clock - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, o) var active [][]byte @@ -280,7 +280,7 @@ func TestMaxTO(t *testing.T) { var o pool.Options o.Algo = pool.MaxTimeout(15, 300*time.Millisecond) o.Testing.Clock = clock - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, o) var active [][]byte @@ -348,7 +348,7 @@ func TestMaxTO(t *testing.T) { var o pool.Options o.Algo = pool.MaxTimeout(15, 300*time.Millisecond) o.Testing.Clock = clock - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, o) var active [][]byte @@ -405,7 +405,7 @@ func TestMaxTO(t *testing.T) { var o pool.Options o.Algo = pool.MaxTimeout(15, 300*time.Millisecond) o.Testing.Clock = clock - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, o) var active [][]byte diff --git a/pool.go b/pool.go index c73de2b..d778fed 100644 --- a/pool.go +++ b/pool.go @@ -2,23 +2,33 @@ package pool import ( "code.squareroundforest.org/arpio/times" + "errors" "time" ) +type key struct { + null bool + value string +} + type state[R any] struct { - items []R + keys []key + items map[key][]R stats Stats forcedCheckPending bool + deletedKeys int } type pool[R any] struct { state chan state[R] - alloc func() (R, error) + alloc func(string) (R, error) free func(r R) options Options } -func makePool[R any](alloc func() (R, error), free func(r R), o Options) pool[R] { +var nullKey = key{null: true} + +func makePool[R any](alloc func(string) (R, error), free func(r R), o Options) pool[R] { if o.Algo == nil { o.Algo = Adaptive() } @@ -32,7 +42,7 @@ func makePool[R any](alloc func() (R, error), free func(r R), o Options) pool[R] } s := make(chan state[R], 1) - s <- state[R]{} + s <- state[R]{items: make(map[key][]R)} return pool[R]{ state: s, alloc: alloc, @@ -70,7 +80,68 @@ func (p pool[R]) sendEvent(e EventType, s Stats) { } } -func (p pool[R]) get(n int) ([]R, error) { +func appendKeys(all []key, k key, n int) []key { + for i := 0; i < n; i++ { + all = append(all, k) + } + + return all +} + +func dropKeys(all []key, k key, n int) []key { + first := len(all) + last := first + for n > 0 { + if first == 0 && last == 0 { + panic(errors.New("damaged pool: missing keys")) + } + + if first > 0 && all[first-1] == k && last-first < n { + first-- + continue + } + + if first == last { + first-- + last-- + continue + } + + var zero key + copy(all[first:], all[last:]) + for i := len(all) + first - last; i < len(all); i++ { + all[i] = zero + } + + all = all[:len(all)+first-last] + n += first - last + first-- + last = first + } + + return all +} + +func resetItemsMap[R any](s state[R]) state[R] { + if s.deletedKeys < 1<<12 { + return s + } + + if s.deletedKeys <= len(s.items)*2 { + return s + } + + next := make(map[key][]R) + for k := range s.items { + next[k] = s.items[k] + } + + s.items = next + s.deletedKeys = 0 + return s +} + +func (p pool[R]) getKey(k key, n int) ([]R, error) { if n <= 0 { return nil, nil } @@ -83,63 +154,76 @@ func (p pool[R]) get(n int) ([]R, error) { var event EventType event |= GetOperation s.stats.Get++ - if len(s.items) < n && p.alloc == nil { + if len(s.items[k]) < n && p.alloc == nil { return nil, ErrNoItems } r := make([]R, n) - if len(s.items) <= n { - copy(r, s.items) + if len(s.items[k]) <= n { + copy(r, s.items[k]) } - if len(s.items) > n { + if len(s.items[k]) > n { // using the items from the end of the list: - copy(r, s.items[len(s.items)-n:]) + copy(r, s.items[k][len(s.items[k])-n:]) } - if len(s.items) < n { + if len(s.items[k]) < n { var err error event |= AllocateOperation - for i := len(s.items); i < n; i++ { + for i := len(s.items[k]); i < n; i++ { s.stats.Alloc++ - r[i], err = p.alloc() - if err == nil { + if r[i], err = p.alloc(k.value); err == nil { continue } - s.items = append(s.items, r[len(s.items):i]...) - s.stats.Idle = len(s.items) + s.items[k] = append(s.items[k], r[len(s.items[k]):i]...) + s.keys = appendKeys(s.keys, k, i-len(s.items[k])) + s.stats.Idle = len(s.keys) event |= AllocateError p.sendEvent(event, s.stats) return nil, err } } - if len(s.items) <= n { - var zero R - for i := 0; i < len(s.items); i++ { - s.items[i] = zero - } - - s.items = nil + // drop enough keys from the end: + d := n + if len(s.items[k]) < n { + d = len(s.items[k]) } - if len(s.items) > n { + s.keys = dropKeys(s.keys, k, d) + if len(s.items[k]) <= n { var zero R - for i := len(s.items) - n; i < len(s.items); i++ { - s.items[i] = zero + for i := 0; i < len(s.items[k]); i++ { + s.items[k][i] = zero } - s.items = s.items[:len(s.items)-n] + delete(s.items, k) + s.deletedKeys++ } - s.stats.Idle = len(s.items) + s = resetItemsMap(s) + if len(s.items[k]) > n { + var zero R + for i := len(s.items[k]) - n; i < len(s.items[k]); i++ { + s.items[k][i] = zero + } + + s.items[k] = s.items[k][:len(s.items[k])-n] + } + + s.stats.Idle = len(s.keys) s.stats.Active += n p.sendEvent(event, s.stats) return r, nil } -func (p pool[R]) put(r ...R) { +func (p pool[R]) get(n int) ([]R, error) { + return p.getKey(nullKey, n) +} + +func (p pool[R]) putKey(k key, r ...R) { if len(r) == 0 { return } @@ -162,25 +246,31 @@ func (p pool[R]) put(r ...R) { t, f := p.options.Algo.Target(s.stats) switch { - case t >= len(s.items)+len(r): - s.items = append(s.items, r...) - case t > len(s.items) && t < len(s.items)+len(r): + case t >= len(s.keys)+len(r): + s.items[k] = append(s.items[k], r...) + s.keys = appendKeys(s.keys, k, len(r)) + case t > len(s.keys) && t < len(s.keys)+len(r): event |= FreeOperation - s = p.freeItems(s, r[t-len(s.items):]) - s.items = append(s.items, r[:t-len(s.items)]...) + s = p.freeItems(s, r[t-len(s.keys):]) + s.items[k] = append(s.items[k], r[:t-len(s.keys)]...) + s.keys = appendKeys(s.keys, k, t-len(s.keys)) default: event |= FreeOperation s = p.freeItems(s, r) s = p.freeAboveTarget(s, t) } - s.stats.Idle = len(s.items) + s.stats.Idle = len(s.keys) p.sendEvent(event, s.stats) if f > 0 { s = p.forcedCheck(s, f) } } +func (p pool[R]) put(r ...R) { + p.putKey(nullKey, r...) +} + func (p pool[R]) drop(n int) { if n <= 0 { return @@ -200,20 +290,20 @@ func (p pool[R]) drop(n int) { } t, f := p.options.Algo.Target(s.stats) - if t < len(s.items) { + if t < len(s.keys) { event |= FreeOperation s = p.freeAboveTarget(s, t) } - s.stats.Idle = len(s.items) + s.stats.Idle = len(s.keys) p.sendEvent(event, s.stats) if f > 0 { s = p.forcedCheck(s, f) } } -func (p pool[R]) load(i []R) { - if len(i) == 0 { +func (p pool[R]) loadKey(k key, r []R) { + if len(r) == 0 { return } @@ -222,14 +312,19 @@ func (p pool[R]) load(i []R) { p.state <- s }() - s.items = append(s.items, i...) - s.stats.Idle = len(s.items) - s.stats.Load += len(i) - p.options.Algo.Load(len(i)) + s.items[k] = append(s.items[k], r...) + s.keys = appendKeys(s.keys, k, len(r)) + s.stats.Idle = len(s.keys) + s.stats.Load += len(r) + p.options.Algo.Load(len(r)) event := LoadOperation p.sendEvent(event, s.stats) } +func (p pool[R]) load(i []R) { + p.loadKey(nullKey, i) +} + func (p pool[R]) forcedCheck(s state[R], timeout time.Duration) state[R] { if s.forcedCheckPending { return s @@ -260,23 +355,38 @@ func (p pool[R]) freeItems(s state[R], r []R) state[R] { } func (p pool[R]) freeAboveTarget(s state[R], target int) state[R] { - if len(s.items) <= target { + if len(s.keys) <= target { return s } - f := s.items[:len(s.items)-target] + var ( + zeroKey key + zeroItem R + ) - var zero R - for i := 0; i < len(s.items)-target; i++ { - s.items[i] = zero + d := len(s.keys) - target + r := make([]R, 0, d) + for _, k := range s.keys[:d] { + if len(s.items[k]) == 0 { + panic(errors.New("damaged pool: missing item")) + } + + r = append(r, s.items[k][0]) + s.items[k][0] = zeroItem + s.items[k] = s.items[k][1:] + if len(s.items[k]) == 0 { + delete(s.items, k) + s.deletedKeys++ + } } - s.items = s.items[len(s.items)-target:] - if len(s.items) == 0 { - s.items = nil + s = resetItemsMap(s) + for i := 0; i < d; i++ { + s.keys[i] = zeroKey } - return p.freeItems(s, f) + s.keys = s.keys[d:] + return p.freeItems(s, r) } func (p pool[R]) checkAndFree() { @@ -289,7 +399,7 @@ func (p pool[R]) checkAndFree() { t, f := p.options.Algo.Target(s.stats) prev := s.stats.Free s = p.freeAboveTarget(s, t) - s.stats.Idle = len(s.items) + s.stats.Idle = len(s.keys) if s.stats.Free > prev { p.sendEvent(FreeOperation, s.stats) } @@ -309,8 +419,13 @@ func (p pool[R]) freePool() { s.stats.Idle = 0 prev := s.stats.Free - s = p.freeItems(s, s.items) - s.items = nil + for k := range s.items { + s = p.freeItems(s, s.items[k]) + } + + s.keys = nil + s.items = make(map[key][]R) + s.deletedKeys = 0 if s.stats.Free > prev { p.sendEvent(FreeOperation, s.stats) } diff --git a/pool_test.go b/pool_test.go index 5ce70f1..dcd88d0 100644 --- a/pool_test.go +++ b/pool_test.go @@ -13,7 +13,7 @@ var errTest = errors.New("test error") func TestPool(t *testing.T) { t.Run("initial stats", func(t *testing.T) { - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) s := p.Stats() e := pool.Stats{} @@ -23,7 +23,7 @@ func TestPool(t *testing.T) { }) t.Run("get when empty", func(t *testing.T) { - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) b, err := p.Get() if err != nil { @@ -42,7 +42,7 @@ func TestPool(t *testing.T) { }) t.Run("get pooled own", func(t *testing.T) { - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) b, err := p.Get() if err != nil { @@ -71,7 +71,7 @@ func TestPool(t *testing.T) { }) t.Run("get pooled foreign", func(t *testing.T) { - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) p.Put(make([]byte, 1<<6)) b, err := p.Get() @@ -124,7 +124,7 @@ func TestPool(t *testing.T) { }) t.Run("allocation error", func(t *testing.T) { - alloc := func() ([]byte, error) { return nil, errTest } + alloc := func(string) ([]byte, error) { return nil, errTest } p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) _, err := p.Get() if !errors.Is(err, errTest) { @@ -298,7 +298,7 @@ func TestPool(t *testing.T) { }) t.Run("use default algo", func(t *testing.T) { - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{}) var bs [][]byte @@ -323,7 +323,7 @@ func TestPool(t *testing.T) { }) t.Run("drop", func(t *testing.T) { - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } p := pool.Make(alloc, nil, pool.Options{}) _, err := p.Get() if err != nil { @@ -339,5 +339,389 @@ func TestPool(t *testing.T) { p.Drop() s = p.Stats() e = pool.Stats{Alloc: 1, Get: 1, Drop: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("key", func(t *testing.T) { + t.Run("distinct", func(t *testing.T) { + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } + p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) + f, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + if len(f) != 1<<9 { + t.Fatal(len(f)) + } + + b, err := p.GetKey("bar") + if err != nil { + t.Fatal(err) + } + + if len(b) != 1<<9 { + t.Fatal(len(b)) + } + + p.PutKey("foo", f) + f, err = p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + if len(f) != 1<<9 { + t.Fatal(len(f)) + } + + b, err = p.GetKey("bar") + if err != nil { + t.Fatal(err) + } + + if len(b) != 1<<9 { + t.Fatal(len(b)) + } + + s := p.Stats() + e := pool.Stats{Alloc: 3, Get: 4, Active: 3, Put: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("get", func(t *testing.T) { + t.Run("empty", func(t *testing.T) { + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } + p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) + b, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + if len(b) != 1<<9 { + t.Fatal(len(b)) + } + + s := p.Stats() + e := pool.Stats{Alloc: 1, Get: 1, Active: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("pooled own", func(t *testing.T) { + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } + p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) + b, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + if len(b) != 1<<9 { + t.Fatal(len(b)) + } + + p.PutKey("foo", b) + b, err = p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + if len(b) != 1<<9 { + t.Fatal(len(b)) + } + + s := p.Stats() + e := pool.Stats{Alloc: 1, Get: 2, Put: 1, Active: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("pooled foreign", func(t *testing.T) { + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } + p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) + p.PutKey("foo", make([]byte, 1<<6)) + b, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + if len(b) != 1<<6 { + t.Fatal(len(b)) + } + + s := p.Stats() + e := pool.Stats{Get: 1, Put: 1, Active: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("own alloc not available", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + _, err := p.GetKey("foo") + if !errors.Is(err, pool.ErrNoItems) { + t.Fatal(err) + } + + s := p.Stats() + e := pool.Stats{Get: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("foreign no alloc", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + p.PutKey("foo", make([]byte, 1<<6)) + b, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + if len(b) != 1<<6 { + t.Fatal(len(b)) + } + + s := p.Stats() + e := pool.Stats{Get: 1, Put: 1, Active: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("multiple", func(t *testing.T) { + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } + p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) + b0, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + b1, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + b2, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + for _, b := range [][]byte{b0, b1, b2} { + if len(b) != 1<<9 { + t.Fatal(len(b)) + } + } + + p.PutKey("foo", b0) + p.PutKey("foo", b1) + p.PutKey("foo", b2) + b, err := p.GetKeyN("foo", 3) + if err != nil { + t.Fatal(err) + } + + if len(b) != 3 { + t.Fatal(len(b)) + } + + for i := range b { + if len(b[i]) != 1<<9 { + t.Fatal(len(b)) + } + } + + s := p.Stats() + e := pool.Stats{Alloc: 3, Get: 4, Put: 3, Active: 3} + if s != e { + t.Fatal(s) + } + }) + }) + + t.Run("put and release", func(t *testing.T) { + t.Run("foreign not empty", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + p.PutKey("foo", make([]byte, 1<<6)) + p.PutKey("foo", make([]byte, 1<<6)) + b, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + if len(b) != 1<<6 { + t.Fatal(len(b)) + } + + s := p.Stats() + e := pool.Stats{Get: 1, Put: 2, Active: 1, Idle: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("release on put no free", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.Max(2)}) + p.PutKey("foo", make([]byte, 1<<9)) + p.PutKey("foo", make([]byte, 1<<9)) + p.PutKey("foo", make([]byte, 1<<9)) + s := p.Stats() + e := pool.Stats{Put: 3, Idle: 2, Free: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("release on put with free", func(t *testing.T) { + var freeCount int + f := func([]byte) { freeCount++ } + p := pool.Make(nil, f, pool.Options{Algo: pool.Max(2)}) + p.PutKey("foo", make([]byte, 1<<9)) + p.PutKey("foo", make([]byte, 1<<9)) + p.PutKey("foo", make([]byte, 1<<9)) + if freeCount != 1 { + t.Fatal(freeCount) + } + + s := p.Stats() + e := pool.Stats{Put: 3, Idle: 2, Free: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("release all no free", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + p.PutKey("foo", make([]byte, 1<<9)) + p.PutKey("foo", make([]byte, 1<<9)) + p.PutKey("foo", make([]byte, 1<<9)) + p.Free() + s := p.Stats() + e := pool.Stats{Put: 3, Idle: 0, Free: 3} + if s != e { + t.Fatal(s) + } + }) + + t.Run("release all with free", func(t *testing.T) { + var freeCount int + f := func([]byte) { freeCount++ } + p := pool.Make[[]byte](nil, f, pool.Options{Algo: pool.NoShrink()}) + p.PutKey("foo", make([]byte, 1<<9)) + p.PutKey("foo", make([]byte, 1<<9)) + p.PutKey("foo", make([]byte, 1<<9)) + p.Free() + if freeCount != 3 { + t.Fatal(freeCount) + } + + s := p.Stats() + e := pool.Stats{Put: 3, Idle: 0, Free: 3} + if s != e { + t.Fatal(s) + } + }) + + t.Run("release on timeout no free", func(t *testing.T) { + c := times.Test() + b := syncbus.New(time.Second) + + var o pool.Options + o.Algo = pool.Timeout(3 * time.Millisecond) + o.Testing.Clock = c + o.Testing.Bus = b + p := pool.Make[[]byte](nil, nil, o) + p.PutKey("foo", make([]byte, 1<<9)) + if err := b.Wait("background-job-waiting"); err != nil { + t.Fatal(err) + } + + c.Pass(2 * time.Millisecond) + p.PutKey("foo", make([]byte, 1<<9)) + c.Pass(2 * time.Millisecond) + if err := b.Wait("free-idle-done"); err != nil { + t.Fatal(err) + } + + s := p.Stats() + e := pool.Stats{Put: 2, Idle: 1, Free: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("release on timeout with free", func(t *testing.T) { + var freeCount int + f := func([]byte) { freeCount++ } + c := times.Test() + b := syncbus.New(time.Second) + + var o pool.Options + o.Algo = pool.Timeout(3 * time.Millisecond) + o.Testing.Clock = c + o.Testing.Bus = b + p := pool.Make[[]byte](nil, f, o) + p.PutKey("foo", make([]byte, 1<<9)) + if err := b.Wait("background-job-waiting"); err != nil { + t.Fatal(err) + } + + c.Pass(2 * time.Millisecond) + p.PutKey("foo", make([]byte, 1<<9)) + c.Pass(2 * time.Millisecond) + if err := b.Wait("free-idle-done"); err != nil { + t.Fatal(err) + } + + s := p.Stats() + e := pool.Stats{Put: 2, Idle: 1, Free: 1} + if s != e || freeCount != 1 { + t.Fatal(s, freeCount) + } + }) + + t.Run("multiple", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + p.PutKey( + "foo", + make([]byte, 1<<6), + make([]byte, 1<<6), + make([]byte, 1<<6), + ) + b, err := p.GetKey("foo") + if err != nil { + t.Fatal(err) + } + + if len(b) != 1<<6 { + t.Fatal(len(b)) + } + + s := p.Stats() + e := pool.Stats{Get: 1, Put: 1, Active: 1, Idle: 2} + if s != e { + t.Fatal(s) + } + }) + }) + + t.Run("load", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + l := make([][]byte, 9) + for i := 0; i < len(l); i++ { + l[i] = make([]byte, 1<<9) + } + + p.LoadKey("foo", l) + s := p.Stats() + if s.Idle != 9 { + t.Fatal(s) + } + }) }) } diff --git a/scenario_test.go b/scenario_test.go index d28bd65..c7b36d9 100644 --- a/scenario_test.go +++ b/scenario_test.go @@ -96,7 +96,7 @@ func (s stats) push(a pool.Stats) { func testScenario(t *testing.T, o scenarioOptions, step scenarioStep, verify verifyScenario) { for _, n := range o.exclude { if strings.HasSuffix(t.Name(), n) { - t.Skip() + t.Skip("test excluded") } } @@ -119,7 +119,7 @@ func testScenario(t *testing.T, o scenarioOptions, step scenarioStep, verify ver clock = testClock } - alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + alloc := func(string) ([]byte, error) { return make([]byte, 1<<9), nil } var po pool.Options po.Algo = o.algo