diff --git a/nanotime_time.go b/nanotime_time.go new file mode 100644 index 0000000..2bca251 --- /dev/null +++ b/nanotime_time.go @@ -0,0 +1,13 @@ +// +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() +} diff --git a/nanotime_unsafe.go b/nanotime_unsafe.go new file mode 100644 index 0000000..99d80ee --- /dev/null +++ b/nanotime_unsafe.go @@ -0,0 +1,12 @@ +// +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 diff --git a/pool.go b/pool.go index 7943433..54acf30 100644 --- a/pool.go +++ b/pool.go @@ -29,7 +29,7 @@ type Resource struct { value interface{} pool *Pool creationTime time.Time - lastUsedTime time.Time + lastUsedNano int64 status byte } @@ -46,16 +46,16 @@ func (res *Resource) Release() { if res.status != resourceStatusAcquired { panic("tried to release resource that is not acquired") } - res.pool.releaseAcquiredResource(res, time.Now()) + res.pool.releaseAcquiredResource(res, nanotime()) } -// Release returns the resource to the pool after it was acquired via AcquireAllIdle. -// It does not updates lastUsedTime. res must not be subsequently used. -func (res *Resource) ReleaseIdle() { +// 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.lastUsedTime) + res.pool.releaseAcquiredResource(res, res.lastUsedNano) } // Destroy returns the resource to the pool for destruction. res must not be @@ -84,13 +84,29 @@ func (res *Resource) CreationTime() time.Time { return res.creationTime } -// LastUsedTime returns when the resource was last used, specifically when -// it was released from a normal Acquire (not from an AcquireAllIdle) -func (res *Resource) LastUsedTime() time.Time { +// LastUsedNanotime returns when Release was last called on the resource measured in nanoseconds from an arbitrary +// time (a monotonic time). Returns 0 is Release as 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.lastUsedTime + + return res.lastUsedNano +} + +// IdleDuration returns the duration since Release was last called on the resource. If Release has never been called +// a zero duration will be returned. 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") + } + + if res.lastUsedNano == 0 { + return time.Duration(0) + } + + return time.Duration(nanotime() - res.lastUsedNano) } // Pool is a concurrency-safe resource pool. @@ -240,7 +256,7 @@ func (p *Pool) Stat() *Stat { // 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) { - startTime := time.Now() + startNano := nanotime() p.cond.L.Lock() if doneChan := ctx.Done(); doneChan != nil { select { @@ -269,7 +285,7 @@ func (p *Pool) Acquire(ctx context.Context) (*Resource, error) { p.emptyAcquireCount += 1 } p.acquireCount += 1 - p.acquireDuration += time.Now().Sub(startTime) + p.acquireDuration += time.Duration(nanotime() - startNano) p.cond.L.Unlock() return res, nil } @@ -278,7 +294,7 @@ func (p *Pool) Acquire(ctx context.Context) (*Resource, error) { // If there is room to create a resource do so if len(p.allResources) < int(p.maxSize) { - res := &Resource{pool: p, creationTime: startTime, status: resourceStatusConstructing} + res := &Resource{pool: p, creationTime: time.Now(), status: resourceStatusConstructing} p.allResources = append(p.allResources, res) p.destructWG.Add(1) p.cond.L.Unlock() @@ -305,7 +321,7 @@ func (p *Pool) Acquire(ctx context.Context) (*Resource, error) { res.status = resourceStatusAcquired p.emptyAcquireCount += 1 p.acquireCount += 1 - p.acquireDuration += time.Now().Sub(startTime) + p.acquireDuration += time.Duration(nanotime() - startNano) p.cond.L.Unlock() return res, nil } @@ -358,11 +374,11 @@ func (p *Pool) AcquireAllIdle() []*Resource { } // releaseAcquiredResource returns res to the the pool. -func (p *Pool) releaseAcquiredResource(res *Resource, lastUsedTime time.Time) { +func (p *Pool) releaseAcquiredResource(res *Resource, lastUsedNano int64) { p.cond.L.Lock() if !p.closed { - res.lastUsedTime = lastUsedTime + res.lastUsedNano = lastUsedNano res.status = resourceStatusIdle p.idleResources = append(p.idleResources, res) } else { diff --git a/pool_test.go b/pool_test.go index eab1366..c822a67 100644 --- a/pool_test.go +++ b/pool_test.go @@ -218,8 +218,6 @@ func TestPoolAcquireAllIdle(t *testing.T) { resources[0], err = pool.Acquire(context.Background()) require.NoError(t, err) - assert.True(t, resources[0].LastUsedTime().IsZero(), "lastUsedTime should start as Zero") - resources[1], err = pool.Acquire(context.Background()) require.NoError(t, err) resources[2], err = pool.Acquire(context.Background()) @@ -228,25 +226,23 @@ func TestPoolAcquireAllIdle(t *testing.T) { 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()) - r0LastUsedTime := resources[0].LastUsedTime() - assert.WithinDuration(t, time.Now(), r0LastUsedTime, time.Second, "should have updated lastUsedTime") - time.Sleep(1 * time.Millisecond) // sleep before releasing - resources[0].ReleaseIdle() - resources[3].ReleaseIdle() + resources[0].Release() + resources[3].Release() resources[1].Release() resources[2].Release() assert.ElementsMatch(t, resources, pool.AcquireAllIdle()) - assert.Equal(t, r0LastUsedTime, resources[0].LastUsedTime(), "should not have updated lastUsedTime") - resources[0].ReleaseIdle() - resources[1].ReleaseIdle() - resources[2].ReleaseIdle() - resources[3].ReleaseIdle() + + resources[0].Release() + resources[1].Release() + resources[2].Release() + resources[3].Release() } func TestPoolCloseClosesAllIdleResources(t *testing.T) { @@ -469,10 +465,10 @@ func TestResourceDestroyRemovesResourceFromPool(t *testing.T) { assert.EqualValues(t, 0, pool.Stat().TotalResources()) assert.EqualValues(t, 0, destructorCalls.Value()) - // Can still call Value, CreationTime and LastUsedTime + // Can still call Value, CreationTime and IdleDuration res.Value() res.CreationTime() - res.LastUsedTime() + res.IdleDuration() } func TestResourceHijackRemovesResourceFromPoolButDoesNotDestroy(t *testing.T) { @@ -488,6 +484,43 @@ func TestResourceHijackRemovesResourceFromPoolButDoesNotDestroy(t *testing.T) { assert.EqualValues(t, 0, pool.Stat().TotalResources()) } +func TestResourceLastUsageTimeTracking(t *testing.T) { + constructor, _ := createConstructor() + pool := puddle.NewPool(constructor, stubDestructor, 1) + + // 0 before initial usage + res, err := pool.Acquire(context.Background()) + require.NoError(t, err) + t1 := res.LastUsedNanotime() + d1 := res.IdleDuration() + assert.EqualValues(t, 0, t1) + assert.EqualValues(t, 0, d1) + 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 > 0) + assert.True(t, d2 > 0) + res.ReleaseUnused() + + // ReleaseUnused does not update usage tracking + res, err = pool.Acquire(context.Background()) + require.NoError(t, err) + t3 := res.LastUsedNanotime() + assert.EqualValues(t, t2, t3) + 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) @@ -497,12 +530,12 @@ func TestResourcePanicsOnUsageWhenNotAcquired(t *testing.T) { 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.ReleaseIdle) + 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.LastUsedTime() }) + assert.PanicsWithValue(t, "tried to access resource that is not acquired or hijacked", func() { res.IdleDuration() }) } func TestPoolAcquireReturnsErrorWhenPoolIsClosed(t *testing.T) {