444 lines
11 KiB
Go
444 lines
11 KiB
Go
|
|
package cache
|
||
|
|
|
||
|
|
import (
|
||
|
|
"sync"
|
||
|
|
"sync/atomic"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestTTLStore_SetAndGet(t *testing.T) {
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
// Test basic set and get
|
||
|
|
store.Set("key1", "value1", 1*time.Hour)
|
||
|
|
|
||
|
|
val := store.GetOrCompute("key1", func() string {
|
||
|
|
t.Error("Should not compute for existing key")
|
||
|
|
return "should-not-compute"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if val != "value1" {
|
||
|
|
t.Errorf("Expected value1, got %s", val)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test getting non-existent key
|
||
|
|
computeCalled := false
|
||
|
|
val = store.GetOrCompute("nonexistent", func() string {
|
||
|
|
computeCalled = true
|
||
|
|
return "computed-value"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if !computeCalled {
|
||
|
|
t.Error("Expected compute function to be called for non-existent key")
|
||
|
|
}
|
||
|
|
if val != "computed-value" {
|
||
|
|
t.Errorf("Expected computed-value for non-existent key, got %s", val)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_Expiration(t *testing.T) {
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
// Set with short TTL
|
||
|
|
store.Set("shortlived", "value", 100*time.Millisecond)
|
||
|
|
|
||
|
|
// Should exist immediately
|
||
|
|
val := store.GetOrCompute("shortlived", func() string {
|
||
|
|
t.Error("Should not compute for existing key")
|
||
|
|
return "should-not-compute"
|
||
|
|
}, 100*time.Millisecond)
|
||
|
|
if val != "value" {
|
||
|
|
t.Errorf("Expected value, got %s", val)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wait for expiration
|
||
|
|
time.Sleep(150 * time.Millisecond)
|
||
|
|
|
||
|
|
// Should be expired now
|
||
|
|
computeCalled := false
|
||
|
|
val = store.GetOrCompute("shortlived", func() string {
|
||
|
|
computeCalled = true
|
||
|
|
return "recomputed-after-expiry"
|
||
|
|
}, 100*time.Millisecond)
|
||
|
|
if !computeCalled {
|
||
|
|
t.Error("Expected compute function to be called for expired key")
|
||
|
|
}
|
||
|
|
if val != "recomputed-after-expiry" {
|
||
|
|
t.Errorf("Expected recomputed value for expired key, got %s", val)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_Delete(t *testing.T) {
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
store.Set("key1", "value1", 1*time.Hour)
|
||
|
|
|
||
|
|
// Verify it exists
|
||
|
|
val := store.GetOrCompute("key1", func() string {
|
||
|
|
t.Error("Should not compute for existing key")
|
||
|
|
return "should-not-compute"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if val != "value1" {
|
||
|
|
t.Errorf("Expected value1, got %s", val)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete it
|
||
|
|
store.Delete("key1")
|
||
|
|
|
||
|
|
// Verify it's gone
|
||
|
|
computeCalled := false
|
||
|
|
val = store.GetOrCompute("key1", func() string {
|
||
|
|
computeCalled = true
|
||
|
|
return "recomputed-after-delete"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if !computeCalled {
|
||
|
|
t.Error("Expected compute function to be called after deletion")
|
||
|
|
}
|
||
|
|
if val != "recomputed-after-delete" {
|
||
|
|
t.Errorf("Expected recomputed value after deletion, got %s", val)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete non-existent key should not panic
|
||
|
|
store.Delete("nonexistent")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_Purge(t *testing.T) {
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
// Add multiple items
|
||
|
|
store.Set("key1", "value1", 1*time.Hour)
|
||
|
|
store.Set("key2", "value2", 1*time.Hour)
|
||
|
|
store.Set("key3", "value3", 1*time.Hour)
|
||
|
|
|
||
|
|
// Verify they exist
|
||
|
|
for i := 1; i <= 3; i++ {
|
||
|
|
key := "key" + string(rune('0'+i))
|
||
|
|
val := store.GetOrCompute(key, func() string {
|
||
|
|
t.Errorf("Should not compute for existing key %s", key)
|
||
|
|
return "should-not-compute"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
expectedVal := "value" + string(rune('0'+i))
|
||
|
|
if val != expectedVal {
|
||
|
|
t.Errorf("Expected to find %s with value %s, got %s", key, expectedVal, val)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Purge all
|
||
|
|
store.Purge()
|
||
|
|
|
||
|
|
// Verify all are gone
|
||
|
|
for i := 1; i <= 3; i++ {
|
||
|
|
key := "key" + string(rune('0'+i))
|
||
|
|
computeCalled := false
|
||
|
|
store.GetOrCompute(key, func() string {
|
||
|
|
computeCalled = true
|
||
|
|
return "recomputed-after-purge"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if !computeCalled {
|
||
|
|
t.Errorf("Expected %s to be purged and recomputed", key)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_ConcurrentAccess(t *testing.T) {
|
||
|
|
store := NewTTLStore[int, int]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
const numGoroutines = 100
|
||
|
|
const numOperations = 1000
|
||
|
|
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
wg.Add(numGoroutines)
|
||
|
|
|
||
|
|
// Concurrent writes and reads
|
||
|
|
for i := 0; i < numGoroutines; i++ {
|
||
|
|
go func(id int) {
|
||
|
|
defer wg.Done()
|
||
|
|
|
||
|
|
for j := 0; j < numOperations; j++ {
|
||
|
|
key := (id * numOperations) + j
|
||
|
|
store.Set(key, key*2, 1*time.Hour)
|
||
|
|
|
||
|
|
// Immediately read it back
|
||
|
|
val := store.GetOrCompute(key, func() int {
|
||
|
|
t.Errorf("Goroutine %d: Should not compute for just-set key %d", id, key)
|
||
|
|
return -1
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if val != key*2 {
|
||
|
|
t.Errorf("Goroutine %d: Expected value %d, got %d", id, key*2, val)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}(i)
|
||
|
|
}
|
||
|
|
|
||
|
|
wg.Wait()
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_UpdateExisting(t *testing.T) {
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
// Set initial value
|
||
|
|
store.Set("key1", "value1", 100*time.Millisecond)
|
||
|
|
|
||
|
|
// Update with new value and longer TTL
|
||
|
|
store.Set("key1", "value2", 1*time.Hour)
|
||
|
|
|
||
|
|
// Verify new value
|
||
|
|
val := store.GetOrCompute("key1", func() string {
|
||
|
|
t.Error("Should not compute for existing key")
|
||
|
|
return "should-not-compute"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if val != "value2" {
|
||
|
|
t.Errorf("Expected value2, got %s", val)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wait for original TTL to pass
|
||
|
|
time.Sleep(150 * time.Millisecond)
|
||
|
|
|
||
|
|
// Should still exist with new TTL
|
||
|
|
val = store.GetOrCompute("key1", func() string {
|
||
|
|
t.Error("Should not compute for key with new TTL")
|
||
|
|
return "should-not-compute"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if val != "value2" {
|
||
|
|
t.Errorf("Expected value2, got %s", val)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_CleanupGoroutine(t *testing.T) {
|
||
|
|
// This test verifies that expired entries are cleaned up automatically
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
// Add many short-lived entries
|
||
|
|
for i := 0; i < 100; i++ {
|
||
|
|
key := "key" + string(rune('0'+i))
|
||
|
|
store.Set(key, "value", 100*time.Millisecond)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cast to access internal state for testing
|
||
|
|
ttlStore := store.(*TTLStore[string, string])
|
||
|
|
|
||
|
|
// Check initial count
|
||
|
|
ttlStore.mutex.RLock()
|
||
|
|
initialCount := len(ttlStore.cache)
|
||
|
|
ttlStore.mutex.RUnlock()
|
||
|
|
|
||
|
|
if initialCount != 100 {
|
||
|
|
t.Errorf("Expected 100 entries initially, got %d", initialCount)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wait for expiration and cleanup cycle
|
||
|
|
// In test mode, cleanup runs every second
|
||
|
|
time.Sleep(1200 * time.Millisecond)
|
||
|
|
|
||
|
|
// Check that entries were cleaned up
|
||
|
|
ttlStore.mutex.RLock()
|
||
|
|
finalCount := len(ttlStore.cache)
|
||
|
|
ttlStore.mutex.RUnlock()
|
||
|
|
|
||
|
|
if finalCount != 0 {
|
||
|
|
t.Errorf("Expected 0 entries after cleanup, got %d", finalCount)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_Close(t *testing.T) {
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
|
||
|
|
// Close should not panic
|
||
|
|
store.Close()
|
||
|
|
|
||
|
|
// Multiple closes should not panic
|
||
|
|
store.Close()
|
||
|
|
store.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_DifferentTypes(t *testing.T) {
|
||
|
|
// Test with different key and value types
|
||
|
|
intStore := NewTTLStore[int, string]()
|
||
|
|
defer intStore.Close()
|
||
|
|
|
||
|
|
intStore.Set(42, "answer", 1*time.Hour)
|
||
|
|
val := intStore.GetOrCompute(42, func() string {
|
||
|
|
t.Error("Should not compute for existing key")
|
||
|
|
return "should-not-compute"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if val != "answer" {
|
||
|
|
t.Error("Failed with int key")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test with struct values
|
||
|
|
type User struct {
|
||
|
|
ID int
|
||
|
|
Name string
|
||
|
|
}
|
||
|
|
|
||
|
|
userStore := NewTTLStore[string, User]()
|
||
|
|
defer userStore.Close()
|
||
|
|
|
||
|
|
user := User{ID: 1, Name: "Alice"}
|
||
|
|
userStore.Set("user1", user, 1*time.Hour)
|
||
|
|
|
||
|
|
retrievedUser := userStore.GetOrCompute("user1", func() User {
|
||
|
|
t.Error("Should not compute for existing user")
|
||
|
|
return User{}
|
||
|
|
}, 1*time.Hour)
|
||
|
|
if retrievedUser.ID != 1 || retrievedUser.Name != "Alice" {
|
||
|
|
t.Error("Retrieved user data doesn't match")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_GetOrCompute(t *testing.T) {
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
computeCount := 0
|
||
|
|
|
||
|
|
// Test computing when not in cache
|
||
|
|
result := store.GetOrCompute("key1", func() string {
|
||
|
|
computeCount++
|
||
|
|
return "computed-value"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
|
||
|
|
if result != "computed-value" {
|
||
|
|
t.Errorf("Expected computed-value, got %s", result)
|
||
|
|
}
|
||
|
|
if computeCount != 1 {
|
||
|
|
t.Errorf("Expected compute to be called once, called %d times", computeCount)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test returning cached value
|
||
|
|
result = store.GetOrCompute("key1", func() string {
|
||
|
|
computeCount++
|
||
|
|
return "should-not-compute"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
|
||
|
|
if result != "computed-value" {
|
||
|
|
t.Errorf("Expected cached value, got %s", result)
|
||
|
|
}
|
||
|
|
if computeCount != 1 {
|
||
|
|
t.Errorf("Expected compute to not be called again, total calls: %d", computeCount)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_GetOrCompute_Expiration(t *testing.T) {
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
computeCount := 0
|
||
|
|
|
||
|
|
// Set with short TTL
|
||
|
|
result := store.GetOrCompute("shortlived", func() string {
|
||
|
|
computeCount++
|
||
|
|
return "value1"
|
||
|
|
}, 100*time.Millisecond)
|
||
|
|
|
||
|
|
if result != "value1" {
|
||
|
|
t.Errorf("Expected value1, got %s", result)
|
||
|
|
}
|
||
|
|
if computeCount != 1 {
|
||
|
|
t.Errorf("Expected 1 compute, got %d", computeCount)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Should return cached value immediately
|
||
|
|
result = store.GetOrCompute("shortlived", func() string {
|
||
|
|
computeCount++
|
||
|
|
return "value2"
|
||
|
|
}, 100*time.Millisecond)
|
||
|
|
|
||
|
|
if result != "value1" {
|
||
|
|
t.Errorf("Expected cached value1, got %s", result)
|
||
|
|
}
|
||
|
|
if computeCount != 1 {
|
||
|
|
t.Errorf("Expected still 1 compute, got %d", computeCount)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wait for expiration
|
||
|
|
time.Sleep(150 * time.Millisecond)
|
||
|
|
|
||
|
|
// Should compute new value after expiration
|
||
|
|
result = store.GetOrCompute("shortlived", func() string {
|
||
|
|
computeCount++
|
||
|
|
return "value2"
|
||
|
|
}, 100*time.Millisecond)
|
||
|
|
|
||
|
|
if result != "value2" {
|
||
|
|
t.Errorf("Expected new value2, got %s", result)
|
||
|
|
}
|
||
|
|
if computeCount != 2 {
|
||
|
|
t.Errorf("Expected 2 computes after expiration, got %d", computeCount)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_GetOrCompute_Concurrent(t *testing.T) {
|
||
|
|
store := NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
var computeCount int32
|
||
|
|
const numGoroutines = 100
|
||
|
|
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
wg.Add(numGoroutines)
|
||
|
|
|
||
|
|
// Launch many goroutines trying to compute the same key
|
||
|
|
for i := 0; i < numGoroutines; i++ {
|
||
|
|
go func(id int) {
|
||
|
|
defer wg.Done()
|
||
|
|
|
||
|
|
result := store.GetOrCompute("shared-key", func() string {
|
||
|
|
// Increment atomically to count calls
|
||
|
|
atomic.AddInt32(&computeCount, 1)
|
||
|
|
// Simulate some work
|
||
|
|
time.Sleep(10 * time.Millisecond)
|
||
|
|
return "shared-value"
|
||
|
|
}, 1*time.Hour)
|
||
|
|
|
||
|
|
if result != "shared-value" {
|
||
|
|
t.Errorf("Goroutine %d: Expected shared-value, got %s", id, result)
|
||
|
|
}
|
||
|
|
}(i)
|
||
|
|
}
|
||
|
|
|
||
|
|
wg.Wait()
|
||
|
|
|
||
|
|
// Only one goroutine should have computed the value
|
||
|
|
if computeCount != 1 {
|
||
|
|
t.Errorf("Expected exactly 1 compute for concurrent access, got %d", computeCount)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLStore_GetOrCompute_MultipleKeys(t *testing.T) {
|
||
|
|
store := NewTTLStore[int, int]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
computeCounts := make(map[int]int)
|
||
|
|
var mu sync.Mutex
|
||
|
|
|
||
|
|
// Test multiple different keys
|
||
|
|
for i := 0; i < 10; i++ {
|
||
|
|
for j := 0; j < 3; j++ { // Access each key 3 times
|
||
|
|
result := store.GetOrCompute(i, func() int {
|
||
|
|
mu.Lock()
|
||
|
|
computeCounts[i]++
|
||
|
|
mu.Unlock()
|
||
|
|
return i * 10
|
||
|
|
}, 1*time.Hour)
|
||
|
|
|
||
|
|
if result != i*10 {
|
||
|
|
t.Errorf("Expected %d, got %d", i*10, result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Each key should be computed exactly once
|
||
|
|
for i := 0; i < 10; i++ {
|
||
|
|
if computeCounts[i] != 1 {
|
||
|
|
t.Errorf("Key %d: Expected 1 compute, got %d", i, computeCounts[i])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|