From b290957fc9538cf013d6d5366733c37a0fec4869 Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Wed, 2 Sep 2026 09:34:08 +0200 Subject: [PATCH] 05-lru-cache: implement mutex-guarded LRU over container/list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit map[string]*list.Element + doubly linked list; Get/Put move hits to the front, Put evicts from the back when at capacity, and every method (including Len) takes the mutex. Add TestLenRacesWithPut, which calls Len concurrently with a writer — the existing test only read Len after wg.Wait(), so an unsynchronized Len would never trip the race detector there. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012MpK9niZm4jcs3mAJbxMfi --- 05-lru-cache/main.go | 55 +++++++++++++++++++++++++++++++++++---- 05-lru-cache/main_test.go | 40 ++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/05-lru-cache/main.go b/05-lru-cache/main.go index 9a0fa06..3ee57f4 100644 --- a/05-lru-cache/main.go +++ b/05-lru-cache/main.go @@ -16,43 +16,88 @@ // Run the tests: go test -race ./05-lru-cache/ package main -import "fmt" +import ( + "container/list" + "fmt" + "sync" +) +type entry struct { + key string + val any +} type LRU struct { // TODO: fields (mutex, capacity, map[string]*list.Element, *list.List) // Remember: the list element must store the KEY too, or you can't // delete the map entry on eviction. + mu sync.Mutex + capacity int + lruList *list.List + kvMap map[string]*list.Element } // NewLRU creates an LRU cache with the given capacity (capacity >= 1). // TODO: implement me. func NewLRU(capacity int) *LRU { - return &LRU{} + return &LRU{capacity: capacity, lruList: list.New(), kvMap: make(map[string]*list.Element)} } // Get returns the value and whether it was present. Hit = mark MRU. // TODO: implement me. func (c *LRU) Get(key string) (any, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + if el, ok := c.kvMap[key]; ok { + c.lruList.MoveToFront(el) + return el.Value.(*entry).val, true + } + return nil, false } // Put inserts/updates a value, evicting the LRU entry if over capacity. // TODO: implement me. func (c *LRU) Put(key string, val any) { + // lock mutex + c.mu.Lock() + defer c.mu.Unlock() + // if key exists, update value and move it to the front of the list and return/quit + if el, ok := c.kvMap[key]; ok { + c.lruList.MoveToFront(el) + el.Value.(*entry).val = val + return + } + + // if len > capacity, remove the last element from both the list and the map + if c.lruList.Len() >= c.capacity { + // remove last element from list + lastEl := c.lruList.Back() + c.lruList.Remove(lastEl) + // remove it by key from map + delete(c.kvMap, lastEl.Value.(*entry).key) + } + + // if key doesn't exist, insert' + el := c.lruList.PushFront(&entry{key: key, val: val}) + c.kvMap[key] = el + } // Len returns the current number of entries. // TODO: implement me. func (c *LRU) Len() int { - return 0 + c.mu.Lock() + defer c.mu.Unlock() + return c.lruList.Len() } func main() { c := NewLRU(2) c.Put("a", 1) c.Put("b", 2) - c.Get("a") // a is now MRU - c.Put("c", 3) // evicts b + c.Get("a") // a is now MRU + c.Put("c", 3) // evicts b _, ok := c.Get("b") fmt.Println("b present:", ok) // false } diff --git a/05-lru-cache/main_test.go b/05-lru-cache/main_test.go index fbbe633..bfa9758 100644 --- a/05-lru-cache/main_test.go +++ b/05-lru-cache/main_test.go @@ -4,6 +4,7 @@ import ( "fmt" "sync" "testing" + "time" ) func TestBasicPutGet(t *testing.T) { @@ -82,3 +83,42 @@ func TestConcurrentAccess(t *testing.T) { t.Fatalf("capacity exceeded under concurrency: %d", c.Len()) } } + +// TestLenRacesWithPut calls Len concurrently WITH writers, not after them. +// TestConcurrentAccess only reads Len after wg.Wait(), which establishes +// happens-before with every writer — so an unsynchronized Len can't race +// there. The race detector reports only races that actually occur. +func TestLenRacesWithPut(t *testing.T) { + c := NewLRU(64) + done := make(chan struct{}) + var wg sync.WaitGroup + + wg.Add(1) + go func() { // writer + defer wg.Done() + for i := 0; ; i++ { + select { + case <-done: + return + default: + c.Put(fmt.Sprintf("k%d", i%128), i) + } + } + }() + + wg.Add(1) + go func() { // reader, concurrently + defer wg.Done() + for i := 0; i < 10000; i++ { + if n := c.Len(); n > 64 { + t.Errorf("Len observed %d > capacity 64 (mid-Put state leaked)", n) + return + } + } + }() + + go func() { wg.Wait() }() + time.Sleep(50 * time.Millisecond) + close(done) + wg.Wait() +}