The commit modernizes the caching implementation by introducing a pluggable store interface that allows different cache backends. Key changes: - Add Store interface for custom cache implementations - Create default TTL-based store for backwards compatibility - Add example LRU store for memory-bounded caching - Support cache store configuration via options pattern - Make cache cleanup logic implementation-specific - Add comprehensive tests and documentation The main goals were to: 1. Prevent unbounded memory growth through pluggable stores 2. Enable distributed caching support 3. Maintain backwards compatibility 4. Improve testability and maintainability Signed-off-by: franchb <hello@franchb.com>
263 lines
5.7 KiB
Go
263 lines
5.7 KiB
Go
package cache
|
|
|
|
import (
|
|
"sync"
|
|
"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, found := store.Get("key1")
|
|
if !found {
|
|
t.Error("Expected to find key1")
|
|
}
|
|
if val != "value1" {
|
|
t.Errorf("Expected value1, got %s", val)
|
|
}
|
|
|
|
// Test getting non-existent key
|
|
val, found = store.Get("nonexistent")
|
|
if found {
|
|
t.Error("Expected not to find nonexistent key")
|
|
}
|
|
if val != "" {
|
|
t.Errorf("Expected empty string 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, found := store.Get("shortlived")
|
|
if !found {
|
|
t.Error("Expected to find shortlived key immediately after setting")
|
|
}
|
|
if val != "value" {
|
|
t.Errorf("Expected value, got %s", val)
|
|
}
|
|
|
|
// Wait for expiration
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// Should be expired now
|
|
val, found = store.Get("shortlived")
|
|
if found {
|
|
t.Error("Expected key to be expired")
|
|
}
|
|
if val != "" {
|
|
t.Errorf("Expected empty string 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
|
|
_, found := store.Get("key1")
|
|
if !found {
|
|
t.Error("Expected to find key1 before deletion")
|
|
}
|
|
|
|
// Delete it
|
|
store.Delete("key1")
|
|
|
|
// Verify it's gone
|
|
_, found = store.Get("key1")
|
|
if found {
|
|
t.Error("Expected key1 to be deleted")
|
|
}
|
|
|
|
// 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))
|
|
_, found := store.Get(key)
|
|
if !found {
|
|
t.Errorf("Expected to find %s before purge", key)
|
|
}
|
|
}
|
|
|
|
// Purge all
|
|
store.Purge()
|
|
|
|
// Verify all are gone
|
|
for i := 1; i <= 3; i++ {
|
|
key := "key" + string(rune('0'+i))
|
|
_, found := store.Get(key)
|
|
if found {
|
|
t.Errorf("Expected %s to be purged", 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, found := store.Get(key)
|
|
if !found {
|
|
t.Errorf("Goroutine %d: Expected to find key %d", id, key)
|
|
}
|
|
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, found := store.Get("key1")
|
|
if !found {
|
|
t.Error("Expected to find key1 after update")
|
|
}
|
|
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, found = store.Get("key1")
|
|
if !found {
|
|
t.Error("Expected key1 to still exist with new TTL")
|
|
}
|
|
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, found := intStore.Get(42)
|
|
if !found || 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, found := userStore.Get("user1")
|
|
if !found {
|
|
t.Error("Failed to retrieve user")
|
|
}
|
|
if retrievedUser.ID != 1 || retrievedUser.Name != "Alice" {
|
|
t.Error("Retrieved user data doesn't match")
|
|
}
|
|
}
|