Skip to content

Commit

Permalink
[#56] Add advanced
Browse files Browse the repository at this point in the history
  • Loading branch information
maypok86 committed Mar 12, 2024
1 parent 18da5c2 commit de3fe57
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 20 deletions.
89 changes: 89 additions & 0 deletions advanced.go
Original file line number Diff line number Diff line change
@@ -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
}
25 changes: 16 additions & 9 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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) {
Expand All @@ -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()
}
Expand All @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
54 changes: 54 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
82 changes: 82 additions & 0 deletions entry.go
Original file line number Diff line number Diff line change
@@ -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
}
69 changes: 69 additions & 0 deletions entry_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading

0 comments on commit de3fe57

Please sign in to comment.