Skip to content

Commit eab6108

Browse files
authored
Fix race in LFU maintenance for add-remove-add of the same key (#533)
1 parent 9441fa9 commit eab6108

File tree

3 files changed

+41
-21
lines changed

3 files changed

+41
-21
lines changed

BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -376,14 +376,7 @@ private void VerifyLruInDictionary(LfuNodeList<K, V> lfuNodes, ITestOutputHelper
376376
node.WasRemoved.Should().BeFalse();
377377
node.WasDeleted.Should().BeFalse();
378378

379-
// This can occur if there is a race between:
380-
// Thread 1: TryRemove, delete node from dictionary, set WasRemoved flag
381-
// Thread 2: Check WasRemoved flag, if not add to lru
382-
// It's not clear how WasRemoved can be false in this situation.
383-
if (!cache.TryGet(node.Key, out _))
384-
{
385-
output.WriteLine($"Orphaned node with key {node.Key} detected.");
386-
}
379+
cache.TryGet(node.Key, out _).Should().BeTrue($"Orphaned node with key {node.Key} detected.");
387380

388381
node = node.Next;
389382
}

BitFaster.Caching/Lfu/ConcurrentLfuCore.cs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public void AddOrUpdate(K key, V value)
133133

134134
public void Clear()
135135
{
136-
this.Trim(this.Count);
136+
Trim(int.MaxValue);
137137

138138
lock (maintenanceLock)
139139
{
@@ -145,15 +145,16 @@ public void Clear()
145145

146146
public void Trim(int itemCount)
147147
{
148-
itemCount = Math.Min(itemCount, this.Count);
149-
var candidates = new List<LfuNode<K, V>>(itemCount);
150-
151-
// TODO: this is LRU order eviction, Caffeine is based on frequency
148+
List<LfuNode<K, V>> candidates;
152149
lock (maintenanceLock)
153150
{
154-
// flush all buffers
155151
Maintenance();
156152

153+
int lruCount = this.windowLru.Count + this.probationLru.Count + this.protectedLru.Count;
154+
itemCount = Math.Min(itemCount, lruCount);
155+
candidates = new (itemCount);
156+
157+
// Note: this is LRU order eviction, Caffeine is based on frequency
157158
// walk in lru order, get itemCount keys to evict
158159
TakeCandidatesInLruOrder(this.probationLru, candidates, itemCount);
159160
TakeCandidatesInLruOrder(this.protectedLru, candidates, itemCount);
@@ -608,12 +609,6 @@ private void OnWrite(LfuNode<K, V> node)
608609

609610
private void PromoteProbation(LfuNode<K, V> node)
610611
{
611-
if (node.list == null)
612-
{
613-
// Ignore stale accesses for an entry that is no longer present
614-
return;
615-
}
616-
617612
this.probationLru.Remove(node);
618613
this.protectedLru.AddLast(node);
619614
node.Position = Position.Protected;
@@ -699,6 +694,22 @@ private void EvictFromMain(LfuNode<K, V> candidateNode)
699694
break;
700695
}
701696

697+
if (candidate.node.WasRemoved)
698+
{
699+
var evictee = candidate.node;
700+
candidate.Next();
701+
Evict(evictee);
702+
continue;
703+
}
704+
705+
if (victim.node.WasRemoved)
706+
{
707+
var evictee = victim.node;
708+
victim.Next();
709+
Evict(evictee);
710+
continue;
711+
}
712+
702713
// Evict the entry with the lowest frequency
703714
if (candidate.freq > victim.freq)
704715
{
@@ -749,7 +760,17 @@ private bool AdmitCandidate(K candidateKey, K victimKey)
749760

750761
private void Evict(LfuNode<K, V> evictee)
751762
{
752-
this.dictionary.TryRemove(evictee.Key, out var _);
763+
evictee.WasRemoved = true;
764+
evictee.WasDeleted = true;
765+
766+
// This handles the case where the same key exists in the write buffer both
767+
// as added and removed. Remove via KVP ensures we don't remove added nodes.
768+
var kvp = new KeyValuePair<K, N>(evictee.Key, (N)evictee);
769+
#if NET6_0_OR_GREATER
770+
this.dictionary.TryRemove(kvp);
771+
#else
772+
((ICollection<KeyValuePair<K, N>>)this.dictionary).Remove(kvp);
773+
#endif
753774
evictee.list.Remove(evictee);
754775
Disposer<V>.Dispose(evictee.Value);
755776
this.metrics.evictedCount++;

BitFaster.Caching/Lfu/LfuNode.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,18 @@ public LfuNode(K k, V v)
2121

2222
public Position Position { get; set; }
2323

24+
/// <summary>
25+
/// Node was removed from the dictionary, but is still present in the LRU lists.
26+
/// </summary>
2427
public bool WasRemoved
2528
{
2629
get => this.wasRemoved;
2730
set => this.wasRemoved = value;
2831
}
2932

33+
/// <summary>
34+
/// Node has been removed both from the dictionary and the LRU lists.
35+
/// </summary>
3036
public bool WasDeleted
3137
{
3238
get => this.wasDeleted;

0 commit comments

Comments
 (0)