@@ -72,7 +72,10 @@ type LRU[K comparable, V comparable] struct {
7272 hash HashKeyCallback [K ]
7373 healthCheck HealthCheckCallback [K , V ]
7474 lifetime time.Duration
75- metrics Metrics
75+ // nextExpire is a conservative lower bound. Refreshing or removing the
76+ // earliest entry may leave it stale, which only causes one extra scan.
77+ nextExpire int64
78+ metrics Metrics
7679
7780 // used for element clearing after removal or expiration
7881 emptyKey K
@@ -241,6 +244,9 @@ func (lru *LRU[K, V]) evict(pos uint32) {
241244 lru .unlinkElement (pos )
242245 lru .unlinkBucket (pos )
243246 lru .len --
247+ if lru .len == 0 {
248+ lru .nextExpire = 0
249+ }
244250
245251 if lru .onEvict != nil {
246252 // Save k/v for the eviction function.
@@ -279,11 +285,12 @@ func (lru *LRU[K, V]) move(to, from uint32) {
279285
280286// insert stores the k/v at pos.
281287// It updates the head to point to this position.
282- func (lru * LRU [K , V ]) insert (pos uint32 , key K , value V , lifetime time.Duration ) {
288+ func (lru * LRU [K , V ]) insert (pos uint32 , key K , value V , lifetime time.Duration , currentTime int64 ) {
283289 lru .elements [pos ].key = key
284290 lru .elements [pos ].value = value
285- lru .elements [pos ].expire = expire ( lifetime )
291+ lru .elements [pos ].expire = expireAt ( currentTime , lifetime )
286292 lru .elements [pos ].lifetime = lifetime
293+ lru .noteExpire (lru .elements [pos ].expire )
287294
288295 if lru .len == 0 {
289296 lru .elements [pos ].prev = pos
@@ -304,7 +311,37 @@ func expire(lifetime time.Duration) int64 {
304311 if lifetime == 0 {
305312 return 0
306313 }
307- return now () + lifetime .Milliseconds ()
314+ return expireAt (now (), lifetime )
315+ }
316+
317+ func expireAt (currentTime int64 , lifetime time.Duration ) int64 {
318+ if lifetime == 0 {
319+ return 0
320+ }
321+ return currentTime + lifetime .Milliseconds ()
322+ }
323+
324+ func (lru * LRU [K , V ]) noteExpire (expireTime int64 ) {
325+ if expireTime != 0 && (lru .nextExpire == 0 || expireTime < lru .nextExpire ) {
326+ lru .nextExpire = expireTime
327+ }
328+ }
329+
330+ func (lru * LRU [K , V ]) refreshLifetimeAt (pos uint32 , currentTime int64 ) {
331+ oldExpire := lru .elements [pos ].expire
332+ newExpire := expireAt (currentTime , lru .elements [pos ].lifetime )
333+ lru .elements [pos ].expire = newExpire
334+ if oldExpire == 0 || newExpire < oldExpire {
335+ lru .noteExpire (newExpire )
336+ }
337+ }
338+
339+ func (lru * LRU [K , V ]) refreshLifetime (pos uint32 ) {
340+ if lru .elements [pos ].lifetime == 0 {
341+ lru .elements [pos ].expire = 0
342+ return
343+ }
344+ lru .refreshLifetimeAt (pos , now ())
308345}
309346
310347// clearKeyAndValue clears stale data to avoid memory leaks
@@ -374,6 +411,16 @@ func (lru *LRU[K, V]) AddWithLifetime(key K, value V, lifetime time.Duration) (e
374411
375412func (lru * LRU [K , V ]) addWithLifetime (hash uint32 , key K , value V ,
376413 lifetime time.Duration ,
414+ ) (evicted bool ) {
415+ var currentTime int64
416+ if lifetime != 0 {
417+ currentTime = now ()
418+ }
419+ return lru .addWithLifetimeAt (hash , key , value , lifetime , currentTime )
420+ }
421+
422+ func (lru * LRU [K , V ]) addWithLifetimeAt (hash uint32 , key K , value V ,
423+ lifetime time.Duration , currentTime int64 ,
377424) (evicted bool ) {
378425 bucketPos , startPos := lru .hashToPos (hash )
379426 if startPos == emptyBucket {
@@ -394,7 +441,7 @@ func (lru *LRU[K, V]) addWithLifetime(hash uint32, key K, value V,
394441
395442 lru .elements [pos ].nextBucket = pos
396443 lru .elements [pos ].prevBucket = pos
397- lru .insert (pos , key , value , lifetime )
444+ lru .insert (pos , key , value , lifetime , currentTime )
398445 return evicted
399446 }
400447
@@ -404,8 +451,9 @@ func (lru *LRU[K, V]) addWithLifetime(hash uint32, key K, value V,
404451 if lru .elements [pos ].key == key {
405452 // Key exists, replace the value and update element to be the head element.
406453 lru .elements [pos ].value = value
407- lru .elements [pos ].expire = expire ( lifetime )
454+ lru .elements [pos ].expire = expireAt ( currentTime , lifetime )
408455 lru .elements [pos ].lifetime = lifetime
456+ lru .noteExpire (lru .elements [pos ].expire )
409457
410458 if pos != lru .head {
411459 lru .unlinkElement (pos )
@@ -445,7 +493,7 @@ func (lru *LRU[K, V]) addWithLifetime(hash uint32, key K, value V,
445493 lru .elements [pos ].prevBucket = lru .elements [startPos ].prevBucket
446494 lru .elements [lru .elements [startPos ].prevBucket ].nextBucket = pos
447495 lru .elements [startPos ].prevBucket = pos
448- lru .insert (pos , key , value , lifetime )
496+ lru .insert (pos , key , value , lifetime , currentTime )
449497
450498 if lru .elements [pos ].prevBucket != pos {
451499 // The bucket now contains more than 1 element.
@@ -537,7 +585,7 @@ func (lru *LRU[K, V]) getAndRefresh(hash uint32, key K) (value V, ok bool) {
537585 lru .setHead (pos )
538586 }
539587 lru .metrics .Hits ++
540- lru .elements [ pos ]. expire = expire ( lru . elements [ pos ]. lifetime )
588+ lru .refreshLifetime ( pos )
541589 return lru .elements [pos ].value , ok
542590 }
543591
@@ -546,30 +594,29 @@ func (lru *LRU[K, V]) getAndRefresh(hash uint32, key K) (value V, ok bool) {
546594}
547595
548596func (lru * LRU [K , V ]) GetAndRefreshOrAdd (key K , constructor func () (V , bool )) (V , bool , bool ) {
549- value , updated , ok := lru .getAndRefreshOrAdd (lru .hash (key ), key , constructor )
550- if ! updated && ok {
551- lru .PurgeExpired ()
552- }
597+ value , updated , ok , _ := lru .getAndRefreshOrAdd (lru .hash (key ), key , constructor )
553598 return value , updated , ok
554599}
555600
556- func (lru * LRU [K , V ]) getAndRefreshOrAdd (hash uint32 , key K , constructor func () (V , bool )) (value V , updated bool , ok bool ) {
601+ func (lru * LRU [K , V ]) getAndRefreshOrAdd (hash uint32 , key K , constructor func () (V , bool )) (value V , updated bool , ok bool , currentTime int64 ) {
557602 if pos , ok := lru .findKeyNoExpire (hash , key ); ok {
558603 if pos != lru .head {
559604 lru .unlinkElement (pos )
560605 lru .setHead (pos )
561606 }
562607 lru .metrics .Hits ++
563- lru .elements [ pos ]. expire = expire ( lru . elements [ pos ]. lifetime )
564- return lru .elements [pos ].value , true , true
608+ lru .refreshLifetime ( pos )
609+ return lru .elements [pos ].value , true , true , 0
565610 }
566611 lru .metrics .Misses ++
567612 value , ok = constructor ()
568613 if ! ok {
569614 return
570615 }
571- lru .addWithLifetime (hash , key , value , lru .lifetime )
572- return value , false , true
616+ currentTime = now ()
617+ lru .purgeExpiredAt (currentTime )
618+ lru .addWithLifetimeAt (hash , key , value , lru .lifetime , currentTime )
619+ return value , false , true , currentTime
573620}
574621
575622// Peek looks up a key's value from the cache, without changing its recent-ness.
@@ -616,6 +663,7 @@ func (lru *LRU[K, V]) updateLifetime(hash uint32, key K, value V, lifetime time.
616663
617664 lru .elements [pos ].lifetime = lifetime
618665 lru .elements [pos ].expire = expire (lifetime )
666+ lru .noteExpire (lru .elements [pos ].expire )
619667
620668 if pos != lru .head {
621669 lru .unlinkElement (pos )
@@ -707,26 +755,33 @@ func (lru *LRU[K, V]) Purge() {
707755 _ , _ , _ = lru .RemoveOldest ()
708756 }
709757
758+ lru .nextExpire = 0
710759 lru .metrics = Metrics {}
711760}
712761
713762// PurgeExpired purges all expired items from the LRU.
714763// The evict function is called for each expired item.
715764func (lru * LRU [K , V ]) PurgeExpired () {
716- n := now ()
717- loop:
718- l := lru .len
719- if l == 0 {
765+ lru .purgeExpiredAt (now ())
766+ }
767+
768+ func (lru * LRU [K , V ]) purgeExpiredAt (currentTime int64 ) {
769+ if lru .nextExpire == 0 || lru .nextExpire > currentTime {
720770 return
721771 }
722- pos := lru .elements [lru .head ].next
723- for i := uint32 (0 ); i < l ; i ++ {
724- if lru .elements [pos ].expire != 0 && lru .elements [pos ].expire <= n {
772+ nextExpire := int64 (0 )
773+ for pos := uint32 (0 ); pos < lru .len ; {
774+ expireTime := lru .elements [pos ].expire
775+ if expireTime != 0 && expireTime <= currentTime {
725776 lru .removeAt (pos )
726- goto loop
777+ continue
727778 }
728- pos = lru .elements [pos ].next
779+ if expireTime != 0 && (nextExpire == 0 || expireTime < nextExpire ) {
780+ nextExpire = expireTime
781+ }
782+ pos ++
729783 }
784+ lru .nextExpire = nextExpire
730785}
731786
732787// Metrics returns the metrics of the cache.
0 commit comments