Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 68 additions & 22 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/binary"
"sync"
"sync/atomic"
"time"

"github.com/cespare/xxhash/v2"
)
Expand All @@ -24,6 +25,9 @@ type Cache struct {

type Updater func(value []byte, found bool) (newValue []byte, replace bool, expireSeconds int)

// UpdaterDuration is similar to Updater but uses time.Duration for expiration.
type UpdaterDuration func(value []byte, found bool) (newValue []byte, replace bool, expireDuration time.Duration)

func hashFunc(data []byte) uint64 {
return xxhash.Sum64(data)
}
Expand Down Expand Up @@ -52,30 +56,46 @@ func NewCacheCustomTimer(size int, timer Timer) (cache *Cache) {
return
}

// Set sets a key, value and expiration for a cache entry and stores it in the cache.
// SetDuration sets a key, value and expiration duration for a cache entry and stores it in the cache.
// If the key is larger than 65535 or value is larger than 1/1024 of the cache size,
// the entry will not be written to the cache. expireSeconds <= 0 means no expire,
// the entry will not be written to the cache. duration <= 0 means no expire,
// but it can be evicted when cache is full.
func (cache *Cache) Set(key, value []byte, expireSeconds int) (err error) {
func (cache *Cache) SetDuration(key, value []byte, duration time.Duration) (err error) {
hashVal := hashFunc(key)
segID := hashVal & segmentAndOpVal
cache.locks[segID].Lock()
err = cache.segments[segID].set(key, value, hashVal, expireSeconds)
err = cache.segments[segID].set(key, value, hashVal, int(duration.Seconds()))
cache.locks[segID].Unlock()
return
}

// Touch updates the expiration time of an existing key. expireSeconds <= 0 means no expire,
// Set sets a key, value and expiration for a cache entry and stores it in the cache.
// If the key is larger than 65535 or value is larger than 1/1024 of the cache size,
// the entry will not be written to the cache. expireSeconds <= 0 means no expire,
// but it can be evicted when cache is full.
func (cache *Cache) Touch(key []byte, expireSeconds int) (err error) {
// Deprecated: Use SetDuration instead for better type safety and clarity.
func (cache *Cache) Set(key, value []byte, expireSeconds int) (err error) {
return cache.SetDuration(key, value, time.Duration(expireSeconds)*time.Second)
}

// TouchDuration updates the expiration time of an existing key with a duration. duration <= 0 means no expire,
// but it can be evicted when cache is full.
func (cache *Cache) TouchDuration(key []byte, duration time.Duration) (err error) {
hashVal := hashFunc(key)
segID := hashVal & segmentAndOpVal
cache.locks[segID].Lock()
err = cache.segments[segID].touch(key, hashVal, expireSeconds)
err = cache.segments[segID].touch(key, hashVal, int(duration.Seconds()))
cache.locks[segID].Unlock()
return
}

// Touch updates the expiration time of an existing key. expireSeconds <= 0 means no expire,
// but it can be evicted when cache is full.
// Deprecated: Use TouchDuration instead for better type safety and clarity.
func (cache *Cache) Touch(key []byte, expireSeconds int) (err error) {
return cache.TouchDuration(key, time.Duration(expireSeconds)*time.Second)
}

// Get returns the value or not found error.
func (cache *Cache) Get(key []byte) (value []byte, err error) {
hashVal := hashFunc(key)
Expand Down Expand Up @@ -145,27 +165,34 @@ func (cache *Cache) GetFn(key []byte, fn func([]byte) error) (err error) {
return
}

// GetOrSet returns existing value or if record doesn't exist
// it sets a new key, value and expiration for a cache entry and stores it in the cache, returns nil in that case
func (cache *Cache) GetOrSet(key, value []byte, expireSeconds int) (retValue []byte, err error) {
// GetOrSetDuration returns existing value or if record doesn't exist
// it sets a new key, value and expiration duration for a cache entry and stores it in the cache, returns nil in that case
func (cache *Cache) GetOrSetDuration(key, value []byte, duration time.Duration) (retValue []byte, err error) {
hashVal := hashFunc(key)
segID := hashVal & segmentAndOpVal
cache.locks[segID].Lock()
defer cache.locks[segID].Unlock()

retValue, _, err = cache.segments[segID].get(key, nil, hashVal, false)
if err != nil {
err = cache.segments[segID].set(key, value, hashVal, expireSeconds)
err = cache.segments[segID].set(key, value, hashVal, int(duration.Seconds()))
}
return
}

// SetAndGet sets a key, value and expiration for a cache entry and stores it in the cache.
// GetOrSet returns existing value or if record doesn't exist
// it sets a new key, value and expiration for a cache entry and stores it in the cache, returns nil in that case
// Deprecated: Use GetOrSetDuration instead for better type safety and clarity.
func (cache *Cache) GetOrSet(key, value []byte, expireSeconds int) (retValue []byte, err error) {
return cache.GetOrSetDuration(key, value, time.Duration(expireSeconds)*time.Second)
}

// SetAndGetDuration sets a key, value and expiration duration for a cache entry and stores it in the cache.
// If the key is larger than 65535 or value is larger than 1/1024 of the cache size,
// the entry will not be written to the cache. expireSeconds <= 0 means no expire,
// the entry will not be written to the cache. duration <= 0 means no expire,
// but it can be evicted when cache is full. Returns existing value if record exists
// with a bool value to indicate whether an existing record was found
func (cache *Cache) SetAndGet(key, value []byte, expireSeconds int) (retValue []byte, found bool, err error) {
func (cache *Cache) SetAndGetDuration(key, value []byte, duration time.Duration) (retValue []byte, found bool, err error) {
hashVal := hashFunc(key)
segID := hashVal & segmentAndOpVal
cache.locks[segID].Lock()
Expand All @@ -175,17 +202,22 @@ func (cache *Cache) SetAndGet(key, value []byte, expireSeconds int) (retValue []
if err == nil {
found = true
}
err = cache.segments[segID].set(key, value, hashVal, expireSeconds)
err = cache.segments[segID].set(key, value, hashVal, int(duration.Seconds()))
return
}

// Update gets value for a key, passes it to updater function that decides if set should be called as well
// This allows for an atomic Get plus Set call using the existing value to decide on whether to call Set.
// SetAndGet sets a key, value and expiration for a cache entry and stores it in the cache.
// If the key is larger than 65535 or value is larger than 1/1024 of the cache size,
// the entry will not be written to the cache. expireSeconds <= 0 means no expire,
// but it can be evicted when cache is full. Returns bool value to indicate if existing record was found along with bool
// value indicating the value was replaced and error if any
func (cache *Cache) Update(key []byte, updater Updater) (found bool, replaced bool, err error) {
// but it can be evicted when cache is full. Returns existing value if record exists
// with a bool value to indicate whether an existing record was found
// Deprecated: Use SetAndGetDuration instead for better type safety and clarity.
func (cache *Cache) SetAndGet(key, value []byte, expireSeconds int) (retValue []byte, found bool, err error) {
return cache.SetAndGetDuration(key, value, time.Duration(expireSeconds)*time.Second)
}

// UpdateDuration is similar to Update but uses time.Duration for expiration.
func (cache *Cache) UpdateDuration(key []byte, updater UpdaterDuration) (found bool, replaced bool, err error) {
hashVal := hashFunc(key)
segID := hashVal & segmentAndOpVal
cache.locks[segID].Lock()
Expand All @@ -197,14 +229,28 @@ func (cache *Cache) Update(key []byte, updater Updater) (found bool, replaced bo
} else {
err = nil // Clear ErrNotFound error since we're returning found flag
}
value, replaced, expireSeconds := updater(retValue, found)
value, replaced, expireDuration := updater(retValue, found)
if !replaced {
return
}
err = cache.segments[segID].set(key, value, hashVal, expireSeconds)
err = cache.segments[segID].set(key, value, hashVal, int(expireDuration.Seconds()))
return
}

// Update gets value for a key, passes it to updater function that decides if set should be called as well
// This allows for an atomic Get plus Set call using the existing value to decide on whether to call Set.
// If the key is larger than 65535 or value is larger than 1/1024 of the cache size,
// the entry will not be written to the cache. expireSeconds <= 0 means no expire,
// but it can be evicted when cache is full. Returns bool value to indicate if existing record was found along with bool
// value indicating the value was replaced and error if any
// Deprecated: Use UpdateDuration instead for better type safety and clarity.
func (cache *Cache) Update(key []byte, updater Updater) (found bool, replaced bool, err error) {
return cache.UpdateDuration(key, func(value []byte, found bool) ([]byte, bool, time.Duration) {
newValue, replace, expireSeconds := updater(value, found)
return newValue, replace, time.Duration(expireSeconds) * time.Second
})
}

// Peek returns the value or not found error, without updating access time or counters.
// Warning: No expiry check is performed so if an expired value is found, it will be
// returned without error
Expand Down
163 changes: 163 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1347,3 +1347,166 @@ func TestBenchmarkCacheSet(t *testing.T) {
t.Errorf("current alloc count '%d' is higher than 0", alloc)
}
}

// Tests for time.Duration support
func TestSetDuration(t *testing.T) {
cache := NewCache(1024)
key := []byte("test-key")
value := []byte("test-value")

// Set with duration
err := cache.SetDuration(key, value, time.Second)
if err != nil {
t.Errorf("SetDuration should not error, got %v", err)
}

// Verify value was set
retrieved, err := cache.Get(key)
if err != nil {
t.Errorf("Get should not error, got %v", err)
}
if !bytes.Equal(retrieved, value) {
t.Errorf("Retrieved value should match, got %v, expected %v", retrieved, value)
}
}

func TestTouchDuration(t *testing.T) {
cache := NewCache(1024)
key := []byte("test-key")
value := []byte("test-value")

// Set initial entry
err := cache.Set(key, value, 1)
if err != nil {
t.Errorf("Set should not error, got %v", err)
}

// Touch with duration
err = cache.TouchDuration(key, 5*time.Second)
if err != nil {
t.Errorf("TouchDuration should not error, got %v", err)
}

// Verify value still exists
retrieved, err := cache.Get(key)
if err != nil {
t.Errorf("Get should not error, got %v", err)
}
if !bytes.Equal(retrieved, value) {
t.Errorf("Retrieved value should match, got %v, expected %v", retrieved, value)
}
}

func TestGetOrSetDuration(t *testing.T) {
cache := NewCache(1024)
key := []byte("test-key")
value := []byte("test-value")

// GetOrSet on non-existent key
retrieved, err := cache.GetOrSetDuration(key, value, time.Second)
if err != nil {
t.Errorf("GetOrSetDuration should not error, got %v", err)
}
if retrieved != nil {
t.Errorf("GetOrSetDuration should return nil for non-existent key, got %v", retrieved)
}

// GetOrSet on existing key
retrieved2, err := cache.GetOrSetDuration(key, []byte("another-value"), 2*time.Second)
if err != nil {
t.Errorf("GetOrSetDuration should not error, got %v", err)
}
if !bytes.Equal(retrieved2, value) {
t.Errorf("GetOrSetDuration should return existing value, got %v, expected %v", retrieved2, value)
}
}

func TestSetAndGetDuration(t *testing.T) {
cache := NewCache(1024)
key := []byte("test-key")
value1 := []byte("test-value-1")
value2 := []byte("test-value-2")

// SetAndGet on non-existent key
retrieved, found, err := cache.SetAndGetDuration(key, value1, time.Second)
if err != nil {
t.Errorf("SetAndGetDuration should not error, got %v", err)
}
if found {
t.Errorf("SetAndGetDuration should return found=false for non-existent key")
}
if retrieved != nil {
t.Errorf("SetAndGetDuration should return nil for non-existent key, got %v", retrieved)
}

// SetAndGet on existing key
retrieved2, found2, err := cache.SetAndGetDuration(key, value2, 2*time.Second)
if err != nil {
t.Errorf("SetAndGetDuration should not error, got %v", err)
}
if !found2 {
t.Errorf("SetAndGetDuration should return found=true for existing key")
}
if !bytes.Equal(retrieved2, value1) {
t.Errorf("SetAndGetDuration should return old value, got %v, expected %v", retrieved2, value1)
}
}

func TestUpdateDuration(t *testing.T) {
cache := NewCache(1024)
key := []byte("test-key")
value1 := []byte("test-value-1")
value2 := []byte("test-value-2")

// UpdateDuration on non-existent key
found, replaced, err := cache.UpdateDuration(key, func(value []byte, found bool) ([]byte, bool, time.Duration) {
if !found {
return value1, true, time.Second
}
return nil, false, 0
})
if err != nil {
t.Errorf("UpdateDuration should not error, got %v", err)
}
if found {
t.Errorf("UpdateDuration should return found=false for non-existent key")
}
if !replaced {
t.Errorf("UpdateDuration should return replaced=true when updater returns true")
}

// Verify value was set
retrieved, err := cache.Get(key)
if err != nil {
t.Errorf("Get should not error, got %v", err)
}
if !bytes.Equal(retrieved, value1) {
t.Errorf("Retrieved value should match, got %v, expected %v", retrieved, value1)
}

// UpdateDuration on existing key
found, replaced, err = cache.UpdateDuration(key, func(value []byte, found bool) ([]byte, bool, time.Duration) {
if found {
return value2, true, 2 * time.Second
}
return nil, false, 0
})
if err != nil {
t.Errorf("UpdateDuration should not error, got %v", err)
}
if !found {
t.Errorf("UpdateDuration should return found=true for existing key")
}
if !replaced {
t.Errorf("UpdateDuration should return replaced=true when updater returns true")
}

// Verify value was updated
retrieved2, err := cache.Get(key)
if err != nil {
t.Errorf("Get should not error, got %v", err)
}
if !bytes.Equal(retrieved2, value2) {
t.Errorf("Retrieved value should match, got %v, expected %v", retrieved2, value2)
}
}