From a71e4b31d3e3bccf54a0be308140c9f387ade7f5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 31 Dec 2025 05:21:36 +0000 Subject: [PATCH 1/3] Add batch operations and metrics monitoring Added SetMulti and GetMulti methods for efficient batch operations. Added StoreMetrics struct and GetMetrics method to expose cache statistics (hits, misses, evictions, item count). Updated Set, Get, and cleanupExpiredItems to track these metrics. Added unit tests for new functionality. --- memorystore/batch_test.go | 64 +++++ memorystore/memorystore.go | 483 +++++++++++++++++++++--------------- memorystore/metrics_test.go | 51 ++++ 3 files changed, 392 insertions(+), 206 deletions(-) create mode 100644 memorystore/batch_test.go create mode 100644 memorystore/metrics_test.go diff --git a/memorystore/batch_test.go b/memorystore/batch_test.go new file mode 100644 index 0000000..60060c1 --- /dev/null +++ b/memorystore/batch_test.go @@ -0,0 +1,64 @@ +package memorystore + +import ( + "testing" + "time" +) + +func TestBatchOperations(t *testing.T) { + ms := NewMemoryStore() + defer ms.Stop() + + // Test SetMulti + items := map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + "key3": []byte("value3"), + } + + if err := ms.SetMulti(items, time.Minute); err != nil { + t.Fatalf("SetMulti failed: %v", err) + } + + // Verify items are set + for k, v := range items { + val, exists := ms.Get(k) + if !exists { + t.Errorf("Key %s not found", k) + } + if string(val) != string(v) { + t.Errorf("Value mismatch for key %s: expected %s, got %s", k, v, val) + } + } + + // Test GetMulti + keys := []string{"key1", "key2", "key3", "nonexistent"} + results := ms.GetMulti(keys) + + if len(results) != 3 { + t.Errorf("Expected 3 items, got %d", len(results)) + } + + for k, v := range items { + if val, ok := results[k]; !ok { + t.Errorf("Key %s missing from results", k) + } else if string(val) != string(v) { + t.Errorf("Value mismatch for key %s: expected %s, got %s", k, v, val) + } + } + + if _, ok := results["nonexistent"]; ok { + t.Error("Non-existent key returned in results") + } + + // Verify metrics update + metrics := ms.GetMetrics() + // Hits: 3 from individual Get calls in loop + 3 from GetMulti call + // Misses: 1 from GetMulti call (nonexistent) + if metrics.Hits != 6 { + t.Errorf("Expected 6 hits, got %d", metrics.Hits) + } + if metrics.Misses != 1 { + t.Errorf("Expected 1 miss, got %d", metrics.Misses) + } +} diff --git a/memorystore/memorystore.go b/memorystore/memorystore.go index 1674985..78938c9 100644 --- a/memorystore/memorystore.go +++ b/memorystore/memorystore.go @@ -1,206 +1,277 @@ -// memorystore/memorystore.go -// Package memorystore provides a simple in-memory cache implementation with automatic cleanup -// of expired items. It supports both raw byte storage and JSON serialization/deserialization -// of structured data. -package memorystore - -import ( - "context" - "sync" - "time" - - "github.com/goccy/go-json" -) - -// item represents a single cache entry with its value and expiration time. -type item struct { - value []byte // Raw data stored as a byte slice - expiresAt time.Time // Time at which this item should be considered expired -} - -// MemoryStore implements an in-memory cache with automatic cleanup of expired items. -// It is safe for concurrent use by multiple goroutines. -type MemoryStore struct { - mu sync.RWMutex // Protects access to the store map - store map[string]item // Internal storage for cache items - ps *pubSubManager // PubSub manager for cache events - ctx context.Context // Context for controlling the cleanup worker - cancelFunc context.CancelFunc // Function to stop the cleanup worker - wg sync.WaitGroup // WaitGroup for cleanup goroutine synchronization -} - -// NewMemoryStore creates and initializes a new MemoryStore instance. -// It starts a background worker that periodically cleans up expired items. -// The returned MemoryStore is ready for use. -func NewMemoryStore() *MemoryStore { - ctx, cancel := context.WithCancel(context.Background()) - ms := &MemoryStore{ - store: make(map[string]item), - ctx: ctx, - cancelFunc: cancel, - } - ms.initPubSub() - ms.startCleanupWorker() - return ms -} - -// Stop gracefully shuts down the MemoryStore by stopping the cleanup goroutine -// and releasing associated resources. After calling Stop, the store cannot be used. -// Multiple calls to Stop will not cause a panic and return nil. -// -// Example: -// -// store := NewMemoryStore() -// defer store.Stop() -func (m *MemoryStore) Stop() error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.cancelFunc == nil { - return nil - } - - m.cancelFunc() - m.cancelFunc = nil - - m.cleanupPubSub() - - // Wait for cleanup goroutine to finish - m.wg.Wait() - - // Clear the store to free up memory - m.store = nil - - return nil -} - -// IsStopped returns true if the MemoryStore has been stopped and can no longer be used. -// This method is safe for concurrent use. -// -// Example: -// -// if store.IsStopped() { -// log.Println("Store is no longer available") -// return -// } -func (m *MemoryStore) IsStopped() bool { - m.mu.RLock() - defer m.mu.RUnlock() - return m.cancelFunc == nil -} - -// startCleanupWorker initiates a background goroutine that periodically -// removes expired items from the cache. The cleanup interval is set to 1 minute. -func (m *MemoryStore) startCleanupWorker() { - m.wg.Add(1) - go func() { - defer m.wg.Done() - ticker := time.NewTicker(1 * time.Minute) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - m.cleanupExpiredItems() - case <-m.ctx.Done(): - return - } - } - }() -} - -// cleanupExpiredItems removes all expired items from the cache. -// This method acquires a write lock on the store while performing the cleanup. -func (m *MemoryStore) cleanupExpiredItems() { - m.mu.Lock() - defer m.mu.Unlock() - for key, item := range m.store { - if time.Now().After(item.expiresAt) { - delete(m.store, key) - } - } -} - -// Set stores a raw byte slice in the cache with the specified key and duration. -// The item will automatically expire after the specified duration. -// If an error occurs, it will be returned to the caller. -func (m *MemoryStore) Set(key string, value []byte, duration time.Duration) error { - m.mu.Lock() - defer m.mu.Unlock() - - m.store[key] = item{ - value: value, - expiresAt: time.Now().Add(duration), - } - - return nil -} - -// SetJSON stores a JSON-serializable value in the cache. -// The value is serialized to JSON before storage. -// Returns an error if JSON marshaling fails. -// -// Example: -// -// type User struct { -// Name string -// Age int -// } -// user := User{Name: "John", Age: 30} -// err := cache.SetJSON("user:123", user, 1*time.Hour) -func (m *MemoryStore) SetJSON(key string, value interface{}, duration time.Duration) error { - data, err := json.Marshal(value) - if err != nil { - return err - } - return m.Set(key, data, duration) -} - -// Get retrieves a value from the cache. -// Returns the value and a boolean indicating whether the key was found. -// If the item has expired, returns (nil, false). -func (m *MemoryStore) Get(key string) ([]byte, bool) { - m.mu.RLock() - defer m.mu.RUnlock() - - it, exists := m.store[key] - if !exists || time.Now().After(it.expiresAt) { - return nil, false - } - - return it.value, true -} - -// GetJSON retrieves and deserializes a JSON value from the cache into the provided interface. -// Returns a boolean indicating if the key was found and any error that occurred during deserialization. -// -// Example: -// -// var user User -// exists, err := cache.GetJSON("user:123", &user) -// if err != nil { -// // Handle error -// } else if exists { -// fmt.Printf("Found user: %+v\n", user) -// } -func (m *MemoryStore) GetJSON(key string, dest interface{}) (bool, error) { - data, exists := m.Get(key) - if !exists { - return false, nil - } - - err := json.Unmarshal(data, dest) - if err != nil { - return true, err - } - - return true, nil -} - -// Delete removes an item from the cache. -// If the key doesn't exist, the operation is a no-op. -func (m *MemoryStore) Delete(key string) { - m.mu.Lock() - defer m.mu.Unlock() - delete(m.store, key) -} +// memorystore/memorystore.go +// Package memorystore provides a simple in-memory cache implementation with automatic cleanup +// of expired items. It supports both raw byte storage and JSON serialization/deserialization +// of structured data. +package memorystore + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "github.com/goccy/go-json" +) + +// item represents a single cache entry with its value and expiration time. +type item struct { + value []byte // Raw data stored as a byte slice + expiresAt time.Time // Time at which this item should be considered expired +} + +// StoreMetrics holds statistics about the cache usage. +type StoreMetrics struct { + Items int // Current number of items in the cache + Hits int64 // Total number of cache hits + Misses int64 // Total number of cache misses + Evictions int64 // Total number of items evicted (expired) +} + +// MemoryStore implements an in-memory cache with automatic cleanup of expired items. +// It is safe for concurrent use by multiple goroutines. +type MemoryStore struct { + mu sync.RWMutex // Protects access to the store map + store map[string]item // Internal storage for cache items + ps *pubSubManager // PubSub manager for cache events + ctx context.Context // Context for controlling the cleanup worker + cancelFunc context.CancelFunc // Function to stop the cleanup worker + wg sync.WaitGroup // WaitGroup for cleanup goroutine synchronization + + // Metrics + hits int64 // Atomic counter for cache hits + misses int64 // Atomic counter for cache misses + evictions int64 // Atomic counter for evicted items +} + +// NewMemoryStore creates and initializes a new MemoryStore instance. +// It starts a background worker that periodically cleans up expired items. +// The returned MemoryStore is ready for use. +func NewMemoryStore() *MemoryStore { + ctx, cancel := context.WithCancel(context.Background()) + ms := &MemoryStore{ + store: make(map[string]item), + ctx: ctx, + cancelFunc: cancel, + } + ms.initPubSub() + ms.startCleanupWorker() + return ms +} + +// Stop gracefully shuts down the MemoryStore by stopping the cleanup goroutine +// and releasing associated resources. After calling Stop, the store cannot be used. +// Multiple calls to Stop will not cause a panic and return nil. +// +// Example: +// +// store := NewMemoryStore() +// defer store.Stop() +func (m *MemoryStore) Stop() error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.cancelFunc == nil { + return nil + } + + m.cancelFunc() + m.cancelFunc = nil + + m.cleanupPubSub() + + // Wait for cleanup goroutine to finish + m.wg.Wait() + + // Clear the store to free up memory + m.store = nil + + return nil +} + +// IsStopped returns true if the MemoryStore has been stopped and can no longer be used. +// This method is safe for concurrent use. +// +// Example: +// +// if store.IsStopped() { +// log.Println("Store is no longer available") +// return +// } +func (m *MemoryStore) IsStopped() bool { + m.mu.RLock() + defer m.mu.RUnlock() + return m.cancelFunc == nil +} + +// startCleanupWorker initiates a background goroutine that periodically +// removes expired items from the cache. The cleanup interval is set to 1 minute. +func (m *MemoryStore) startCleanupWorker() { + m.wg.Add(1) + go func() { + defer m.wg.Done() + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + m.cleanupExpiredItems() + case <-m.ctx.Done(): + return + } + } + }() +} + +// cleanupExpiredItems removes all expired items from the cache. +// This method acquires a write lock on the store while performing the cleanup. +func (m *MemoryStore) cleanupExpiredItems() { + m.mu.Lock() + defer m.mu.Unlock() + for key, item := range m.store { + if time.Now().After(item.expiresAt) { + delete(m.store, key) + atomic.AddInt64(&m.evictions, 1) + } + } +} + +// Set stores a raw byte slice in the cache with the specified key and duration. +// The item will automatically expire after the specified duration. +// If an error occurs, it will be returned to the caller. +func (m *MemoryStore) Set(key string, value []byte, duration time.Duration) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.store[key] = item{ + value: value, + expiresAt: time.Now().Add(duration), + } + + return nil +} + +// SetJSON stores a JSON-serializable value in the cache. +// The value is serialized to JSON before storage. +// Returns an error if JSON marshaling fails. +// +// Example: +// +// type User struct { +// Name string +// Age int +// } +// user := User{Name: "John", Age: 30} +// err := cache.SetJSON("user:123", user, 1*time.Hour) +func (m *MemoryStore) SetJSON(key string, value interface{}, duration time.Duration) error { + data, err := json.Marshal(value) + if err != nil { + return err + } + return m.Set(key, data, duration) +} + +// Get retrieves a value from the cache. +// Returns the value and a boolean indicating whether the key was found. +// If the item has expired, returns (nil, false). +func (m *MemoryStore) Get(key string) ([]byte, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + + it, exists := m.store[key] + if !exists || time.Now().After(it.expiresAt) { + atomic.AddInt64(&m.misses, 1) + return nil, false + } + + atomic.AddInt64(&m.hits, 1) + return it.value, true +} + +// GetJSON retrieves and deserializes a JSON value from the cache into the provided interface. +// Returns a boolean indicating if the key was found and any error that occurred during deserialization. +// +// Example: +// +// var user User +// exists, err := cache.GetJSON("user:123", &user) +// if err != nil { +// // Handle error +// } else if exists { +// fmt.Printf("Found user: %+v\n", user) +// } +func (m *MemoryStore) GetJSON(key string, dest interface{}) (bool, error) { + data, exists := m.Get(key) + if !exists { + return false, nil + } + + err := json.Unmarshal(data, dest) + if err != nil { + return true, err + } + + return true, nil +} + +// Delete removes an item from the cache. +// If the key doesn't exist, the operation is a no-op. +func (m *MemoryStore) Delete(key string) { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.store, key) +} + +// SetMulti stores multiple key-value pairs in the cache. +// This is more efficient than calling Set multiple times as it acquires the lock only once. +// All items will have the same expiration duration. +func (m *MemoryStore) SetMulti(items map[string][]byte, duration time.Duration) error { + m.mu.Lock() + defer m.mu.Unlock() + + expiresAt := time.Now().Add(duration) + for key, value := range items { + m.store[key] = item{ + value: value, + expiresAt: expiresAt, + } + } + return nil +} + +// GetMulti retrieves multiple values from the cache. +// It returns a map of found items. Keys that don't exist or are expired are omitted. +func (m *MemoryStore) GetMulti(keys []string) map[string][]byte { + m.mu.RLock() + defer m.mu.RUnlock() + + result := make(map[string][]byte) + now := time.Now() + + for _, key := range keys { + it, exists := m.store[key] + if exists && !now.After(it.expiresAt) { + result[key] = it.value + atomic.AddInt64(&m.hits, 1) + } else { + atomic.AddInt64(&m.misses, 1) + } + } + + return result +} + +// GetMetrics returns the current statistics of the MemoryStore. +// It returns a copy of the metrics to ensure thread safety. +func (m *MemoryStore) GetMetrics() StoreMetrics { + m.mu.RLock() + itemCount := len(m.store) + m.mu.RUnlock() + + return StoreMetrics{ + Items: itemCount, + Hits: atomic.LoadInt64(&m.hits), + Misses: atomic.LoadInt64(&m.misses), + Evictions: atomic.LoadInt64(&m.evictions), + } +} diff --git a/memorystore/metrics_test.go b/memorystore/metrics_test.go new file mode 100644 index 0000000..7f84366 --- /dev/null +++ b/memorystore/metrics_test.go @@ -0,0 +1,51 @@ +package memorystore + +import ( + "testing" + "time" +) + +func TestMetrics(t *testing.T) { + ms := NewMemoryStore() + defer ms.Stop() + + // Initial metrics should be zero + metrics := ms.GetMetrics() + if metrics.Hits != 0 || metrics.Misses != 0 || metrics.Evictions != 0 || metrics.Items != 0 { + t.Errorf("Expected initial metrics to be zero, got %+v", metrics) + } + + // Test Hits + ms.Set("key1", []byte("value1"), time.Minute) + ms.Get("key1") + metrics = ms.GetMetrics() + if metrics.Hits != 1 { + t.Errorf("Expected 1 hit, got %d", metrics.Hits) + } + + // Test Misses + ms.Get("nonexistent") + metrics = ms.GetMetrics() + if metrics.Misses != 1 { + t.Errorf("Expected 1 miss, got %d", metrics.Misses) + } + + // Test Items + ms.Set("key2", []byte("value2"), time.Minute) + metrics = ms.GetMetrics() + if metrics.Items != 2 { + t.Errorf("Expected 2 items, got %d", metrics.Items) + } + + // Test Evictions + ms.Set("expired", []byte("expired"), 1*time.Millisecond) + time.Sleep(100 * time.Millisecond) // Wait for expiration + + // Trigger cleanup + ms.cleanupExpiredItems() + + metrics = ms.GetMetrics() + if metrics.Evictions != 1 { + t.Errorf("Expected 1 eviction, got %d", metrics.Evictions) + } +} From 26e6da57e2c52627874550df97d46e548f8ac5bc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 31 Dec 2025 05:41:12 +0000 Subject: [PATCH 2/3] Refactor MemoryStore to use sharded storage for better concurrency Refactored `MemoryStore` to use a sharded map approach (Concurrent Map pattern) with 256 shards. This reduces lock contention significantly compared to the previous single global lock design. Changes: - Replaced `mu` and `store` with `shards` slice in `MemoryStore`. - Implemented `shard` struct with individual mutex and map. - Added `getShard` method using FNV-1a hashing. - Updated `Set`, `Get`, `Delete` to operate on specific shards. - Optimized `SetMulti` and `GetMulti` to group keys by shard. - Updated `cleanupExpiredItems` to iterate and lock shards individually, preventing "stop-the-world" pauses. - Preserved lifecycle management using a dedicated `lifecycleMu`. - Updated `GetMetrics` to aggregate stats from all shards. --- memorystore/memorystore.go | 142 ++++++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 41 deletions(-) diff --git a/memorystore/memorystore.go b/memorystore/memorystore.go index 78938c9..9f76aee 100644 --- a/memorystore/memorystore.go +++ b/memorystore/memorystore.go @@ -6,6 +6,7 @@ package memorystore import ( "context" + "hash/fnv" "sync" "sync/atomic" "time" @@ -13,6 +14,8 @@ import ( "github.com/goccy/go-json" ) +const numShards = 256 + // item represents a single cache entry with its value and expiration time. type item struct { value []byte // Raw data stored as a byte slice @@ -27,11 +30,18 @@ type StoreMetrics struct { Evictions int64 // Total number of items evicted (expired) } +type shard struct { + mu sync.RWMutex + store map[string]item +} + // MemoryStore implements an in-memory cache with automatic cleanup of expired items. // It is safe for concurrent use by multiple goroutines. type MemoryStore struct { - mu sync.RWMutex // Protects access to the store map - store map[string]item // Internal storage for cache items + // lifecycleMu protects the lifecycle state (cancelFunc) + lifecycleMu sync.RWMutex + + shards []*shard // Sharded storage ps *pubSubManager // PubSub manager for cache events ctx context.Context // Context for controlling the cleanup worker cancelFunc context.CancelFunc // Function to stop the cleanup worker @@ -49,15 +59,29 @@ type MemoryStore struct { func NewMemoryStore() *MemoryStore { ctx, cancel := context.WithCancel(context.Background()) ms := &MemoryStore{ - store: make(map[string]item), + shards: make([]*shard, numShards), ctx: ctx, cancelFunc: cancel, } + + for i := 0; i < numShards; i++ { + ms.shards[i] = &shard{ + store: make(map[string]item), + } + } + ms.initPubSub() ms.startCleanupWorker() return ms } +// getShard returns the shard responsible for the given key. +func (m *MemoryStore) getShard(key string) *shard { + h := fnv.New64a() + h.Write([]byte(key)) + return m.shards[h.Sum64()%numShards] +} + // Stop gracefully shuts down the MemoryStore by stopping the cleanup goroutine // and releasing associated resources. After calling Stop, the store cannot be used. // Multiple calls to Stop will not cause a panic and return nil. @@ -67,8 +91,8 @@ func NewMemoryStore() *MemoryStore { // store := NewMemoryStore() // defer store.Stop() func (m *MemoryStore) Stop() error { - m.mu.Lock() - defer m.mu.Unlock() + m.lifecycleMu.Lock() + defer m.lifecycleMu.Unlock() if m.cancelFunc == nil { return nil @@ -83,7 +107,11 @@ func (m *MemoryStore) Stop() error { m.wg.Wait() // Clear the store to free up memory - m.store = nil + for _, s := range m.shards { + s.mu.Lock() + s.store = nil + s.mu.Unlock() + } return nil } @@ -98,8 +126,8 @@ func (m *MemoryStore) Stop() error { // return // } func (m *MemoryStore) IsStopped() bool { - m.mu.RLock() - defer m.mu.RUnlock() + m.lifecycleMu.RLock() + defer m.lifecycleMu.RUnlock() return m.cancelFunc == nil } @@ -124,15 +152,19 @@ func (m *MemoryStore) startCleanupWorker() { } // cleanupExpiredItems removes all expired items from the cache. -// This method acquires a write lock on the store while performing the cleanup. +// It iterates over shards and cleans them one by one to avoid global locking. func (m *MemoryStore) cleanupExpiredItems() { - m.mu.Lock() - defer m.mu.Unlock() - for key, item := range m.store { - if time.Now().After(item.expiresAt) { - delete(m.store, key) - atomic.AddInt64(&m.evictions, 1) + now := time.Now() + for _, s := range m.shards { + // Lock only the current shard + s.mu.Lock() + for key, item := range s.store { + if now.After(item.expiresAt) { + delete(s.store, key) + atomic.AddInt64(&m.evictions, 1) + } } + s.mu.Unlock() } } @@ -140,10 +172,11 @@ func (m *MemoryStore) cleanupExpiredItems() { // The item will automatically expire after the specified duration. // If an error occurs, it will be returned to the caller. func (m *MemoryStore) Set(key string, value []byte, duration time.Duration) error { - m.mu.Lock() - defer m.mu.Unlock() + s := m.getShard(key) + s.mu.Lock() + defer s.mu.Unlock() - m.store[key] = item{ + s.store[key] = item{ value: value, expiresAt: time.Now().Add(duration), } @@ -175,10 +208,11 @@ func (m *MemoryStore) SetJSON(key string, value interface{}, duration time.Durat // Returns the value and a boolean indicating whether the key was found. // If the item has expired, returns (nil, false). func (m *MemoryStore) Get(key string) ([]byte, bool) { - m.mu.RLock() - defer m.mu.RUnlock() + s := m.getShard(key) + s.mu.RLock() + defer s.mu.RUnlock() - it, exists := m.store[key] + it, exists := s.store[key] if !exists || time.Now().After(it.expiresAt) { atomic.AddInt64(&m.misses, 1) return nil, false @@ -217,45 +251,68 @@ func (m *MemoryStore) GetJSON(key string, dest interface{}) (bool, error) { // Delete removes an item from the cache. // If the key doesn't exist, the operation is a no-op. func (m *MemoryStore) Delete(key string) { - m.mu.Lock() - defer m.mu.Unlock() - delete(m.store, key) + s := m.getShard(key) + s.mu.Lock() + defer s.mu.Unlock() + delete(s.store, key) } // SetMulti stores multiple key-value pairs in the cache. -// This is more efficient than calling Set multiple times as it acquires the lock only once. +// This is more efficient than calling Set multiple times as it groups keys by shard. // All items will have the same expiration duration. func (m *MemoryStore) SetMulti(items map[string][]byte, duration time.Duration) error { - m.mu.Lock() - defer m.mu.Unlock() - + // Group items by shard + shardItems := make(map[*shard]map[string]item) expiresAt := time.Now().Add(duration) + for key, value := range items { - m.store[key] = item{ + s := m.getShard(key) + if _, ok := shardItems[s]; !ok { + shardItems[s] = make(map[string]item) + } + shardItems[s][key] = item{ value: value, expiresAt: expiresAt, } } + + // Apply updates per shard + for s, items := range shardItems { + s.mu.Lock() + for k, v := range items { + s.store[k] = v + } + s.mu.Unlock() + } return nil } // GetMulti retrieves multiple values from the cache. // It returns a map of found items. Keys that don't exist or are expired are omitted. func (m *MemoryStore) GetMulti(keys []string) map[string][]byte { - m.mu.RLock() - defer m.mu.RUnlock() - result := make(map[string][]byte) now := time.Now() + // Group keys by shard + shardKeys := make(map[*shard][]string) for _, key := range keys { - it, exists := m.store[key] - if exists && !now.After(it.expiresAt) { - result[key] = it.value - atomic.AddInt64(&m.hits, 1) - } else { - atomic.AddInt64(&m.misses, 1) + s := m.getShard(key) + shardKeys[s] = append(shardKeys[s], key) + } + + // Retrieve from each shard + for s, keys := range shardKeys { + s.mu.RLock() + for _, key := range keys { + it, exists := s.store[key] + if exists && !now.After(it.expiresAt) { + result[key] = it.value + atomic.AddInt64(&m.hits, 1) + } else { + atomic.AddInt64(&m.misses, 1) + } } + s.mu.RUnlock() } return result @@ -264,9 +321,12 @@ func (m *MemoryStore) GetMulti(keys []string) map[string][]byte { // GetMetrics returns the current statistics of the MemoryStore. // It returns a copy of the metrics to ensure thread safety. func (m *MemoryStore) GetMetrics() StoreMetrics { - m.mu.RLock() - itemCount := len(m.store) - m.mu.RUnlock() + itemCount := 0 + for _, s := range m.shards { + s.mu.RLock() + itemCount += len(s.store) + s.mu.RUnlock() + } return StoreMetrics{ Items: itemCount, From bd0a739b3a84f03217b9d8932023156e7c7f3683 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 23:41:26 +0000 Subject: [PATCH 3/3] Initial plan