New Upstream Snapshot - golang-github-karrick-goswarm
Ready changes
Summary
Merged new upstream version: 1.10.0 (was: 1.4.7).
Resulting package
Built on 2022-11-10T06:46 (took 6m37s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-snapshots golang-github-karrick-goswarm-dev
Lintian Result
Diff
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index daf913b..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Compiled Object files, Static and Dynamic libs (Shared Objects)
-*.o
-*.a
-*.so
-
-# Folders
-_obj
-_test
-
-# Architecture specific extensions/prefixes
-*.[568vq]
-[568vq].out
-
-*.cgo1.go
-*.cgo2.c
-_cgo_defun.c
-_cgo_gotypes.go
-_cgo_export.*
-
-_testmain.go
-
-*.exe
-*.test
-*.prof
diff --git a/debian/changelog b/debian/changelog
index 51af554..174c74d 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-karrick-goswarm (1.10.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Thu, 10 Nov 2022 06:41:11 -0000
+
golang-github-karrick-goswarm (1.4.7-1) unstable; urgency=medium
* Initial release. (Closes: #888673)
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..4a7adbc
--- /dev/null
+++ b/go.mod
@@ -0,0 +1 @@
+module github.com/karrick/goswarm
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e69de29
diff --git a/simple.go b/simple.go
index a22b2ef..0c7a5aa 100644
--- a/simple.go
+++ b/simple.go
@@ -23,6 +23,22 @@ type Simple struct {
halt chan struct{}
closeError chan error
gcFlag int32
+ stats Stats
+}
+
+// Stats contains various cache statistics.
+type Stats struct {
+ Count int64 // Count represents the number of items in the cache.
+ Creates int64 // Creates represents how many new cache items were created since the previous Stats call.
+ Deletes int64 // Deletes represents how many Delete calls were made since the previous Stats call. Note this counts when Delete is called with a key that is not in the cache.
+ Evictions int64 // Evictions represents how many key-value pairs were evicted since the previous Stats call.
+ Hits int64 // Hits represents how many Load and Query calls found and returned a Fresh item.
+ LookupErrors int64 // LookupErrors represents the number of Lookup invocations that returned an error since the previous Stats call.
+ Misses int64 // Misses represents how many Load and Query calls either did not find the item, or found an expired item.
+ Queries int64 // Queries represents how many Load and Query calls were made since the previous Stats call.
+ Stales int64 // Stales represents how many Load and Query calls found and returned a Stale item.
+ Stores int64 // Stores represents how many Store calls were made since the previous Stats call.
+ Updates int64 // Updates represents how many Update calls were made since the previous Stats call.
}
// NewSimple returns Swarm that attempts to respond to Query methods by
@@ -108,6 +124,8 @@ func (s *Simple) Close() error {
// Delete removes the key and associated value from the data map.
func (s *Simple) Delete(key string) {
+ atomic.AddInt64(&s.stats.Deletes, 1)
+
s.lock.RLock()
_, ok := s.data[key]
s.lock.RUnlock()
@@ -177,7 +195,7 @@ func (s *Simple) GC() {
if av := atv.av.Load(); av != nil {
allPairs <- gcPair{
key: key,
- doomed: av.(*TimedValue).isExpired(now),
+ doomed: av.(*TimedValue).IsExpiredAt(now),
}
}
}(key, atv, allPairs)
@@ -217,18 +235,22 @@ loop:
for _, key := range doomed {
delete(s.data, key)
}
+ atomic.AddInt64(&s.stats.Evictions, int64(len(doomed)))
s.lock.Unlock()
}
// Load returns the value associated with the specified key, and a boolean value
// indicating whether or not the key was found in the map.
func (s *Simple) Load(key string) (interface{}, bool) {
+ atomic.AddInt64(&s.stats.Queries, 1)
+
// Do not want to use getOrCreateLockingTimeValue, because there's no reason
// to create ATV if key is not present in data map.
s.lock.RLock()
atv, ok := s.data[key]
s.lock.RUnlock()
if !ok {
+ atomic.AddInt64(&s.stats.Misses, 1)
return nil, false
}
@@ -237,9 +259,60 @@ func (s *Simple) Load(key string) (interface{}, bool) {
// Element either recently erased by another routine while this method
// was waiting for element lock above, or has not been populated by
// fetch, in which case the value is not really there yet.
+ atomic.AddInt64(&s.stats.Misses, 1)
return nil, false
}
- return av.(*TimedValue).Value, true
+
+ now := time.Now()
+ tv := av.(*TimedValue)
+
+ if tv.IsExpiredAt(now) {
+ atomic.AddInt64(&s.stats.Misses, 1)
+ return nil, false
+ }
+ if tv.IsStaleAt(now) {
+ atomic.AddInt64(&s.stats.Stales, 1)
+ return tv.Value, true
+ }
+ atomic.AddInt64(&s.stats.Hits, 1)
+ return tv.Value, true
+}
+
+// LoadTimedValue returns the TimedValue associated with the specified key, or
+// false if the key is not found in the map.
+func (s *Simple) LoadTimedValue(key string) *TimedValue {
+ atomic.AddInt64(&s.stats.Queries, 1)
+
+ // Do not want to use getOrCreateLockingTimeValue, because there's no reason
+ // to create ATV if key is not present in data map.
+ s.lock.RLock()
+ atv, ok := s.data[key]
+ s.lock.RUnlock()
+ if !ok {
+ atomic.AddInt64(&s.stats.Misses, 1)
+ return nil
+ }
+
+ av := atv.av.Load()
+ if av == nil {
+ // Element either recently erased by another routine while this method
+ // was waiting for element lock above, or has not been populated by
+ // fetch, in which case the value is not really there yet.
+ atomic.AddInt64(&s.stats.Misses, 1)
+ return nil
+ }
+
+ now := time.Now()
+ tv := av.(*TimedValue)
+
+ if tv.IsExpiredAt(now) {
+ atomic.AddInt64(&s.stats.Misses, 1)
+ } else if tv.IsStaleAt(now) {
+ atomic.AddInt64(&s.stats.Stales, 1)
+ } else {
+ atomic.AddInt64(&s.stats.Hits, 1)
+ }
+ return tv
}
// Query loads the value associated with the specified key from the data
@@ -249,27 +322,40 @@ func (s *Simple) Load(key string) (interface{}, bool) {
// lookup of a new value is triggered, then the new value is stored and
// returned.
func (s *Simple) Query(key string) (interface{}, error) {
+ atomic.AddInt64(&s.stats.Queries, 1)
+
atv := s.getOrCreateAtomicTimedValue(key)
av := atv.av.Load()
if av == nil {
+ atomic.AddInt64(&s.stats.Misses, 1)
tv := s.update(key, atv)
return tv.Value, tv.Err
- } else {
- now := time.Now()
- tv := av.(*TimedValue)
- if tv.isExpired(now) {
- tv = s.update(key, atv)
- } else if tv.isStale(now) {
- // If no other goroutine is looking up this value, spin one off
- if atomic.CompareAndSwapInt32(&atv.pending, 0, 1) {
- go func() {
- defer atomic.StoreInt32(&atv.pending, 0)
- _ = s.update(key, atv)
- }()
- }
+ }
+
+ now := time.Now()
+ tv := av.(*TimedValue)
+
+ if tv.IsExpiredAt(now) {
+ // Expired is considered a blocking miss.
+ atomic.AddInt64(&s.stats.Misses, 1)
+ tv := s.update(key, atv)
+ return tv.Value, tv.Err
+ }
+
+ if tv.IsStaleAt(now) {
+ // If no other goroutine is looking up this value, spin one off.
+ if atomic.CompareAndSwapInt32(&atv.pending, 0, 1) {
+ go func() {
+ defer atomic.StoreInt32(&atv.pending, 0)
+ _ = s.update(key, atv)
+ }()
}
+ atomic.AddInt64(&s.stats.Stales, 1)
return tv.Value, tv.Err
}
+
+ atomic.AddInt64(&s.stats.Hits, 1)
+ return tv.Value, tv.Err
}
// Range invokes specified callback function for each non-expired key in the
@@ -300,16 +386,79 @@ func (s *Simple) Range(callback func(key string, value *TimedValue)) {
s.lock.RUnlock()
}
+// RangeBreak invokes specified callback function for each non-expired key in
+// the data map. Each key-value pair is independently locked until the callback
+// function invoked with the specified key returns. This method does not block
+// access to the Simple instance, allowing keys to be added and removed like
+// normal even while the callbacks are running. When the callback returns true,
+// this function performs an early termination of enumerating the cache,
+// returning true it its caller.
+func (s *Simple) RangeBreak(callback func(key string, value *TimedValue) bool) bool {
+ // Need to have read lock while enumerating key-value pairs from map
+ s.lock.RLock()
+ for key, atv := range s.data {
+ // Now that we have a key-value pair from the map, we can release the
+ // map's lock to prevent blocking other routines that need it.
+ s.lock.RUnlock()
+
+ if av := atv.av.Load(); av != nil {
+ // We have an element. If it's not yet expired, invoke the user's
+ // callback with the key and value.
+ if tv := av.(*TimedValue); !tv.IsExpired() {
+ if callback(key, tv) {
+ return true
+ }
+ }
+ }
+
+ // After callback is done with element, re-acquire map-level lock before
+ // we grab the next key-value pair from the map.
+ s.lock.RLock()
+ }
+ s.lock.RUnlock()
+ return false
+}
+
+// Stats returns a snapshot of the cache's statistics. Note all statistics will
+// be reset when this method is invoked, allowing the client to determine the
+// number of each respective events that have taken place since the previous
+// time this method was invoked.
+func (s *Simple) Stats() Stats {
+ s.lock.RLock()
+ count := int64(len(s.data))
+ s.lock.RUnlock()
+
+ return Stats{
+ Count: count,
+ Creates: atomic.SwapInt64(&s.stats.Creates, 0),
+ Deletes: atomic.SwapInt64(&s.stats.Deletes, 0),
+ Evictions: atomic.SwapInt64(&s.stats.Evictions, 0),
+ Hits: atomic.SwapInt64(&s.stats.Hits, 0),
+ LookupErrors: atomic.SwapInt64(&s.stats.LookupErrors, 0),
+ Misses: atomic.SwapInt64(&s.stats.Misses, 0),
+ Queries: atomic.SwapInt64(&s.stats.Queries, 0),
+ Stales: atomic.SwapInt64(&s.stats.Stales, 0),
+ Stores: atomic.SwapInt64(&s.stats.Stores, 0),
+ Updates: atomic.SwapInt64(&s.stats.Updates, 0),
+ }
+}
+
// Store saves the key-value pair to the cache, overwriting whatever was
// previously stored.
func (s *Simple) Store(key string, value interface{}) {
+ atomic.AddInt64(&s.stats.Stores, 1)
atv := s.getOrCreateAtomicTimedValue(key)
+
+ // NOTE: Below invocation ignores the provided durations when value is
+ // already a TimedValue.
tv := newTimedValue(value, nil, s.config.GoodStaleDuration, s.config.GoodExpiryDuration)
+
atv.av.Store(tv)
}
// Update forces an update of the value associated with the specified key.
func (s *Simple) Update(key string) {
+ atomic.AddInt64(&s.stats.Updates, 1)
atv := s.getOrCreateAtomicTimedValue(key)
s.update(key, atv)
}
@@ -341,6 +490,7 @@ func (s *Simple) getOrCreateAtomicTimedValue(key string) *atomicTimedValue {
if !ok {
atv = new(atomicTimedValue)
s.data[key] = atv
+ atomic.AddInt64(&s.stats.Creates, 1)
}
s.lock.Unlock()
}
@@ -351,19 +501,17 @@ func (s *Simple) getOrCreateAtomicTimedValue(key string) *atomicTimedValue {
// the update is successful, it stores the value in the TimedValue associated
// with the key.
func (s *Simple) update(key string, atv *atomicTimedValue) *TimedValue {
- staleDuration := s.config.GoodStaleDuration
- expiryDuration := s.config.GoodExpiryDuration
-
value, err := s.config.Lookup(key)
if err == nil {
- tv := newTimedValue(value, err, staleDuration, expiryDuration)
+ tv := newTimedValue(value, nil, s.config.GoodStaleDuration, s.config.GoodExpiryDuration)
atv.av.Store(tv)
return tv
}
// lookup gave us an error
- staleDuration = s.config.BadStaleDuration
- expiryDuration = s.config.BadExpiryDuration
+ atomic.AddInt64(&s.stats.LookupErrors, 1)
+ staleDuration := s.config.BadStaleDuration
+ expiryDuration := s.config.BadExpiryDuration
// new error overwrites previous error, and also used when initial value
av := atv.av.Load()
diff --git a/simple_test.go b/simple_test.go
index 7946423..9b947cc 100644
--- a/simple_test.go
+++ b/simple_test.go
@@ -12,8 +12,8 @@ import (
"time"
)
-func ensureErrorL(t *testing.T, swr *Simple, key, expectedError string) {
- value, err := swr.Query(key)
+func ensureErrorL(t *testing.T, querier Querier, key, expectedError string) {
+ value, err := querier.Query(key)
if value != nil {
t.Errorf("Actual: %v; Expected: %v", value, nil)
}
@@ -22,8 +22,8 @@ func ensureErrorL(t *testing.T, swr *Simple, key, expectedError string) {
}
}
-func ensureValueL(t *testing.T, swr *Simple, key string, expectedValue uint64) {
- value, err := swr.Query(key)
+func ensureValueL(t *testing.T, querier Querier, key string, expectedValue uint64) {
+ value, err := querier.Query(key)
if value.(uint64) != expectedValue {
t.Errorf("Actual: %d; Expected: %d", value, expectedValue)
}
@@ -37,7 +37,7 @@ func ensureValueL(t *testing.T, swr *Simple, key string, expectedValue uint64) {
func TestSimpleSynchronousLookupWhenMiss(t *testing.T) {
var invoked uint64
swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) {
- atomic.AddUint64(&invoked, 1)
+ invoked++
return uint64(42), nil
}})
if err != nil {
@@ -47,7 +47,7 @@ func TestSimpleSynchronousLookupWhenMiss(t *testing.T) {
ensureValueL(t, swr, "miss", 42)
- if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected {
+ if actual, expected := invoked, uint64(1); actual != expected {
t.Errorf("Actual: %d; Expected: %d", actual, expected)
}
}
@@ -79,11 +79,54 @@ func TestSimpleNoStaleExpireNoLookupWhenBeforeExpire(t *testing.T) {
defer func() { _ = swr.Close() }()
// NOTE: storing a value that expires one minute in the future
- swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Expiry: time.Now().Add(time.Minute)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Created: now, Expiry: now.Add(time.Minute)})
ensureValueL(t, swr, "hit", 13)
}
+func TestSimpleStaleExpireLoadReturnsFalse(t *testing.T) {
+ swr, err := NewSimple(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = swr.Close() }()
+
+ now := time.Now()
+ swr.Store("expired", &TimedValue{Value: uint64(42), Created: now, Expiry: now.Add(-time.Minute)})
+
+ value, ok := swr.Load("expired")
+
+ if got, want := ok, false; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ if value != nil {
+ t.Errorf("GOT: %v; WANT: %v", value, nil)
+ }
+}
+
+func TestSimpleStaleExpireLoadTimedValueReturnsExpiredValue(t *testing.T) {
+ swr, err := NewSimple(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = swr.Close() }()
+
+ now := time.Now()
+ swr.Store("expired", &TimedValue{Value: uint64(42), Created: now, Expiry: now.Add(-time.Minute)})
+
+ tv := swr.LoadTimedValue("expired")
+
+ if got, want := tv.IsExpired(), true; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ if got, want := tv.Value, uint64(42); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+}
+
func TestSimpleNoStaleExpireSynchronousLookupWhenAfterExpire(t *testing.T) {
var invoked uint64
swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) {
@@ -96,7 +139,8 @@ func TestSimpleNoStaleExpireSynchronousLookupWhenAfterExpire(t *testing.T) {
defer func() { _ = swr.Close() }()
// NOTE: storing a value that expired one minute ago
- swr.Store("hit", &TimedValue{Value: uint64(42), Err: nil, Expiry: time.Now().Add(-time.Minute)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: uint64(42), Err: nil, Created: now, Expiry: now.Add(-time.Minute)})
ensureValueL(t, swr, "hit", 42)
@@ -117,7 +161,8 @@ func TestSimpleStaleNoExpireNoLookupWhenBeforeStale(t *testing.T) {
defer func() { _ = swr.Close() }()
// NOTE: storing a value that goes stale one minute in the future
- swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(time.Minute)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Created: now, Stale: now.Add(time.Minute)})
ensureValueL(t, swr, "hit", 13)
}
@@ -137,7 +182,8 @@ func TestSimpleStaleNoExpireSynchronousLookupOnlyOnceWhenAfterStale(t *testing.T
defer func() { _ = swr.Close() }()
// NOTE: storing a value that went stale one minute ago
- swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(-time.Minute)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Created: now, Stale: now.Add(-time.Minute)})
wg.Add(1)
ensureValueL(t, swr, "hit", 13)
@@ -165,7 +211,8 @@ func TestSimpleStaleExpireNoLookupWhenBeforeStale(t *testing.T) {
defer func() { _ = swr.Close() }()
// NOTE: storing a value that goes stale one minute in the future and expires one hour in the future
- swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(time.Minute), Expiry: time.Now().Add(time.Hour)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Created: now, Stale: now.Add(time.Minute), Expiry: now.Add(time.Hour)})
ensureValueL(t, swr, "hit", 13)
}
@@ -184,7 +231,8 @@ func TestSimpleStaleExpireSynchronousLookupWhenAfterStaleAndBeforeExpire(t *test
defer func() { _ = swr.Close() }()
// NOTE: storing a value that went stale one minute ago and expires one minute in the future
- swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(-time.Minute), Expiry: time.Now().Add(time.Minute)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Created: now, Stale: now.Add(-time.Minute), Expiry: now.Add(time.Minute)})
// expect to receive the old value back immediately, then expect lookup to be asynchronously invoked
wg.Add(1)
@@ -212,7 +260,8 @@ func TestSimpleStaleExpireSynchronousLookupWhenAfterExpire(t *testing.T) {
defer func() { _ = swr.Close() }()
// NOTE: storing a value that went stale one hour ago and expired one minute ago
- swr.Store("hit", &TimedValue{Value: uint64(42), Err: nil, Stale: time.Now().Add(-time.Hour), Expiry: time.Now().Add(-time.Minute)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: uint64(42), Err: nil, Created: now, Stale: now.Add(-time.Hour), Expiry: now.Add(-time.Minute)})
ensureValueL(t, swr, "hit", 42)
@@ -235,7 +284,8 @@ func TestSimpleErrDoesNotReplaceStaleValue(t *testing.T) {
defer func() { _ = swr.Close() }()
// NOTE: storing a value that went stale one minute ago
- swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(-time.Minute)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Created: now, Stale: now.Add(-time.Minute)})
wg.Add(1)
ensureValueL(t, swr, "hit", 13)
@@ -266,7 +316,8 @@ func TestSimpleNewErrReplacesOldError(t *testing.T) {
defer func() { _ = swr.Close() }()
// NOTE: storing a value that went stale one minute ago
- swr.Store("hit", &TimedValue{Value: nil, Err: errors.New("original error"), Stale: time.Now().Add(-time.Minute)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: nil, Err: errors.New("original error"), Created: now, Stale: now.Add(-time.Minute)})
wg.Add(1)
ensureErrorL(t, swr, "hit", "new error")
@@ -292,7 +343,8 @@ func TestSimpleErrReplacesExpiredValue(t *testing.T) {
defer func() { _ = swr.Close() }()
// NOTE: storing a value is already stale, but will expire during the fetch
- swr.Store("hit", &TimedValue{Value: nil, Err: errors.New("original error"), Stale: time.Now().Add(-time.Hour), Expiry: time.Now().Add(5 * time.Millisecond)})
+ now := time.Now()
+ swr.Store("hit", &TimedValue{Value: nil, Err: errors.New("original error"), Created: now, Stale: now.Add(-time.Hour), Expiry: now.Add(5 * time.Millisecond)})
wg.Add(1)
ensureErrorL(t, swr, "hit", "original error")
@@ -317,13 +369,26 @@ func TestSimpleRange(t *testing.T) {
defer func() { _ = swr.Close() }()
swr.Store("no expiry", "shall not expire")
- swr.Store("stale value", TimedValue{Value: "stale value", Stale: time.Now().Add(-time.Minute)})
- swr.Store("expired value", TimedValue{Value: "expired value", Expiry: time.Now().Add(-time.Minute)})
+ swr.Store("expired value", TimedValue{Value: "expired value", Created: time.Now(), Expiry: time.Now().Add(-time.Minute)})
+ swr.Store("stale value", TimedValue{Value: "stale value", Created: time.Now(), Stale: time.Now().Add(-time.Minute)})
+ swr.Store("will update expiry", "soon to be expired")
+
+ swr.Store("will update stale", TimedValue{Value: "stale value", Created: time.Now(), Stale: time.Now().Add(-time.Minute)})
+ // make sure already stale
+ if got, want := swr.LoadTimedValue("will update stale").IsStale(), true; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
called := make(map[string]struct{})
swr.Range(func(key string, value *TimedValue) {
called[key] = struct{}{}
swr.Store(strconv.Itoa(rand.Intn(50)), "make sure we can invoke methods that require locking")
+ switch key {
+ case "will update stale":
+ value.Stale = time.Now().Add(time.Minute)
+ case "will update expiry":
+ value.Expiry = time.Now().Add(-time.Minute)
+ }
})
if _, ok := called["no expiry"]; !ok {
@@ -335,6 +400,46 @@ func TestSimpleRange(t *testing.T) {
if _, ok := called["expired value"]; ok {
t.Errorf("Actual: %#v; Expected: %#v", ok, false)
}
+ if _, ok := called["will update stale"]; !ok {
+ t.Errorf("Actual: %#v; Expected: %#v", ok, true)
+ }
+ if got, want := swr.LoadTimedValue("will update stale").IsStale(), false; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := swr.LoadTimedValue("will update expiry").IsExpired(), true; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ swr.Store("ensure range released top level lock", struct{}{})
+}
+
+func TestSimpleRangeBreak(t *testing.T) {
+ swr, err := NewSimple(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = swr.Close() }()
+
+ swr.Store("alpha", 1)
+ swr.Store("bravo", 2)
+ swr.Store("charlie", 3)
+ swr.Store("delta", 4)
+
+ called := make(map[string]struct{})
+ terminated := swr.RangeBreak(func(key string, value *TimedValue) bool {
+ called[key] = struct{}{}
+ if key == "charlie" {
+ return true
+ }
+ return false
+ })
+
+ if got, want := terminated, true; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if _, ok := called["charlie"]; !ok {
+ t.Errorf("Actual: %#v; Expected: %#v", ok, true)
+ }
swr.Store("ensure range released top level lock", struct{}{})
}
@@ -384,3 +489,401 @@ func TestSimpleGC(t *testing.T) {
t.Errorf("Actual: %s; Expected: %s", actual, expected)
}
}
+
+func TestStats(t *testing.T) {
+ t.Run("query", func(t *testing.T) {
+ var haveLookupFail bool
+
+ swr, err := NewSimple(&Config{
+ Lookup: func(key string) (interface{}, error) {
+ if haveLookupFail {
+ return nil, errors.New("lookup failure")
+ }
+ time.Sleep(10 * time.Millisecond)
+ return key, nil
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Get stats before cache methods invoked.
+ stats := swr.Stats()
+ if got, want := stats.Count, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Creates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Deletes, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Evictions, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.LookupErrors, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Queries, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Hits, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Misses, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stales, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stores, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Updates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ // Invoke Query with key not yet in cache.
+ _, err = swr.Query("foo")
+ if got, want := err, error(nil); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ // Get stats after new key-value pair added.
+ stats = swr.Stats()
+ if got, want := stats.Count, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Creates, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Deletes, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Evictions, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.LookupErrors, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Queries, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Hits, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Misses, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stales, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stores, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Updates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ // Invoke Query with key already in cache.
+ _, err = swr.Query("foo")
+ if got, want := err, error(nil); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ // Get stats after new key-value pair added.
+ stats = swr.Stats()
+ if got, want := stats.Count, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Creates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Deletes, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Evictions, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.LookupErrors, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Queries, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Hits, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Misses, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stales, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stores, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Updates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ // Invoke Query with key already in cache.
+ haveLookupFail = true
+ _, err = swr.Query("bar")
+ if err == nil || !strings.Contains(err.Error(), "lookup failure") {
+ t.Errorf("GOT: %v; WANT: %v", err, "lookup failure")
+ }
+
+ // Get stats after new key-value pair added.
+ stats = swr.Stats()
+ if got, want := stats.Count, int64(2); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Creates, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Deletes, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Evictions, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.LookupErrors, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Queries, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Hits, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Misses, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stales, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stores, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Updates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ })
+
+ t.Run("load", func(t *testing.T) {
+ t.Run("stale", func(t *testing.T) {
+ swr, err := NewSimple(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ swr.Store("foo", TimedValue{
+ Value: "foo",
+ Stale: time.Now().Add(-time.Second),
+ })
+ _, ok := swr.Load("foo")
+ if got, want := ok, true; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ stats := swr.Stats()
+
+ if got, want := stats.Count, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Creates, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Deletes, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Evictions, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.LookupErrors, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Queries, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Hits, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Misses, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stales, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stores, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Updates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ })
+
+ t.Run("expired", func(t *testing.T) {
+ swr, err := NewSimple(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ swr.Store("foo", TimedValue{
+ Value: "foo",
+ Expiry: time.Now().Add(-time.Second),
+ })
+ _, ok := swr.Load("foo")
+ if got, want := ok, false; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ stats := swr.Stats()
+
+ if got, want := stats.Count, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Creates, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Deletes, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Evictions, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.LookupErrors, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Queries, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Hits, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Misses, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stales, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stores, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Updates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ })
+ })
+
+ t.Run("load-timed-value", func(t *testing.T) {
+ t.Run("stale", func(t *testing.T) {
+ swr, err := NewSimple(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ swr.Store("foo", TimedValue{
+ Value: "foo",
+ Stale: time.Now().Add(-time.Second),
+ Expiry: time.Now().Add(time.Second),
+ })
+ tv := swr.LoadTimedValue("foo")
+ if got, want := tv.IsStale(), true; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := tv.IsExpired(), false; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ stats := swr.Stats()
+
+ if got, want := stats.Count, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Creates, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Deletes, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Evictions, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.LookupErrors, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Queries, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Hits, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Misses, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stales, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stores, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Updates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ })
+
+ t.Run("expired", func(t *testing.T) {
+ swr, err := NewSimple(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ swr.Store("foo", TimedValue{
+ Value: "foo",
+ Stale: time.Now().Add(-time.Second),
+ Expiry: time.Now().Add(-time.Second),
+ })
+ tv := swr.LoadTimedValue("foo")
+ if got, want := tv.IsStale(), true; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := tv.IsExpired(), true; got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+
+ stats := swr.Stats()
+
+ if got, want := stats.Count, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Creates, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Deletes, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Evictions, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.LookupErrors, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Queries, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Hits, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Misses, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stales, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Stores, int64(1); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ if got, want := stats.Updates, int64(0); got != want {
+ t.Errorf("GOT: %v; WANT: %v", got, want)
+ }
+ })
+ })
+}
diff --git a/timedValue.go b/timedValue.go
index 432ad23..ca0ccf3 100644
--- a/timedValue.go
+++ b/timedValue.go
@@ -5,8 +5,29 @@ import (
"time"
)
-// TimedValue couples a value or the error with both a stale and expiry time for the value and
-// error.
+// TimedValueStatus is an enumeration of the states of a TimedValue: Fresh,
+// Stale, or Expired.
+type TimedValueStatus int
+
+const (
+ // Fresh items are not yet Stale, and will be returned immediately on Query
+ // without scheduling an asynchronous Lookup.
+ Fresh TimedValueStatus = iota
+
+ // Stale items have exceeded their Fresh status, and will be returned
+ // immediately on Query, but will also schedule an asynchronous Lookup if a
+ // Lookup for this key is not yet in flight.
+ Stale
+
+ // Expired items have exceeded their Fresh and Stale status, and will be
+ // evicted during the next background cache eviction loop, controlled by
+ // GCPeriodicity parameter, or upon Query, which will cause the Query to
+ // block until a new value can be obtained from the Lookup function.
+ Expired
+)
+
+// TimedValue couples a value or the error with both a stale and expiry time for
+// the value and error.
type TimedValue struct {
// Value stores the datum returned by the lookup function.
Value interface{}
@@ -14,64 +35,103 @@ type TimedValue struct {
// Err stores the error returned by the lookup function.
Err error
- // Stale stores the time at which the value becomes stale. On Query, a stale value will
- // trigger an asynchronous lookup of a replacement value, and the original value is
- // returned. A zero-value for Stale implies the value never goes stale, and querying the key
- // associated for this value will never trigger an asynchronous lookup of a replacement
- // value.
+ // Stale stores the time at which the value becomes stale. On Query, a stale
+ // value will trigger an asynchronous lookup of a replacement value, and the
+ // original value is returned. A zero-value for Stale implies the value
+ // never goes stale, and querying the key associated for this value will
+ // never trigger an asynchronous lookup of a replacement value.
Stale time.Time
- // Expiry stores the time at which the value expires. On Query, an expired value will block
- // until a synchronous lookup of a replacement value is attempted. Once the lookup returns,
- // the Query method will return with the new value or the error returned by the lookup
- // function.
+ // Expiry stores the time at which the value expires. On Query, an expired
+ // value will block until a synchronous lookup of a replacement value is
+ // attempted. Once the lookup returns, the Query method will return with the
+ // new value or the error returned by the lookup function.
Expiry time.Time
+
+ // Created stores the time at which the value was created.
+ Created time.Time
}
-// IsExpired returns true if and only if value is expired. A value is expired when its non-zero
-// expiry time is before the current time, or when the value represents an error and expiry time is
-// the time.Time zero-value.
+// IsExpired returns true when the value is expired.
+//
+// A value is expired when its non-zero expiry time is before the current time,
+// or when the value represents an error and expiry time is the time.Time
+// zero-value.
func (tv *TimedValue) IsExpired() bool {
- return tv.isExpired(time.Now())
+ return tv.IsExpiredAt(time.Now())
}
-// provided for internal use so we don't need to repeatedly get the current time
-func (tv *TimedValue) isExpired(when time.Time) bool {
+// IsExpiredAt returns true when the value is expired at the specified time.
+//
+// A value is expired when its non-zero expiry time is before the specified
+// time, or when the value represents an error and expiry time is the time.Time
+// zero-value.
+func (tv *TimedValue) IsExpiredAt(when time.Time) bool {
if tv.Err == nil {
return !tv.Expiry.IsZero() && when.After(tv.Expiry)
}
- // NOTE: When a TimedValue stores an error result, then Expiry and Expiry zero-values imply
- // the value is immediately expired.
+ // NOTE: When a TimedValue stores an error result, then a zero-value for the
+ // Expiry imply the value is immediately expired.
return tv.Expiry.IsZero() || when.After(tv.Expiry)
}
-// IsStale returns true if and only if value is stale. A value is stale when its non-zero stale time
-// is before the current time, or when the value represents an error and stale time is the time.Time
+// IsStale returns true when the value is stale.
+//
+// A value is stale when its non-zero stale time is before the current time, or
+// when the value represents an error and stale time is the time.Time
// zero-value.
func (tv *TimedValue) IsStale() bool {
- return tv.isStale(time.Now())
+ return tv.IsStaleAt(time.Now())
}
-// provided for internal use so we don't need to repeatedly get the current time
-func (tv *TimedValue) isStale(when time.Time) bool {
+// IsStaleAt returns true when the value is stale at the specified time.
+//
+// A value is stale when its non-zero stale time is before the specified time,
+// or when the value represents an error and stale time is the time.Time
+// zero-value.
+func (tv *TimedValue) IsStaleAt(when time.Time) bool {
if tv.Err == nil {
return !tv.Stale.IsZero() && when.After(tv.Stale)
}
- // NOTE: When a TimedValue stores an error result, then Stale and Expiry zero-values imply
- // the value is immediately stale.
+ // NOTE: When a TimedValue stores an error result, then a zero-value for the
+ // Stale or Expiry imply the value is immediately stale.
return tv.Stale.IsZero() || when.After(tv.Stale)
}
+// Status returns Fresh, Stale, or Exired, depending on the status of the
+// TimedValue item at the current time.
+func (tv *TimedValue) Status() TimedValueStatus {
+ return tv.StatusAt(time.Now())
+}
+
+// StatusAt returns Fresh, Stale, or Exired, depending on the status of the
+// TimedValue item at the specified time.
+func (tv *TimedValue) StatusAt(when time.Time) TimedValueStatus {
+ if tv.IsExpiredAt(when) {
+ return Expired
+ }
+ if tv.IsStaleAt(when) {
+ return Stale
+ }
+ return Fresh
+}
+
// helper function to wrap non TimedValue items as TimedValue items.
func newTimedValue(value interface{}, err error, staleDuration, expiryDuration time.Duration) *TimedValue {
switch val := value.(type) {
case TimedValue:
+ if val.Created.IsZero() {
+ val.Created = time.Now()
+ }
return &val
case *TimedValue:
+ if val.Created.IsZero() {
+ val.Created = time.Now()
+ }
return val
default:
if staleDuration == 0 && expiryDuration == 0 {
- return &TimedValue{Value: value, Err: err}
+ return &TimedValue{Value: value, Err: err, Created: time.Now()}
}
var stale, expiry time.Time
now := time.Now()
@@ -81,7 +141,7 @@ func newTimedValue(value interface{}, err error, staleDuration, expiryDuration t
if expiryDuration > 0 {
expiry = now.Add(expiryDuration)
}
- return &TimedValue{Value: value, Err: err, Stale: stale, Expiry: expiry}
+ return &TimedValue{Value: value, Err: err, Created: time.Now(), Stale: stale, Expiry: expiry}
}
}
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/share/gocode/src/github.com/karrick/goswarm/go.mod -rw-r--r-- root/root /usr/share/gocode/src/github.com/karrick/goswarm/go.sum
No differences were encountered in the control files