Skip to content

Commit

Permalink
Add Peek method (#56)
Browse files Browse the repository at this point in the history
* Add Peek method

* Remove redundant var
  • Loading branch information
erni27 authored Jan 15, 2024
1 parent 62fa76e commit feac194
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 141 deletions.
199 changes: 115 additions & 84 deletions imcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ type Cache[K comparable, V any] struct {
defaultExp time.Duration
policy EvictionPolicy
limit int
mu sync.Mutex
mu sync.RWMutex
sliding bool
closed bool
}
Expand Down Expand Up @@ -185,6 +185,84 @@ func (c *Cache[K, V]) GetMultiple(keys ...K) map[K]V {
return got
}

// GetAll returns a copy of all entries in the cache.
//
// If it encounters an expired entry, the expired entry is evicted.
func (c *Cache[K, V]) GetAll() map[K]V {
return c.getAll(time.Now())
}

func (c *Cache[K, V]) getAll(now time.Time) map[K]V {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return nil
}
// To avoid copying the expired entries if there's no eviction callback.
if c.onEviction == nil {
got := make(map[K]V, len(c.m))
for key, node := range c.m {
entry := node.entry()
if entry.expired(now) {
c.queue.remove(node)
delete(c.m, key)
continue
}
entry.slide(now)
node.setEntry(entry)
got[key] = entry.val
}
c.queue.touchall()
c.mu.Unlock()
return got
}
var expired []entry[K, V]
got := make(map[K]V, len(c.m))
for key, node := range c.m {
entry := node.entry()
if entry.expired(now) {
expired = append(expired, entry)
delete(c.m, key)
c.queue.remove(node)
continue
}
entry.slide(now)
node.setEntry(entry)
got[key] = entry.val
}
c.queue.touchall()
c.mu.Unlock()
if len(expired) != 0 {
go func() {
for _, kv := range expired {
c.onEviction(kv.key, kv.val, EvictionReasonExpired)
}
}()
}
return got
}

// Peek returns the value for the given key without
// actively evicting the entry if it is expired and
// updating the entry's sliding expiration.
//
// If the max entries limit is set, it doesn't update
// the entry's position in the eviction queue.
func (c *Cache[K, V]) Peek(key K) (V, bool) {
now := time.Now()
var zero V
c.mu.RLock()
defer c.mu.RUnlock()
if c.closed {
return zero, false
}
n, ok := c.m[key]
if !ok || n.entry().expired(now) {
return zero, false
}
return n.entry().val, true
}

// Set sets the value for the given key.
// If the entry already exists, it is replaced.
//
Expand Down Expand Up @@ -620,63 +698,6 @@ func (c *Cache[K, V]) removeExpired(now time.Time) {
}
}

// GetAll returns a copy of all entries in the cache.
//
// If it encounters an expired entry, the expired entry is evicted.
func (c *Cache[K, V]) GetAll() map[K]V {
return c.getAll(time.Now())
}

func (c *Cache[K, V]) getAll(now time.Time) map[K]V {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return nil
}
// To avoid copying the expired entries if there's no eviction callback.
if c.onEviction == nil {
got := make(map[K]V, len(c.m))
for key, node := range c.m {
entry := node.entry()
if entry.expired(now) {
c.queue.remove(node)
delete(c.m, key)
continue
}
entry.slide(now)
node.setEntry(entry)
got[key] = entry.val
}
c.queue.touchall()
c.mu.Unlock()
return got
}
var expired []entry[K, V]
got := make(map[K]V, len(c.m))
for key, node := range c.m {
entry := node.entry()
if entry.expired(now) {
expired = append(expired, entry)
delete(c.m, key)
c.queue.remove(node)
continue
}
entry.slide(now)
node.setEntry(entry)
got[key] = entry.val
}
c.queue.touchall()
c.mu.Unlock()
if len(expired) != 0 {
go func() {
for _, kv := range expired {
c.onEviction(kv.key, kv.val, EvictionReasonExpired)
}
}()
}
return got
}

