From 9192b384abdd9f57d0d2373968bbe0b7ed0eebdb Mon Sep 17 00:00:00 2001 From: Michael Dresner Date: Fri, 18 Mar 2022 22:18:52 +0300 Subject: [PATCH] Introduce generic ordered cache --- ordered.go | 74 ++++++++++++++++++++++++++++++++++++++++++ ordered_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 ordered.go create mode 100644 ordered_test.go diff --git a/ordered.go b/ordered.go new file mode 100644 index 0000000..ff304b3 --- /dev/null +++ b/ordered.go @@ -0,0 +1,74 @@ +package cache + +import ( + "fmt" + "time" + + "golang.org/x/exp/constraints" +) + +type OrderedCache[K comparable, V constraints.Ordered] struct { + *orderedCache[K, V] +} + +type orderedCache[K comparable, V constraints.Ordered] struct { + *Cache[K, V] +} + +// Increment an item of type by n. +// Returns incremented item or an error if it was not found. +func (c *orderedCache[K, V]) Increment(k K, n V) (*V, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return nil, fmt.Errorf("Item %v not found", k) + } + res := v.Object + n + v.Object = res + c.items[k] = v + c.mu.Unlock() + return &res, nil +} + +// Return a new ordered cache with a given default expiration duration and cleanup +// interval. If the expiration duration is less than one (or NoExpiration), +// the items in the cache never expire (by default), and must be deleted +// manually. If the cleanup interval is less than one, expired items are not +// deleted from the cache before calling c.DeleteExpired(). +func NewOrderedCache[K comparable, V constraints.Ordered](defaultExpiration, cleanupInterval time.Duration) *OrderedCache[K, V] { + return &OrderedCache[K, V]{ + orderedCache: &orderedCache[K, V]{ + Cache: New[K, V](defaultExpiration, cleanupInterval), + }, + } +} + +// Return a new ordered cache with a given default expiration duration and cleanup +// interval. If the expiration duration is less than one (or NoExpiration), +// the items in the cache never expire (by default), and must be deleted +// manually. If the cleanup interval is less than one, expired items are not +// deleted from the cache before calling c.DeleteExpired(). +// +// NewFrom() also accepts an items map which will serve as the underlying map +// for the cache. This is useful for starting from a deserialized cache +// (serialized using e.g. gob.Encode() on c.Items()), or passing in e.g. +// make(map[string]Item[int], 500) to improve startup performance when the cache +// is expected to reach a certain minimum size. +// +// Only the cache's methods synchronize access to this map, so it is not +// recommended to keep any references to the map around after creating a cache. +// If need be, the map can be accessed at a later point using c.Items() (subject +// to the same caveat.) +// +// Note regarding serialization: When using e.g. gob, make sure to +// gob.Register() the individual types stored in the cache before encoding a +// map retrieved with c.Items(), and to register those same types before +// decoding a blob containing an items map. +func NewOrderedCacheFrom[K comparable, V constraints.Ordered](defaultExpiration, cleanupInterval time.Duration, items map[K]Item[V]) *OrderedCache[K, V] { + return &OrderedCache[K, V]{ + orderedCache: &orderedCache[K, V]{ + Cache: NewFrom(defaultExpiration, cleanupInterval, items), + }, + } +} diff --git a/ordered_test.go b/ordered_test.go new file mode 100644 index 0000000..1784b7c --- /dev/null +++ b/ordered_test.go @@ -0,0 +1,86 @@ +package cache + +import "testing" + +func TestIncrementWithInt(t *testing.T) { + tc := NewOrderedCache[string, int](DefaultExpiration, 0) + tc.Set("tint", 1, DefaultExpiration) + n, err := tc.Increment("tint", 2) + if err != nil { + t.Error("Error incrementing:", err) + } + if *n != 3 { + t.Error("Returned number is not 3:", n) + } + x, found := tc.Get("tint") + if !found { + t.Error("tint was not found") + } + if *x != 3 { + t.Error("tint is not 3:", x) + } +} + +func TestIncrementInt8(t *testing.T) { + tc := NewOrderedCache[string, int8](DefaultExpiration, 0) + tc.Set("int8", 1, DefaultExpiration) + n, err := tc.Increment("int8", 2) + if err != nil { + t.Error("Error decrementing:", err) + } + if *n != 3 { + t.Error("Returned number is not 3:", n) + } + x, found := tc.Get("int8") + if !found { + t.Error("int8 was not found") + } + if *x != 3 { + t.Error("int8 is not 3:", x) + } +} + +func TestIncrementOverflowInt(t *testing.T) { + tc := NewOrderedCache[string, int8](DefaultExpiration, 0) + tc.Set("int8", 127, DefaultExpiration) + n, err := tc.Increment("int8", 1) + if err != nil { + t.Error("Error incrementing int8:", err) + } + if *n != -128 { + t.Error("Returned number is not -128:", n) + } + x, _ := tc.Get("int8") + int8 := *x + if int8 != -128 { + t.Error("int8 did not overflow as expected; value:", int8) + } + +} + +func TestIncrementOverflowUint(t *testing.T) { + tc := NewOrderedCache[string, uint8](DefaultExpiration, 0) + tc.Set("uint8", 255, DefaultExpiration) + n, err := tc.Increment("uint8", 1) + if err != nil { + t.Error("Error incrementing int8:", err) + } + if *n != 0 { + t.Error("Returned number is not 0:", n) + } + x, _ := tc.Get("uint8") + uint8 := *x + if uint8 != 0 { + t.Error("uint8 did not overflow as expected; value:", uint8) + } +} + +func BenchmarkIncrement(b *testing.B) { + b.StopTimer() + tc := NewOrderedCache[string, int](DefaultExpiration, 0) + tc.Set("foo", 0, DefaultExpiration) + b.StartTimer() + for i := 0; i < b.N; i++ { + tc.Increment("foo", 1) + } +}