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{}
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 {
+49 -16
View File
@@ -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) {