diff --git a/cache.go b/cache.go index dea114d..0e8e2a4 100644 --- a/cache.go +++ b/cache.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "sync" "sync/atomic" + "time" "github.com/cespare/xxhash/v2" ) @@ -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) } @@ -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) @@ -145,9 +165,9 @@ 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() @@ -155,17 +175,24 @@ func (cache *Cache) GetOrSet(key, value []byte, expireSeconds int) (retValue []b 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() @@ -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() @@ -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 diff --git a/cache_test.go b/cache_test.go index 49fb20c..c04cf22 100644 --- a/cache_test.go +++ b/cache_test.go @@ -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) + } +}