A map using a lock-free concurrency using left-right algo, written in Go

Test wether weak pointers / cleanup works

+67 -1
+29 -1
lrmap.go
··· 20 20 redoLog []operation[K, V] 21 21 readHandlers map[weak.Pointer[ReadHandler[K, V]]]struct{} 22 22 readHandlerPool sync.Pool 23 + metrics *metrics 24 + } 25 + 26 + metrics struct { 27 + readHandlersCreated uint64 28 + readHandlersClosed uint64 29 + readHandlersRecycled uint64 30 + readHandlersCleanedUp uint64 23 31 } 24 32 25 33 arena[K comparable, V any] map[K]V ··· 38 46 39 47 return m 40 48 } 49 + 50 + // recordMetrics enables the collection of metrics for read handlers. This is not thread-safe, you must 51 + // call this before sharing the LRMap with other goroutines. 52 + func (m *LRMap[K, V]) recordMetrics() { m.metrics = new(metrics) } 41 53 42 54 func (m *LRMap[K, V]) Set(key K, value V) { 43 55 m.mu.Lock() ··· 120 132 rh := &ReadHandler[K, V]{lrmap: m} 121 133 122 134 wp := weak.Make(rh) 135 + rh.wp = wp 123 136 124 137 m.mu.Lock() 125 138 m.readHandlers[wp] = struct{}{} ··· 129 142 lrmap.mu.Lock() 130 143 defer lrmap.mu.Unlock() 131 144 145 + if lrmap.metrics != nil { 146 + atomic.AddUint64(&lrmap.metrics.readHandlersCleanedUp, 1) 147 + } 148 + 132 149 delete(lrmap.readHandlers, wp) 133 150 }, m) 134 151 135 - rh.wp = wp 152 + if m.metrics != nil { 153 + atomic.AddUint64(&m.metrics.readHandlersCreated, 1) 154 + } 155 + 136 156 return rh 137 157 } 138 158 ··· 278 298 func (rh *ReadHandler[K, V]) Close() { 279 299 rh.assertReady() 280 300 301 + if rh.lrmap.metrics != nil { 302 + atomic.AddUint64(&rh.lrmap.metrics.readHandlersClosed, 1) 303 + } 304 + 281 305 rh.ready = false 282 306 283 307 rh.lrmap.mu.Lock() ··· 293 317 294 318 if rh.entered() { 295 319 panic("reader illegal state: must call Leave() before recycling") 320 + } 321 + 322 + if rh.lrmap.metrics != nil { 323 + atomic.AddUint64(&rh.lrmap.metrics.readHandlersRecycled, 1) 296 324 } 297 325 298 326 rh.ready = false
+38
lrmap_test.go
··· 2 2 3 3 import ( 4 4 "math" 5 + "runtime" 6 + "sync/atomic" 5 7 "testing" 6 8 ) 7 9 ··· 128 130 t.Errorf("overflow(%d) is odd", overflow) 129 131 } 130 132 } 133 + 134 + func TestCleanup(t *testing.T) { 135 + const n = 1000 136 + 137 + lrmap := New[int, int]() 138 + lrmap.recordMetrics() 139 + 140 + readHandlers := make([]*ReadHandler[int, int], n) 141 + for i := 0; i < n; i++ { 142 + readHandlers[i] = lrmap.NewReadHandler() 143 + } 144 + 145 + runtime.GC() 146 + 147 + if created, cleanedUp := atomic.LoadUint64(&lrmap.metrics.readHandlersCreated), atomic.LoadUint64(&lrmap.metrics.readHandlersCleanedUp); created != n || cleanedUp != 0 { 148 + t.Errorf("created %d read handlers, cleaned up %d read handlers", created, cleanedUp) 149 + } 150 + 151 + if len(lrmap.readHandlers) != n { 152 + t.Errorf("expected %d read handlers, got %d", n, len(lrmap.readHandlers)) 153 + } 154 + 155 + for i := range readHandlers { 156 + readHandlers[i] = nil 157 + } 158 + 159 + runtime.GC() 160 + 161 + if created, cleanedUp := atomic.LoadUint64(&lrmap.metrics.readHandlersCreated), atomic.LoadUint64(&lrmap.metrics.readHandlersCleanedUp); created != n || cleanedUp != n { 162 + t.Errorf("created %d read handlers, cleaned up %d read handlers", created, cleanedUp) 163 + } 164 + 165 + if len(lrmap.readHandlers) != 0 { 166 + t.Errorf("expected no read handlers, got %d", len(lrmap.readHandlers)) 167 + } 168 + }