From 8495fcf619255a8a0aad619e0851eed48fd38578 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Thu, 5 Mar 2026 12:34:35 +0100 Subject: [PATCH] testing --- adapative.go | 12 +- adaptive_test.go | 6 + event_test.go | 124 ++++++++++++++++++ go.mod | 7 + go.sum | 2 + lib.go | 55 +++++++- maxto.go | 12 +- pool.go | 20 ++- pool_test.go | 330 ++++++++++++++++++++++++++++++++++++++++++++--- 9 files changed, 543 insertions(+), 25 deletions(-) create mode 100644 adaptive_test.go create mode 100644 go.sum diff --git a/adapative.go b/adapative.go index ffbcc6f..a38aedc 100644 --- a/adapative.go +++ b/adapative.go @@ -1,6 +1,9 @@ package pool -import "time" +import ( + "code.squareroundforest.org/arpio/times" + "time" +) const ( // arbitrary values to be most likely out of sync with anything else: @@ -9,6 +12,7 @@ const ( ) type adaptive struct { + clock times.Clock activeTime time.Time nsTO time.Duration idle bool @@ -20,6 +24,10 @@ func makeAdaptiveAlgo() *adaptive { return &adaptive{idle: true} } +func (a *adaptive) setClock(c times.Clock) { + a.clock = c +} + func abs(v int) int { if v >= 0 { return v @@ -65,7 +73,7 @@ func (a *adaptive) nightshift(s Stats) time.Duration { return 0 } - now := time.Now() + now := a.clock.Now() a.idle = !a.idle if !a.idle { a.activeTime = now diff --git a/adaptive_test.go b/adaptive_test.go new file mode 100644 index 0000000..5764bfe --- /dev/null +++ b/adaptive_test.go @@ -0,0 +1,6 @@ +package pool_test + +import "testing" + +func TestAdaptive(t *testing.T) { +} diff --git a/event_test.go b/event_test.go index 2c4370a..2f7881e 100644 --- a/event_test.go +++ b/event_test.go @@ -1 +1,125 @@ package pool_test + +import ( + "code.squareroundforest.org/arpio/pool" + "errors" + "testing" +) + +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 } + e := make(chan pool.Event, 7) + o := pool.Options{ + Algo: pool.Max(2), + Events: e, + EventMask: pool.AllEvents, + } + + p := pool.Make(alloc, nil, o) + + var b [][]byte + for i := 0; i < 3; i++ { + bi, err := p.Get() + if err != nil { + t.Fatal(err) + } + + b = append(b, bi) + } + + for _, bi := range b { + p.Put(bi) + } + + for i := 0; i < 3; i++ { + ev := <-e + if ev.Type != pool.GetOperation|pool.AllocateOperation { + t.Fatal(ev) + } + } + + for i := 0; i < 2; i++ { + ev := <-e + if ev.Type != pool.PutOperation { + t.Fatal(ev) + } + } + + ev := <-e + if ev.Type != pool.PutOperation|pool.FreeOperation { + t.Fatal(ev) + } + }) + + t.Run("allocate error", func(t *testing.T) { + alloc := func() ([]byte, error) { return nil, errTest } + e := make(chan pool.Event, 1) + o := pool.Options{ + Algo: pool.NoShrink(), + Events: e, + EventMask: pool.AllEvents, + } + + p := pool.Make(alloc, nil, o) + _, err := p.Get() + if !errors.Is(err, errTest) { + t.Fatal(err) + } + + ev := <-e + if ev.Type != pool.GetOperation|pool.AllocateOperation|pool.AllocateError { + t.Fatal(ev) + } + }) + + t.Run("drop", func(t *testing.T) { + alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + e := make(chan pool.Event) + o := pool.Options{ + Algo: pool.NoShrink(), + Events: e, + EventMask: pool.AllEvents, + } + + p := pool.Make(alloc, nil, o) + _, err := p.Get() + if err != nil { + t.Fatal(err) + } + + select { + case <-e: + t.Fatal("unexpected event") + default: + } + }) + + t.Run("mask", func(t *testing.T) { + alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + e := make(chan pool.Event, 1) + o := pool.Options{ + Algo: pool.NoShrink(), + Events: e, + EventMask: pool.GetOperation, + } + + p := pool.Make(alloc, nil, o) + b, err := p.Get() + if err != nil { + t.Fatal(err) + } + + p.Put(b) + ev := <-e + if ev.Type != pool.AllocateOperation|pool.GetOperation { + t.Fatal(ev) + } + + select { + case <-e: + t.Fatal("unexpected event") + default: + } + }) +} diff --git a/go.mod b/go.mod index 51d8013..e112747 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module code.squareroundforest.org/arpio/pool go 1.25.6 + +require ( + code.squareroundforest.org/arpio/syncbus v0.0.0-20260222175441-f7da66ad4045 + code.squareroundforest.org/arpio/times v0.0.0-20260304202452-0bdc043a8aa6 +) + +replace code.squareroundforest.org/arpio/times => ../times diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..649789f --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +code.squareroundforest.org/arpio/syncbus v0.0.0-20260222175441-f7da66ad4045 h1:eSg4fnu8x6/7B6aem2ibxHX8SxFs9Mo2n2etWg4eGFY= +code.squareroundforest.org/arpio/syncbus v0.0.0-20260222175441-f7da66ad4045/go.mod h1:xZqPFR30EESkog+JzR40zDKVoBc7zmrV1X+Wo0v86p4= diff --git a/lib.go b/lib.go index df0b57f..57f92a0 100644 --- a/lib.go +++ b/lib.go @@ -6,7 +6,11 @@ package pool import ( + "code.squareroundforest.org/arpio/syncbus" + "code.squareroundforest.org/arpio/times" "errors" + "fmt" + "strings" "time" ) @@ -28,6 +32,8 @@ const ( AllocateOperation FreeOperation AllocateError + + AllEvents = GetOperation | PutOperation | AllocateOperation | FreeOperation | AllocateError ) type Event struct { @@ -54,13 +60,60 @@ type Options struct { EventMask EventType Algo Algo + Clock times.Clock + TestBus *syncbus.SyncBus } type Pool[R any] struct { pool pool[R] } -var ErrEmptyPool = errors.New("empty pool") +var ErrEmpty = errors.New("empty pool") + +func (et EventType) String() string { + var s []string + if et&GetOperation != 0 { + s = append(s, "get") + } + + if et&PutOperation != 0 { + s = append(s, "put") + } + + if et&AllocateOperation != 0 { + s = append(s, "allocate") + } + + if et&FreeOperation != 0 { + s = append(s, "free") + } + + if et&AllocateError != 0 { + s = append(s, "allocerr") + } + + if len(s) == 0 { + return "none" + } + + return strings.Join(s, "|") +} + +func (ev Event) String() string { + return fmt.Sprintf("%v; %v", ev.Type, ev.Stats) +} + +func (s Stats) String() string { + return fmt.Sprintf( + "idle: %d, active: %d, get: %d, put: %d, alloc: %d, free: %d", + s.Idle, + s.Active, + s.Get, + s.Put, + s.Alloc, + s.Free, + ) +} // zero-config func Adaptive() Algo { diff --git a/maxto.go b/maxto.go index a8ec955..3d59eeb 100644 --- a/maxto.go +++ b/maxto.go @@ -1,8 +1,12 @@ package pool -import "time" +import ( + "code.squareroundforest.org/arpio/times" + "time" +) type maxTimeout struct { + clock times.Clock max int to time.Duration items []time.Time @@ -16,6 +20,10 @@ func makeMaxTimeout(max int, to time.Duration) *maxTimeout { } } +func (a *maxTimeout) setClock(c times.Clock) { + a.clock = c +} + func (a *maxTimeout) Target(s Stats) (int, time.Duration) { if a.max <= 0 && a.to <= 0 { return s.Idle, 0 @@ -42,7 +50,7 @@ func (a *maxTimeout) Target(s Stats) (int, time.Duration) { } } - now := time.Now() + now := a.clock.Now() for len(a.items) < s.Idle { a.items = append(a.items, now) } diff --git a/pool.go b/pool.go index 4fa1548..f548c26 100644 --- a/pool.go +++ b/pool.go @@ -1,6 +1,9 @@ package pool -import "time" +import ( + "code.squareroundforest.org/arpio/times" + "time" +) type state[R any] struct { items []R @@ -20,6 +23,14 @@ func makePool[R any](alloc func() (R, error), free func(r R), o Options) pool[R] o.Algo = Adaptive() } + if o.Clock == nil { + o.Clock = times.Sys() + } + + if sc, ok := o.Algo.(interface{ setClock(times.Clock) }); ok { + sc.setClock(o.Clock) + } + s := make(chan state[R], 1) s <- state[R]{} return pool[R]{ @@ -78,7 +89,7 @@ func (p pool[R]) get() (R, error) { s.stats.Alloc++ event |= AllocateOperation event |= AllocateError - err = ErrEmptyPool + err = ErrEmpty case len(s.items) == 0: s.stats.Alloc++ event |= AllocateOperation @@ -142,7 +153,8 @@ func (p pool[R]) forcedCheck(s state[R], timeout time.Duration) state[R] { s.forcedCheckPending = true go func(to time.Duration) { - <-time.After(to) + p.options.TestBus.Signal("background-job-running") + <-p.options.Clock.After(to) p.freeIdle() }(timeout) @@ -210,6 +222,8 @@ func (p pool[R]) freeIdle() { if f > 0 { s = p.forcedCheck(s, f) } + + p.options.TestBus.Signal("free-idle-done") } func (p pool[R]) freePool() { diff --git a/pool_test.go b/pool_test.go index 8671a87..76cacf6 100644 --- a/pool_test.go +++ b/pool_test.go @@ -1,22 +1,318 @@ package pool_test -import "testing" +import ( + "code.squareroundforest.org/arpio/pool" + "code.squareroundforest.org/arpio/syncbus" + "code.squareroundforest.org/arpio/times" + "errors" + "testing" + "time" +) + +var errTest = errors.New("test error") func TestPool(t *testing.T) { - // initial stats - // get empty - // get pooled own - // get pooled foreign - // get own no alloc - // get foreign no alloc - // put own - // put foreign empty - // put foreign not empty - // release on put no free - // release on put with free - // release all no free - // release all with free - // release on timeout no free - // release on timeout with free - // use default algo + t.Run("initial stats", func(t *testing.T) { + alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) + s := p.Stats() + e := pool.Stats{} + if s != e { + t.Fatal(s) + } + }) + + t.Run("get when empty", func(t *testing.T) { + alloc := func() ([]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 { + 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("get pooled own", func(t *testing.T) { + alloc := func() ([]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 { + t.Fatal(err) + } + + if len(b) != 1<<9 { + t.Fatal(len(b)) + } + + p.Put(b) + b, err = p.Get() + 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("get pooled foreign", func(t *testing.T) { + alloc := func() ([]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() + 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("get own alloc not available", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + _, err := p.Get() + if !errors.Is(err, pool.ErrEmpty) { + t.Fatal(err) + } + + s := p.Stats() + e := pool.Stats{Alloc: 1, Get: 1} + if s != e { + t.Fatal(s) + } + }) + + t.Run("get foreign no alloc", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + p.Put(make([]byte, 1<<6)) + b, err := p.Get() + 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("allocation error", func(t *testing.T) { + alloc := func() ([]byte, error) { return nil, errTest } + p := pool.Make(alloc, nil, pool.Options{Algo: pool.NoShrink()}) + _, err := p.Get() + if !errors.Is(err, errTest) { + t.Fatal(err) + } + }) + + t.Run("put foreign not empty", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + p.Put(make([]byte, 1<<6)) + p.Put(make([]byte, 1<<6)) + b, err := p.Get() + 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.Put(make([]byte, 1<<9)) + p.Put(make([]byte, 1<<9)) + p.Put(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.Put(make([]byte, 1<<9)) + p.Put(make([]byte, 1<<9)) + p.Put(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.Put(make([]byte, 1<<9)) + p.Put(make([]byte, 1<<9)) + p.Put(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.Put(make([]byte, 1<<9)) + p.Put(make([]byte, 1<<9)) + p.Put(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 all when empty", func(t *testing.T) { + p := pool.Make[[]byte](nil, nil, pool.Options{Algo: pool.NoShrink()}) + p.Free() + s := p.Stats() + + var e pool.Stats + 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) + o := pool.Options{ + Algo: pool.Timeout(3 * time.Millisecond), + Clock: c, + TestBus: b, + } + + p := pool.Make[[]byte](nil, nil, o) + p.Put(make([]byte, 1<<9)) + if err := b.Wait("background-job-running"); err != nil { + t.Fatal(err) + } + + c.Pass(2 * time.Millisecond) + p.Put(make([]byte, 1<<9)) + if err := b.Wait("background-job-running"); err != nil { + t.Fatal(err) + } + + c.Pass(2 * time.Millisecond) + b.Wait("free-idle-done") + s := p.Stats() + e := pool.Stats{Put: 2, Idle: 1, Free: 1} + if s == e { + return + } + }) + + 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) + o := pool.Options{ + Algo: pool.Timeout(3 * time.Millisecond), + Clock: c, + TestBus: b, + } + + p := pool.Make[[]byte](nil, f, o) + p.Put(make([]byte, 1<<9)) + if err := b.Wait("background-job-running"); err != nil { + t.Fatal(err) + } + + c.Pass(2 * time.Millisecond) + p.Put(make([]byte, 1<<9)) + if err := b.Wait("background-job-running"); err != nil { + t.Fatal(err) + } + + c.Pass(2 * time.Millisecond) + b.Wait("free-idle-done") + s := p.Stats() + e := pool.Stats{Put: 2, Idle: 1, Free: 1} + if s == e { + if freeCount != 1 { + t.Fatal(freeCount) + } + + return + } + }) + + t.Run("use default algo", func(t *testing.T) { + alloc := func() ([]byte, error) { return make([]byte, 1<<9), nil } + p := pool.Make(alloc, nil, pool.Options{}) + + var bs [][]byte + for i := 0; i < 9; i++ { + b, err := p.Get() + if err != nil { + t.Fatal(err) + } + + bs = append(bs, b) + } + + for _, b := range bs[:2*len(bs)/3] { + p.Put(b) + } + + s := p.Stats() + e := pool.Stats{Alloc: 9, Get: 9, Put: 6, Active: 3, Idle: 3, Free: 3} + if s != e { + t.Fatal(s) + } + }) }