Skip to content

Commit 175e9d5

Browse files
authored
Fix LRU clear when warm/hot items are touched (#453)
* Fix LRU clear when warm/hot items are touched * add trim test * special case * cleanup ---------
1 parent 5275b07 commit 175e9d5

File tree

2 files changed

+114
-21
lines changed

2 files changed

+114
-21
lines changed

BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using Xunit.Abstractions;
1010
using System.Collections.Concurrent;
1111
using System.Reflection;
12-
using System.Runtime.CompilerServices;
1312

1413
namespace BitFaster.Caching.UnitTests.Lru
1514
{
@@ -875,6 +874,67 @@ public void WhenItemsExistClearRemovesAllItems()
875874
lru.ColdCount.Should().Be(0);
876875
}
877876

877+
// This is a special case:
878+
// Cycle 1: hot => warm
879+
// Cycle 2: warm => warm
880+
// Cycle 3: warm => cold
881+
// Cycle 4: cold => remove
882+
// Cycle 5: cold => remove
883+
[Fact]
884+
public void WhenCacheIsSize3ItemsExistAndItemsAccessedClearRemovesAllItems()
885+
{
886+
lru = new ConcurrentLru<int, string>(3);
887+
888+
lru.AddOrUpdate(1, "1");
889+
lru.AddOrUpdate(2, "1");
890+
891+
lru.TryGet(1, out _);
892+
lru.TryGet(2, out _);
893+
894+
lru.Clear();
895+
896+
lru.Count.Should().Be(0);
897+
}
898+
899+
[Theory]
900+
[InlineData(1)]
901+
[InlineData(2)]
902+
[InlineData(3)]
903+
[InlineData(4)]
904+
[InlineData(5)]
905+
[InlineData(6)]
906+
[InlineData(7)]
907+
[InlineData(8)]
908+
[InlineData(9)]
909+
[InlineData(10)]
910+
public void WhenItemsExistAndItemsAccessedClearRemovesAllItems(int itemCount)
911+
{
912+
// By default capacity is 9. Test all possible states of touched items
913+
// in the cache.
914+
915+
for (int i = 0; i < itemCount; i++)
916+
{
917+
lru.AddOrUpdate(i, "1");
918+
}
919+
920+
// touch n items
921+
for (int i = 0; i < itemCount; i++)
922+
{
923+
lru.TryGet(i, out _);
924+
}
925+
926+
lru.Clear();
927+
928+
this.testOutputHelper.WriteLine("LRU " + string.Join(" ", lru.Keys));
929+
930+
lru.Count.Should().Be(0);
931+
932+
// verify queues are purged
933+
lru.HotCount.Should().Be(0);
934+
lru.WarmCount.Should().Be(0);
935+
lru.ColdCount.Should().Be(0);
936+
}
937+
878938
[Fact]
879939
public void WhenWarmThenClearedIsWarmIsReset()
880940
{
@@ -1079,6 +1139,45 @@ public void WhenColdItemsAreTouchedTrimRemovesExpectedItemCount(int trimCount, i
10791139
this.testOutputHelper.WriteLine("exp " + string.Join(" ", expected));
10801140

10811141
lru.Keys.Should().BeEquivalentTo(expected);
1142+
}
1143+
1144+
[Theory]
1145+
[InlineData(1)]
1146+
[InlineData(2)]
1147+
[InlineData(3)]
1148+
[InlineData(4)]
1149+
[InlineData(5)]
1150+
[InlineData(6)]
1151+
[InlineData(7)]
1152+
[InlineData(8)]
1153+
[InlineData(9)]
1154+
[InlineData(10)]
1155+
public void WhenItemsExistAndItemsAccessedTrimRemovesAllItems(int itemCount)
1156+
{
1157+
// By default capacity is 9. Test all possible states of touched items
1158+
// in the cache.
1159+
1160+
for (int i = 0; i < itemCount; i++)
1161+
{
1162+
lru.AddOrUpdate(i, "1");
1163+
}
1164+
1165+
// touch n items
1166+
for (int i = 0; i < itemCount; i++)
1167+
{
1168+
lru.TryGet(i, out _);
1169+
}
1170+
1171+
lru.Trim(Math.Min(itemCount, lru.Capacity));
1172+
1173+
this.testOutputHelper.WriteLine("LRU " + string.Join(" ", lru.Keys));
1174+
1175+
lru.Count.Should().Be(0);
1176+
1177+
// verify queues are purged
1178+
lru.HotCount.Should().Be(0);
1179+
lru.WarmCount.Should().Be(0);
1180+
lru.ColdCount.Should().Be(0);
10821181
}
10831182

10841183
[Fact]

BitFaster.Caching/Lru/ConcurrentLruCore.cs

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -422,15 +422,7 @@ public void AddOrUpdate(K key, V value)
422422
///<inheritdoc/>
423423
public void Clear()
424424
{
425-
int count = this.Count;
426-
427-
for (int i = 0; i < count; i++)
428-
{
429-
CycleHotUnchecked(ItemRemovedReason.Cleared);
430-
CycleWarmUnchecked(ItemRemovedReason.Cleared);
431-
TryRemoveCold(ItemRemovedReason.Cleared);
432-
}
433-
425+
this.TrimLiveItems(itemsRemoved: 0, this.Count, ItemRemovedReason.Cleared);
434426
Volatile.Write(ref this.isWarm, false);
435427
}
436428

@@ -458,7 +450,7 @@ public void Trim(int itemCount)
458450
// first scan each queue for discardable items and remove them immediately. Note this can remove > itemCount items.
459451
int itemsRemoved = this.itemPolicy.CanDiscard() ? TrimAllDiscardedItems() : 0;
460452

461-
TrimLiveItems(itemsRemoved, itemCount);
453+
TrimLiveItems(itemsRemoved, itemCount, ItemRemovedReason.Trimmed);
462454
}
463455

464456
private void TrimExpired()
@@ -507,42 +499,44 @@ void RemoveDiscardableItems(ConcurrentQueue<I> q, ref int queueCounter)
507499
return itemsRemoved;
508500
}
509501

