From bddf7f4ca174d495c4ddbcd3dfa2655447245d8d Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 19 Dec 2024 20:25:00 +0400 Subject: [PATCH] adding timestamp to item --- internal/cache/cache.go | 121 +++++++++++++++++++++------------- internal/cache/cache_test.go | 122 +++++++++++++++++------------------ 2 files changed, 136 insertions(+), 107 deletions(-) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 505c93e7..9a23ef9d 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -7,39 +7,78 @@ import ( "github.com/patrickmn/go-cache" ) -const ( - NoExpiration = cache.NoExpiration -) - // Package cache provides a generic caching implementation that wraps the go-cache library // with additional support for custom eviction policies. It allows both time-based expiration // (inherited from go-cache) and custom eviction rules through user-defined policies. // // The cache is type-safe through Go generics, thread-safe through mutex locks, and supports -// all basic cache operations (Get, Set, Delete, Flush). Keys are strings, and values can -// be of any type. +// all basic cache operations. Keys are strings, and values can be of any type. Each cached +// value stores its insertion timestamp, allowing for time-based validation in custom policies. +// +// Example usage with contract reader: +// type Object { +// Value interface{} +// } +// +// type Event struct { +// Timestamp int64 +// Data string +// } // -// Example usage: +// type ContractReader interface { +// QueryEvents(ctx context.Context, filter QueryFilter) ([]Event, error) +// } // -// // Create a cache with custom eviction policy -// cache := cache.NewCustomCache[int]( -// 5*time.Minute, // Default expiration -// 10*time.Minute, // Cleanup interval -// func(v int) bool { -// return v < 0 // Evict negative numbers +// reader := NewContractReader() +// +// // Create cache with contract reader in closure +// cache := NewCustomCache[Object]( +// 5*time.Minute, // Default expiration +// 10*time.Minute, // Cleanup interval +// func(o Event, storedAt time.Time) bool { +// ctx := context.Background() +// filter := QueryFilter{ +// FromTimestamp: storedAt.Unix(), +// Confidence: Finalized, +// } +// +// // Query for any events after our cache insertion time +// newEvents, err := reader.QueryEvents(ctx, filter) +// if err != nil { +// return false // Keep cache on error +// } +// +// // Evict if new events exist after our cache time +// return len(newEvents) > 0 // }, // ) // -// // Use NoExpiration for items that shouldn't expire -// cache.Set("key", 42, cache.NoExpiration) +// // Cache an object +// o := Object{Data: "..."} +// cache.Set("key", o, NoExpiration) // -// The cache can be used with any value type while maintaining type safety: -// - Time-based expiration is handled by the underlying go-cache -// - Custom policies can implement domain-specific eviction logic +// // Later: event will be evicted if newer ones exist on chain +// o, found := cache.Get("key") +// +// The cache ensures data freshness through: +// - Automatic time-based expiration from go-cache +// - Custom eviction policies with access to storage timestamps // - Thread-safe operations for concurrent access +// - Type safety through Go generics + +const ( + NoExpiration = cache.NoExpiration +) + +// timestampedValue wraps a value with its storage timestamp +type timestampedValue[V any] struct { + Value V + StoredAt time.Time +} + type CustomCache[V any] struct { - cache *cache.Cache - customPolicy func(V) bool + *cache.Cache + customPolicy func(V, time.Time) bool // Updated to include storage time mutex sync.RWMutex } @@ -47,17 +86,21 @@ type CustomCache[V any] struct { func NewCustomCache[V any]( defaultExpiration time.Duration, cleanupInterval time.Duration, - customPolicy func(V) bool, + customPolicy func(V, time.Time) bool, ) *CustomCache[V] { return &CustomCache[V]{ - cache: cache.New(defaultExpiration, cleanupInterval), + Cache: cache.New(defaultExpiration, cleanupInterval), customPolicy: customPolicy, } } -// Set adds an item to the cache +// Set adds an item to the cache with current timestamp func (c *CustomCache[V]) Set(key string, value V, expiration time.Duration) { - c.cache.Set(key, value, expiration) + wrapped := timestampedValue[V]{ + Value: value, + StoredAt: time.Now(), + } + c.Cache.Set(key, wrapped, expiration) } // Get retrieves an item from the cache, checking both time-based and custom policies @@ -66,29 +109,24 @@ func (c *CustomCache[V]) Get(key string) (V, bool) { defer c.mutex.Unlock() var zero V - value, found := c.cache.Get(key) + value, found := c.Cache.Get(key) if !found { return zero, false } - // Type assertion - typedValue, ok := value.(V) + // Type assertion for timestamped value + wrapped, ok := value.(timestampedValue[V]) if !ok { return zero, false } - // Check custom policy - if c.customPolicy != nil && c.customPolicy(typedValue) { - c.cache.Delete(key) + // Check custom policy with timestamp + if c.customPolicy != nil && c.customPolicy(wrapped.Value, wrapped.StoredAt) { + c.Cache.Delete(key) return zero, false } - return typedValue, true -} - -// Delete removes an item from the cache -func (c *CustomCache[V]) Delete(key string) { - c.cache.Delete(key) + return wrapped.Value, true } // Items returns all items in the cache @@ -96,19 +134,14 @@ func (c *CustomCache[V]) Items() map[string]V { c.mutex.RLock() defer c.mutex.RUnlock() - items := c.cache.Items() + items := c.Cache.Items() result := make(map[string]V) for k, v := range items { - if value, ok := v.Object.(V); ok { - result[k] = value + if wrapped, ok := v.Object.(timestampedValue[V]); ok { + result[k] = wrapped.Value } } return result } - -// Flush removes all items from the cache -func (c *CustomCache[V]) Flush() { - c.cache.Flush() -} diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index 4d011af3..e28b0b2f 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -27,22 +27,29 @@ func TestCustomCache(t *testing.T) { assert.False(t, found) }) - t.Run("custom policy eviction", func(t *testing.T) { - isEven := func(v int) bool { - return v%2 == 0 + t.Run("custom policy with timestamp", func(t *testing.T) { + now := time.Now() + isStale := func(v int, storedAt time.Time) bool { + return storedAt.Before(now) } - cache := NewCustomCache[int](5*time.Minute, 10*time.Minute, isEven) + cache := NewCustomCache[int](5*time.Minute, 10*time.Minute, isStale) - // Even number should be evicted - cache.Set("even", 2, NoExpiration) - _, found := cache.Get("even") - assert.False(t, found) - - // Odd number should remain - cache.Set("odd", 3, NoExpiration) - value, found := cache.Get("odd") + // Value stored now should not be evicted + cache.Set("fresh", 1, NoExpiration) + value, found := cache.Get("fresh") assert.True(t, found) - assert.Equal(t, 3, value) + assert.Equal(t, 1, value) + + // Simulate old value by manipulating timestamp + oldValue := timestampedValue[int]{ + Value: 2, + StoredAt: now.Add(-1 * time.Hour), + } + cache.Cache.Set("stale", oldValue, NoExpiration) + + // Stale value should be evicted + _, found = cache.Get("stale") + assert.False(t, found) }) t.Run("time based expiration", func(t *testing.T) { @@ -73,37 +80,7 @@ func TestCustomCache(t *testing.T) { assert.Equal(t, 2, items["two"]) }) - t.Run("flush operation", func(t *testing.T) { - cache := NewCustomCache[int](5*time.Minute, 10*time.Minute, nil) - - cache.Set("one", 1, NoExpiration) - cache.Set("two", 2, NoExpiration) - - cache.Flush() - items := cache.Items() - assert.Len(t, items, 0) - }) - - t.Run("type safety", func(t *testing.T) { - cache := NewCustomCache[int](5*time.Minute, 10*time.Minute, nil) - - // Set with correct type - cache.Set("good", 123, NoExpiration) - - // Simulate wrong type in underlying cache - cache.cache.Set("bad", "not an int", NoExpiration) - - // Good type should work - value, found := cache.Get("good") - assert.True(t, found) - assert.Equal(t, 123, value) - - // Bad type should fail safely - _, found = cache.Get("bad") - assert.False(t, found) - }) - - t.Run("concurrent access", func(t *testing.T) { + t.Run("concurrent access with timestamps", func(t *testing.T) { cache := NewCustomCache[int](5*time.Minute, 10*time.Minute, nil) // Run multiple goroutines accessing the cache @@ -126,39 +103,58 @@ func TestCustomCache(t *testing.T) { assert.True(t, found) }) - t.Run("complex types", func(t *testing.T) { + t.Run("complex types with timestamp eviction", func(t *testing.T) { type ComplexType struct { - ID int - Name string + ID int + Name string + Timestamp time.Time } - cache := NewCustomCache[ComplexType](5*time.Minute, 10*time.Minute, nil) + threshold := time.Now() + cache := NewCustomCache[ComplexType]( + 5*time.Minute, + 10*time.Minute, + func(v ComplexType, storedAt time.Time) bool { + return storedAt.Before(threshold) + }, + ) - value := ComplexType{ID: 1, Name: "test"} + value := ComplexType{ID: 1, Name: "test", Timestamp: time.Now()} cache.Set("complex", value, NoExpiration) + // Fresh value should not be evicted retrieved, found := cache.Get("complex") assert.True(t, found) assert.Equal(t, value, retrieved) - }) - t.Run("custom policy with nil value", func(t *testing.T) { - isNil := func(v *string) bool { - return v == nil + // Simulate old value + oldValue := timestampedValue[ComplexType]{ + Value: ComplexType{ID: 2, Name: "old"}, + StoredAt: threshold.Add(-1 * time.Hour), } - cache := NewCustomCache[*string](5*time.Minute, 10*time.Minute, isNil) + cache.Cache.Set("old", oldValue, NoExpiration) + + // Old value should be evicted + _, found = cache.Get("old") + assert.False(t, found) + }) - str := "test" - cache.Set("nonnil", &str, NoExpiration) - cache.Set("nil", nil, NoExpiration) + t.Run("correct timestamp storage", func(t *testing.T) { + cache := NewCustomCache[string](5*time.Minute, 10*time.Minute, nil) - // Non-nil value should remain - value, found := cache.Get("nonnil") + before := time.Now() + cache.Set("key", "value", NoExpiration) + after := time.Now() + + // Get the raw timestamped value + raw, found := cache.Cache.Get("key") assert.True(t, found) - assert.Equal(t, &str, value) - // Nil value should be evicted - _, found = cache.Get("nil") - assert.False(t, found) + wrapped, ok := raw.(timestampedValue[string]) + assert.True(t, ok) + + // StoredAt should be between before and after + assert.True(t, wrapped.StoredAt.After(before) || wrapped.StoredAt.Equal(before)) + assert.True(t, wrapped.StoredAt.Before(after) || wrapped.StoredAt.Equal(after)) }) }