diff --git a/advanced.go b/advanced.go new file mode 100644 index 0000000..60f6bbc --- /dev/null +++ b/advanced.go @@ -0,0 +1,89 @@ +// Copyright (c) 2024 Alexey Mayshev. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otter + +import ( + "github.com/maypok86/otter/internal/core" + "github.com/maypok86/otter/internal/generated/node" + "github.com/maypok86/otter/internal/unixtime" +) + +func zeroValue[V any]() V { + var zero V + return zero +} + +// Advanced is an access point for inspecting and performing low-level operations based on the cache's runtime +// characteristics. These operations are optional and dependent on how the cache was constructed +// and what abilities the implementation exposes. +type Advanced[K comparable, V any] struct { + cache *core.Cache[K, V] +} + +func newAdvanced[K comparable, V any](cache *core.Cache[K, V]) Advanced[K, V] { + return Advanced[K, V]{ + cache: cache, + } +} + +func (a Advanced[K, V]) createEntry(n node.Node[K, V]) Entry[K, V] { + var expiration int64 + if a.cache.WithExpiration() { + expiration = unixtime.StartTime() + int64(n.Expiration()) + } + + return Entry[K, V]{ + key: n.Key(), + value: n.Value(), + expiration: expiration, + cost: n.Cost(), + } +} + +// GetQuietly returns the value associated with the key in this cache. +// +// Unlike Get in the cache, this function does not produce any side effects +// such as updating statistics or the eviction policy. +func (a Advanced[K, V]) GetQuietly(key K) (V, bool) { + n, ok := a.cache.GetNodeQuietly(key) + if !ok { + return zeroValue[V](), false + } + + return n.Value(), true +} + +// GetEntry returns the cache entry associated with the key in this cache. +func (a Advanced[K, V]) GetEntry(key K) (Entry[K, V], bool) { + n, ok := a.cache.GetNode(key) + if !ok { + return Entry[K, V]{}, false + } + + return a.createEntry(n), true +} + +// GetEntryQuietly returns the cache entry associated with the key in this cache. +// +// Unlike GetEntry, this function does not produce any side effects +// such as updating statistics or the eviction policy. +func (a Advanced[K, V]) GetEntryQuietly(key K) (Entry[K, V], bool) { + n, ok := a.cache.GetNodeQuietly(key) + if !ok { + return Entry[K, V]{}, false + } + + return a.createEntry(n), true +} diff --git a/cache.go b/cache.go index b2704d8..c696cce 100644 --- a/cache.go +++ b/cache.go @@ -44,7 +44,7 @@ func newBaseCache[K comparable, V any](c core.Config[K, V]) baseCache[K, V] { } } -// Has checks if there is an item with the given key in the cache. +// Has checks if there is an entry with the given key in the cache. func (bs baseCache[K, V]) Has(key K) bool { return bs.cache.Has(key) } @@ -64,7 +64,7 @@ func (bs baseCache[K, V]) DeleteByFunc(f func(key K, value V) bool) { bs.cache.DeleteByFunc(f) } -// Range iterates over all items in the cache. +// Range iterates over all entries in the cache. // // Iteration stops early when the given function returns false. func (bs baseCache[K, V]) Range(f func(key K, value V) bool) { @@ -85,7 +85,7 @@ func (bs baseCache[K, V]) Close() { bs.cache.Close() } -// Size returns the current number of items in the cache. +// Size returns the current number of entries in the cache. func (bs baseCache[K, V]) Size() int { return bs.cache.Size() } @@ -100,6 +100,13 @@ func (bs baseCache[K, V]) Stats() Stats { return newStats(bs.cache.Stats()) } +// Advanced returns access to inspect and perform low-level operations on this cache based on its runtime +// characteristics. These operations are optional and dependent on how the cache was constructed +// and what abilities the implementation exposes. +func (bs baseCache[K, V]) Advanced() Advanced[K, V] { + return newAdvanced(bs.cache) +} + // Cache is a structure performs a best-effort bounding of a hash table using eviction algorithm // to determine which entries to evict when the capacity is exceeded. type Cache[K comparable, V any] struct { @@ -114,7 +121,7 @@ func newCache[K comparable, V any](c core.Config[K, V]) Cache[K, V] { // Set associates the value with the key in this cache. // -// If it returns false, then the key-value item had too much setCostFunc and the Set was dropped. +// If it returns false, then the key-value pair had too much cost and the Set was dropped. func (c Cache[K, V]) Set(key K, value V) bool { return c.cache.Set(key, value) } @@ -123,7 +130,7 @@ func (c Cache[K, V]) Set(key K, value V) bool { // // If the specified key is not already associated with a value, then it returns false. // -// Also, it returns false if the key-value item had too much setCostFunc and the SetIfAbsent was dropped. +// Also, it returns false if the key-value pair had too much cost and the SetIfAbsent was dropped. func (c Cache[K, V]) SetIfAbsent(key K, value V) bool { return c.cache.SetIfAbsent(key, value) } @@ -140,19 +147,19 @@ func newCacheWithVariableTTL[K comparable, V any](c core.Config[K, V]) CacheWith } } -// Set associates the value with the key in this cache and sets the custom ttl for this key-value item. +// Set associates the value with the key in this cache and sets the custom ttl for this key-value pair. // -// If it returns false, then the key-value item had too much setCostFunc and the Set was dropped. +// If it returns false, then the key-value pair had too much cost and the Set was dropped. func (c CacheWithVariableTTL[K, V]) Set(key K, value V, ttl time.Duration) bool { return c.cache.SetWithTTL(key, value, ttl) } // SetIfAbsent if the specified key is not already associated with a value associates it with the given value -// and sets the custom ttl for this key-value item. +// and sets the custom ttl for this key-value pair. // // If the specified key is not already associated with a value, then it returns false. // -// Also, it returns false if the key-value item had too much setCostFunc and the SetIfAbsent was dropped. +// Also, it returns false if the key-value pair had too much cost and the SetIfAbsent was dropped. func (c CacheWithVariableTTL[K, V]) SetIfAbsent(key K, value V, ttl time.Duration) bool { return c.cache.SetIfAbsentWithTTL(key, value, ttl) } diff --git a/cache_test.go b/cache_test.go index 39969dc..0f98971 100644 --- a/cache_test.go +++ b/cache_test.go @@ -317,6 +317,60 @@ func TestCache_DeleteByFunc(t *testing.T) { } } +func TestCache_Advanced(t *testing.T) { + size := 256 + defaultTTL := time.Hour + c, err := MustBuilder[int, int](size). + WithTTL(defaultTTL). + Build() + if err != nil { + t.Fatalf("can not create builder: %v", err) + } + + for i := 0; i < size; i++ { + c.Set(i, i) + } + + k1 := 4 + v1, ok := c.Advanced().GetQuietly(k1) + if !ok { + t.Fatalf("not found key %d", k1) + } + + e1, ok := c.Advanced().GetEntryQuietly(k1) + if !ok { + t.Fatalf("not found key %d", k1) + } + + e2, ok := c.Advanced().GetEntry(k1) + if !ok { + t.Fatalf("not found key %d", k1) + } + + time.Sleep(time.Second) + + isValidEntries := e1.Key() == k1 && + e1.Value() == v1 && + e1.Cost() == 1 && + e1 == e2 && + e1.TTL() < defaultTTL && + !e1.IsExpired() + + if !isValidEntries { + t.Fatalf("found not valid entries. e1: %+v, e2: %+v, v1:%d", e1, e2, v1) + } + + if _, ok := c.Advanced().GetQuietly(size); ok { + t.Fatalf("found not valid key: %d", size) + } + if _, ok := c.Advanced().GetEntryQuietly(size); ok { + t.Fatalf("found not valid key: %d", size) + } + if _, ok := c.Advanced().GetEntry(size); ok { + t.Fatalf("found not valid key: %d", size) + } +} + func TestCache_Ratio(t *testing.T) { var mutex sync.Mutex m := make(map[DeletionCause]int) diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..842e9ae --- /dev/null +++ b/entry.go @@ -0,0 +1,82 @@ +// Copyright (c) 2024 Alexey Mayshev. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otter + +import "time" + +// Entry is a key-value pair that may include policy metadata for the cached entry. +// +// It is an immutable snapshot of the cached data at the time of this entry's creation, and it will not +// reflect changes afterward. +type Entry[K comparable, V any] struct { + key K + value V + expiration int64 + cost uint32 +} + +// Key returns the entry's key. +func (e Entry[K, V]) Key() K { + return e.key +} + +// Value returns the entry's value. +func (e Entry[K, V]) Value() V { + return e.value +} + +// Expiration returns the entry's expiration time as a unix time, +// the number of seconds elapsed since January 1, 1970 UTC. +// +// If the cache was not configured with an expiration policy then this value is always 0. +func (e Entry[K, V]) Expiration() int64 { + return e.expiration +} + +// TTL returns the entry's ttl. +// +// If the cache was not configured with an expiration policy then this value is always -1. +// +// If the entry is expired then this value is always 0. +func (e Entry[K, V]) TTL() time.Duration { + expiration := e.Expiration() + if expiration == 0 { + return -1 + } + + now := time.Now().Unix() + if expiration <= now { + return 0 + } + + return time.Duration(expiration-now) * time.Second +} + +// IsExpired returns true if the entry is expired. +func (e Entry[K, V]) IsExpired() bool { + expiration := e.Expiration() + if expiration == 0 { + return false + } + + return expiration <= time.Now().Unix() +} + +// Cost returns the entry's cost. +// +// If the cache was not configured with a cost then this value is always 1. +func (e Entry[K, V]) Cost() uint32 { + return e.cost +} diff --git a/entry_test.go b/entry_test.go new file mode 100644 index 0000000..6ab47d4 --- /dev/null +++ b/entry_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2024 Alexey Mayshev. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otter + +import ( + "testing" + "time" +) + +func TestEntry(t *testing.T) { + k := 2 + v := 3 + exp := int64(0) + c := uint32(5) + e := Entry[int, int]{ + key: k, + value: v, + expiration: exp, + cost: c, + } + + if e.Key() != k { + t.Fatalf("not valid key. want %d, got %d", k, e.Key()) + } + if e.Value() != v { + t.Fatalf("not valid value. want %d, got %d", v, e.Value()) + } + if e.Cost() != c { + t.Fatalf("not valid cost. want %d, got %d", c, e.Cost()) + } + if e.Expiration() != exp { + t.Fatalf("not valid expiration. want %d, got %d", exp, e.Expiration()) + } + if ttl := e.TTL(); ttl != -1 { + t.Fatalf("not valid ttl. want -1, got %d", ttl) + } + if e.IsExpired() { + t.Fatal("entry should not be expire") + } + + newTTL := int64(10) + e.expiration = time.Now().Unix() + newTTL + if ttl := e.TTL(); ttl <= 0 || ttl > time.Duration(newTTL)*time.Second { + t.Fatalf("ttl should be in the range (0, %d] seconds, but got %d seconds", newTTL, ttl/time.Second) + } + if e.IsExpired() { + t.Fatal("entry should not be expire") + } + + e.expiration -= 2 * newTTL + if ttl := e.TTL(); ttl != 0 { + t.Fatalf("ttl should be 0 seconds, but got %d seconds", ttl/time.Second) + } + if !e.IsExpired() { + t.Fatalf("entry should have expired") + } +} diff --git a/internal/core/cache.go b/internal/core/cache.go index d05b0b6..3deb3f4 100644 --- a/internal/core/cache.go +++ b/internal/core/cache.go @@ -178,22 +178,45 @@ func (c *Cache[K, V]) Has(key K) bool { // Get returns the value associated with the key in this cache. func (c *Cache[K, V]) Get(key K) (V, bool) { - got, ok := c.hashmap.Get(key) - if !ok || !got.IsAlive() { - c.stats.IncMisses() + n, ok := c.GetNode(key) + if !ok { return zeroValue[V](), false } - if got.IsExpired() { - c.writeBuffer.Push(newDeleteTask(got)) + return n.Value(), true +} + +// GetNode returns the node associated with the key in this cache. +func (c *Cache[K, V]) GetNode(key K) (node.Node[K, V], bool) { + n, ok := c.hashmap.Get(key) + if !ok || !n.IsAlive() { c.stats.IncMisses() - return zeroValue[V](), false + return nil, false + } + + if n.IsExpired() { + c.writeBuffer.Push(newDeleteTask(n)) + c.stats.IncMisses() + return nil, false } - c.afterGet(got) + c.afterGet(n) c.stats.IncHits() - return got.Value(), ok + return n, true +} + +// GetNodeQuietly returns the node associated with the key in this cache. +// +// Unlike GetNode, this function does not produce any side effects +// such as updating statistics or the eviction policy. +func (c *Cache[K, V]) GetNodeQuietly(key K) (node.Node[K, V], bool) { + n, ok := c.hashmap.Get(key) + if !ok || !n.IsAlive() || n.IsExpired() { + return nil, false + } + + return n, true } func (c *Cache[K, V]) afterGet(got node.Node[K, V]) { @@ -500,6 +523,11 @@ func (c *Cache[K, V]) Stats() *stats.Stats { return c.stats } +// WithExpiration returns true if the cache was configured with the expiration policy enabled. +func (c *Cache[K, V]) WithExpiration() bool { + return c.withExpiration +} + func clearBuffer[T any](buffer []T) []T { var zero T for i := 0; i < len(buffer); i++ { diff --git a/internal/unixtime/unixtime.go b/internal/unixtime/unixtime.go index 0737adf..a446921 100644 --- a/internal/unixtime/unixtime.go +++ b/internal/unixtime/unixtime.go @@ -23,7 +23,8 @@ import ( var ( // We need this package because time.Now() is slower, allocates memory, // and we don't need a more precise time for the expiry time (and most other operations). - now uint32 + now uint32 + startTime int64 mutex sync.Mutex countInstance int @@ -32,7 +33,7 @@ var ( func startTimer() { done = make(chan struct{}) - startTime := time.Now().Unix() + atomic.StoreInt64(&startTime, time.Now().Unix()) atomic.StoreUint32(&now, uint32(0)) go func() { @@ -41,7 +42,7 @@ func startTimer() { for { select { case t := <-ticker.C: - atomic.StoreUint32(&now, uint32(t.Unix()-startTime)) + atomic.StoreUint32(&now, uint32(t.Unix()-StartTime())) case <-done: return } @@ -84,3 +85,8 @@ func Now() uint32 { func SetNow(t uint32) { atomic.StoreUint32(&now, t) } + +// StartTime returns the start time of the program. +func StartTime() int64 { + return atomic.LoadInt64(&startTime) +}