diff --git a/cache/cache.go b/cache/cache.go index 41a1908..43ffe7c 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -169,7 +169,7 @@ func EnableCache(cfg *CacheConfig) { } // Shutdown closes the underlying cache client if it owns external resources -// (the Redis connection pool). No-op for in-memory caches. +// (the Redis connection pool, or the in-memory cleanup goroutine). func Shutdown() { if config != nil && config.Client != nil { if closer, ok := config.Client.(io.Closer); ok { diff --git a/cache/memory/memory.go b/cache/memory/memory.go index 3578312..e53f1ba 100644 --- a/cache/memory/memory.go +++ b/cache/memory/memory.go @@ -108,6 +108,15 @@ func (c *Cache) Shutdown() { } } +// Close implements io.Closer so the cache layer (cache.Shutdown) can stop the +// cleanup goroutine for the in-memory backend, matching the Redis backend +// which closes its connection pool. Idempotent: cancelling the context does +// not render the cache unusable, it only stops the background cleaner. +func (c *Cache) Close() error { + c.Shutdown() + return nil +} + func (c *Cache) Set(key string, value []byte, ttl time.Duration) { // Calculate the memory size of this entry entrySize := int64(len(key) + len(value) + approxEntryOverhead) diff --git a/cache/memory/memory_additional_test.go b/cache/memory/memory_additional_test.go index 495fc02..5ede35f 100644 --- a/cache/memory/memory_additional_test.go +++ b/cache/memory/memory_additional_test.go @@ -1,6 +1,7 @@ package libpack_cache_memory import ( + "io" "sync" "testing" "time" @@ -146,3 +147,30 @@ func TestMemoryCacheConcurrentSetSameNewKey_CounterNotDoubled(t *testing.T) { t.Fatal("key should be retrievable after Set") } } + +func TestMemoryCacheClose_IdempotentAndStillUsable(t *testing.T) { + // Compile-time guard: the memory backend must satisfy io.Closer so + // cache.Shutdown() can stop its cleanup goroutine (it otherwise leaks + // the background timer until process exit), matching the Redis backend. + var _ io.Closer = (*Cache)(nil) + + cache := New(DefaultTestExpiration) + cache.Set("key", []byte("value"), DefaultTestExpiration) + + // Close is idempotent: it only cancels the background cleaner. + if err := cache.Close(); err != nil { + t.Fatalf("first Close: %v", err) + } + if err := cache.Close(); err != nil { + t.Fatalf("second Close: %v", err) + } + + // Stopping the cleaner must not render the cache unusable. + if _, ok := cache.Get("key"); !ok { + t.Fatal("entry should survive Close; Close only stops the background cleaner") + } + cache.Set("key2", []byte("v2"), DefaultTestExpiration) + if _, ok := cache.Get("key2"); !ok { + t.Fatal("cache should remain writable after Close") + } +}