diff --git a/BitFaster.Caching.UnitTests/CachePolicyTests.cs b/BitFaster.Caching.UnitTests/CachePolicyTests.cs index a66ebd2a..ae2215a0 100644 --- a/BitFaster.Caching.UnitTests/CachePolicyTests.cs +++ b/BitFaster.Caching.UnitTests/CachePolicyTests.cs @@ -17,6 +17,7 @@ public void WhenCtorFieldsAreAssigned() cp.Eviction.Value.Should().Be(eviction.Object); cp.ExpireAfterWrite.Value.Should().Be(expire.Object); cp.ExpireAfterAccess.HasValue.Should().BeFalse(); + cp.ExpireAfter.HasValue.Should().BeFalse(); } [Fact] @@ -40,7 +41,16 @@ public void TryTrimWhenExpireAfterWriteReturnsTrue() public void TryTrimWhenExpireAfterAccessReturnsTrue() { var expire = new Mock(); - var cp = new CachePolicy(Optional.None(), Optional.None(), new Optional(expire.Object)); + var cp = new CachePolicy(Optional.None(), Optional.None(), new Optional(expire.Object), Optional.None()); + + cp.TryTrimExpired().Should().BeTrue(); + } + + [Fact] + public void TryTrimWhenExpireAfterReturnsTrue() + { + var expire = new Mock(); + var cp = new CachePolicy(Optional.None(), Optional.None(), Optional.None(), new Optional(expire.Object)); cp.TryTrimExpired().Should().BeTrue(); } diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs new file mode 100644 index 00000000..e64b513d --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lru +{ + public class ConcurrentLruAfterDiscreteTests + { + private readonly ICapacityPartition capacity = new EqualCapacityPartition(9); + private ICache lru; + + private ValueFactory valueFactory = new ValueFactory(); + private TestExpiryCalculator expiryCalculator = new TestExpiryCalculator(); + + private List> removedItems = new List>(); + + // on MacOS time measurement seems to be less stable, give longer pause + private int ttlWaitMlutiplier = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 8 : 2; + + private static readonly TimeSpan delta = TimeSpan.FromMilliseconds(20); + + private void OnLruItemRemoved(object sender, ItemRemovedEventArgs e) + { + removedItems.Add(e); + } + + public ConcurrentLruAfterDiscreteTests() + { + lru = new ConcurrentLruBuilder() + .WithCapacity(capacity) + .WithExpireAfter(expiryCalculator) + .Build(); + } + + [Fact] + public void WhenKeyIsWrongTypeTryGetTimeToExpireIsFalse() + { + lru.Policy.ExpireAfter.Value.TryGetTimeToExpire("foo", out _).Should().BeFalse(); + } + + [Fact] + public void WhenKeyDoesNotExistTryGetTimeToExpireIsFalse() + { + lru.Policy.ExpireAfter.Value.TryGetTimeToExpire(1, out _).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryGetTimeToExpireReturnsExpiryTime() + { + lru.GetOrAdd(1, k => "1"); + lru.Policy.ExpireAfter.Value.TryGetTimeToExpire(1, out var expiry).Should().BeTrue(); + expiry.Should().BeCloseTo(TestExpiryCalculator.DefaultTimeToExpire, delta); + } + + [Fact] + public void WhenItemIsExpiredItIsRemoved() + { + Timed.Execute( + lru, + lru => + { + lru.GetOrAdd(1, valueFactory.Create); + return lru; + }, + TestExpiryCalculator.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier), + lru => + { + lru.TryGet(1, out var value).Should().BeFalse(); + } + ); + } + + [Fact] + public void WhenItemIsUpdatedTtlIsExtended() + { + Timed.Execute( + lru, + lru => + { + lru.GetOrAdd(1, valueFactory.Create); + return lru; + }, + TestExpiryCalculator.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier), + lru => + { + lru.TryUpdate(1, "3"); + lru.TryGet(1, out var value).Should().BeTrue(); + } + ); + } + + [Fact] + public void WhenItemIsReadTtlIsExtended() + { + expiryCalculator.ExpireAfterCreate = (_, _) => TimeSpan.FromMilliseconds(100); + + var lru = new ConcurrentLruBuilder() + .WithCapacity(capacity) + .WithExpireAfter(expiryCalculator) + .Build(); + + // execute the method to ensure it is always jitted + lru.GetOrAdd(-1, valueFactory.Create); + lru.GetOrAdd(-2, valueFactory.Create); + lru.GetOrAdd(-3, valueFactory.Create); + + Timed.Execute( + lru, + lru => + { + lru.GetOrAdd(1, valueFactory.Create); + return lru; + }, + TimeSpan.FromMilliseconds(50), + lru => + { + lru.TryGet(1, out _).Should().BeTrue($"First"); + }, + TimeSpan.FromMilliseconds(75), + lru => + { + lru.TryGet(1, out var value).Should().BeTrue($"Second"); + } + ); + } + + [Fact] + public void WhenValueEvictedItemRemovedEventIsFired() + { + expiryCalculator.ExpireAfterCreate = (_, _) => TimeSpan.FromSeconds(10); + + var lruEvents = new ConcurrentLruBuilder() + .WithCapacity(new EqualCapacityPartition(6)) + .WithExpireAfter(expiryCalculator) + .WithMetrics() + .Build(); + + lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; + + // First 6 adds + // hot[6, 5], warm[2, 1], cold[4, 3] + // => + // hot[8, 7], warm[1, 0], cold[6, 5], evicted[4, 3] + for (int i = 0; i < 8; i++) + { + lruEvents.GetOrAdd(i + 1, i => $"{i + 1}"); + } + + removedItems.Count.Should().Be(2); + + removedItems[0].Key.Should().Be(1); + removedItems[0].Value.Should().Be("2"); + removedItems[0].Reason.Should().Be(ItemRemovedReason.Evicted); + + removedItems[1].Key.Should().Be(4); + removedItems[1].Value.Should().Be("5"); + removedItems[1].Reason.Should().Be(ItemRemovedReason.Evicted); + } + + [Fact] + public void WhenItemsAreExpiredExpireRemovesExpiredItems() + { + Timed.Execute( + lru, + lru => + { + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(2, "2"); + lru.AddOrUpdate(3, "3"); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + + lru.AddOrUpdate(4, "4"); + lru.AddOrUpdate(5, "5"); + lru.AddOrUpdate(6, "6"); + + lru.AddOrUpdate(7, "7"); + lru.AddOrUpdate(8, "8"); + lru.AddOrUpdate(9, "9"); + + return lru; + }, + TestExpiryCalculator.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier), + lru => + { + lru.Policy.ExpireAfter.Value.TrimExpired(); + + lru.Count.Should().Be(0); + } + ); + } + + [Fact] + public void WhenCacheHasExpiredAndFreshItemsExpireRemovesOnlyExpiredItems() + { + Timed.Execute( + lru, + lru => + { + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(2, "2"); + lru.AddOrUpdate(3, "3"); + + lru.AddOrUpdate(4, "4"); + lru.AddOrUpdate(5, "5"); + lru.AddOrUpdate(6, "6"); + + return lru; + }, + TestExpiryCalculator.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier), + lru => + { + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + + lru.Policy.ExpireAfter.Value.TrimExpired(); + + lru.Count.Should().Be(3); + } + ); + } + + [Fact] + public void WhenItemsAreExpiredTrimRemovesExpiredItems() + { + Timed.Execute( + lru, + lru => + { + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(2, "2"); + lru.AddOrUpdate(3, "3"); + + return lru; + }, + TestExpiryCalculator.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier), + lru => + { + lru.Policy.Eviction.Value.Trim(1); + + lru.Count.Should().Be(0); + } + ); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 75f6f748..27bff8ce 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -1,435 +1,557 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using BitFaster.Caching.Lru; -using BitFaster.Caching.Atomic; -using FluentAssertions; -using Xunit; - -namespace BitFaster.Caching.UnitTests.Lru -{ - public class ConcurrentLruBuilderTests - { - [Fact] - public void TestFastLru() - { - ICache lru = new ConcurrentLruBuilder() - .Build(); - - lru.Should().BeOfType>(); - } - - [Fact] - public void TestMetricsLru() - { - ICache lru = new ConcurrentLruBuilder() - .WithMetrics() - .Build(); - - lru.Should().BeOfType>(); - } - - [Fact] - public void TestFastTLru() - { - ICache lru = new ConcurrentLruBuilder() - .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) - .Build(); - - lru.Should().BeOfType>(); - } - - [Fact] - public void TestMetricsTLru() - { - ICache lru = new ConcurrentLruBuilder() - .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) - .WithMetrics() - .Build(); - - lru.Should().BeOfType>(); - lru.Policy.Eviction.Value.Capacity.Should().Be(128); - } - - [Fact] - public void AsAsyncTestFastLru() - { - IAsyncCache lru = new ConcurrentLruBuilder() - .AsAsyncCache() - .Build(); - - lru.Should().BeOfType>(); - } - - [Fact] - public void AsAsyncTestMetricsLru() - { - IAsyncCache lru = new ConcurrentLruBuilder() - .WithMetrics() - .AsAsyncCache() - .Build(); - - lru.Should().BeOfType>(); - } - - [Fact] - public void AsAsyncTestFastTLru() - { - IAsyncCache lru = new ConcurrentLruBuilder() - .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) - .AsAsyncCache() - .Build(); - - lru.Should().BeOfType>(); - } - - [Fact] - public void AsAsyncTestMetricsTLru() - { - IAsyncCache lru = new ConcurrentLruBuilder() - .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) - .WithMetrics() - .AsAsyncCache() - .Build(); - - lru.Should().BeOfType>(); - lru.Policy.Eviction.Value.Capacity.Should().Be(128); - } - - [Fact] - public void TestComparer() - { - ICache fastLru = new ConcurrentLruBuilder() - .WithKeyComparer(StringComparer.OrdinalIgnoreCase) - .Build(); - - fastLru.GetOrAdd("a", k => 1); - fastLru.TryGet("A", out var value).Should().BeTrue(); - } - - [Fact] - public void TestConcurrencyLevel() - { - var b = new ConcurrentLruBuilder() - .WithConcurrencyLevel(-1); - - Action constructor = () => { var x = b.Build(); }; - - constructor.Should().Throw(); - } - - [Fact] - public void TestIntCapacity() - { - ICache lru = new ConcurrentLruBuilder() - .WithCapacity(3) - .Build(); - - lru.Policy.Eviction.Value.Capacity.Should().Be(3); - } - - [Fact] - public void TestPartitionCapacity() - { - ICache lru = new ConcurrentLruBuilder() - .WithCapacity(new FavorWarmPartition(6)) - .Build(); - - lru.Policy.Eviction.Value.Capacity.Should().Be(6); - } - - [Fact] - public void TestExpireAfterAccess() - { - ICache expireAfterAccess = new ConcurrentLruBuilder() - .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) - .Build(); - - expireAfterAccess.Metrics.HasValue.Should().BeFalse(); - expireAfterAccess.Policy.ExpireAfterAccess.HasValue.Should().BeTrue(); - expireAfterAccess.Policy.ExpireAfterAccess.Value.TimeToLive.Should().Be(TimeSpan.FromSeconds(1)); - expireAfterAccess.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); - } - - [Fact] - public void TestExpireAfterAccessWithMetrics() - { - ICache expireAfterAccess = new ConcurrentLruBuilder() - .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) - .WithMetrics() - .Build(); - - expireAfterAccess.Metrics.HasValue.Should().BeTrue(); - expireAfterAccess.Policy.ExpireAfterAccess.HasValue.Should().BeTrue(); - expireAfterAccess.Policy.ExpireAfterAccess.Value.TimeToLive.Should().Be(TimeSpan.FromSeconds(1)); - expireAfterAccess.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); - } - - [Fact] - public void TestExpireAfterReadAndExpireAfterWriteThrows() - { - var builder = new ConcurrentLruBuilder() - .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) - .WithExpireAfterWrite(TimeSpan.FromSeconds(2)); - - Action act = () => builder.Build(); - act.Should().Throw(); - } - - // There are 15 combinations to test: - // ----------------------------- - //1 WithAtomic - //2 WithScoped - //3 AsAsync - // - // ----------------------------- - //4 WithAtomic - // WithScoped - // - //5 WithScoped - // WithAtomic - // - //6 AsAsync - // WithScoped - // - //7 WithScoped - // AsAsync - // - //8 WithAtomic - // AsAsync - // - //9 AsAsync - // WithAtomic - // - // ----------------------------- - //10 WithAtomic - // WithScoped - // AsAsync - // - //11 WithAtomic - // AsAsync - // WithScoped - // - //12 WithScoped - // WithAtomic - // AsAsync - // - //13 WithScoped - // AsAsync - // WithAtomic - // - //14 AsAsync - // WithScoped - // WithAtomic - // - //15 AsAsync - // WithAtomic - // WithScoped - - // 1 - [Fact] - public void WithScopedValues() - { - IScopedCache lru = new ConcurrentLruBuilder() - .AsScopedCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeOfType>(); - lru.Policy.Eviction.Value.Capacity.Should().Be(3); - } - - // 2 - [Fact] - public void WithAtomicFactory() - { - ICache lru = new ConcurrentLruBuilder() - .WithAtomicGetOrAdd() - .WithCapacity(3) - .Build(); - - lru.Should().BeOfType>(); - } - - // 3 - [Fact] - public void AsAsync() - { - IAsyncCache lru = new ConcurrentLruBuilder() - .AsAsyncCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - } - - // 4 - [Fact] - public void WithAtomicWithScope() - { - IScopedCache lru = new ConcurrentLruBuilder() - .WithAtomicGetOrAdd() - .AsScopedCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeOfType>(); - lru.Policy.Eviction.Value.Capacity.Should().Be(3); - } - - // 5 - [Fact] - public void WithScopedWithAtomic() - { - IScopedCache lru = new ConcurrentLruBuilder() - .AsScopedCache() - .WithAtomicGetOrAdd() - .WithCapacity(3) - .Build(); - - lru.Should().BeOfType>(); - lru.Policy.Eviction.Value.Capacity.Should().Be(3); - } - - // 6 - [Fact] - public void AsAsyncWithScoped() - { - IScopedAsyncCache lru = new ConcurrentLruBuilder() - .AsAsyncCache() - .AsScopedCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - - lru.Policy.Eviction.Value.Capacity.Should().Be(3); - } - - // 7 - [Fact] - public void WithScopedAsAsync() - { - IScopedAsyncCache lru = new ConcurrentLruBuilder() - .AsScopedCache() - .AsAsyncCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - lru.Policy.Eviction.Value.Capacity.Should().Be(3); - } - - // 8 - [Fact] - public void WithAtomicAsAsync() - { - IAsyncCache lru = new ConcurrentLruBuilder() - .WithAtomicGetOrAdd() - .AsAsyncCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - } - - // 9 - [Fact] - public void AsAsyncWithAtomic() - { - IAsyncCache lru = new ConcurrentLruBuilder() - .AsAsyncCache() - .WithAtomicGetOrAdd() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - } - - // 10 - [Fact] - public void WithAtomicWithScopedAsAsync() - { - IScopedAsyncCache lru = new ConcurrentLruBuilder() - .WithAtomicGetOrAdd() - .AsScopedCache() - .AsAsyncCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - } - - // 11 - [Fact] - public void WithAtomicAsAsyncWithScoped() - { - IScopedAsyncCache lru = new ConcurrentLruBuilder() - .WithAtomicGetOrAdd() - .AsAsyncCache() - .AsScopedCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - } - - // 12 - [Fact] - public void WithScopedWithAtomicAsAsync() - { - IScopedAsyncCache lru = new ConcurrentLruBuilder() - .AsScopedCache() - .WithAtomicGetOrAdd() - .AsAsyncCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - } - - // 13 - [Fact] - public void WithScopedAsAsyncWithAtomic() - { - IScopedAsyncCache lru = new ConcurrentLruBuilder() - .AsScopedCache() - .AsAsyncCache() - .WithAtomicGetOrAdd() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - } - - // 14 - [Fact] - public void AsAsyncWithScopedWithAtomic() - { - IScopedAsyncCache lru = new ConcurrentLruBuilder() - .AsAsyncCache() - .AsScopedCache() - .WithAtomicGetOrAdd() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - } - - // 15 - [Fact] - public void AsAsyncWithAtomicWithScoped() - { - IScopedAsyncCache lru = new ConcurrentLruBuilder() - .AsAsyncCache() - .WithAtomicGetOrAdd() - .AsScopedCache() - .WithCapacity(3) - .Build(); - - lru.Should().BeAssignableTo>(); - } - } -} +using System; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Atomic; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lru +{ + public class ConcurrentLruBuilderTests + { + [Fact] + public void TestFastLru() + { + ICache lru = new ConcurrentLruBuilder() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestMetricsLru() + { + ICache lru = new ConcurrentLruBuilder() + .WithMetrics() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestFastTLru() + { + ICache lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestMetricsTLru() + { + ICache lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .WithMetrics() + .Build(); + + lru.Should().BeOfType>(); + lru.Policy.Eviction.Value.Capacity.Should().Be(128); + } + + [Fact] + public void AsAsyncTestFastLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void AsAsyncTestMetricsLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithMetrics() + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void AsAsyncTestFastTLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void AsAsyncTestMetricsTLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .WithMetrics() + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + lru.Policy.Eviction.Value.Capacity.Should().Be(128); + } + + [Fact] + public void TestComparer() + { + ICache fastLru = new ConcurrentLruBuilder() + .WithKeyComparer(StringComparer.OrdinalIgnoreCase) + .Build(); + + fastLru.GetOrAdd("a", k => 1); + fastLru.TryGet("A", out var value).Should().BeTrue(); + } + + [Fact] + public void TestConcurrencyLevel() + { + var b = new ConcurrentLruBuilder() + .WithConcurrencyLevel(-1); + + Action constructor = () => { var x = b.Build(); }; + + constructor.Should().Throw(); + } + + [Fact] + public void TestIntCapacity() + { + ICache lru = new ConcurrentLruBuilder() + .WithCapacity(3) + .Build(); + + lru.Policy.Eviction.Value.Capacity.Should().Be(3); + } + + [Fact] + public void TestPartitionCapacity() + { + ICache lru = new ConcurrentLruBuilder() + .WithCapacity(new FavorWarmPartition(6)) + .Build(); + + lru.Policy.Eviction.Value.Capacity.Should().Be(6); + } + + [Fact] + public void TestExpireAfterAccess() + { + ICache expireAfterAccess = new ConcurrentLruBuilder() + .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) + .Build(); + + expireAfterAccess.Metrics.HasValue.Should().BeFalse(); + expireAfterAccess.Policy.ExpireAfterAccess.HasValue.Should().BeTrue(); + expireAfterAccess.Policy.ExpireAfterAccess.Value.TimeToLive.Should().Be(TimeSpan.FromSeconds(1)); + expireAfterAccess.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); + } + + [Fact] + public void TestExpireAfterAccessWithMetrics() + { + ICache expireAfterAccess = new ConcurrentLruBuilder() + .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) + .WithMetrics() + .Build(); + + expireAfterAccess.Metrics.HasValue.Should().BeTrue(); + expireAfterAccess.Policy.ExpireAfterAccess.HasValue.Should().BeTrue(); + expireAfterAccess.Policy.ExpireAfterAccess.Value.TimeToLive.Should().Be(TimeSpan.FromSeconds(1)); + expireAfterAccess.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); + } + + [Fact] + public void TestExpireAfterReadAndExpireAfterWriteThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) + .WithExpireAfterWrite(TimeSpan.FromSeconds(2)); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestExpireAfter() + { + ICache expireAfter = new ConcurrentLruBuilder() + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .Build(); + + expireAfter.Metrics.HasValue.Should().BeFalse(); + expireAfter.Policy.ExpireAfter.HasValue.Should().BeTrue(); + + expireAfter.Policy.ExpireAfterAccess.HasValue.Should().BeFalse(); + expireAfter.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); + } + + [Fact] + public void TestAsyncExpireAfter() + { + IAsyncCache expireAfter = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .Build(); + + expireAfter.Metrics.HasValue.Should().BeFalse(); + expireAfter.Policy.ExpireAfter.HasValue.Should().BeTrue(); + + expireAfter.Policy.ExpireAfterAccess.HasValue.Should().BeFalse(); + expireAfter.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); + } + + [Fact] + public void TestExpireAfterWithMetrics() + { + ICache expireAfter = new ConcurrentLruBuilder() + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .WithMetrics() + .Build(); + + expireAfter.Metrics.HasValue.Should().BeTrue(); + expireAfter.Policy.ExpireAfter.HasValue.Should().BeTrue(); + + expireAfter.Policy.ExpireAfterAccess.HasValue.Should().BeFalse(); + expireAfter.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); + } + + [Fact] + public void TestExpireAfterWriteAndExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestExpireAfterAccessAndExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestExpireAfterAccessAndWriteAndExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestScopedWithExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .AsScopedCache(); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestScopedAtomicWithExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .AsScopedCache() + .WithAtomicGetOrAdd(); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestAsyncScopedWithExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .AsAsyncCache() + .AsScopedCache(); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestAsyncScopedAtomicWithExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .AsAsyncCache() + .AsScopedCache() + .WithAtomicGetOrAdd(); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + // There are 15 combinations to test: + // ----------------------------- + //1 WithAtomic + //2 WithScoped + //3 AsAsync + // + // ----------------------------- + //4 WithAtomic + // WithScoped + // + //5 WithScoped + // WithAtomic + // + //6 AsAsync + // WithScoped + // + //7 WithScoped + // AsAsync + // + //8 WithAtomic + // AsAsync + // + //9 AsAsync + // WithAtomic + // + // ----------------------------- + //10 WithAtomic + // WithScoped + // AsAsync + // + //11 WithAtomic + // AsAsync + // WithScoped + // + //12 WithScoped + // WithAtomic + // AsAsync + // + //13 WithScoped + // AsAsync + // WithAtomic + // + //14 AsAsync + // WithScoped + // WithAtomic + // + //15 AsAsync + // WithAtomic + // WithScoped + + // 1 + [Fact] + public void WithScopedValues() + { + IScopedCache lru = new ConcurrentLruBuilder() + .AsScopedCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + lru.Policy.Eviction.Value.Capacity.Should().Be(3); + } + + // 2 + [Fact] + public void WithAtomicFactory() + { + ICache lru = new ConcurrentLruBuilder() + .WithAtomicGetOrAdd() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + } + + // 3 + [Fact] + public void AsAsync() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 4 + [Fact] + public void WithAtomicWithScope() + { + IScopedCache lru = new ConcurrentLruBuilder() + .WithAtomicGetOrAdd() + .AsScopedCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + lru.Policy.Eviction.Value.Capacity.Should().Be(3); + } + + // 5 + [Fact] + public void WithScopedWithAtomic() + { + IScopedCache lru = new ConcurrentLruBuilder() + .AsScopedCache() + .WithAtomicGetOrAdd() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + lru.Policy.Eviction.Value.Capacity.Should().Be(3); + } + + // 6 + [Fact] + public void AsAsyncWithScoped() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .AsScopedCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + + lru.Policy.Eviction.Value.Capacity.Should().Be(3); + } + + // 7 + [Fact] + public void WithScopedAsAsync() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .AsScopedCache() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + lru.Policy.Eviction.Value.Capacity.Should().Be(3); + } + + // 8 + [Fact] + public void WithAtomicAsAsync() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithAtomicGetOrAdd() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 9 + [Fact] + public void AsAsyncWithAtomic() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithAtomicGetOrAdd() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 10 + [Fact] + public void WithAtomicWithScopedAsAsync() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .WithAtomicGetOrAdd() + .AsScopedCache() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 11 + [Fact] + public void WithAtomicAsAsyncWithScoped() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .WithAtomicGetOrAdd() + .AsAsyncCache() + .AsScopedCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 12 + [Fact] + public void WithScopedWithAtomicAsAsync() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .AsScopedCache() + .WithAtomicGetOrAdd() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 13 + [Fact] + public void WithScopedAsAsyncWithAtomic() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .AsScopedCache() + .AsAsyncCache() + .WithAtomicGetOrAdd() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 14 + [Fact] + public void AsAsyncWithScopedWithAtomic() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .AsScopedCache() + .WithAtomicGetOrAdd() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 15 + [Fact] + public void AsAsyncWithAtomicWithScoped() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithAtomicGetOrAdd() + .AsScopedCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs new file mode 100644 index 00000000..9fd99ea8 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs @@ -0,0 +1,188 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lru +{ + public class DiscretePolicyTests + { + private readonly TestExpiryCalculator expiryCalculator; + private readonly DiscretePolicy policy; + + private static readonly ulong stopwatchDelta = (ulong)StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(20)); + private static readonly ulong tickCountDelta = (ulong)TimeSpan.FromMilliseconds(20).ToEnvTick64(); + + public DiscretePolicyTests() + { + expiryCalculator = new TestExpiryCalculator(); + policy = new DiscretePolicy(expiryCalculator); + } + + [Fact] + public void TimeToLiveShouldBeZero() + { + this.policy.TimeToLive.Should().Be(TimeSpan.Zero); + } + + [Fact] + public void ConvertTicksReturnsTimeSpan() + { +#if NETFRAMEWORK + this.policy.ConvertTicks(StopwatchTickConverter.ToTicks(TestExpiryCalculator.DefaultTimeToExpire) + Stopwatch.GetTimestamp()).Should().BeCloseTo(TestExpiryCalculator.DefaultTimeToExpire, TimeSpan.FromMilliseconds(20)); +#else + this.policy.ConvertTicks(TestExpiryCalculator.DefaultTimeToExpire.ToEnvTick64() + Environment.TickCount64).Should().BeCloseTo(TestExpiryCalculator.DefaultTimeToExpire, TimeSpan.FromMilliseconds(20)); +#endif + } + + [Fact] + public void CreateItemInitializesKeyValueAndTicks() + { + var timeToExpire = TimeSpan.FromHours(1); + + expiryCalculator.ExpireAfterCreate = (k, v) => + { + k.Should().Be(1); + v.Should().Be(2); + return timeToExpire; + }; + + var item = this.policy.CreateItem(1, 2); + + item.Key.Should().Be(1); + item.Value.Should().Be(2); +#if NETFRAMEWORK + item.TickCount.Should().BeCloseTo(StopwatchTickConverter.ToTicks(timeToExpire) + Stopwatch.GetTimestamp(), stopwatchDelta); +#else + item.TickCount.Should().BeCloseTo((long)timeToExpire.TotalMilliseconds + Environment.TickCount64, tickCountDelta); +#endif + } + + [Fact] + public void TouchUpdatesItemWasAccessed() + { + var item = this.policy.CreateItem(1, 2); + item.WasAccessed = false; + + this.policy.Touch(item); + + item.WasAccessed.Should().BeTrue(); + } + + [Fact] + public async Task TouchUpdatesTicksCount() + { + var item = this.policy.CreateItem(1, 2); + var tc = item.TickCount; + await Task.Delay(TimeSpan.FromMilliseconds(1)); + + this.policy.ShouldDiscard(item); // set the time in the policy + this.policy.Touch(item); + + item.TickCount.Should().BeGreaterThan(tc); + } + + [Fact] + public async Task UpdateUpdatesTickCount() + { + var item = this.policy.CreateItem(1, 2); + var tc = item.TickCount; + + await Task.Delay(TimeSpan.FromMilliseconds(20)); + + this.policy.Update(item); + + item.TickCount.Should().BeGreaterThan(tc); + } + + [Fact] + public void WhenItemIsExpiredShouldDiscardIsTrue() + { + var item = this.policy.CreateItem(1, 2); + +#if NETFRAMEWORK + item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(11)); +#else + item.TickCount = item.TickCount - TimeSpan.FromMilliseconds(11).ToEnvTick64(); +#endif + this.policy.ShouldDiscard(item).Should().BeTrue(); + } + + [Fact] + public void WhenItemIsNotExpiredShouldDiscardIsFalse() + { + var item = this.policy.CreateItem(1, 2); + +#if NETFRAMEWORK + item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(9)); +#else + item.TickCount = item.TickCount - (int)TimeSpan.FromMilliseconds(9).ToEnvTick64(); +#endif + + this.policy.ShouldDiscard(item).Should().BeFalse(); + } + + [Fact] + 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) + { + var item = CreateItem(wasAccessed, 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); + + 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); + + this.policy.RouteCold(item).Should().Be(expectedDestination); + } + + private LongTickCountLruItem CreateItem(bool wasAccessed, bool isExpired) + { + var item = this.policy.CreateItem(1, 2); + + item.WasAccessed = wasAccessed; + + if (isExpired) + { +#if NETFRAMEWORK + item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(11)); +#else + item.TickCount = item.TickCount - TimeSpan.FromMilliseconds(11).ToEnvTick64(); +#endif + } + + return item; + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lru/LruInfoTests.cs b/BitFaster.Caching.UnitTests/Lru/LruInfoTests.cs new file mode 100644 index 00000000..133cb469 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/LruInfoTests.cs @@ -0,0 +1,29 @@ +using System; +using BitFaster.Caching.Lru.Builder; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lru +{ + public class LruInfoTests + { + [Fact] + public void WhenExpiryNullGetExpiryReturnsNull() + { + var info = new LruInfo(); + + info.GetExpiry().Should().BeNull(); + } + + [Fact] + public void WhenExpiryCalcValueTypeDoesNotMatchThrows() + { + var info = new LruInfo(); + + info.SetExpiry(new TestExpiryCalculator()); + + Action act = () => info.GetExpiry(); + act.Should().Throw(); + } + } +} diff --git a/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs b/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs new file mode 100644 index 00000000..5ed5b35a --- /dev/null +++ b/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs @@ -0,0 +1,63 @@ +using System; + +namespace BitFaster.Caching.UnitTests +{ + /// + /// Defines a mechanism to determine the time to live for a cache item using function delegates. + /// + public class TestExpiryCalculator : IExpiryCalculator + { + public static readonly TimeSpan DefaultTimeToExpire = TimeSpan.FromMilliseconds(10); + + public Func ExpireAfterCreate { get; set; } + public Func ExpireAfterRead { get; set; } + public Func ExpireAfterUpdate { get; set; } + + public TestExpiryCalculator() + { + ExpireAfterCreate = (_, _) => DefaultTimeToExpire; + } + + /// + /// Initializes a new instance of the Expiry class. + /// + /// The delegate that computes the item time to expire. + public TestExpiryCalculator(Func expireAfter) + { + this.ExpireAfterCreate = expireAfter; + this.ExpireAfterRead = null; + this.ExpireAfterUpdate = null; + } + + /// + /// Initializes a new instance of the Expiry class. + /// + /// The delegate that computes the item time to expire at creation. + /// The delegate that computes the item time to expire after a read operation. + /// The delegate that computes the item time to expire after an update operation. + public TestExpiryCalculator(Func expireAfterCreate, Func expireAfterRead, Func expireAfterUpdate) + { + this.ExpireAfterCreate = expireAfterCreate; + this.ExpireAfterRead = expireAfterRead; + this.ExpireAfterUpdate = expireAfterUpdate; + } + + /// + public TimeSpan GetExpireAfterCreate(K key, V value) + { + return this.ExpireAfterCreate(key, value); + } + + /// + public TimeSpan GetExpireAfterRead(K key, V value, TimeSpan currentTtl) + { + return this.ExpireAfterRead == null ? this.ExpireAfterCreate(key, value) : this.ExpireAfterRead(key, value, currentTtl); + } + + /// + public TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan currentTtl) + { + return this.ExpireAfterUpdate == null ? this.ExpireAfterCreate(key, value) : this.ExpireAfterUpdate(key, value, currentTtl); + } + } +} diff --git a/BitFaster.Caching/CachePolicy.cs b/BitFaster.Caching/CachePolicy.cs index c4feb13e..c8dc7922 100644 --- a/BitFaster.Caching/CachePolicy.cs +++ b/BitFaster.Caching/CachePolicy.cs @@ -1,74 +1,86 @@ - -namespace BitFaster.Caching -{ - /// - /// Represents the cache policy. Cache policy is dependent on the parameters chosen - /// when constructing the cache. - /// - public class CachePolicy - { - /// - /// Initializes a new instance of the CachePolicy class with the specified eviction and expire after write policies. - /// - /// The eviction policy. - /// The expire after write policy. - public CachePolicy(Optional eviction, Optional expireAfterWrite) - { - this.Eviction = eviction; - this.ExpireAfterWrite = expireAfterWrite; - this.ExpireAfterAccess = Optional.None(); - } - - /// - /// Initializes a new instance of the CachePolicy class with the specified policies. - /// - /// The eviction policy. - /// The expire after write policy. - /// The expire after access policy. - public CachePolicy(Optional eviction, Optional expireAfterWrite, Optional expireAfterAccess) - { - this.Eviction = eviction; - this.ExpireAfterWrite = expireAfterWrite; - this.ExpireAfterAccess = expireAfterAccess; - } - - /// - /// Gets the bounded size eviction policy. This policy evicts items from the cache - /// if it exceeds capacity. - /// - public Optional Eviction { get; } - - /// - /// Gets the expire after write policy, if any. This policy evicts items after a - /// fixed duration since an entry's creation or most recent replacement. - /// - public Optional ExpireAfterWrite { get; } - - /// - /// Gets the expire after access policy, if any. This policy evicts items after a - /// fixed duration since an entry's creation or most recent read/write access. - /// - public Optional ExpireAfterAccess { get; } - - /// - /// If supported, trim expired items from the cache. - /// - /// True if expiry is supported and expired items were trimmed, otherwise false. - public bool TryTrimExpired() - { - if (ExpireAfterWrite.HasValue) - { - ExpireAfterWrite.Value.TrimExpired(); - return true; - } - - if (ExpireAfterAccess.HasValue) - { - ExpireAfterAccess.Value.TrimExpired(); - return true; - } - - return false; - } - } -} + +namespace BitFaster.Caching +{ + /// + /// Represents the cache policy. Cache policy is dependent on the parameters chosen + /// when constructing the cache. + /// + public class CachePolicy + { + /// + /// Initializes a new instance of the CachePolicy class with the specified eviction and expire after write policies. + /// + /// The eviction policy. + /// The expire after write policy. + public CachePolicy(Optional eviction, Optional expireAfterWrite) + : this(eviction, expireAfterWrite, Optional.None(), Optional.None()) + { + } + + /// + /// Initializes a new instance of the CachePolicy class with the specified policies. + /// + /// The eviction policy. + /// The expire after write policy. + /// The expire after access policy. + /// The expire after policy. + public CachePolicy(Optional eviction, Optional expireAfterWrite, Optional expireAfterAccess, Optional expireAfter) + { + this.Eviction = eviction; + this.ExpireAfterWrite = expireAfterWrite; + this.ExpireAfterAccess = expireAfterAccess; + this.ExpireAfter = expireAfter; + } + + /// + /// Gets the bounded size eviction policy. This policy evicts items from the cache + /// if it exceeds capacity. + /// + public Optional Eviction { get; } + + /// + /// Gets the expire after write policy, if any. This policy evicts items after a + /// fixed duration since an entry's creation or most recent replacement. + /// + public Optional ExpireAfterWrite { get; } + + /// + /// Gets the expire after access policy, if any. This policy evicts items after a + /// fixed duration since an entry's creation or most recent read/write access. + /// + public Optional ExpireAfterAccess { get; } + + /// + /// Gets the expire after policy, if any. This policy evicts items based on + /// a time to expire computed from the key and value. + /// + public Optional ExpireAfter { get; } + + /// + /// If supported, trim expired items from the cache. + /// + /// True if expiry is supported and expired items were trimmed, otherwise false. + public bool TryTrimExpired() + { + if (ExpireAfterWrite.HasValue) + { + ExpireAfterWrite.Value.TrimExpired(); + return true; + } + + if (ExpireAfterAccess.HasValue) + { + ExpireAfterAccess.Value.TrimExpired(); + return true; + } + + if (ExpireAfter.HasValue) + { + ExpireAfter.Value.TrimExpired(); + return true; + } + + return false; + } + } +} diff --git a/BitFaster.Caching/IDiscreteTimePolicy.cs b/BitFaster.Caching/IDiscreteTimePolicy.cs new file mode 100644 index 00000000..68f53864 --- /dev/null +++ b/BitFaster.Caching/IDiscreteTimePolicy.cs @@ -0,0 +1,23 @@ +using System; + +namespace BitFaster.Caching +{ + /// + /// Represents a per item time based cache policy. + /// + public interface IDiscreteTimePolicy + { + /// + /// Gets the time to live for an item in the cache. + /// + /// The key of the item. + /// If the key exists, the time to live for the item with the specified key. + /// True if the key exists, otherwise false. + bool TryGetTimeToExpire(K key, out TimeSpan timeToExpire); + + /// + /// Remove all expired items from the cache. + /// + void TrimExpired(); + } +} diff --git a/BitFaster.Caching/IExpiryCalculator.cs b/BitFaster.Caching/IExpiryCalculator.cs new file mode 100644 index 00000000..c7a16c41 --- /dev/null +++ b/BitFaster.Caching/IExpiryCalculator.cs @@ -0,0 +1,28 @@ +using System; + +namespace BitFaster.Caching +{ + /// + /// Defines a mechanism to calculate when cache entries expire based on the item key, value + /// or existing time to expire. + /// + public interface IExpiryCalculator + { + /// + /// Specify the inital time to expire after an entry is created. + /// + TimeSpan GetExpireAfterCreate(K key, V value); + + /// + /// Specify the time to expire after an entry is read. The current time to expire may be + /// be returned to not modify the expiration time. + /// + TimeSpan GetExpireAfterRead(K key, V value, TimeSpan current); + + /// + /// Specify the time to expire after an entry is updated.The current time to expire may be + /// be returned to not modify the expiration time. + /// + TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan current); + } +} diff --git a/BitFaster.Caching/ITimePolicy.cs b/BitFaster.Caching/ITimePolicy.cs index 791ea10e..f698b1af 100644 --- a/BitFaster.Caching/ITimePolicy.cs +++ b/BitFaster.Caching/ITimePolicy.cs @@ -1,24 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace BitFaster.Caching -{ - /// - /// Represents a time based cache policy. - /// - public interface ITimePolicy - { - /// - /// Gets the time to live for items in the cache. - /// - TimeSpan TimeToLive { get; } - - /// - /// Remove all expired items from the cache. - /// - void TrimExpired(); - } -} +using System; + +namespace BitFaster.Caching +{ + /// + /// Represents a fixed time based cache policy. + /// + public interface ITimePolicy + { + /// + /// Gets the time to expire for items in the cache. + /// + TimeSpan TimeToLive { get; } + + /// + /// Remove all expired items from the cache. + /// + void TrimExpired(); + } +} diff --git a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs index 5fc7871c..c7c2ce29 100644 --- a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs @@ -13,6 +13,17 @@ internal AsyncConcurrentLruBuilder(LruInfo info) { } + /// + /// Evict after a duration calculated for each item using the specified IExpiryCalculator. + /// + /// The expiry calculator that determines item time to expire. + /// A ConcurrentLruBuilder + public AsyncConcurrentLruBuilder WithExpireAfter(IExpiryCalculator expiry) + { + this.info.SetExpiry(expiry); + return this; + } + /// public override IAsyncCache Build() { diff --git a/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs index d0930c97..2f4ba57e 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs @@ -20,6 +20,8 @@ internal AtomicAsyncConcurrentLruBuilder(ConcurrentLruBuilder public override IAsyncCache Build() { + info.ThrowIfExpirySpecified("AsAtomic"); + var level1 = inner.Build(); return new AtomicFactoryAsyncCache(level1); } diff --git a/BitFaster.Caching/Lru/Builder/AtomicConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicConcurrentLruBuilder.cs index a2b65668..81dcf4ae 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicConcurrentLruBuilder.cs @@ -20,6 +20,8 @@ internal AtomicConcurrentLruBuilder(ConcurrentLruBuilder> /// public override ICache Build() { + info.ThrowIfExpirySpecified("AsAtomic"); + var level1 = inner.Build(); return new AtomicFactoryCache(level1); } diff --git a/BitFaster.Caching/Lru/Builder/AtomicScopedAsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicScopedAsyncConcurrentLruBuilder.cs index 709cf6aa..f359a98c 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicScopedAsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicScopedAsyncConcurrentLruBuilder.cs @@ -21,6 +21,8 @@ internal AtomicScopedAsyncConcurrentLruBuilder(AsyncConcurrentLruBuilder public override IScopedAsyncCache Build() { + info.ThrowIfExpirySpecified("AsAtomic or AsScoped"); + // this is a legal type conversion due to the generic constraint on W var scopedInnerCache = inner.Build() as ICache>; diff --git a/BitFaster.Caching/Lru/Builder/AtomicScopedConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicScopedConcurrentLruBuilder.cs index fa38c331..eff590f4 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicScopedConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicScopedConcurrentLruBuilder.cs @@ -21,6 +21,8 @@ internal AtomicScopedConcurrentLruBuilder(ConcurrentLruBuilder public override IScopedCache Build() { + info.ThrowIfExpirySpecified("AsAtomic or AsScoped"); + var level1 = inner.Build() as ICache>; return new AtomicFactoryScopedCache(level1); } diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index 166b32c4..411c4829 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -1,43 +1,76 @@ -using System; -using System.Collections.Generic; - -namespace BitFaster.Caching.Lru.Builder -{ - /// - /// Parameters for buiding an LRU. - /// - /// The LRU key type - // backcompat: make class internal - public sealed class LruInfo - { - /// - /// Gets or sets the capacity partition. - /// - public ICapacityPartition Capacity { get; set; } = new FavorWarmPartition(128); - - /// - /// Gets or sets the concurrency level. - /// - public int ConcurrencyLevel { get; set; } = Defaults.ConcurrencyLevel; - - /// - /// Gets or sets the time to expire after write. - /// - public TimeSpan? TimeToExpireAfterWrite { get; set; } = null; - - /// - /// Gets or sets the time to expire after access. - /// - public TimeSpan? TimeToExpireAfterAccess { get; set; } = null; - - /// - /// Gets or sets a value indicating whether to use metrics. - /// - public bool WithMetrics { get; set; } = false; - - /// - /// Gets or sets the KeyComparer. - /// - public IEqualityComparer KeyComparer { get; set; } = EqualityComparer.Default; - } -} +using System; +using System.Collections.Generic; + +namespace BitFaster.Caching.Lru.Builder +{ + /// + /// Parameters for buiding an LRU. + /// + /// The LRU key type + // backcompat: make class internal + public sealed class LruInfo + { + private object expiry = null; + + /// + /// Gets or sets the capacity partition. + /// + public ICapacityPartition Capacity { get; set; } = new FavorWarmPartition(128); + + /// + /// Gets or sets the concurrency level. + /// + public int ConcurrencyLevel { get; set; } = Defaults.ConcurrencyLevel; + + /// + /// Gets or sets the time to expire after write. + /// + public TimeSpan? TimeToExpireAfterWrite { get; set; } = null; + + /// + /// Gets or sets the time to expire after access. + /// + public TimeSpan? TimeToExpireAfterAccess { get; set; } = null; + + /// + /// Set the custom expiry. + /// + /// The expiry + public void SetExpiry(IExpiryCalculator expiry) => this.expiry = expiry; + + /// + /// Get the custom expiry. + /// + /// The expiry. + public IExpiryCalculator GetExpiry() + { + if (this.expiry == null) + { + return null; + } + + var e = this.expiry as IExpiryCalculator; + + if (e == null) + Throw.InvalidOp($"Incompatible IExpiryCalculator value generic type argument, expected {typeof(IExpiryCalculator)} but found {this.expiry.GetType()}"); + + return e; + } + + /// + /// Gets or sets a value indicating whether to use metrics. + /// + public bool WithMetrics { get; set; } = false; + + /// + /// Gets or sets the KeyComparer. + /// + public IEqualityComparer KeyComparer { get; set; } = EqualityComparer.Default; + + internal void ThrowIfExpirySpecified(string extensionName) + { + if (this.expiry != null) + Throw.InvalidOp("WithExpireAfter is not compatible with " + extensionName); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs index 7b6140b3..98325447 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs @@ -20,6 +20,8 @@ internal ScopedAsyncConcurrentLruBuilder(AsyncConcurrentLruBuilder> /// public override IScopedAsyncCache Build() { + info.ThrowIfExpirySpecified("AsScoped"); + // this is a legal type conversion due to the generic constraint on W var scopedInnerCache = inner.Build() as IAsyncCache>; diff --git a/BitFaster.Caching/Lru/Builder/ScopedConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedConcurrentLruBuilder.cs index d6fe4708..5bc24073 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedConcurrentLruBuilder.cs @@ -21,6 +21,8 @@ internal ScopedConcurrentLruBuilder(ConcurrentLruBuilder inner) /// public override IScopedCache Build() { + info.ThrowIfExpirySpecified("AsScoped"); + // this is a legal type conversion due to the generic constraint on W var scopedInnerCache = inner.Build() as ICache>; diff --git a/BitFaster.Caching/Lru/ConcurrentLru.cs b/BitFaster.Caching/Lru/ConcurrentLru.cs index 2ef86e4b..6e06ac88 100644 --- a/BitFaster.Caching/Lru/ConcurrentLru.cs +++ b/BitFaster.Caching/Lru/ConcurrentLru.cs @@ -18,13 +18,23 @@ internal static ICache Create(LruInfo info) if (info.TimeToExpireAfterWrite.HasValue && info.TimeToExpireAfterAccess.HasValue) Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfterAccess is not supported."); - return (info.WithMetrics, info.TimeToExpireAfterWrite.HasValue, info.TimeToExpireAfterAccess.HasValue) switch + var expiry = info.GetExpiry(); + + if (info.TimeToExpireAfterWrite.HasValue && expiry != null) + Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfter is not supported."); + + if (info.TimeToExpireAfterAccess.HasValue && expiry != null) + Throw.InvalidOp("Specifying both ExpireAfterAccess and ExpireAfter is not supported."); + + return (info.WithMetrics, info.TimeToExpireAfterWrite.HasValue, info.TimeToExpireAfterAccess.HasValue, expiry != null) switch { - (true, false, false) => new ConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer), - (true, true, false) => new ConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value), - (false, true, false) => new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value), - (true, false, true) => CreateExpireAfterAccess>(info), - (false, false, true) => CreateExpireAfterAccess>(info), + (true, false, false, false) => new ConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer), + (true, true, false, false) => new ConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value), + (false, true, false, false) => new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value), + (true, false, true, false) => CreateExpireAfterAccess>(info), + (false, false, true, false) => CreateExpireAfterAccess>(info), + (true, false, false, true) => CreateExpireAfter>(info, expiry), + (false, false, false, true) => CreateExpireAfter>(info, expiry), _ => new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer), }; } @@ -34,5 +44,11 @@ private static ICache CreateExpireAfterAccess(LruInfo info) w return new ConcurrentLruCore, AfterAccessLongTicksPolicy, TP>( info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new AfterAccessLongTicksPolicy(info.TimeToExpireAfterAccess.Value), default); } + + private static ICache CreateExpireAfter(LruInfo info, IExpiryCalculator expiry) where TP : struct, ITelemetryPolicy + { + return new ConcurrentLruCore, DiscretePolicy, TP>( + info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new DiscretePolicy(expiry), default); + } } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 35f30187..e58c7cff 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -37,6 +37,17 @@ internal ConcurrentLruBuilder(LruInfo info) { } + /// + /// Evict after a duration calculated for each item using the specified IExpiryCalculator. + /// + /// The expiry calculator that determines item time to expire. + /// A ConcurrentLruBuilder + public ConcurrentLruBuilder WithExpireAfter(IExpiryCalculator expiry) + { + this.info.SetExpiry(expiry); + return this; + } + /// public override ICache Build() { diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index ac35e27d..1a3105dd 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -199,7 +199,7 @@ private bool TryAdd(K key, V value) public V GetOrAdd(K key, Func valueFactory) { while (true) - { + { if (this.TryGet(key, out var value)) { return value; @@ -396,12 +396,12 @@ public bool TryUpdate(K key, V value) /// ///Note: Updates to existing items do not affect LRU order. Added items are at the top of the LRU. public void AddOrUpdate(K key, V value) - { + { while (true) - { + { // first, try to update if (this.TryUpdate(key, value)) - { + { return; } @@ -568,7 +568,7 @@ private void Cycle(int hotCount) (dest, count) = CycleCold(count); } } - + // If nothing was removed yet, constrain the size of warm and cold by discarding the coldest item. if (dest != ItemDestination.Remove) { @@ -787,15 +787,21 @@ IEnumerator IEnumerable.GetEnumerator() } private static CachePolicy CreatePolicy(ConcurrentLruCore lru) - { + { var p = new Proxy(lru); if (typeof(P) == typeof(AfterAccessLongTicksPolicy)) { - return new CachePolicy(new Optional(p), Optional.None(), new Optional(p)); + return new CachePolicy(new Optional(p), Optional.None(), new Optional(p), Optional.None()); } - return new CachePolicy(new Optional(p), lru.itemPolicy.CanDiscard() ? new Optional(p) : Optional.None()); + // IsAssignableFrom is a jit intrinsic https://github.com/dotnet/runtime/issues/4920 + if (typeof(IDiscreteItemPolicy).IsAssignableFrom(typeof(P))) + { + return new CachePolicy(new Optional(p), Optional.None(), Optional.None(), new Optional(new DiscreteExpiryProxy(lru))); + } + + return new CachePolicy(new Optional(p), lru.itemPolicy.CanDiscard() ? new Optional(p) : Optional.None()); } private static Optional CreateMetrics(ConcurrentLruCore lru) @@ -882,5 +888,38 @@ public void TrimExpired() lru.TrimExpired(); } } + + private class DiscreteExpiryProxy : IDiscreteTimePolicy + { + private readonly ConcurrentLruCore lru; + private readonly IDiscreteItemPolicy policy; + + public DiscreteExpiryProxy(ConcurrentLruCore lru) + { + this.lru = lru; + + // note: using the interface here will box the policy (since it is constrained to be a value type) + // store in the proxy so that repeatedly calling TryGetTimeToExpire on the same instance won't allocate. + this.policy = lru.itemPolicy as IDiscreteItemPolicy; + } + + public void TrimExpired() + { + lru.TrimExpired(); + } + + public bool TryGetTimeToExpire(TKey key, out TimeSpan timeToLive) + { + if (key is K k && lru.dictionary.TryGetValue(k, out var item)) + { + LongTickCountLruItem tickItem = item as LongTickCountLruItem; + timeToLive = policy.ConvertTicks(tickItem.TickCount); + return true; + } + + timeToLive = default; + return false; + } + } } } diff --git a/BitFaster.Caching/Lru/DiscreteStopwatchPolicy.cs b/BitFaster.Caching/Lru/DiscreteStopwatchPolicy.cs new file mode 100644 index 00000000..081550cc --- /dev/null +++ b/BitFaster.Caching/Lru/DiscreteStopwatchPolicy.cs @@ -0,0 +1,124 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace BitFaster.Caching.Lru +{ +#if !NETCOREAPP3_0_OR_GREATER + internal readonly struct DiscretePolicy : IDiscreteItemPolicy + { + private readonly IExpiryCalculator expiry; + private readonly Time time; + + public TimeSpan TimeToLive => TimeSpan.Zero; + + /// + public TimeSpan ConvertTicks(long ticks) => StopwatchTickConverter.FromTicks(ticks - Stopwatch.GetTimestamp()); + + public DiscretePolicy(IExpiryCalculator expiry) + { + this.expiry = expiry; + this.time = new Time(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LongTickCountLruItem CreateItem(K key, V value) + { + var expiry = this.expiry.GetExpireAfterCreate(key, value); + return new LongTickCountLruItem(key, value, StopwatchTickConverter.ToTicks(expiry) + Stopwatch.GetTimestamp()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Touch(LongTickCountLruItem item) + { + var currentExpiry = StopwatchTickConverter.FromTicks(item.TickCount - this.time.Last); + var newExpiry = expiry.GetExpireAfterRead(item.Key, item.Value, currentExpiry); + item.TickCount = this.time.Last + StopwatchTickConverter.ToTicks(newExpiry); + item.WasAccessed = true; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(LongTickCountLruItem item) + { + var time = Stopwatch.GetTimestamp(); + var currentExpiry = StopwatchTickConverter.FromTicks(item.TickCount - time); + var newExpiry = expiry.GetExpireAfterUpdate(item.Key, item.Value, currentExpiry); + item.TickCount = time + StopwatchTickConverter.ToTicks(newExpiry); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldDiscard(LongTickCountLruItem item) + { + this.time.Last = Stopwatch.GetTimestamp(); + if (this.time.Last > item.TickCount) + { + return true; + } + + return false; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CanDiscard() + { + return true; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteHot(LongTickCountLruItem item) + { + if (this.ShouldDiscard(item)) + { + return ItemDestination.Remove; + } + + if (item.WasAccessed) + { + return ItemDestination.Warm; + } + + return ItemDestination.Cold; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteWarm(LongTickCountLruItem item) + { + if (this.ShouldDiscard(item)) + { + return ItemDestination.Remove; + } + + if (item.WasAccessed) + { + return ItemDestination.Warm; + } + + return ItemDestination.Cold; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteCold(LongTickCountLruItem item) + { + if (this.ShouldDiscard(item)) + { + return ItemDestination.Remove; + } + + if (item.WasAccessed) + { + return ItemDestination.Warm; + } + + return ItemDestination.Remove; + } + } +#endif +} diff --git a/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs b/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs new file mode 100644 index 00000000..f7daefed --- /dev/null +++ b/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs @@ -0,0 +1,124 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitFaster.Caching.Lru +{ +#if NETCOREAPP3_0_OR_GREATER + // Here the time values are in milliseconds + internal readonly struct DiscretePolicy : IDiscreteItemPolicy + { + private readonly IExpiryCalculator expiry; + private readonly Time time; + + public TimeSpan TimeToLive => TimeSpan.Zero; + + /// + public TimeSpan ConvertTicks(long ticks) => TimeSpan.FromMilliseconds(ticks - Environment.TickCount64); + + public DiscretePolicy(IExpiryCalculator expiry) + { + this.expiry = expiry; + this.time = new Time(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LongTickCountLruItem CreateItem(K key, V value) + { + var expiry = this.expiry.GetExpireAfterCreate(key, value); + return new LongTickCountLruItem(key, value, (long)expiry.TotalMilliseconds + Environment.TickCount64); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Touch(LongTickCountLruItem item) + { + var currentExpiry = TimeSpan.FromMilliseconds(item.TickCount - this.time.Last); + var newExpiry = expiry.GetExpireAfterRead(item.Key, item.Value, currentExpiry); + item.TickCount = this.time.Last + (long)newExpiry.TotalMilliseconds; + item.WasAccessed = true; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(LongTickCountLruItem item) + { + var time = Environment.TickCount64; + var currentExpiry = TimeSpan.FromMilliseconds(item.TickCount - time); + var newExpiry = expiry.GetExpireAfterUpdate(item.Key, item.Value, currentExpiry); + item.TickCount = time + (long)newExpiry.TotalMilliseconds; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldDiscard(LongTickCountLruItem item) + { + this.time.Last = Environment.TickCount64; + if (this.time.Last > item.TickCount) + { + return true; + } + + return false; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CanDiscard() + { + return true; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteHot(LongTickCountLruItem item) + { + if (this.ShouldDiscard(item)) + { + return ItemDestination.Remove; + } + + if (item.WasAccessed) + { + return ItemDestination.Warm; + } + + return ItemDestination.Cold; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteWarm(LongTickCountLruItem item) + { + if (this.ShouldDiscard(item)) + { + return ItemDestination.Remove; + } + + if (item.WasAccessed) + { + return ItemDestination.Warm; + } + + return ItemDestination.Cold; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ItemDestination RouteCold(LongTickCountLruItem item) + { + if (this.ShouldDiscard(item)) + { + return ItemDestination.Remove; + } + + if (item.WasAccessed) + { + return ItemDestination.Warm; + } + + return ItemDestination.Remove; + } + } +#endif +} diff --git a/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs b/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs new file mode 100644 index 00000000..da934cb3 --- /dev/null +++ b/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs @@ -0,0 +1,19 @@ +using System; + +namespace BitFaster.Caching.Lru +{ + /// + /// A marker interface for discrete expiry policies. + /// + /// + /// + public interface IDiscreteItemPolicy : IItemPolicy> + { + /// + /// Convert ticks to a TimeSpan. + /// + /// The number of ticks to convert. + /// Ticks converted to a TimeSpan + TimeSpan ConvertTicks(long ticks); + } +}