@@ -29,6 +29,8 @@ type LRU[K comparable, V any] struct {
2929 buckets []bucket [K , V ]
3030 // uint8 because it's number between 0 and numBuckets
3131 nextCleanupBucket uint8
32+
33+ cleanupStopped bool
3234}
3335
3436// bucket is a container for holding entries to be expired
@@ -60,12 +62,13 @@ func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V], ttl time
6062 }
6163
6264 res := LRU [K , V ]{
63- ttl : ttl ,
64- size : size ,
65- evictList : internal .NewList [K , V ](),
66- items : make (map [K ]* internal.Entry [K , V ]),
67- onEvict : onEvict ,
68- done : make (chan struct {}),
65+ ttl : ttl ,
66+ size : size ,
67+ evictList : internal .NewList [K , V ](),
68+ items : make (map [K ]* internal.Entry [K , V ]),
69+ onEvict : onEvict ,
70+ done : make (chan struct {}),
71+ cleanupStopped : false ,
6972 }
7073
7174 // initialize the buckets
@@ -74,24 +77,7 @@ func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V], ttl time
7477 res .buckets [i ] = bucket [K , V ]{entries : make (map [K ]* internal.Entry [K , V ])}
7578 }
7679
77- // enable deleteExpired() running in separate goroutine for cache with non-zero TTL
78- //
79- // Important: done channel is never closed, so deleteExpired() goroutine will never exit,
80- // it's decided to add functionality to close it in the version later than v2.
81- if res .ttl != noEvictionTTL {
82- go func (done <- chan struct {}) {
83- ticker := time .NewTicker (res .ttl / numBuckets )
84- defer ticker .Stop ()
85- for {
86- select {
87- case <- done :
88- return
89- case <- ticker .C :
90- res .deleteExpired ()
91- }
92- }
93- }(res .done )
94- }
80+ res .startGoroutine ()
9581 return & res
9682}
9783
@@ -152,7 +138,7 @@ func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
152138 var ent * internal.Entry [K , V ]
153139 if ent , ok = c .items [key ]; ok {
154140 // Expired item check
155- if time .Now ().After (ent .ExpiresAt ) {
141+ if ! c . cleanupStopped && time .Now ().After (ent .ExpiresAt ) {
156142 return value , false
157143 }
158144 c .evictList .MoveToFront (ent )
@@ -178,7 +164,7 @@ func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
178164 var ent * internal.Entry [K , V ]
179165 if ent , ok = c .items [key ]; ok {
180166 // Expired item check
181- if time .Now ().After (ent .ExpiresAt ) {
167+ if ! c . cleanupStopped && time .Now ().After (ent .ExpiresAt ) {
182168 return value , false
183169 }
184170 return ent .Value , true
@@ -227,7 +213,7 @@ func (c *LRU[K, V]) Keys() []K {
227213 keys := make ([]K , 0 , len (c .items ))
228214 now := time .Now ()
229215 for ent := c .evictList .Back (); ent != nil ; ent = ent .PrevEntry () {
230- if now .After (ent .ExpiresAt ) {
216+ if ! c . cleanupStopped && now .After (ent .ExpiresAt ) {
231217 continue
232218 }
233219 keys = append (keys , ent .Key )
@@ -243,7 +229,7 @@ func (c *LRU[K, V]) Values() []V {
243229 values := make ([]V , 0 , len (c .items ))
244230 now := time .Now ()
245231 for ent := c .evictList .Back (); ent != nil ; ent = ent .PrevEntry () {
246- if now .After (ent .ExpiresAt ) {
232+ if ! c . cleanupStopped && now .After (ent .ExpiresAt ) {
247233 continue
248234 }
249235 values = append (values , ent .Value )
@@ -278,16 +264,17 @@ func (c *LRU[K, V]) Resize(size int) (evicted int) {
278264}
279265
280266// Close destroys cleanup goroutine. To clean up the cache, run Purge() before Close().
281- // func (c *LRU[K, V]) Close() {
282- // c.mu.Lock()
283- // defer c.mu.Unlock()
284- // select {
285- // case <-c.done:
286- // return
287- // default:
288- // }
289- // close(c.done)
290- // }
267+ func (c * LRU [K , V ]) Close () {
268+ c .mu .Lock ()
269+ defer c .mu .Unlock ()
270+ select {
271+ case <- c .done :
272+ return
273+ default :
274+ }
275+ close (c .done )
276+ c .cleanupStopped = true
277+ }
291278
292279// removeOldest removes the oldest item from the cache. Has to be called with lock!
293280func (c * LRU [K , V ]) removeOldest () {
@@ -310,13 +297,29 @@ func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
310297// in it to expire first.
311298func (c * LRU [K , V ]) deleteExpired () {
312299 c .mu .Lock ()
300+
301+ // grab done channel to detect Closes
302+ done := c .done
303+
313304 bucketIdx := c .nextCleanupBucket
314305 timeToExpire := time .Until (c .buckets [bucketIdx ].newestEntry )
315306 // wait for newest entry to expire before cleanup without holding lock
316307 if timeToExpire > 0 {
317308 c .mu .Unlock ()
318- time .Sleep (timeToExpire )
309+ select {
310+ case <- time .After (timeToExpire ):
311+ case <- done :
312+ return
313+ }
319314 c .mu .Lock ()
315+
316+ select {
317+ case <- done :
318+ // Done channel closed while sleeping, return without deleting entries
319+ c .mu .Unlock ()
320+ return
321+ default :
322+ }
320323 }
321324 for _ , ent := range c .buckets [bucketIdx ].entries {
322325 c .removeElement (ent )
@@ -343,4 +346,41 @@ func (c *LRU[K, V]) removeFromBucket(e *internal.Entry[K, V]) {
343346// Cap returns the capacity of the cache
344347func (c * LRU [K , V ]) Cap () int {
345348 return c .size
346- }
349+ }
350+
351+ // Restart recreates the cleanup goroutine after Close() has been called.
352+ func (c * LRU [K , V ]) Restart () {
353+ c .mu .Lock ()
354+ defer c .mu .Unlock ()
355+
356+ // Check if the goroutine is already running
357+ select {
358+ case <- c .done :
359+ // Channel is closed, need to recreate it
360+ c .done = make (chan struct {})
361+ c .cleanupStopped = false
362+ c .startGoroutine ()
363+ default :
364+ // Goroutine is already running, nothing to do
365+ }
366+ }
367+
368+ // startGoroutine starts the cleanup goroutine for expired entries.
369+ func (c * LRU [K , V ]) startGoroutine () {
370+ if c .ttl == noEvictionTTL {
371+ return
372+ }
373+
374+ go func (done <- chan struct {}) {
375+ ticker := time .NewTicker (c .ttl / numBuckets )
376+ defer ticker .Stop ()
377+ for {
378+ select {
379+ case <- done :
380+ return
381+ case <- ticker .C :
382+ c .deleteExpired ()
383+ }
384+ }
385+ }(c .done )
386+ }
0 commit comments