Skip to content

Commit 7a9ba6d

Browse files
authored
Fix race in LFU concurrent update and TryRemove(kvp) (#618)
1 parent e35411d commit 7a9ba6d

File tree

2 files changed

+52
-17
lines changed

2 files changed

+52
-17
lines changed

BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,32 @@ await Threaded.Run(threads, i =>
290290
RunIntegrityCheck(cache, this.output);
291291
}
292292

293+
[Fact]
294+
public async Task WhenConcurrentUpdateAndRemoveKvp()
295+
{
296+
var cache = new ConcurrentLfu<int, string>(1, 20, new BackgroundThreadScheduler(), EqualityComparer<int>.Default);
297+
TaskCompletionSource<int> tcs = new TaskCompletionSource<int> ();
298+
299+
var removal = Task.Run(() =>
300+
{
301+
while (!tcs.Task.IsCompleted)
302+
{
303+
cache.TryRemove(new KeyValuePair<int, string>(5, "x"));
304+
}
305+
});
306+
307+
for (var i = 0; i < 100_000; i++)
308+
{
309+
cache.AddOrUpdate(5, "a");
310+
cache.TryGet(5, out _).Should().BeTrue("key 'a' should not be deleted");
311+
cache.AddOrUpdate(5, "x");
312+
}
313+
314+
tcs.SetResult(int.MaxValue);
315+
316+
await removal;
317+
}
318+
293319
private ConcurrentLfu<int, string> CreateWithBackgroundScheduler()
294320
{
295321
var scheduler = new BackgroundThreadScheduler();

BitFaster.Caching/Lfu/ConcurrentLfuCore.cs

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -317,20 +317,23 @@ public bool TryRemove(KeyValuePair<K, V> item)
317317
{
318318
if (this.dictionary.TryGetValue(item.Key, out var node))
319319
{
320-
if (EqualityComparer<V>.Default.Equals(node.Value, item.Value))
321-
{
322-
var kvp = new KeyValuePair<K, N>(item.Key, node);
320+
lock (node)
321+
{
322+
if (EqualityComparer<V>.Default.Equals(node.Value, item.Value))
323+
{
324+
var kvp = new KeyValuePair<K, N>(item.Key, node);
323325

324326
#if NET6_0_OR_GREATER
325-
if (this.dictionary.TryRemove(kvp))
327+
if (this.dictionary.TryRemove(kvp))
326328
#else
327-
// https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
328-
if (((ICollection<KeyValuePair<K, N>>)this.dictionary).Remove(kvp))
329+
// https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
330+
if (((ICollection<KeyValuePair<K, N>>)this.dictionary).Remove(kvp))
329331
#endif
330-
{
331-
node.WasRemoved = true;
332-
AfterWrite(node);
333-
return true;
332+
{
333+
node.WasRemoved = true;
334+
AfterWrite(node);
335+
return true;
336+
}
334337
}
335338
}
336339
}
@@ -361,14 +364,20 @@ public bool TryUpdate(K key, V value)
361364
{
362365
if (this.dictionary.TryGetValue(key, out var node))
363366
{
364-
node.Value = value;
367+
lock (node)
368+
{
369+
if (!node.WasRemoved)
370+
{
371+
node.Value = value;
365372

366-
// It's ok for this to be lossy, since the node is already tracked
367-
// and we will just lose ordering/hit count, but not orphan the node.
368-
this.writeBuffer.TryAdd(node);
369-
TryScheduleDrain();
370-
this.policy.OnWrite(node);
371-
return true;
373+
// It's ok for this to be lossy, since the node is already tracked
374+
// and we will just lose ordering/hit count, but not orphan the node.
375+
this.writeBuffer.TryAdd(node);
376+
TryScheduleDrain();
377+
this.policy.OnWrite(node);
378+
return true;
379+
}
380+
}
372381
}
373382

374383
return false;

0 commit comments

Comments
 (0)