pax_global_header00006660000000000000000000000064143025221630014510gustar00rootroot0000000000000052 comment=2ded1af540f1f94602e17b2c918125f25d32c25d puddle-1.3.0/000077500000000000000000000000001430252216300127665ustar00rootroot00000000000000puddle-1.3.0/.github/000077500000000000000000000000001430252216300143265ustar00rootroot00000000000000puddle-1.3.0/.github/workflows/000077500000000000000000000000001430252216300163635ustar00rootroot00000000000000puddle-1.3.0/.github/workflows/ci.yml000066400000000000000000000007441430252216300175060ustar00rootroot00000000000000name: CI on: push: branches: [ master, v1 ] pull_request: branches: [ master, v1 ] jobs: test: name: Test runs-on: ubuntu-22.04 strategy: matrix: go-version: [1.18, 1.19] steps: - name: Set up Go 1.x uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Test run: go test -v -race ./... puddle-1.3.0/CHANGELOG.md000066400000000000000000000031371430252216300146030ustar00rootroot00000000000000# 1.3.0 (August 27, 2022) * Acquire creates resources in background to allow creation to continue after Acquire is canceled (James Hartig) # 1.2.1 (December 2, 2021) * TryAcquire now does not block when background constructing resource # 1.2.0 (November 20, 2021) * Add TryAcquire (A. Jensen) * Fix: remove memory leak / unintentionally pinned memory when shrinking slices (Alexander Staubo) * Fix: Do not leave pool locked after panic from nil context # 1.1.4 (September 11, 2021) * Fix: Deadlock in CreateResource if pool was closed during resource acquisition (Dmitriy Matrenichev) # 1.1.3 (December 3, 2020) * Fix: Failed resource creation could cause concurrent Acquire to hang. (Evgeny Vanslov) # 1.1.2 (September 26, 2020) * Fix: Resource.Destroy no longer removes itself from the pool before its destructor has completed. * Fix: Prevent crash when pool is closed while resource is being created. # 1.1.1 (April 2, 2020) * Pool.Close can be safely called multiple times * AcquireAllIDle immediately returns nil if pool is closed * CreateResource checks if pool is closed before taking any action * Fix potential race condition when CreateResource and Close are called concurrently. CreateResource now checks if pool is closed before adding newly created resource to pool. # 1.1.0 (February 5, 2020) * Use runtime.nanotime for faster tracking of acquire time and last usage time. * Track resource idle time to enable client health check logic. (Patrick Ellul) * Add CreateResource to construct a new resource without acquiring it. (Patrick Ellul) * Fix deadlock race when acquire is cancelled. (Michael Tharp) puddle-1.3.0/LICENSE000066400000000000000000000020611430252216300137720ustar00rootroot00000000000000Copyright (c) 2018 Jack Christensen MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. puddle-1.3.0/README.md000066400000000000000000000032551430252216300142520ustar00rootroot00000000000000[![](https://godoc.org/github.com/jackc/puddle?status.svg)](https://godoc.org/github.com/jackc/puddle) ![Build Status](https://github.com/jackc/puddle/actions/workflows/ci.yml/badge.svg) # Puddle Puddle is a tiny generic resource pool library for Go that uses the standard context library to signal cancellation of acquires. It is designed to contain the minimum functionality required for a resource pool. It can be used directly or it can be used as the base for a domain specific resource pool. For example, a database connection pool may use puddle internally and implement health checks and keep-alive behavior without needing to implement any concurrent code of its own. ## Features * Acquire cancellation via context standard library * Statistics API for monitoring pool pressure * No dependencies outside of standard library * High performance * 100% test coverage ## Example Usage ```go constructor := func(context.Context) (interface{}, error) { return net.Dial("tcp", "127.0.0.1:8080") } destructor := func(value interface{}) { value.(net.Conn).Close() } maxPoolSize := 10 pool := puddle.NewPool(constructor, destructor, maxPoolSize) // Acquire resource from the pool. res, err := pool.Acquire(context.Background()) if err != nil { // ... } // Use resource. _, err = res.Value().(net.Conn).Write([]byte{1}) if err != nil { // ... } // Release when done. res.Release() ``` ## Status Puddle v1 is complete. No changes are planned. * Bug reports and fixes are welcome. * New features will not be accepted if they can be feasibly implemented in a wrapper. * Performance optimizations will not be accepted unless the performance issue rises to the level of a bug. ## License MIT puddle-1.3.0/doc.go000066400000000000000000000007731430252216300140710ustar00rootroot00000000000000// Package puddle is a generic resource pool. /* Puddle is a tiny generic resource pool library for Go that uses the standard context library to signal cancellation of acquires. It is designed to contain the minimum functionality a resource pool needs that cannot be implemented without concurrency concerns. For example, a database connection pool may use puddle internally and implement health checks and keep-alive behavior without needing to implement any concurrent code of its own. */ package puddle puddle-1.3.0/go.mod000066400000000000000000000001241430252216300140710ustar00rootroot00000000000000module github.com/jackc/puddle go 1.12 require github.com/stretchr/testify v1.3.0 puddle-1.3.0/go.sum000066400000000000000000000011401430252216300141150ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= puddle-1.3.0/internal_test.go000066400000000000000000000005331430252216300161710ustar00rootroot00000000000000package puddle import ( "testing" "github.com/stretchr/testify/assert" ) func TestRemoveResourcePanicsWithBugReportIfResourceDoesNotExist(t *testing.T) { s := []*Resource{new(Resource), new(Resource), new(Resource)} assert.PanicsWithValue(t, "BUG: removeResource could not find res in slice", func() { removeResource(s, new(Resource)) }) } puddle-1.3.0/nanotime_time.go000066400000000000000000000003101430252216300161370ustar00rootroot00000000000000// +build purego appengine js // This file contains the safe implementation of nanotime using time.Now(). package puddle import ( "time" ) func nanotime() int64 { return time.Now().UnixNano() } puddle-1.3.0/nanotime_unsafe.go000066400000000000000000000003511430252216300164670ustar00rootroot00000000000000// +build !purego,!appengine,!js // This file contains the implementation of nanotime using runtime.nanotime. package puddle import "unsafe" var _ = unsafe.Sizeof(0) //go:linkname nanotime runtime.nanotime func nanotime() int64 puddle-1.3.0/pool.go000066400000000000000000000374141430252216300142770ustar00rootroot00000000000000package puddle import ( "context" "errors" "sync" "time" ) const ( resourceStatusConstructing = 0 resourceStatusIdle = iota resourceStatusAcquired = iota resourceStatusHijacked = iota ) // ErrClosedPool occurs on an attempt to acquire a connection from a closed pool // or a pool that is closed while the acquire is waiting. var ErrClosedPool = errors.New("closed pool") // ErrNotAvailable occurs on an attempt to acquire a resource from a pool // that is at maximum capacity and has no available resources. var ErrNotAvailable = errors.New("resource not available") // Constructor is a function called by the pool to construct a resource. type Constructor func(ctx context.Context) (res interface{}, err error) // Destructor is a function called by the pool to destroy a resource. type Destructor func(res interface{}) // Resource is the resource handle returned by acquiring from the pool. type Resource struct { value interface{} pool *Pool creationTime time.Time lastUsedNano int64 status byte } // Value returns the resource value. func (res *Resource) Value() interface{} { if !(res.status == resourceStatusAcquired || res.status == resourceStatusHijacked) { panic("tried to access resource that is not acquired or hijacked") } return res.value } // Release returns the resource to the pool. res must not be subsequently used. func (res *Resource) Release() { if res.status != resourceStatusAcquired { panic("tried to release resource that is not acquired") } res.pool.releaseAcquiredResource(res, nanotime()) } // ReleaseUnused returns the resource to the pool without updating when it was last used used. i.e. LastUsedNanotime // will not change. res must not be subsequently used. func (res *Resource) ReleaseUnused() { if res.status != resourceStatusAcquired { panic("tried to release resource that is not acquired") } res.pool.releaseAcquiredResource(res, res.lastUsedNano) } // Destroy returns the resource to the pool for destruction. res must not be // subsequently used. func (res *Resource) Destroy() { if res.status != resourceStatusAcquired { panic("tried to destroy resource that is not acquired") } go res.pool.destroyAcquiredResource(res) } // Hijack assumes ownership of the resource from the pool. Caller is responsible // for cleanup of resource value. func (res *Resource) Hijack() { if res.status != resourceStatusAcquired { panic("tried to hijack resource that is not acquired") } res.pool.hijackAcquiredResource(res) } // CreationTime returns when the resource was created by the pool. func (res *Resource) CreationTime() time.Time { if !(res.status == resourceStatusAcquired || res.status == resourceStatusHijacked) { panic("tried to access resource that is not acquired or hijacked") } return res.creationTime } // LastUsedNanotime returns when Release was last called on the resource measured in nanoseconds from an arbitrary time // (a monotonic time). Returns creation time if Release has never been called. This is only useful to compare with // other calls to LastUsedNanotime. In almost all cases, IdleDuration should be used instead. func (res *Resource) LastUsedNanotime() int64 { if !(res.status == resourceStatusAcquired || res.status == resourceStatusHijacked) { panic("tried to access resource that is not acquired or hijacked") } return res.lastUsedNano } // IdleDuration returns the duration since Release was last called on the resource. This is equivalent to subtracting // LastUsedNanotime to the current nanotime. func (res *Resource) IdleDuration() time.Duration { if !(res.status == resourceStatusAcquired || res.status == resourceStatusHijacked) { panic("tried to access resource that is not acquired or hijacked") } return time.Duration(nanotime() - res.lastUsedNano) } // Pool is a concurrency-safe resource pool. type Pool struct { cond *sync.Cond destructWG *sync.WaitGroup allResources []*Resource idleResources []*Resource constructor Constructor destructor Destructor maxSize int32 acquireCount int64 acquireDuration time.Duration emptyAcquireCount int64 canceledAcquireCount int64 closed bool } // NewPool creates a new pool. Panics if maxSize is less than 1. func NewPool(constructor Constructor, destructor Destructor, maxSize int32) *Pool { if maxSize < 1 { panic("maxSize is less than 1") } return &Pool{ cond: sync.NewCond(new(sync.Mutex)), destructWG: &sync.WaitGroup{}, maxSize: maxSize, constructor: constructor, destructor: destructor, } } // Close destroys all resources in the pool and rejects future Acquire calls. // Blocks until all resources are returned to pool and destroyed. func (p *Pool) Close() { p.cond.L.Lock() if p.closed { p.cond.L.Unlock() return } p.closed = true for _, res := range p.idleResources { p.allResources = removeResource(p.allResources, res) go p.destructResourceValue(res.value) } p.idleResources = nil p.cond.L.Unlock() // Wake up all go routines waiting for a resource to be returned so they can terminate. p.cond.Broadcast() p.destructWG.Wait() } // Stat is a snapshot of Pool statistics. type Stat struct { constructingResources int32 acquiredResources int32 idleResources int32 maxResources int32 acquireCount int64 acquireDuration time.Duration emptyAcquireCount int64 canceledAcquireCount int64 } // TotalResource returns the total number of resources currently in the pool. // The value is the sum of ConstructingResources, AcquiredResources, and // IdleResources. func (s *Stat) TotalResources() int32 { return s.constructingResources + s.acquiredResources + s.idleResources } // ConstructingResources returns the number of resources with construction in progress in // the pool. func (s *Stat) ConstructingResources() int32 { return s.constructingResources } // AcquiredResources returns the number of currently acquired resources in the pool. func (s *Stat) AcquiredResources() int32 { return s.acquiredResources } // IdleResources returns the number of currently idle resources in the pool. func (s *Stat) IdleResources() int32 { return s.idleResources } // MaxResources returns the maximum size of the pool. func (s *Stat) MaxResources() int32 { return s.maxResources } // AcquireCount returns the cumulative count of successful acquires from the pool. func (s *Stat) AcquireCount() int64 { return s.acquireCount } // AcquireDuration returns the total duration of all successful acquires from // the pool. func (s *Stat) AcquireDuration() time.Duration { return s.acquireDuration } // EmptyAcquireCount returns the cumulative count of successful acquires from the pool // that waited for a resource to be released or constructed because the pool was // empty. func (s *Stat) EmptyAcquireCount() int64 { return s.emptyAcquireCount } // CanceledAcquireCount returns the cumulative count of acquires from the pool // that were canceled by a context. func (s *Stat) CanceledAcquireCount() int64 { return s.canceledAcquireCount } // Stat returns the current pool statistics. func (p *Pool) Stat() *Stat { p.cond.L.Lock() s := &Stat{ maxResources: p.maxSize, acquireCount: p.acquireCount, emptyAcquireCount: p.emptyAcquireCount, canceledAcquireCount: p.canceledAcquireCount, acquireDuration: p.acquireDuration, } for _, res := range p.allResources { switch res.status { case resourceStatusConstructing: s.constructingResources += 1 case resourceStatusIdle: s.idleResources += 1 case resourceStatusAcquired: s.acquiredResources += 1 } } p.cond.L.Unlock() return s } // Acquire gets a resource from the pool. If no resources are available and the pool // is not at maximum capacity it will create a new resource. If the pool is at // maximum capacity it will block until a resource is available. ctx can be used // to cancel the Acquire. func (p *Pool) Acquire(ctx context.Context) (*Resource, error) { startNano := nanotime() if doneChan := ctx.Done(); doneChan != nil { select { case <-ctx.Done(): p.cond.L.Lock() p.canceledAcquireCount += 1 p.cond.L.Unlock() return nil, ctx.Err() default: } } p.cond.L.Lock() emptyAcquire := false for { if p.closed { p.cond.L.Unlock() return nil, ErrClosedPool } // If a resource is available now if len(p.idleResources) > 0 { res := p.idleResources[len(p.idleResources)-1] p.idleResources[len(p.idleResources)-1] = nil // Avoid memory leak p.idleResources = p.idleResources[:len(p.idleResources)-1] res.status = resourceStatusAcquired if emptyAcquire { p.emptyAcquireCount += 1 } p.acquireCount += 1 p.acquireDuration += time.Duration(nanotime() - startNano) p.cond.L.Unlock() return res, nil } emptyAcquire = true // If there is room to create a resource do so if len(p.allResources) < int(p.maxSize) { res := &Resource{pool: p, creationTime: time.Now(), lastUsedNano: nanotime(), status: resourceStatusConstructing} p.allResources = append(p.allResources, res) p.destructWG.Add(1) p.cond.L.Unlock() // we create the resource in the background because the constructor might // outlive the context and we want to continue constructing it as long as // necessary but the acquire should be cancelled when the context is cancelled // see: https://github.com/jackc/pgx/issues/1287 and https://github.com/jackc/pgx/issues/1259 constructErrCh := make(chan error) go func() { value, err := p.constructResourceValue(ctx) p.cond.L.Lock() if err != nil { p.allResources = removeResource(p.allResources, res) p.destructWG.Done() // we can't use default here in case we get here before the caller is // in the select select { case constructErrCh <- err: case <-ctx.Done(): p.canceledAcquireCount += 1 } p.cond.L.Unlock() p.cond.Signal() return } res.value = value // assume that we will acquire it res.status = resourceStatusAcquired // we can't use default here in case we get here before the caller is // in the select select { case constructErrCh <- nil: p.emptyAcquireCount += 1 p.acquireCount += 1 p.acquireDuration += time.Duration(nanotime() - startNano) p.cond.L.Unlock() // we don't call Signal here we didn't change any of the resource pools case <-ctx.Done(): p.canceledAcquireCount += 1 p.cond.L.Unlock() // we don't call Signal here we didn't change any of the resopurce pools // since we couldn't send the constructed resource to the acquire // function that means the caller has stopped waiting and we should // just put this resource back in the pool p.releaseAcquiredResource(res, res.lastUsedNano) } }() select { case <-ctx.Done(): return nil, ctx.Err() case err := <-constructErrCh: if err != nil { return nil, err } // we don't call signal here because we didn't change the resource pools // at all so waking anything else up won't help return res, nil } } if ctx.Done() == nil { p.cond.Wait() } else { // Convert p.cond.Wait into a channel waitChan := make(chan struct{}, 1) go func() { p.cond.Wait() waitChan <- struct{}{} }() select { case <-ctx.Done(): // Allow goroutine waiting for signal to exit. Re-signal since we couldn't // do anything with it. Another goroutine might be waiting. go func() { <-waitChan p.cond.L.Unlock() p.cond.Signal() }() p.cond.L.Lock() p.canceledAcquireCount += 1 p.cond.L.Unlock() return nil, ctx.Err() case <-waitChan: } } } } // TryAcquire gets a resource from the pool if one is immediately available. If not, it returns ErrNotAvailable. If no // resources are available but the pool has room to grow, a resource will be created in the background. ctx is only // used to cancel the background creation. func (p *Pool) TryAcquire(ctx context.Context) (*Resource, error) { p.cond.L.Lock() defer p.cond.L.Unlock() if p.closed { return nil, ErrClosedPool } // If a resource is available now if len(p.idleResources) > 0 { res := p.idleResources[len(p.idleResources)-1] p.idleResources[len(p.idleResources)-1] = nil // Avoid memory leak p.idleResources = p.idleResources[:len(p.idleResources)-1] p.acquireCount += 1 res.status = resourceStatusAcquired return res, nil } if len(p.allResources) < int(p.maxSize) { res := &Resource{pool: p, creationTime: time.Now(), lastUsedNano: nanotime(), status: resourceStatusConstructing} p.allResources = append(p.allResources, res) p.destructWG.Add(1) go func() { value, err := p.constructResourceValue(ctx) defer p.cond.Signal() p.cond.L.Lock() defer p.cond.L.Unlock() if err != nil { p.allResources = removeResource(p.allResources, res) p.destructWG.Done() return } res.value = value res.status = resourceStatusIdle p.idleResources = append(p.idleResources, res) }() } return nil, ErrNotAvailable } // AcquireAllIdle atomically acquires all currently idle resources. Its intended // use is for health check and keep-alive functionality. It does not update pool // statistics. func (p *Pool) AcquireAllIdle() []*Resource { p.cond.L.Lock() if p.closed { p.cond.L.Unlock() return nil } for _, res := range p.idleResources { res.status = resourceStatusAcquired } resources := p.idleResources // Swap out current slice p.idleResources = nil p.cond.L.Unlock() return resources } // CreateResource constructs a new resource without acquiring it. // It goes straight in the IdlePool. It does not check against maxSize. // It can be useful to maintain warm resources under little load. func (p *Pool) CreateResource(ctx context.Context) error { p.cond.L.Lock() if p.closed { p.cond.L.Unlock() return ErrClosedPool } p.cond.L.Unlock() value, err := p.constructResourceValue(ctx) if err != nil { return err } res := &Resource{ pool: p, creationTime: time.Now(), status: resourceStatusIdle, value: value, lastUsedNano: nanotime(), } p.destructWG.Add(1) p.cond.L.Lock() // If closed while constructing resource then destroy it and return an error if p.closed { go p.destructResourceValue(res.value) p.cond.L.Unlock() return ErrClosedPool } p.allResources = append(p.allResources, res) p.idleResources = append(p.idleResources, res) p.cond.L.Unlock() return nil } // releaseAcquiredResource returns res to the the pool. func (p *Pool) releaseAcquiredResource(res *Resource, lastUsedNano int64) { p.cond.L.Lock() if !p.closed { res.lastUsedNano = lastUsedNano res.status = resourceStatusIdle p.idleResources = append(p.idleResources, res) } else { p.allResources = removeResource(p.allResources, res) go p.destructResourceValue(res.value) } p.cond.L.Unlock() p.cond.Signal() } // Remove removes res from the pool and closes it. If res is not part of the // pool Remove will panic. func (p *Pool) destroyAcquiredResource(res *Resource) { p.destructResourceValue(res.value) p.cond.L.Lock() p.allResources = removeResource(p.allResources, res) p.cond.L.Unlock() p.cond.Signal() } func (p *Pool) hijackAcquiredResource(res *Resource) { p.cond.L.Lock() p.allResources = removeResource(p.allResources, res) res.status = resourceStatusHijacked p.destructWG.Done() // not responsible for destructing hijacked resources p.cond.L.Unlock() p.cond.Signal() } func removeResource(slice []*Resource, res *Resource) []*Resource { for i := range slice { if slice[i] == res { slice[i] = slice[len(slice)-1] slice[len(slice)-1] = nil // Avoid memory leak return slice[:len(slice)-1] } } panic("BUG: removeResource could not find res in slice") } func (p *Pool) constructResourceValue(ctx context.Context) (interface{}, error) { return p.constructor(ctx) } func (p *Pool) destructResourceValue(value interface{}) { p.destructor(value) p.destructWG.Done() } puddle-1.3.0/pool_test.go000066400000000000000000000635051430252216300153360ustar00rootroot00000000000000package puddle_test import ( "context" "errors" "fmt" "log" "math/rand" "net" "os" "runtime" "strconv" "sync" "testing" "time" "github.com/jackc/puddle" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type Counter struct { mutex sync.Mutex n int } // Next increments the counter and returns the value func (c *Counter) Next() int { c.mutex.Lock() c.n += 1 n := c.n c.mutex.Unlock() return n } // Value returns the counter func (c *Counter) Value() int { c.mutex.Lock() n := c.n c.mutex.Unlock() return n } func createConstructor() (puddle.Constructor, *Counter) { var c Counter f := func(ctx context.Context) (interface{}, error) { return c.Next(), nil } return f, &c } func createConstructorWithNotifierChan() (puddle.Constructor, *Counter, chan int) { ch := make(chan int) var c Counter f := func(ctx context.Context) (interface{}, error) { n := c.Next() // Because the tests will not read from ch until after the create function f returns. go func() { ch <- n }() return n, nil } return f, &c, ch } func stubDestructor(interface{}) {} func waitForRead(ch chan int) bool { select { case <-ch: return true case <-time.NewTimer(time.Second).C: return false } } func TestNewPoolRequiresMaxSizeGreaterThan0(t *testing.T) { constructor, _ := createConstructor() assert.Panics(t, func() { puddle.NewPool(constructor, stubDestructor, -1) }) assert.Panics(t, func() { puddle.NewPool(constructor, stubDestructor, 0) }) } func TestPoolAcquireCreatesResourceWhenNoneIdle(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) defer pool.Close() res, err := pool.Acquire(context.Background()) require.NoError(t, err) assert.Equal(t, 1, res.Value()) assert.WithinDuration(t, time.Now(), res.CreationTime(), time.Second) res.Release() } func TestPoolAcquireDoesNotCreatesResourceWhenItWouldExceedMaxSize(t *testing.T) { constructor, createCounter := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 1) wg := &sync.WaitGroup{} for i := 0; i < 100; i++ { wg.Add(1) go func() { for j := 0; j < 100; j++ { res, err := pool.Acquire(context.Background()) assert.NoError(t, err) assert.Equal(t, 1, res.Value()) res.Release() } wg.Done() }() } wg.Wait() assert.EqualValues(t, 1, createCounter.Value()) assert.EqualValues(t, 1, pool.Stat().TotalResources()) } func TestPoolAcquireWithCancellableContext(t *testing.T) { constructor, createCounter := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 1) wg := &sync.WaitGroup{} for i := 0; i < 100; i++ { wg.Add(1) go func() { for j := 0; j < 100; j++ { ctx, cancel := context.WithCancel(context.Background()) res, err := pool.Acquire(ctx) assert.NoError(t, err) assert.Equal(t, 1, res.Value()) res.Release() cancel() } wg.Done() }() } wg.Wait() assert.EqualValues(t, 1, createCounter.Value()) assert.EqualValues(t, 1, pool.Stat().TotalResources()) } func TestPoolAcquireReturnsErrorFromFailedResourceCreate(t *testing.T) { errCreateFailed := errors.New("create failed") constructor := func(ctx context.Context) (interface{}, error) { return nil, errCreateFailed } pool := puddle.NewPool(constructor, stubDestructor, 10) res, err := pool.Acquire(context.Background()) assert.Equal(t, errCreateFailed, err) assert.Nil(t, res) } func TestPoolAcquireCreatesResourceRespectingContext(t *testing.T) { var cancel func() constructor := func(ctx context.Context) (interface{}, error) { cancel() // sleep to give a chance for the acquire to recognize it's cancelled time.Sleep(10 * time.Millisecond) return 1, nil } pool := puddle.NewPool(constructor, stubDestructor, 1) defer pool.Close() var ctx context.Context ctx, cancel = context.WithCancel(context.Background()) defer cancel() _, err := pool.Acquire(ctx) assert.Equal(t, context.Canceled, err) // wait for the constructor to sleep and then for the resource to be added back // to the idle pool time.Sleep(100 * time.Millisecond) stat := pool.Stat() assert.EqualValues(t, 1, stat.IdleResources()) assert.EqualValues(t, 1, stat.TotalResources()) } func TestPoolAcquireReusesResources(t *testing.T) { constructor, createCounter := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) res, err := pool.Acquire(context.Background()) require.NoError(t, err) assert.Equal(t, 1, res.Value()) res.Release() res, err = pool.Acquire(context.Background()) require.NoError(t, err) assert.Equal(t, 1, res.Value()) res.Release() assert.Equal(t, 1, createCounter.Value()) } func TestPoolTryAcquire(t *testing.T) { constructor, createCounter := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 1) // Pool is initially empty so TryAcquire fails but starts construction of resource in the background. res, err := pool.TryAcquire(context.Background()) require.EqualError(t, err, puddle.ErrNotAvailable.Error()) assert.Nil(t, res) // Wait for background creation to complete. time.Sleep(100 * time.Millisecond) res, err = pool.TryAcquire(context.Background()) require.NoError(t, err) assert.Equal(t, 1, res.Value()) defer res.Release() res, err = pool.TryAcquire(context.Background()) require.EqualError(t, err, puddle.ErrNotAvailable.Error()) assert.Nil(t, res) assert.Equal(t, 1, createCounter.Value()) } func TestPoolTryAcquireReturnsErrorWhenPoolIsClosed(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) pool.Close() res, err := pool.TryAcquire(context.Background()) assert.Equal(t, puddle.ErrClosedPool, err) assert.Nil(t, res) } func TestPoolTryAcquireWithFailedResourceCreate(t *testing.T) { errCreateFailed := errors.New("create failed") constructor := func(ctx context.Context) (interface{}, error) { return nil, errCreateFailed } pool := puddle.NewPool(constructor, stubDestructor, 10) res, err := pool.TryAcquire(context.Background()) require.EqualError(t, err, puddle.ErrNotAvailable.Error()) assert.Nil(t, res) } func TestPoolAcquireNilContextDoesNotLeavePoolLocked(t *testing.T) { constructor, createCounter := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) assert.Panics(t, func() { pool.Acquire(nil) }) res, err := pool.Acquire(context.Background()) require.NoError(t, err) assert.Equal(t, 1, res.Value()) res.Release() assert.Equal(t, 1, createCounter.Value()) } func TestPoolAcquireContextAlreadyCanceled(t *testing.T) { constructor := func(ctx context.Context) (interface{}, error) { panic("should never be called") } pool := puddle.NewPool(constructor, stubDestructor, 10) ctx, cancel := context.WithCancel(context.Background()) cancel() res, err := pool.Acquire(ctx) assert.Equal(t, context.Canceled, err) assert.Nil(t, res) } func TestPoolAcquireContextCanceledDuringCreate(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) time.AfterFunc(100*time.Millisecond, cancel) timeoutChan := time.After(1 * time.Second) var constructorCalls Counter constructor := func(ctx context.Context) (interface{}, error) { select { case <-ctx.Done(): return nil, ctx.Err() case <-timeoutChan: } return constructorCalls.Next(), nil } pool := puddle.NewPool(constructor, stubDestructor, 10) res, err := pool.Acquire(ctx) assert.Equal(t, context.Canceled, err) assert.Nil(t, res) } func TestPoolAcquireAllIdle(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) defer pool.Close() resources := make([]*puddle.Resource, 4) var err error resources[0], err = pool.Acquire(context.Background()) require.NoError(t, err) resources[1], err = pool.Acquire(context.Background()) require.NoError(t, err) resources[2], err = pool.Acquire(context.Background()) require.NoError(t, err) resources[3], err = pool.Acquire(context.Background()) require.NoError(t, err) assert.Len(t, pool.AcquireAllIdle(), 0) resources[0].Release() resources[3].Release() assert.ElementsMatch(t, []*puddle.Resource{resources[0], resources[3]}, pool.AcquireAllIdle()) resources[0].Release() resources[3].Release() resources[1].Release() resources[2].Release() assert.ElementsMatch(t, resources, pool.AcquireAllIdle()) resources[0].Release() resources[1].Release() resources[2].Release() resources[3].Release() } func TestPoolAcquireAllIdleWhenClosedIsNil(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) pool.Close() assert.Nil(t, pool.AcquireAllIdle()) } func TestPoolCreateResource(t *testing.T) { constructor, counter := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) defer pool.Close() var err error err = pool.CreateResource(context.Background()) require.NoError(t, err) stats := pool.Stat() assert.EqualValues(t, 1, stats.IdleResources()) res, err := pool.Acquire(context.Background()) require.NoError(t, err) assert.Equal(t, counter.Value(), res.Value()) assert.True(t, res.LastUsedNanotime() > 0, "should set LastUsedNanotime so that idle calculations can still work") assert.Equal(t, 1, res.Value()) assert.WithinDuration(t, time.Now(), res.CreationTime(), time.Second) res.Release() assert.EqualValues(t, 0, pool.Stat().EmptyAcquireCount(), "should have been a warm resource") } func TestPoolCreateResourceReturnsErrorFromFailedResourceCreate(t *testing.T) { errCreateFailed := errors.New("create failed") constructor := func(ctx context.Context) (interface{}, error) { return nil, errCreateFailed } pool := puddle.NewPool(constructor, stubDestructor, 10) err := pool.CreateResource(context.Background()) assert.Equal(t, errCreateFailed, err) } func TestPoolCreateResourceReturnsErrorWhenAlreadyClosed(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) pool.Close() err := pool.CreateResource(context.Background()) assert.Equal(t, puddle.ErrClosedPool, err) } func TestPoolCreateResourceReturnsErrorWhenClosedWhileCreatingResource(t *testing.T) { // There is no way to guarantee the correct order of the pool being closed while the resource is being constructed. // But these sleeps should make it extremely likely. (Ah, the lengths we go for 100% test coverage...) constructor := func(ctx context.Context) (interface{}, error) { time.Sleep(500 * time.Millisecond) return "abc", nil } pool := puddle.NewPool(constructor, stubDestructor, 10) acquireErrChan := make(chan error) go func() { err := pool.CreateResource(context.Background()) acquireErrChan <- err }() time.Sleep(250 * time.Millisecond) pool.Close() err := <-acquireErrChan assert.Equal(t, puddle.ErrClosedPool, err) } func TestPoolCloseClosesAllIdleResources(t *testing.T) { constructor, _ := createConstructor() var destructorCalls Counter destructor := func(interface{}) { destructorCalls.Next() } p := puddle.NewPool(constructor, destructor, 10) resources := make([]*puddle.Resource, 4) for i := range resources { var err error resources[i], err = p.Acquire(context.Background()) require.Nil(t, err) } for _, res := range resources { res.Release() } p.Close() assert.Equal(t, len(resources), destructorCalls.Value()) } func TestPoolCloseBlocksUntilAllResourcesReleasedAndClosed(t *testing.T) { constructor, _ := createConstructor() var destructorCalls Counter destructor := func(interface{}) { destructorCalls.Next() } p := puddle.NewPool(constructor, destructor, 10) resources := make([]*puddle.Resource, 4) for i := range resources { var err error resources[i], err = p.Acquire(context.Background()) require.Nil(t, err) } for _, res := range resources { go func(res *puddle.Resource) { time.Sleep(100 * time.Millisecond) res.Release() }(res) } p.Close() assert.Equal(t, len(resources), destructorCalls.Value()) } func TestPoolCloseIsSafeToCallMultipleTimes(t *testing.T) { constructor, _ := createConstructor() p := puddle.NewPool(constructor, stubDestructor, 10) p.Close() p.Close() } func TestPoolStatResources(t *testing.T) { startWaitChan := make(chan struct{}) waitingChan := make(chan struct{}) endWaitChan := make(chan struct{}) var constructorCalls Counter constructor := func(ctx context.Context) (interface{}, error) { select { case <-startWaitChan: close(waitingChan) <-endWaitChan default: } return constructorCalls.Next(), nil } pool := puddle.NewPool(constructor, stubDestructor, 10) defer pool.Close() resAcquired, err := pool.Acquire(context.Background()) require.Nil(t, err) close(startWaitChan) go func() { res, err := pool.Acquire(context.Background()) require.Nil(t, err) res.Release() }() <-waitingChan stat := pool.Stat() assert.EqualValues(t, 2, stat.TotalResources()) assert.EqualValues(t, 1, stat.ConstructingResources()) assert.EqualValues(t, 1, stat.AcquiredResources()) assert.EqualValues(t, 0, stat.IdleResources()) assert.EqualValues(t, 10, stat.MaxResources()) resAcquired.Release() stat = pool.Stat() assert.EqualValues(t, 2, stat.TotalResources()) assert.EqualValues(t, 1, stat.ConstructingResources()) assert.EqualValues(t, 0, stat.AcquiredResources()) assert.EqualValues(t, 1, stat.IdleResources()) assert.EqualValues(t, 10, stat.MaxResources()) close(endWaitChan) } func TestPoolStatSuccessfulAcquireCounters(t *testing.T) { constructor, _ := createConstructor() sleepConstructor := func(ctx context.Context) (interface{}, error) { // sleep to make sure we don't fail the AcquireDuration test time.Sleep(time.Nanosecond) return constructor(ctx) } pool := puddle.NewPool(sleepConstructor, stubDestructor, 1) defer pool.Close() res, err := pool.Acquire(context.Background()) require.NoError(t, err) res.Release() stat := pool.Stat() assert.Equal(t, int64(1), stat.AcquireCount()) assert.Equal(t, int64(1), stat.EmptyAcquireCount()) assert.True(t, stat.AcquireDuration() > 0, "expected stat.AcquireDuration() > 0 but %v", stat.AcquireDuration()) lastAcquireDuration := stat.AcquireDuration() res, err = pool.Acquire(context.Background()) require.NoError(t, err) res.Release() stat = pool.Stat() assert.Equal(t, int64(2), stat.AcquireCount()) assert.Equal(t, int64(1), stat.EmptyAcquireCount()) assert.True(t, stat.AcquireDuration() > lastAcquireDuration) lastAcquireDuration = stat.AcquireDuration() wg := &sync.WaitGroup{} for i := 0; i < 2; i++ { wg.Add(1) go func() { res, err = pool.Acquire(context.Background()) require.NoError(t, err) time.Sleep(50 * time.Millisecond) res.Release() wg.Done() }() } wg.Wait() stat = pool.Stat() assert.Equal(t, int64(4), stat.AcquireCount()) assert.Equal(t, int64(2), stat.EmptyAcquireCount()) assert.True(t, stat.AcquireDuration() > lastAcquireDuration) lastAcquireDuration = stat.AcquireDuration() } func TestPoolStatCanceledAcquireBeforeStart(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 1) defer pool.Close() ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := pool.Acquire(ctx) require.Equal(t, context.Canceled, err) stat := pool.Stat() assert.Equal(t, int64(0), stat.AcquireCount()) assert.Equal(t, int64(1), stat.CanceledAcquireCount()) } func TestPoolStatCanceledAcquireDuringCreate(t *testing.T) { constructor := func(ctx context.Context) (interface{}, error) { <-ctx.Done() return nil, ctx.Err() } pool := puddle.NewPool(constructor, stubDestructor, 1) defer pool.Close() ctx, cancel := context.WithCancel(context.Background()) time.AfterFunc(50*time.Millisecond, cancel) _, err := pool.Acquire(ctx) require.Equal(t, context.Canceled, err) // sleep to give the constructor goroutine time to mark cancelled time.Sleep(10 * time.Millisecond) stat := pool.Stat() assert.Equal(t, int64(0), stat.AcquireCount()) assert.Equal(t, int64(1), stat.CanceledAcquireCount()) } func TestPoolStatCanceledAcquireDuringWait(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 1) defer pool.Close() res, err := pool.Acquire(context.Background()) require.Nil(t, err) ctx, cancel := context.WithCancel(context.Background()) time.AfterFunc(50*time.Millisecond, cancel) _, err = pool.Acquire(ctx) require.Equal(t, context.Canceled, err) res.Release() stat := pool.Stat() assert.Equal(t, int64(1), stat.AcquireCount()) assert.Equal(t, int64(1), stat.CanceledAcquireCount()) } func TestResourceHijackRemovesResourceFromPoolButDoesNotDestroy(t *testing.T) { constructor, _ := createConstructor() var destructorCalls Counter destructor := func(interface{}) { destructorCalls.Next() } pool := puddle.NewPool(constructor, destructor, 10) res, err := pool.Acquire(context.Background()) require.NoError(t, err) assert.Equal(t, 1, res.Value()) res.Hijack() assert.EqualValues(t, 0, pool.Stat().TotalResources()) assert.EqualValues(t, 0, destructorCalls.Value()) // Can still call Value, CreationTime and IdleDuration res.Value() res.CreationTime() res.IdleDuration() } func TestResourceDestroyRemovesResourceFromPool(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) res, err := pool.Acquire(context.Background()) require.NoError(t, err) assert.Equal(t, 1, res.Value()) assert.EqualValues(t, 1, pool.Stat().TotalResources()) res.Destroy() for i := 0; i < 1000; i++ { if pool.Stat().TotalResources() == 0 { break } time.Sleep(time.Millisecond) } assert.EqualValues(t, 0, pool.Stat().TotalResources()) } func TestResourceLastUsageTimeTracking(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 1) res, err := pool.Acquire(context.Background()) require.NoError(t, err) t1 := res.LastUsedNanotime() res.Release() // Greater than zero after initial usage res, err = pool.Acquire(context.Background()) require.NoError(t, err) t2 := res.LastUsedNanotime() d2 := res.IdleDuration() assert.True(t, t2 > t1) res.ReleaseUnused() // ReleaseUnused does not update usage tracking res, err = pool.Acquire(context.Background()) require.NoError(t, err) t3 := res.LastUsedNanotime() d3 := res.IdleDuration() assert.EqualValues(t, t2, t3) assert.True(t, d3 > d2) res.Release() // Release does update usage tracking res, err = pool.Acquire(context.Background()) require.NoError(t, err) t4 := res.LastUsedNanotime() assert.True(t, t4 > t3) res.Release() } func TestResourcePanicsOnUsageWhenNotAcquired(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) res, err := pool.Acquire(context.Background()) require.NoError(t, err) res.Release() assert.PanicsWithValue(t, "tried to release resource that is not acquired", res.Release) assert.PanicsWithValue(t, "tried to release resource that is not acquired", res.ReleaseUnused) assert.PanicsWithValue(t, "tried to destroy resource that is not acquired", res.Destroy) assert.PanicsWithValue(t, "tried to hijack resource that is not acquired", res.Hijack) assert.PanicsWithValue(t, "tried to access resource that is not acquired or hijacked", func() { res.Value() }) assert.PanicsWithValue(t, "tried to access resource that is not acquired or hijacked", func() { res.CreationTime() }) assert.PanicsWithValue(t, "tried to access resource that is not acquired or hijacked", func() { res.LastUsedNanotime() }) assert.PanicsWithValue(t, "tried to access resource that is not acquired or hijacked", func() { res.IdleDuration() }) } func TestPoolAcquireReturnsErrorWhenPoolIsClosed(t *testing.T) { constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, 10) pool.Close() res, err := pool.Acquire(context.Background()) assert.Equal(t, puddle.ErrClosedPool, err) assert.Nil(t, res) } func TestSignalIsSentWhenResourceFailedToCreate(t *testing.T) { var c Counter constructor := func(context.Context) (a interface{}, err error) { if c.Next() == 2 { return nil, errors.New("outage") } return 1, nil } destructor := func(value interface{}) {} pool := puddle.NewPool(constructor, destructor, 1) res1, err := pool.Acquire(context.Background()) require.NoError(t, err) var wg sync.WaitGroup for i := 0; i < 2; i++ { wg.Add(1) go func(name string) { defer wg.Done() _, _ = pool.Acquire(context.Background()) }(strconv.Itoa(i)) } // ensure that both goroutines above are waiting for condition variable signal time.Sleep(500 * time.Millisecond) res1.Destroy() wg.Wait() } func TestStress(t *testing.T) { constructor, _ := createConstructor() var destructorCalls Counter destructor := func(interface{}) { destructorCalls.Next() } poolSize := runtime.NumCPU() if poolSize < 4 { poolSize = 4 } pool := puddle.NewPool(constructor, destructor, int32(poolSize)) finishChan := make(chan struct{}) wg := &sync.WaitGroup{} releaseOrDestroyOrHijack := func(res *puddle.Resource) { n := rand.Intn(100) if n < 5 { res.Hijack() destructor(res) } else if n < 10 { res.Destroy() } else { res.Release() } } actions := []func(){ // Acquire func() { res, err := pool.Acquire(context.Background()) if err != nil { if err != puddle.ErrClosedPool { assert.Failf(t, "stress acquire", "pool.Acquire returned unexpected err: %v", err) } return } time.Sleep(time.Duration(rand.Int63n(100)) * time.Millisecond) releaseOrDestroyOrHijack(res) }, // Acquire possibly canceled by context func() { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(rand.Int63n(2000))*time.Nanosecond) defer cancel() res, err := pool.Acquire(ctx) if err != nil { if err != puddle.ErrClosedPool && err != context.Canceled && err != context.DeadlineExceeded { assert.Failf(t, "stress acquire possibly canceled by context", "pool.Acquire returned unexpected err: %v", err) } return } time.Sleep(time.Duration(rand.Int63n(2000)) * time.Nanosecond) releaseOrDestroyOrHijack(res) }, // TryAcquire func() { res, err := pool.TryAcquire(context.Background()) if err != nil { if err != puddle.ErrClosedPool && err != puddle.ErrNotAvailable { assert.Failf(t, "stress TryAcquire", "pool.TryAcquire returned unexpected err: %v", err) } return } time.Sleep(time.Duration(rand.Int63n(100)) * time.Millisecond) releaseOrDestroyOrHijack(res) }, // AcquireAllIdle (though under heavy load this will almost certainly always get an empty slice) func() { resources := pool.AcquireAllIdle() for _, res := range resources { res.Release() } }, } workerCount := int(poolSize) * 2 for i := 0; i < workerCount; i++ { wg.Add(1) go func() { for { select { case <-finishChan: wg.Done() return default: } actions[rand.Intn(len(actions))]() } }() } s := os.Getenv("STRESS_TEST_DURATION") if s == "" { s = "1s" } testDuration, err := time.ParseDuration(s) require.Nil(t, err) time.AfterFunc(testDuration, func() { close(finishChan) }) wg.Wait() pool.Close() } func startAcceptOnceDummyServer(laddr string) { ln, err := net.Listen("tcp", laddr) if err != nil { log.Fatalln("Listen:", err) } // Listen one time go func() { conn, err := ln.Accept() if err != nil { log.Fatalln("Accept:", err) } for { buf := make([]byte, 1) _, err := conn.Read(buf) if err != nil { return } } }() } func ExamplePool() { // Dummy server laddr := "127.0.0.1:8080" startAcceptOnceDummyServer(laddr) // Pool creation constructor := func(context.Context) (interface{}, error) { return net.Dial("tcp", laddr) } destructor := func(value interface{}) { value.(net.Conn).Close() } maxPoolSize := int32(10) pool := puddle.NewPool(constructor, destructor, maxPoolSize) // Use pool multiple times for i := 0; i < 10; i++ { // Acquire resource res, err := pool.Acquire(context.Background()) if err != nil { log.Fatalln("Acquire", err) } // Type-assert value and use _, err = res.Value().(net.Conn).Write([]byte{1}) if err != nil { log.Fatalln("Write", err) } // Release when done. res.Release() } stats := pool.Stat() pool.Close() fmt.Println("Connections:", stats.TotalResources()) fmt.Println("Acquires:", stats.AcquireCount()) // Output: // Connections: 1 // Acquires: 10 } func BenchmarkPoolAcquireAndRelease(b *testing.B) { benchmarks := []struct { poolSize int32 clientCount int cancellable bool }{ {8, 1, false}, {8, 2, false}, {8, 8, false}, {8, 32, false}, {8, 128, false}, {8, 512, false}, {8, 2048, false}, {8, 8192, false}, {64, 2, false}, {64, 8, false}, {64, 32, false}, {64, 128, false}, {64, 512, false}, {64, 2048, false}, {64, 8192, false}, {512, 2, false}, {512, 8, false}, {512, 32, false}, {512, 128, false}, {512, 512, false}, {512, 2048, false}, {512, 8192, false}, {8, 2, true}, {8, 8, true}, {8, 32, true}, {8, 128, true}, {8, 512, true}, {8, 2048, true}, {8, 8192, true}, {64, 2, true}, {64, 8, true}, {64, 32, true}, {64, 128, true}, {64, 512, true}, {64, 2048, true}, {64, 8192, true}, {512, 2, true}, {512, 8, true}, {512, 32, true}, {512, 128, true}, {512, 512, true}, {512, 2048, true}, {512, 8192, true}, } for _, bm := range benchmarks { name := fmt.Sprintf("PoolSize=%d/ClientCount=%d/Cancellable=%v", bm.poolSize, bm.clientCount, bm.cancellable) b.Run(name, func(b *testing.B) { ctx := context.Background() cancel := func() {} if bm.cancellable { ctx, cancel = context.WithCancel(ctx) } wg := &sync.WaitGroup{} constructor, _ := createConstructor() pool := puddle.NewPool(constructor, stubDestructor, bm.poolSize) for i := 0; i < bm.clientCount; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < b.N; j++ { res, err := pool.Acquire(ctx) if err != nil { b.Fatal(err) } res.Release() } }() } wg.Wait() cancel() }) } }