510-
private void TrimLiveItems(int itemsRemoved, int itemCount)
502+
private void TrimLiveItems(int itemsRemoved, int itemCount, ItemRemovedReason reason)
511503
{
504+
// When items are touched, they are moved to warm by cycling. Therefore, to guarantee
505+
// that we can remove itemCount items, we must cycle (2 * capacity.Warm) + capacity.Hot times.
512506
// If clear is called during trimming, it would be possible to get stuck in an infinite
513-
// loop here. Instead quit after n consecutive failed attempts to move warm/hot to cold.
507+
// loop here. The warm + hot limit also guards against this case.
514508
int trimWarmAttempts = 0;
515-
int maxAttempts = this.capacity.Cold + 1;
509+
int maxWarmHotAttempts = (this.capacity.Warm * 2) + this.capacity.Hot;
516510

517-
while (itemsRemoved < itemCount && trimWarmAttempts < maxAttempts)
511+
while (itemsRemoved < itemCount && trimWarmAttempts < maxWarmHotAttempts)
518512
{
519513
if (Volatile.Read(ref this.counter.cold) > 0)
520514
{
521-
if (TryRemoveCold(ItemRemovedReason.Trimmed) == (ItemDestination.Remove, 0))
515+
if (TryRemoveCold(reason) == (ItemDestination.Remove, 0))
522516
{
523517
itemsRemoved++;
524518
trimWarmAttempts = 0;
525519
}
526520

527-
TrimWarmOrHot();
521+
TrimWarmOrHot(reason);
528522
}
529523
else
530524
{
531-
TrimWarmOrHot();
525+
TrimWarmOrHot(reason);
532526
trimWarmAttempts++;
533527
}
534528
}
535529
}
536530

537-
private void TrimWarmOrHot()
531+
private void TrimWarmOrHot(ItemRemovedReason reason)
538532
{
539533
if (Volatile.Read(ref this.counter.warm) > 0)
540534
{
541-
CycleWarmUnchecked(ItemRemovedReason.Trimmed);
535+
CycleWarmUnchecked(reason);
542536
}
543537
else if (Volatile.Read(ref this.counter.hot) > 0)
544538
{
545-
CycleHotUnchecked(ItemRemovedReason.Trimmed);
539+
CycleHotUnchecked(reason);
546540
}
547541
}
548542

0 commit comments

Comments
 (0)