diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs index e52015d3..dc8fd785 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs @@ -678,6 +678,113 @@ public void WhenItemIsRemovedRemovedEventIsFired() removedItems[0].Reason.Should().Be(ItemRemovedReason.Removed); } + [Fact] + public void WhenItemRemovedFromHotDuringWarmupItIsEagerlyCycledOut() + { + lru.GetOrAdd(1, valueFactory.Create); + + lru.TryRemove(1); + Print(); // Hot [1] Warm [] Cold [] + + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + Print(); // Hot [1,2,3] Warm [] Cold [] + + lru.WarmCount.Should().Be(0); + lru.ColdCount.Should().Be(0); + } + + [Fact] + public void WhenItemRemovedFromHotAfterWarmupItIsEagerlyCycledOut() + { + for (int i = 0; i < lru.Capacity; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + } + + Print(); // Hot [6,7,8] Warm [1,2,3] Cold [0,4,5] + lru.Metrics.Value.Evicted.Should().Be(0); + + lru.GetOrAdd(-1, valueFactory.Create); + + lru.TryRemove(-1); + Print(); // Hot[7, 8, -1] Warm[1, 2, 3] Cold[4, 5, 6] + + // fully cycle hot, which is 3 items + lru.GetOrAdd(-2, valueFactory.Create); + lru.GetOrAdd(-3, valueFactory.Create); + lru.GetOrAdd(-4, valueFactory.Create); + + Print(); // Hot [-2,-3,-4] Warm [1,2,3] Cold [6,7,8] + + // without eager eviction as -1 is purged from hot, a 4th item will pushed out since hot queue is full + lru.Metrics.Value.Evicted.Should().Be(3); + } + + [Fact] + public void WhenItemRemovedFromWarmDuringWarmupItIsEagerlyCycledOut() + { + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + lru.GetOrAdd(4, valueFactory.Create); + Print(); // Hot [2,3,4] Warm [1] Cold [] + + lru.TryRemove(1); + + lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); + lru.GetOrAdd(7, valueFactory.Create); + Print(); // Hot [5,6,7] Warm [2,3,4] Cold [] + + lru.WarmCount.Should().Be(3); + lru.ColdCount.Should().Be(0); + } + + + [Fact] + public void WhenItemRemovedFromWarmAfterWarmupItIsEagerlyCycledOut() + { + for (int i = 0; i < lru.Capacity; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + } + + Print(); // Hot [6,7,8] Warm [1,2,3] Cold [0,4,5] + lru.Metrics.Value.Evicted.Should().Be(0); + + lru.TryRemove(1); + + lru.GetOrAdd(6, valueFactory.Create); // 6 -> W + lru.GetOrAdd(9, valueFactory.Create); + + Print(); // Hot [7,8,9] Warm [2,3,6] Cold [0,4,5] + + lru.Metrics.Value.Evicted.Should().Be(0); + } + + [Fact] + public void WhenItemRemovedFromColdAfterWarmupItIsEagerlyCycledOut() + { + for (int i = 0; i < lru.Capacity; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + } + + Print(); // Hot [6,7,8] Warm [1,2,3] Cold [0,4,5] + lru.Metrics.Value.Evicted.Should().Be(0); + + lru.GetOrAdd(0, valueFactory.Create); + lru.TryRemove(0); + + lru.GetOrAdd(9, valueFactory.Create); + + Print(); // Hot [7,8,9] Warm [1,2,3] Cold [4,5,6] + + lru.Metrics.Value.Evicted.Should().Be(0); + } + [Fact] public void WhenKeyDoesNotExistTryRemoveReturnsFalse() { @@ -686,6 +793,29 @@ public void WhenKeyDoesNotExistTryRemoveReturnsFalse() lru.TryRemove(2).Should().BeFalse(); } + [Fact] + public void WhenItemsAreRemovedTrimRemovesDeletedItemsFromQueues() + { + for (int i = 0; i < lru.Capacity; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + } + + Print(); // Hot [6,7,8] Warm [1,2,3] Cold [0,4,5] + + lru.TryRemove(0); + lru.TryRemove(1); + lru.TryRemove(6); + + lru.Policy.Eviction.Value.Trim(1); + + Print(); // Hot [7,8] Warm [2,3] Cold [5] + + lru.HotCount.Should().Be(2); + lru.WarmCount.Should().Be(2); + lru.ColdCount.Should().Be(1); + } + [Fact] public void WhenRepeatedlyAddingAndRemovingSameValueLruRemainsInConsistentState() { @@ -1305,6 +1435,14 @@ private void Warmup() lru.GetOrAdd(-8, valueFactory.Create); lru.GetOrAdd(-9, valueFactory.Create); } + + + private void Print() + { +#if DEBUG + this.testOutputHelper.WriteLine(this.lru.FormatLruString()); +#endif + } } public class ConcurrentLruIntegrityChecker diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs index 69004bef..5e91d18d 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs @@ -5,11 +5,13 @@ using Xunit; using System.Runtime.InteropServices; using BitFaster.Caching.UnitTests.Retry; +using Xunit.Abstractions; namespace BitFaster.Caching.UnitTests.Lru { public class ConcurrentTLruTests { + private readonly ITestOutputHelper testOutputHelper; private readonly TimeSpan timeToLive = TimeSpan.FromMilliseconds(10); private readonly ICapacityPartition capacity = new EqualCapacityPartition(9); private ConcurrentTLru lru; @@ -31,8 +33,9 @@ public ConcurrentTLru CreateTLru(ICapacityPartition capacity, TimeSp return new ConcurrentTLru(1, capacity, EqualityComparer.Default, timeToLive); } - public ConcurrentTLruTests() + public ConcurrentTLruTests(ITestOutputHelper testOutputHelper) { + this.testOutputHelper = testOutputHelper; lru = CreateTLru(capacity, timeToLive); } @@ -315,6 +318,31 @@ public void WhenItemsAreExpiredEnumerateFiltersExpiredItems() ); } + [Fact] + public void WhenItemsAreRemovedTrimExpiredRemovesDeletedItemsFromQueues() + { + lru = CreateTLru(capacity, TimeSpan.FromMinutes(1)); + + for (int i = 0; i < lru.Capacity; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + } + + Print(); // Hot [6,7,8] Warm [1,2,3] Cold [0,4,5] + + lru.TryRemove(0); + lru.TryRemove(1); + lru.TryRemove(6); + + lru.Policy.ExpireAfterWrite.Value.TrimExpired(); + + Print(); // Hot [7,8] Warm [2,3] Cold [4,5] + + lru.HotCount.Should().Be(2); + lru.WarmCount.Should().Be(2); + lru.ColdCount.Should().Be(2); + } + [Fact] public void ConstructWithDefaultCtorReturnsCapacity() { @@ -338,5 +366,12 @@ public void ConstructPartitionCtorReturnsCapacity() x.Capacity.Should().Be(3); } + + private void Print() + { +#if DEBUG + this.testOutputHelper.WriteLine(this.lru.FormatLruString()); +#endif + } } } diff --git a/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs index 84b58dbb..06c0ad5d 100644 --- a/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs @@ -114,48 +114,60 @@ public void CanDiscardIsTrue() this.policy.CanDiscard().Should().BeTrue(); } - [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteHot(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteHot(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteHot(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteWarm(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteWarm(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteWarm(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Remove)] - public void RouteCold(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Remove)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteCold(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteCold(item).Should().Be(expectedDestination); } - private LongTickCountLruItem CreateItem(bool wasAccessed, bool isExpired) + private LongTickCountLruItem CreateItem(bool wasAccessed, bool wasRemoved, bool isExpired) { var item = this.policy.CreateItem(1, 2); item.WasAccessed = wasAccessed; + item.WasRemoved = wasRemoved; if (isExpired) { diff --git a/BitFaster.Caching.UnitTests/Lru/LruPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/LruPolicyTests.cs index 89cdc4ba..16f8244e 100644 --- a/BitFaster.Caching.UnitTests/Lru/LruPolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/LruPolicyTests.cs @@ -69,40 +69,47 @@ public void CanDiscardIsFalse() } [Theory] - [InlineData(true, ItemDestination.Warm)] - [InlineData(false, ItemDestination.Cold)] - public void RouteHot(bool wasAccessed, ItemDestination expectedDestination) + [InlineData(true, false, ItemDestination.Warm)] + [InlineData(false, false, ItemDestination.Cold)] + [InlineData(false, true, ItemDestination.Remove)] + [InlineData(true, true, ItemDestination.Remove)] + public void RouteHot(bool wasAccessed, bool wasRemoved, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed); + var item = CreateItem(wasAccessed, wasRemoved); this.policy.RouteHot(item).Should().Be(expectedDestination); } [Theory] - [InlineData(true, ItemDestination.Warm)] - [InlineData(false, ItemDestination.Cold)] - public void RouteWarm(bool wasAccessed, ItemDestination expectedDestination) + [InlineData(true, false, ItemDestination.Warm)] + [InlineData(false, false, ItemDestination.Cold)] + [InlineData(true, true, ItemDestination.Remove)] + [InlineData(false, true, ItemDestination.Remove)] + public void RouteWarm(bool wasAccessed, bool wasRemoved, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed); + var item = CreateItem(wasAccessed, wasRemoved); this.policy.RouteWarm(item).Should().Be(expectedDestination); } [Theory] - [InlineData(true, ItemDestination.Warm)] - [InlineData(false, ItemDestination.Remove)] - public void RouteCold(bool wasAccessed, ItemDestination expectedDestination) + [InlineData(true, false, ItemDestination.Warm)] + [InlineData(false, false, ItemDestination.Remove)] + [InlineData(true, true, ItemDestination.Remove)] + [InlineData(false, true, ItemDestination.Remove)] + public void RouteCold(bool wasAccessed, bool wasRemoved, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed); + var item = CreateItem(wasAccessed, wasRemoved); this.policy.RouteCold(item).Should().Be(expectedDestination); } - private LruItem CreateItem(bool wasAccessed) + private LruItem CreateItem(bool wasAccessed, bool wasRemoved) { var item = this.policy.CreateItem(1, 2); item.WasAccessed = wasAccessed; + item.WasRemoved = wasRemoved; return item; } diff --git a/BitFaster.Caching.UnitTests/Lru/TLruTickCount64PolicyTests .cs b/BitFaster.Caching.UnitTests/Lru/TLruTickCount64PolicyTests .cs index 1acb2aaf..5803d8e7 100644 --- a/BitFaster.Caching.UnitTests/Lru/TLruTickCount64PolicyTests .cs +++ b/BitFaster.Caching.UnitTests/Lru/TLruTickCount64PolicyTests .cs @@ -108,46 +108,59 @@ public void CanDiscardIsTrue() } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteHot(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) - { - var item = CreateItem(wasAccessed, isExpired); + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteHot(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) + { + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteHot(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteWarm(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) - { - var item = CreateItem(wasAccessed, isExpired); + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteWarm(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) + { + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteWarm(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Remove)] - public void RouteCold(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) - { - var item = CreateItem(wasAccessed, isExpired); + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Remove)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteCold(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) + { + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteCold(item).Should().Be(expectedDestination); } - private LongTickCountLruItem CreateItem(bool wasAccessed, bool isExpired) + private LongTickCountLruItem CreateItem(bool wasAccessed, bool wasRemoved, bool isExpired) { var item = this.policy.CreateItem(1, 2); item.WasAccessed = wasAccessed; + item.WasRemoved = wasRemoved; if (isExpired) { diff --git a/BitFaster.Caching.UnitTests/Lru/TlruDateTimePolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/TlruDateTimePolicyTests.cs index 4cdb7b78..faaf438d 100644 --- a/BitFaster.Caching.UnitTests/Lru/TlruDateTimePolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/TlruDateTimePolicyTests.cs @@ -86,46 +86,59 @@ public void CanDiscardIsTrue() } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteHot(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteHot(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteHot(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteWarm(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteWarm(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteWarm(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Remove)] - public void RouteCold(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Remove)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteCold(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteCold(item).Should().Be(expectedDestination); } - private TimeStampedLruItem CreateItem(bool wasAccessed, bool isExpired) + private TimeStampedLruItem CreateItem(bool wasAccessed, bool wasRemoved, bool isExpired) { var item = this.policy.CreateItem(1, 2); item.WasAccessed = wasAccessed; + item.WasRemoved = wasRemoved; if (isExpired) { diff --git a/BitFaster.Caching.UnitTests/Lru/TlruStopwatchPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/TlruStopwatchPolicyTests.cs index 1b228af4..842d0843 100644 --- a/BitFaster.Caching.UnitTests/Lru/TlruStopwatchPolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/TlruStopwatchPolicyTests.cs @@ -110,46 +110,59 @@ public void CanDiscardIsTrue() } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteHot(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteHot(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteHot(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteWarm(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteWarm(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteWarm(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Remove)] - public void RouteCold(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Remove)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteCold(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteCold(item).Should().Be(expectedDestination); } - private LongTickCountLruItem CreateItem(bool wasAccessed, bool isExpired) + private LongTickCountLruItem CreateItem(bool wasAccessed, bool wasRemoved, bool isExpired) { var item = this.policy.CreateItem(1, 2); item.WasAccessed = wasAccessed; + item.WasRemoved = wasRemoved; if (isExpired) { diff --git a/BitFaster.Caching.UnitTests/Lru/TlruTicksPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/TlruTicksPolicyTests.cs index db1e5fef..9b4a374d 100644 --- a/BitFaster.Caching.UnitTests/Lru/TlruTicksPolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/TlruTicksPolicyTests.cs @@ -87,46 +87,59 @@ public void CanDiscardIsTrue() } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteHot(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteHot(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteHot(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Cold)] - public void RouteWarm(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Cold)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteWarm(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteWarm(item).Should().Be(expectedDestination); } [Theory] - [InlineData(false, true, ItemDestination.Remove)] - [InlineData(true, true, ItemDestination.Remove)] - [InlineData(true, false, ItemDestination.Warm)] - [InlineData(false, false, ItemDestination.Remove)] - public void RouteCold(bool wasAccessed, bool isExpired, ItemDestination expectedDestination) + [InlineData(false, false, true, ItemDestination.Remove)] + [InlineData(true, false, true, ItemDestination.Remove)] + [InlineData(true, false, false, ItemDestination.Warm)] + [InlineData(false, false, false, ItemDestination.Remove)] + [InlineData(false, true, true, ItemDestination.Remove)] + [InlineData(true, true, true, ItemDestination.Remove)] + [InlineData(true, true, false, ItemDestination.Remove)] + [InlineData(false, true, false, ItemDestination.Remove)] + public void RouteCold(bool wasAccessed, bool wasRemoved, bool isExpired, ItemDestination expectedDestination) { - var item = CreateItem(wasAccessed, isExpired); + var item = CreateItem(wasAccessed, wasRemoved, isExpired); this.policy.RouteCold(item).Should().Be(expectedDestination); } - private TickCountLruItem CreateItem(bool wasAccessed, bool isExpired) + private TickCountLruItem CreateItem(bool wasAccessed, bool wasRemoved, bool isExpired) { var item = this.policy.CreateItem(1, 2); item.WasAccessed = wasAccessed; + item.WasRemoved = wasRemoved; if (isExpired) { diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 937d6747..6e66987c 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -468,7 +468,7 @@ public void Trim(int itemCount) lock (this.dictionary) { // first scan each queue for discardable items and remove them immediately. Note this can remove > itemCount items. - int itemsRemoved = this.itemPolicy.CanDiscard() ? TrimAllDiscardedItems() : 0; + int itemsRemoved = TrimAllDiscardedItems(); TrimLiveItems(itemsRemoved, itemCount, ItemRemovedReason.Trimmed); } @@ -478,7 +478,10 @@ private void TrimExpired() { if (this.itemPolicy.CanDiscard()) { - this.TrimAllDiscardedItems(); + lock (this.dictionary) + { + this.TrimAllDiscardedItems(); + } } } @@ -507,6 +510,10 @@ int RemoveDiscardableItems(ConcurrentQueue q, ref int queueCounter) this.Move(item, ItemDestination.Remove, ItemRemovedReason.Trimmed); itemsRemoved++; } + else if (item.WasRemoved) + { + Interlocked.Decrement(ref queueCounter); + } else { q.Enqueue(item); @@ -548,8 +555,10 @@ private void TrimLiveItems(int itemsRemoved, int itemCount, ItemRemovedReason re itemsRemoved++; trimWarmAttempts = 0; } - - TrimWarmOrHot(reason); + else + { + TrimWarmOrHot(reason); + } } else { @@ -624,6 +633,12 @@ private void CycleDuringWarmup(int hotCount) if (this.hotQueue.TryDequeue(out var item)) { + // special case: removed during warmup + if (item.WasRemoved) + { + return; + } + int count = this.Move(item, ItemDestination.Warm, ItemRemovedReason.Evicted); // if warm is now full, overflow to cold and mark as warm @@ -685,6 +700,11 @@ private void CycleDuringWarmup(int hotCount) if (this.warmQueue.TryDequeue(out var item)) { + if (item.WasRemoved) + { + return (ItemDestination.Remove, 0); + } + var where = this.itemPolicy.RouteWarm(item); // When the warm queue is full, we allow an overflow of 1 item before redirecting warm items to cold. @@ -748,7 +768,8 @@ private int LastWarmToCold() if (this.warmQueue.TryDequeue(out var item)) { - return this.Move(item, ItemDestination.Cold, ItemRemovedReason.Evicted); + var destination = item.WasRemoved ? ItemDestination.Remove : ItemDestination.Cold; + return this.Move(item, destination, ItemRemovedReason.Evicted); } else { @@ -812,6 +833,27 @@ IEnumerator IEnumerable.GetEnumerator() return ((ConcurrentLruCore)this).GetEnumerator(); } +#if DEBUG + /// + /// Format the LRU as a string by converting all the keys to strings. + /// + /// The LRU formatted as a string. + internal string FormatLruString() + { + var sb = new System.Text.StringBuilder(); + + sb.Append("Hot ["); + sb.Append(string.Join(",", this.hotQueue.Select(n => n.Key.ToString()))); + sb.Append("] Warm ["); + sb.Append(string.Join(",", this.warmQueue.Select(n => n.Key.ToString()))); + sb.Append("] Cold ["); + sb.Append(string.Join(",", this.coldQueue.Select(n => n.Key.ToString()))); + sb.Append(']'); + + return sb.ToString(); + } +#endif + private static CachePolicy CreatePolicy(ConcurrentLruCore lru) { var p = new Proxy(lru); diff --git a/BitFaster.Caching/Lru/DiscretePolicy.cs b/BitFaster.Caching/Lru/DiscretePolicy.cs index 636244d9..300732b2 100644 --- a/BitFaster.Caching/Lru/DiscretePolicy.cs +++ b/BitFaster.Caching/Lru/DiscretePolicy.cs @@ -72,7 +72,7 @@ public bool CanDiscard() [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteHot(LongTickCountLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } @@ -89,7 +89,7 @@ public ItemDestination RouteHot(LongTickCountLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteWarm(LongTickCountLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } @@ -106,7 +106,7 @@ public ItemDestination RouteWarm(LongTickCountLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteCold(LongTickCountLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } diff --git a/BitFaster.Caching/Lru/LruPolicy.cs b/BitFaster.Caching/Lru/LruPolicy.cs index 12495dd9..0120c45f 100644 --- a/BitFaster.Caching/Lru/LruPolicy.cs +++ b/BitFaster.Caching/Lru/LruPolicy.cs @@ -50,6 +50,11 @@ public bool CanDiscard() [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteHot(LruItem item) { + if (item.WasRemoved) + { + return ItemDestination.Remove; + } + if (item.WasAccessed) { return ItemDestination.Warm; @@ -62,6 +67,11 @@ public ItemDestination RouteHot(LruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteWarm(LruItem item) { + if (item.WasRemoved) + { + return ItemDestination.Remove; + } + if (item.WasAccessed) { return ItemDestination.Warm; @@ -74,7 +84,7 @@ public ItemDestination RouteWarm(LruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteCold(LruItem item) { - if (item.WasAccessed) + if (item.WasAccessed & !item.WasRemoved) { return ItemDestination.Warm; } diff --git a/BitFaster.Caching/Lru/TLruLongTicksPolicy.cs b/BitFaster.Caching/Lru/TLruLongTicksPolicy.cs index 41ee8355..1d511fec 100644 --- a/BitFaster.Caching/Lru/TLruLongTicksPolicy.cs +++ b/BitFaster.Caching/Lru/TLruLongTicksPolicy.cs @@ -71,7 +71,7 @@ public bool CanDiscard() [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteHot(LongTickCountLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } @@ -88,7 +88,7 @@ public ItemDestination RouteHot(LongTickCountLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteWarm(LongTickCountLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } @@ -105,7 +105,7 @@ public ItemDestination RouteWarm(LongTickCountLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteCold(LongTickCountLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } diff --git a/BitFaster.Caching/Lru/TlruDateTimePolicy.cs b/BitFaster.Caching/Lru/TlruDateTimePolicy.cs index b7207a5b..48b14b5c 100644 --- a/BitFaster.Caching/Lru/TlruDateTimePolicy.cs +++ b/BitFaster.Caching/Lru/TlruDateTimePolicy.cs @@ -68,7 +68,7 @@ public bool CanDiscard() [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteHot(TimeStampedLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } @@ -85,7 +85,7 @@ public ItemDestination RouteHot(TimeStampedLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteWarm(TimeStampedLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } @@ -102,7 +102,7 @@ public ItemDestination RouteWarm(TimeStampedLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteCold(TimeStampedLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } diff --git a/BitFaster.Caching/Lru/TlruTicksPolicy.cs b/BitFaster.Caching/Lru/TlruTicksPolicy.cs index d6bc0047..40ccfe6c 100644 --- a/BitFaster.Caching/Lru/TlruTicksPolicy.cs +++ b/BitFaster.Caching/Lru/TlruTicksPolicy.cs @@ -73,7 +73,7 @@ public bool CanDiscard() [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteHot(TickCountLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } @@ -90,7 +90,7 @@ public ItemDestination RouteHot(TickCountLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteWarm(TickCountLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; } @@ -107,7 +107,7 @@ public ItemDestination RouteWarm(TickCountLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ItemDestination RouteCold(TickCountLruItem item) { - if (this.ShouldDiscard(item)) + if (this.ShouldDiscard(item) | item.WasRemoved) { return ItemDestination.Remove; }