// Len returns the number of entries in the cache.
func (c *Cache[K, V]) Len() int {
c.mu.Lock()
Expand Down Expand Up @@ -837,6 +858,42 @@ func (s *Sharded[K, V]) GetMultiple(keys ...K) map[K]V {
return result
}

// GetAll returns a copy of all entries in the cache.
//
// If it encounters an expired entry, the expired entry is evicted.
func (s *Sharded[K, V]) GetAll() map[K]V {
now := time.Now()
var n int
ms := make([]map[K]V, 0, len(s.shards))
for _, shard := range s.shards {
m := shard.getAll(now)
// If Cache.getAll returns nil, it means that the shard is closed
// hence Sharded is closed too.
if m == nil {
return nil
}
n += len(m)
ms = append(ms, m)
}
all := make(map[K]V, n)
for _, m := range ms {
for key, val := range m {
all[key] = val
}
}
return all
}

// Peek returns the value for the given key without
// actively evicting the entry if it is expired and
// updating the entry's sliding expiration.
//
// If the max entries limit is set, it doesn't update
// the entry's position in the eviction queue.
func (s *Sharded[K, V]) Peek(key K) (V, bool) {
return s.shard(key).Peek(key)
}

// Set sets the value for the given key.
// If the entry already exists, it is replaced.
//
Expand Down Expand Up @@ -1030,32 +1087,6 @@ func (s *Sharded[K, V]) RemoveExpired() {
}
}

// GetAll returns a copy of all entries in the cache.
//
// If it encounters an expired entry, the expired entry is evicted.
func (s *Sharded[K, V]) GetAll() map[K]V {
now := time.Now()
var n int
ms := make([]map[K]V, 0, len(s.shards))
for _, shard := range s.shards {
m := shard.getAll(now)
// If Cache.getAll returns nil, it means that the shard is closed
// hence Sharded is closed too.
if m == nil {
return nil
}
n += len(m)
ms = append(ms, m)
}
all := make(map[K]V, n)
for _, m := range ms {
for key, val := range m {
all[key] = val
}
}
return all
}

// Len returns the number of entries in the cache.
func (s *Sharded[K, V]) Len() int {
var n int
Expand Down
72 changes: 72 additions & 0 deletions imcache_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,40 @@ func BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyRandom(b *testing.B) {
}
}

func BenchmarkCache_Peek(b *testing.B) {
var c Cache[string, token]
for i := 0; i < b.N; i++ {
c.Set(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if v, ok := c.Peek(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = v
}
}
}

func BenchmarkSharded_Peek(b *testing.B) {
for _, n := range []int{2, 4, 8, 16, 32, 64, 128} {
b.Run(fmt.Sprintf("%d_Shards", n), func(b *testing.B) {
c := NewSharded[string, token](n, DefaultStringHasher64{})
for i := 0; i < b.N; i++ {
c.Set(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if v, ok := c.Peek(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = v
}
}
})
}
}

func BenchmarkMap_Get(b *testing.B) {
m := make(map[string]token)
var mu sync.Mutex
Expand Down Expand Up @@ -317,6 +351,44 @@ func BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyRandom_Parallel(b *testi
}
}

func BenchmarkCache_Peek_Parallel(b *testing.B) {
var c Cache[string, token]
for i := 0; i < b.N; i++ {
c.Set(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
}

b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if v, ok := c.Peek(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = (token)(v)
}
}
})
}

func BenchmarkSharded_Peek_Parallel(b *testing.B) {
for _, n := range []int{2, 4, 8, 16, 32, 64, 128} {
b.Run(fmt.Sprintf("%d_Shards", n), func(b *testing.B) {
c := NewSharded[string, token](n, DefaultStringHasher64{})
for i := 0; i < b.N; i++ {
c.Set(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
}

b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if v, ok := c.Peek(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = (token)(v)
}
}
})
})
}
}

func BenchmarkMap_Get_Parallel(b *testing.B) {
m := make(map[string]token)
var mu sync.Mutex
Expand Down
Loading

0 comments on commit feac194

Please sign in to comment.