diff --git a/cache.go b/cache.go index ce372bc..870eccb 100644 --- a/cache.go +++ b/cache.go @@ -61,7 +61,29 @@ const ( ) // Config configures the cache. -type Config[T any] struct { +type Config struct { + // Maximum number of items in the cache + Capacity int + // Optional max duration before an item expires. Must be greater than or + // equal to MinAge. If zero, expiration is disabled. + MaxAge time.Duration + // Optional min duration before an item expires. Must be less than or equal + // to MaxAge. When less than MaxAge, uniformly distributed random jitter is + // added to the expiration time. If equal or zero, jitter is disabled. + MinAge time.Duration + // Type of key expiration: Passive or Active + ExpirationType ExpirationType + // For active expiration, how often to iterate over the keyspace. Defaults + // to the MaxAge + ExpirationInterval time.Duration + // Optional callback invoked when an item is evicted due to the LRU policy + OnEviction func(key interface{}, value interface{}) + // Optional callback invoked when an item expired + OnExpiration func(key interface{}, value interface{}) +} + +// ConfigGeneric configures the cache. +type ConfigGeneric[T any] struct { // Maximum number of items in the cache Capacity int // Optional max duration before an item expires. Must be greater than or @@ -117,7 +139,23 @@ type Cache[T any] struct { // must be a positive int, and config.MaxAge a zero or positive duration. A // duration of zero disables item expiration. Panics given an invalid // config.Capacity or config.MaxAge. -func New[T any](config Config[T]) *Cache[T] { +func New(config Config) *Cache[interface{}] { + return NewGeneric(ConfigGeneric[interface{}]{ + Capacity: config.Capacity, + MaxAge: config.MaxAge, + MinAge: config.MinAge, + ExpirationType: config.ExpirationType, + ExpirationInterval: config.ExpirationInterval, + OnEviction: config.OnEviction, + OnExpiration: config.OnExpiration, + }) +} + +// NewGeneric constructs an LRU Cache with the given Config object. config.Capacity +// must be a positive int, and config.MaxAge a zero or positive duration. A +// duration of zero disables item expiration. Panics given an invalid +// config.Capacity or config.MaxAge. +func NewGeneric[T any](config ConfigGeneric[T]) *Cache[T] { if config.Capacity <= 0 { panic("Must supply a positive config.Capacity") } diff --git a/cache_test.go b/cache_test.go index 07a9e2f..629bde1 100644 --- a/cache_test.go +++ b/cache_test.go @@ -10,23 +10,23 @@ import ( func TestInvalidCapacity(t *testing.T) { assert.Panics(t, func() { - New(Config[string]{Capacity: 0}) + NewGeneric(Config[string]{Capacity: 0}) }) } func TestInvalidMaxAge(t *testing.T) { assert.Panics(t, func() { - New(Config[string]{Capacity: 1, MaxAge: -1 * time.Hour}) + NewGeneric(Config[string]{Capacity: 1, MaxAge: -1 * time.Hour}) }) } func TestInvalidMinAge(t *testing.T) { assert.Panics(t, func() { - New(Config[string]{Capacity: 1, MinAge: -1 * time.Hour}) + NewGeneric(Config[string]{Capacity: 1, MinAge: -1 * time.Hour}) }) assert.Panics(t, func() { - New(Config[string]{ + NewGeneric(Config[string]{ Capacity: 1, MaxAge: time.Hour, MinAge: 2 * time.Hour, @@ -35,7 +35,7 @@ func TestInvalidMinAge(t *testing.T) { } func TestBasicSetGet(t *testing.T) { - cache := New(Config[int]{Capacity: 2}) + cache := NewGeneric(Config[int]{Capacity: 2}) cache.Set("foo", 1) cache.Set("bar", 2) @@ -49,7 +49,7 @@ func TestBasicSetGet(t *testing.T) { } func TestBasicSetOverwrite(t *testing.T) { - cache := New(Config[int]{Capacity: 2}) + cache := NewGeneric(Config[int]{Capacity: 2}) cache.Set("foo", 1) evict := cache.Set("foo", 2) val, ok := cache.Get("foo") @@ -62,7 +62,7 @@ func TestBasicSetOverwrite(t *testing.T) { func TestEviction(t *testing.T) { var k, v interface{} - cache := New(Config[int]{ + cache := NewGeneric(Config[int]{ Capacity: 2, OnEviction: func(key interface{}, value int) { k = key @@ -86,7 +86,7 @@ func TestExpiration(t *testing.T) { var k, v interface{} var eviction bool - cache := New(Config[int]{ + cache := NewGeneric(Config[int]{ Capacity: 1, MaxAge: time.Millisecond, OnExpiration: func(key interface{}, value int) { @@ -125,7 +125,7 @@ func (mock *MockRandGenerator) Int63n(n int64) int64 { } func TestJitter(t *testing.T) { - cache := New(Config[string]{ + cache := NewGeneric(Config[string]{ Capacity: 1, MaxAge: 350 * time.Millisecond, MinAge: time.Millisecond, @@ -165,7 +165,7 @@ func TestJitter(t *testing.T) { } func TestHas(t *testing.T) { - cache := New(Config[string]{Capacity: 1, MaxAge: time.Millisecond}) + cache := NewGeneric(Config[string]{Capacity: 1, MaxAge: time.Millisecond}) cache.Set("foo", "bar") <-time.After(time.Millisecond * 2) @@ -174,7 +174,7 @@ func TestHas(t *testing.T) { } func TestPeek(t *testing.T) { - cache := New(Config[string]{Capacity: 1, MaxAge: time.Millisecond}) + cache := NewGeneric(Config[string]{Capacity: 1, MaxAge: time.Millisecond}) cache.Set("foo", "bar") <-time.After(time.Millisecond * 2) @@ -186,7 +186,7 @@ func TestPeek(t *testing.T) { func TestRemove(t *testing.T) { var eviction bool - cache := New(Config[string]{ + cache := NewGeneric(Config[string]{ Capacity: 1, OnEviction: func(key interface{}, value string) { eviction = true @@ -207,7 +207,7 @@ func TestRemove(t *testing.T) { func TestEvictOldest(t *testing.T) { var eviction bool - cache := New(Config[string]{ + cache := NewGeneric(Config[string]{ Capacity: 1, OnEviction: func(key interface{}, value string) { eviction = true @@ -231,7 +231,7 @@ func TestEvictOldest(t *testing.T) { } func TestLen(t *testing.T) { - cache := New(Config[int]{Capacity: 10}) + cache := NewGeneric(Config[int]{Capacity: 10}) for i := 0; i <= 9; i++ { evict := cache.Set(i, i) assert.False(t, evict) @@ -241,7 +241,7 @@ func TestLen(t *testing.T) { } func TestClear(t *testing.T) { - cache := New(Config[int]{Capacity: 10}) + cache := NewGeneric(Config[int]{Capacity: 10}) for i := 0; i <= 9; i++ { evict := cache.Set(i, i) assert.False(t, evict) @@ -257,7 +257,7 @@ func TestClear(t *testing.T) { } func TestKeys(t *testing.T) { - cache := New(Config[int]{Capacity: 10}) + cache := NewGeneric(Config[int]{Capacity: 10}) cache.Set("foo", 1) cache.Set("bar", 2) @@ -272,7 +272,7 @@ func TestKeys(t *testing.T) { } func TestOrderedKeys(t *testing.T) { - cache := New(Config[int]{Capacity: 10}) + cache := NewGeneric(Config[int]{Capacity: 10}) cache.Set("foo", 1) cache.Set("bar", 2) @@ -284,7 +284,7 @@ func TestOrderedKeys(t *testing.T) { } func TestSetMaxAge(t *testing.T) { - cache := New(Config[int]{Capacity: 10}) + cache := NewGeneric(Config[int]{Capacity: 10}) err := cache.SetMaxAge(-1 * time.Hour) assert.Error(t, err) @@ -293,7 +293,7 @@ func TestSetMaxAge(t *testing.T) { } func TestSetMinAge(t *testing.T) { - cache := New(Config[int]{Capacity: 10, MaxAge: time.Hour}) + cache := NewGeneric(Config[int]{Capacity: 10, MaxAge: time.Hour}) err := cache.SetMinAge(-1 * time.Hour) assert.Error(t, err) @@ -304,7 +304,7 @@ func TestSetMinAge(t *testing.T) { func TestOnEviction(t *testing.T) { var eviction bool - cache := New(Config[int]{Capacity: 1}) + cache := NewGeneric(Config[int]{Capacity: 1}) cache.OnEviction(func(key interface{}, value int) { eviction = true }) @@ -318,7 +318,7 @@ func TestOnEviction(t *testing.T) { func TestOnExpiration(t *testing.T) { var expiration bool - cache := New(Config[int]{ + cache := NewGeneric(Config[int]{ Capacity: 1, MaxAge: time.Millisecond, }) @@ -336,7 +336,7 @@ func TestOnExpiration(t *testing.T) { func TestActiveExpiration(t *testing.T) { invoked := make(chan bool) - cache := New(Config[int]{ + cache := NewGeneric(Config[int]{ Capacity: 1, MaxAge: time.Millisecond, ExpirationType: ActiveExpiration, @@ -355,7 +355,7 @@ func TestActiveExpiration(t *testing.T) { } func TestResize(t *testing.T) { - cache := New(Config[int]{ + cache := NewGeneric(Config[int]{ Capacity: 2, }) cache.Set("a", 1) @@ -387,12 +387,12 @@ func TestResize(t *testing.T) { func TestStats(t *testing.T) { t.Run("reports capacity", func(t *testing.T) { - cache := New(Config[int]{Capacity: 100}) + cache := NewGeneric(Config[int]{Capacity: 100}) assert.Equal(t, int64(100), cache.Stats().Capacity) }) t.Run("reports count", func(t *testing.T) { - cache := New(Config[int]{Capacity: 100}) + cache := NewGeneric(Config[int]{Capacity: 100}) for i := 0; i < 10; i++ { cache.Set(i, i) } @@ -400,7 +400,7 @@ func TestStats(t *testing.T) { }) t.Run("increments sets", func(t *testing.T) { - cache := New(Config[string]{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(Config[string]{Capacity: 100, MaxAge: time.Second}) for i := 0; i < 10; i++ { cache.Set("foo", "bar") } @@ -408,7 +408,7 @@ func TestStats(t *testing.T) { }) t.Run("increments gets", func(t *testing.T) { - cache := New(Config[int]{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(Config[int]{Capacity: 100, MaxAge: time.Second}) for i := 0; i < 10; i++ { cache.Get("foo") } @@ -416,20 +416,20 @@ func TestStats(t *testing.T) { }) t.Run("increments hits", func(t *testing.T) { - cache := New(Config[string]{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(Config[string]{Capacity: 100, MaxAge: time.Second}) cache.Set("foo", "bar") cache.Get("foo") assert.Equal(t, int64(1), cache.Stats().Gets) }) t.Run("increments misses", func(t *testing.T) { - cache := New(Config[int]{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(Config[int]{Capacity: 100, MaxAge: time.Second}) cache.Get("foo") assert.Equal(t, int64(1), cache.Stats().Misses) }) t.Run("increments evictions", func(t *testing.T) { - cache := New(Config[int]{Capacity: 1, MaxAge: time.Second}) + cache := NewGeneric(Config[int]{Capacity: 1, MaxAge: time.Second}) for i := 0; i < 10; i++ { cache.Set(i, i) } @@ -437,7 +437,7 @@ func TestStats(t *testing.T) { }) t.Run("delta stats", func(t *testing.T) { - cache := New(Config[string]{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(Config[string]{Capacity: 100, MaxAge: time.Second}) cache.Set("a", "1") prev := cache.Stats() @@ -460,7 +460,7 @@ func TestStats(t *testing.T) { }) t.Run("copy", func(t *testing.T) { - cache := New(Config[int]{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(Config[int]{Capacity: 100, MaxAge: time.Second}) stats := cache.Stats() stats.Hits++ stats.Misses++ @@ -470,7 +470,7 @@ func TestStats(t *testing.T) { } func BenchmarkCache(b *testing.B) { - cache := New(Config[string]{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(Config[string]{Capacity: 100, MaxAge: time.Second}) b.RunParallel(func(pb *testing.PB) { for pb.Next() { diff --git a/example_test.go b/example_test.go index 309afca..68cc3ef 100644 --- a/example_test.go +++ b/example_test.go @@ -7,7 +7,7 @@ import ( func ExampleNew() { // Create a new cache of type string, that expires after 10 mintues - cache := New(Config[string]{ + cache := NewGeneric(Config[string]{ Capacity: 10, ExpirationInterval: time.Minute * 10, })