2
0

Use runtime.nanotime for duration tracking plus related restructuring

runtime.nanotime is significantly faster than time.Now. Use it unless
build tags prevent usage of unsafe.

Change LastUsedTime to LastUsedNanotime as a time.Time is no longer
available.

Add IdleDuration to make it more convenient to get the time a resource
has been idle.

Rename ReleaseIdle to ReleaseUnused to better indicate it is releasing
the resource without using it rather than that it is releasing a
resource acquired with AcquireAllIdle.

Refactor tests of usage tracking to not be intermingled with
AcquireAllIdle tests.
This commit is contained in:
Jack Christensen
2020-01-25 18:39:01 -06:00
parent e6e0e7fd81
commit 807afe48a8
4 changed files with 106 additions and 32 deletions
+13
View File
@@ -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()
}
+12
View File
@@ -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
+32 -16
View File
@@ -29,7 +29,7 @@ type Resource struct {
value interface{} value interface{}
pool *Pool pool *Pool
creationTime time.Time creationTime time.Time
lastUsedTime time.Time lastUsedNano int64
status byte status byte
} }
@@ -46,16 +46,16 @@ func (res *Resource) Release() {
if res.status != resourceStatusAcquired { if res.status != resourceStatusAcquired {
panic("tried to release resource that is not acquired") 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. // ReleaseUnused returns the resource to the pool without updating when it was last used used. i.e. LastUsedNanotime
// It does not updates lastUsedTime. res must not be subsequently used. // will not change. res must not be subsequently used.
func (res *Resource) ReleaseIdle() { func (res *Resource) ReleaseUnused() {
if res.status != resourceStatusAcquired { if res.status != resourceStatusAcquired {
panic("tried to release resource that is not acquired") 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 // 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 return res.creationTime
} }
// LastUsedTime returns when the resource was last used, specifically when // LastUsedNanotime returns when Release was last called on the resource measured in nanoseconds from an arbitrary
// it was released from a normal Acquire (not from an AcquireAllIdle) // time (a monotonic time). Returns 0 is Release as never been called. This is only useful to compare with other
func (res *Resource) LastUsedTime() time.Time { // calls to LastUsedNanotime. In almost all cases, IdleDuration should be used instead.
func (res *Resource) LastUsedNanotime() int64 {
if !(res.status == resourceStatusAcquired || res.status == resourceStatusHijacked) { if !(res.status == resourceStatusAcquired || res.status == resourceStatusHijacked) {
panic("tried to access resource that is not acquired or hijacked") 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. // 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 // maximum capacity it will block until a resource is available. ctx can be used
// to cancel the Acquire. // to cancel the Acquire.
func (p *Pool) Acquire(ctx context.Context) (*Resource, error) { func (p *Pool) Acquire(ctx context.Context) (*Resource, error) {
startTime := time.Now() startNano := nanotime()
p.cond.L.Lock() p.cond.L.Lock()
if doneChan := ctx.Done(); doneChan != nil { if doneChan := ctx.Done(); doneChan != nil {
select { select {
@@ -269,7 +285,7 @@ func (p *Pool) Acquire(ctx context.Context) (*Resource, error) {
p.emptyAcquireCount += 1 p.emptyAcquireCount += 1
} }
p.acquireCount += 1 p.acquireCount += 1
p.acquireDuration += time.Now().Sub(startTime) p.acquireDuration += time.Duration(nanotime() - startNano)
p.cond.L.Unlock() p.cond.L.Unlock()
return res, nil 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 there is room to create a resource do so
if len(p.allResources) < int(p.maxSize) { 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.allResources = append(p.allResources, res)
p.destructWG.Add(1) p.destructWG.Add(1)
p.cond.L.Unlock() p.cond.L.Unlock()
@@ -305,7 +321,7 @@ func (p *Pool) Acquire(ctx context.Context) (*Resource, error) {
res.status = resourceStatusAcquired res.status = resourceStatusAcquired
p.emptyAcquireCount += 1 p.emptyAcquireCount += 1
p.acquireCount += 1 p.acquireCount += 1
p.acquireDuration += time.Now().Sub(startTime) p.acquireDuration += time.Duration(nanotime() - startNano)
p.cond.L.Unlock() p.cond.L.Unlock()
return res, nil return res, nil
} }
@@ -358,11 +374,11 @@ func (p *Pool) AcquireAllIdle() []*Resource {
} }
// releaseAcquiredResource returns res to the the pool. // 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() p.cond.L.Lock()
if !p.closed { if !p.closed {
res.lastUsedTime = lastUsedTime res.lastUsedNano = lastUsedNano
res.status = resourceStatusIdle res.status = resourceStatusIdle
p.idleResources = append(p.idleResources, res) p.idleResources = append(p.idleResources, res)
} else { } else {
+49 -16
View File
@@ -218,8 +218,6 @@ func TestPoolAcquireAllIdle(t *testing.T) {
resources[0], err = pool.Acquire(context.Background()) resources[0], err = pool.Acquire(context.Background())
require.NoError(t, err) require.NoError(t, err)
assert.True(t, resources[0].LastUsedTime().IsZero(), "lastUsedTime should start as Zero")
resources[1], err = pool.Acquire(context.Background()) resources[1], err = pool.Acquire(context.Background())
require.NoError(t, err) require.NoError(t, err)
resources[2], err = pool.Acquire(context.Background()) resources[2], err = pool.Acquire(context.Background())
@@ -228,25 +226,23 @@ func TestPoolAcquireAllIdle(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, pool.AcquireAllIdle(), 0) assert.Len(t, pool.AcquireAllIdle(), 0)
resources[0].Release() resources[0].Release()
resources[3].Release() resources[3].Release()
assert.ElementsMatch(t, []*puddle.Resource{resources[0], resources[3]}, pool.AcquireAllIdle()) 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[1].Release()
resources[2].Release() resources[2].Release()
assert.ElementsMatch(t, resources, pool.AcquireAllIdle()) assert.ElementsMatch(t, resources, pool.AcquireAllIdle())
assert.Equal(t, r0LastUsedTime, resources[0].LastUsedTime(), "should not have updated lastUsedTime")
resources[0].ReleaseIdle() resources[0].Release()
resources[1].ReleaseIdle() resources[1].Release()
resources[2].ReleaseIdle() resources[2].Release()
resources[3].ReleaseIdle() resources[3].Release()
} }
func TestPoolCloseClosesAllIdleResources(t *testing.T) { 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, pool.Stat().TotalResources())
assert.EqualValues(t, 0, destructorCalls.Value()) assert.EqualValues(t, 0, destructorCalls.Value())
// Can still call Value, CreationTime and LastUsedTime // Can still call Value, CreationTime and IdleDuration
res.Value() res.Value()
res.CreationTime() res.CreationTime()
res.LastUsedTime() res.IdleDuration()
} }
func TestResourceHijackRemovesResourceFromPoolButDoesNotDestroy(t *testing.T) { func TestResourceHijackRemovesResourceFromPoolButDoesNotDestroy(t *testing.T) {
@@ -488,6 +484,43 @@ func TestResourceHijackRemovesResourceFromPoolButDoesNotDestroy(t *testing.T) {
assert.EqualValues(t, 0, pool.Stat().TotalResources()) 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) { func TestResourcePanicsOnUsageWhenNotAcquired(t *testing.T) {
constructor, _ := createConstructor() constructor, _ := createConstructor()
pool := puddle.NewPool(constructor, stubDestructor, 10) pool := puddle.NewPool(constructor, stubDestructor, 10)
@@ -497,12 +530,12 @@ func TestResourcePanicsOnUsageWhenNotAcquired(t *testing.T) {
res.Release() 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.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 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 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.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.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) { func TestPoolAcquireReturnsErrorWhenPoolIsClosed(t *testing.T) {