diff --git a/cache.go b/cache.go index db88d2f..5224c2c 100644 --- a/cache.go +++ b/cache.go @@ -1052,6 +1052,35 @@ func (c *cache) Items() map[string]Item { return m } +// Copies all unexpired items from the cache into a new map, +// then deletes all items from the cache and returns the original map. This +// approach ensures that no items are lost due to race conditions that could +// occur if the operations of copying and deleting were performed separately. +// For example, a race condition might occur if an item is added to the cache +// between the calls to Items() and Flush(), resulting in data loss. By +// combining these operations within a single lock, we maintain data integrity +// during the cache cleanup process. +func (c *cache) GetItemsAndFlush() map[string]Item { + c.mu.Lock() + defer c.mu.Unlock() + + m := make(map[string]Item, len(c.items)) + now := time.Now().UnixNano() + for k, v := range c.items { + // "Inlining" of Expired + if v.Expiration > 0 { + if now > v.Expiration { + continue + } + } + m[k] = v + } + + c.items = map[string]Item{} + + return m +} + // Returns the number of items in the cache. This may include items that have // expired, but have not yet been cleaned up. func (c *cache) ItemCount() int { diff --git a/cache_test.go b/cache_test.go index de3e9d6..cd0ab24 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1769,3 +1769,68 @@ func TestGetWithExpiration(t *testing.T) { t.Error("expiration for e is in the past") } } + +func TestGetItemsAndFlush(t *testing.T) { + tc := New(DefaultExpiration, 0) + tc.Set("foo", "bar", DefaultExpiration) + tc.Set("baz", "yes", DefaultExpiration) + m := tc.GetItemsAndFlush() + + // Assert get items was executed + x, found := m["foo"] + if !found { + t.Error("foo was not found in the map, but it should be returned") + } + + if x.Expiration != int64(DefaultExpiration) { + t.Errorf("foo was found, but its expiration is %v, which is different than the setted one", x.Expiration) + } + + v, ok := x.Object.(string) + if !ok { + t.Error("foo was found, but its value can't be parsed to string") + } + + if v != "bar" { + t.Errorf("foo was found, but its actual value is %s, different than the original one", v) + } + + x, found = m["baz"] + if !found { + t.Error("baz was not found in the map, but it should be returned") + } + + if x.Expiration != int64(DefaultExpiration) { + t.Errorf("baz was found, but its expiration is %v, which is different than the setted one", x.Expiration) + } + + v, ok = x.Object.(string) + if !ok { + t.Error("baz was found, but its value can't be parsed to string") + } + + if v != "yes" { + t.Errorf("baz was found, but its actual value is %s, different than the original one", v) + } + + x, found = m["baz"] + if !found { + t.Error("baz was not found in the map, but it should be returned") + } + + // Assert flush was executed + y, found := tc.Get("foo") + if found { + t.Error("foo was found, but it should have been deleted") + } + if y != nil { + t.Error("x is not nil:", x) + } + y, found = tc.Get("baz") + if found { + t.Error("baz was found, but it should have been deleted") + } + if y != nil { + t.Error("x is not nil:", x) + } +}