mirror of
https://github.com/thecodingrobot/echoip.git
synced 2026-05-15 11:07:07 +02:00
fix(cache): make cache truly LRU and resize correctly
Get() never promoted accessed entries, so eviction was insertion order (FIFO), not LRU. Get() now MoveToBacks the element; cache mu becomes sync.Mutex. Resize() updated capacity but never evicted to fit the new size, and incorrectly reset the evictions counter. It now evicts from the front until len(entries) <= capacity and preserves the counter. Also cleaned up the Set() eviction loop and added tests for LRU promotion, immediate-shrink resize, and concurrent access.
This commit is contained in:
parent
27957c928f
commit
29746d5c99
2 changed files with 103 additions and 15 deletions
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
type Cache struct {
|
||||
capacity int
|
||||
mu sync.RWMutex
|
||||
mu sync.Mutex
|
||||
entries map[uint64]*list.Element
|
||||
values *list.List
|
||||
evictions uint64
|
||||
|
|
@ -49,12 +49,13 @@ func (c *Cache) Set(ip netip.Addr, resp Response) {
|
|||
minEvictions := len(c.entries) - c.capacity + 1
|
||||
if minEvictions > 0 { // At or above capacity. Shrink the cache
|
||||
evicted := 0
|
||||
for el := c.values.Front(); el != nil && evicted < minEvictions; {
|
||||
value := el.Value.(Response)
|
||||
delete(c.entries, key(value.IP))
|
||||
next := el.Next()
|
||||
for evicted < minEvictions {
|
||||
el := c.values.Front()
|
||||
if el == nil {
|
||||
break
|
||||
}
|
||||
delete(c.entries, key(el.Value.(Response).IP))
|
||||
c.values.Remove(el)
|
||||
el = next
|
||||
evicted++
|
||||
}
|
||||
c.evictions += uint64(evicted)
|
||||
|
|
@ -68,29 +69,38 @@ func (c *Cache) Set(ip netip.Addr, resp Response) {
|
|||
|
||||
func (c *Cache) Get(ip netip.Addr) (Response, bool) {
|
||||
k := key(ip)
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
r, ok := c.entries[k]
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
el, ok := c.entries[k]
|
||||
if !ok {
|
||||
return Response{}, false
|
||||
}
|
||||
return r.Value.(Response), true
|
||||
c.values.MoveToBack(el)
|
||||
return el.Value.(Response), true
|
||||
}
|
||||
|
||||
func (c *Cache) Resize(capacity int) error {
|
||||
if capacity < 0 {
|
||||
return fmt.Errorf("invalid capacity: %d\n", capacity)
|
||||
return fmt.Errorf("invalid capacity: %d", capacity)
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.capacity = capacity
|
||||
c.evictions = 0
|
||||
for len(c.entries) > c.capacity {
|
||||
el := c.values.Front()
|
||||
if el == nil {
|
||||
break
|
||||
}
|
||||
delete(c.entries, key(el.Value.(Response).IP))
|
||||
c.values.Remove(el)
|
||||
c.evictions++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) Stats() CacheStats {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return CacheStats{
|
||||
Size: len(c.entries),
|
||||
Capacity: c.capacity,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package http
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -76,7 +78,8 @@ func TestCacheResize(t *testing.T) {
|
|||
if err := c.Resize(5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := c.evictions, uint64(0); got != want {
|
||||
// Resize should evict to fit and accumulate (not reset) the evictions counter.
|
||||
if got, want := c.evictions, uint64(15); got != want {
|
||||
t.Errorf("want %d evictions, got %d", want, got)
|
||||
}
|
||||
ip, _ := netip.ParseAddr("192.0.2.42")
|
||||
|
|
@ -86,3 +89,78 @@ func TestCacheResize(t *testing.T) {
|
|||
t.Errorf("want %d entries, got %d", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheLRUPromotion(t *testing.T) {
|
||||
c := NewCache(2)
|
||||
ip1, _ := netip.ParseAddr("192.0.2.1")
|
||||
ip2, _ := netip.ParseAddr("192.0.2.2")
|
||||
ip3, _ := netip.ParseAddr("192.0.2.3")
|
||||
c.Set(ip1, Response{IP: ip1})
|
||||
c.Set(ip2, Response{IP: ip2})
|
||||
// Access ip1 — should promote it to most-recently-used.
|
||||
if _, ok := c.Get(ip1); !ok {
|
||||
t.Fatalf("expected Get(ip1) to hit")
|
||||
}
|
||||
// Inserting ip3 should evict the least-recently-used: ip2.
|
||||
c.Set(ip3, Response{IP: ip3})
|
||||
if _, ok := c.Get(ip1); !ok {
|
||||
t.Errorf("ip1 was evicted; expected ip1 to remain after promotion")
|
||||
}
|
||||
if _, ok := c.Get(ip2); ok {
|
||||
t.Errorf("ip2 still present; expected ip2 to be evicted as LRU")
|
||||
}
|
||||
if _, ok := c.Get(ip3); !ok {
|
||||
t.Errorf("ip3 missing; expected ip3 to be present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheResizeShrink(t *testing.T) {
|
||||
c := NewCache(10)
|
||||
for i := 0; i < 10; i++ {
|
||||
ip, _ := netip.ParseAddr(fmt.Sprintf("192.0.2.%d", i))
|
||||
c.Set(ip, Response{IP: ip})
|
||||
}
|
||||
if got, want := len(c.entries), 10; got != want {
|
||||
t.Fatalf("pre-resize len(entries) = %d, want %d", got, want)
|
||||
}
|
||||
preEvictions := c.evictions
|
||||
if err := c.Resize(5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Shrink must take effect immediately, not on next Set.
|
||||
if got, want := len(c.entries), 5; got != want {
|
||||
t.Errorf("post-resize len(entries) = %d, want %d", got, want)
|
||||
}
|
||||
if got, want := c.values.Len(), 5; got != want {
|
||||
t.Errorf("post-resize values.Len = %d, want %d", got, want)
|
||||
}
|
||||
// Evictions counter must increment by 5, not reset.
|
||||
if got, want := c.evictions, preEvictions+5; got != want {
|
||||
t.Errorf("post-resize evictions = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheConcurrent(t *testing.T) {
|
||||
c := NewCache(100)
|
||||
var wg sync.WaitGroup
|
||||
const goroutines = 1000
|
||||
wg.Add(goroutines)
|
||||
for i := 0; i < goroutines; i++ {
|
||||
go func(seed int64) {
|
||||
defer wg.Done()
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
for j := 0; j < 50; j++ {
|
||||
ip, _ := netip.ParseAddr(fmt.Sprintf("192.0.2.%d", r.Intn(256)))
|
||||
if r.Intn(2) == 0 {
|
||||
c.Set(ip, Response{IP: ip})
|
||||
} else {
|
||||
c.Get(ip)
|
||||
}
|
||||
}
|
||||
}(int64(i))
|
||||
}
|
||||
wg.Wait()
|
||||
if got := len(c.entries); got > 100 {
|
||||
t.Errorf("len(entries) = %d, exceeds capacity 100", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue