From 4c899f1e90bb7a95fb40155b659f810db9d2c020 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 12:12:56 -0800 Subject: [PATCH 01/32] variable policy outline --- .../Lru/ConcurrentLruBuilderTests.cs | 14 + BitFaster.Caching/CachePolicy.cs | 46 ++- BitFaster.Caching/ITimePolicy.cs | 26 +- .../Lru/Builder/LruBuilderBase.cs | 6 + BitFaster.Caching/Lru/Builder/LruInfo.cs | 7 +- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 24 +- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 60 +++- BitFaster.Caching/Lru/CustomPolicy.cs | 265 ++++++++++++++++++ 8 files changed, 429 insertions(+), 19 deletions(-) create mode 100644 BitFaster.Caching/Lru/CustomPolicy.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 75f6f748..90d3e406 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -178,6 +178,20 @@ public void TestExpireAfterReadAndExpireAfterWriteThrows() act.Should().Throw(); } + [Fact] + public void TestExpireAfter() + { + ICache expireAfter = new ConcurrentLruBuilder() + .WithExpireAfter(new Expiry((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(); + } + // There are 15 combinations to test: // ----------------------------- //1 WithAtomic diff --git a/BitFaster.Caching/CachePolicy.cs b/BitFaster.Caching/CachePolicy.cs index 10873b88..8c8ed746 100644 --- a/BitFaster.Caching/CachePolicy.cs +++ b/BitFaster.Caching/CachePolicy.cs @@ -29,6 +29,21 @@ public CachePolicy(Optional eviction, Optional expi this.Eviction = eviction; this.ExpireAfterWrite = expireAfterWrite; this.ExpireAfterAccess = expireAfterAccess; + } + + /// + /// 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; } /// @@ -47,6 +62,35 @@ public CachePolicy(Optional eviction, Optional expi /// 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; } + public Optional ExpireAfterAccess { get; } + + /// + /// Gets the expire after policy, if any. This policy evicts items after a + /// variable time to live computed from the key and value. + /// + public Optional ExpireAfter { get; } + + 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/ITimePolicy.cs b/BitFaster.Caching/ITimePolicy.cs index 791ea10e..5a1e1c5a 100644 --- a/BitFaster.Caching/ITimePolicy.cs +++ b/BitFaster.Caching/ITimePolicy.cs @@ -1,13 +1,9 @@ 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. + /// Represents a fixed time based cache policy. /// public interface ITimePolicy { @@ -16,6 +12,26 @@ public interface ITimePolicy /// TimeSpan TimeToLive { get; } + /// + /// Remove all expired items from the cache. + /// + void TrimExpired(); + } + + /// + /// Represents a variable time based cache policy. + /// + /// backcompat: this should be generic in terms of Key to avoid object arg. + public interface IVariableTimePolicy + { + /// + /// 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 TryGetTimeToLive(object key, out TimeSpan timeToLive); + /// /// Remove all expired items from the cache. /// diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs index 77834fac..d3ad651a 100644 --- a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -98,6 +98,12 @@ public TBuilder WithExpireAfterAccess(TimeSpan expiration) return this as TBuilder; } + public TBuilder WithExpireAfter(IExpiry expiry) + { + this.info.ExpireAfter = expiry; + return this as TBuilder; + } + /// /// Builds a cache configured via the method calls invoked on the builder instance. /// diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index 166b32c4..fc3ebbec 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -28,7 +28,12 @@ public sealed class LruInfo /// /// Gets or sets the time to expire after access. /// - public TimeSpan? TimeToExpireAfterAccess { get; set; } = null; + public TimeSpan? TimeToExpireAfterAccess { get; set; } = null; + + /// + /// Gets or sets the custom expire after. + /// + public object ExpireAfter { get; set; } = null; /// /// Gets or sets a value indicating whether to use metrics. diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 96f5192a..6b70fc32 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -41,7 +41,13 @@ internal ConcurrentLruBuilder(LruInfo info) public override ICache Build() { if (info.TimeToExpireAfterWrite.HasValue && info.TimeToExpireAfterAccess.HasValue) - Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfterAccess is not supported."); + Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfterAccess is not supported."); + + if (info.TimeToExpireAfterWrite.HasValue && info.ExpireAfter != null) + Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfter is not supported."); + + if (info.TimeToExpireAfterAccess.HasValue && info.ExpireAfter != null) + Throw.InvalidOp("Specifying both ExpireAfterAccess and ExpireAfter is not supported."); return info switch { @@ -50,6 +56,8 @@ public override ICache Build() LruInfo i when i.TimeToExpireAfterWrite.HasValue && !i.TimeToExpireAfterAccess.HasValue => new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value), LruInfo i when i.WithMetrics && !i.TimeToExpireAfterWrite.HasValue && i.TimeToExpireAfterAccess.HasValue => CreateExpireAfterAccess>(info), LruInfo i when !i.TimeToExpireAfterWrite.HasValue && i.TimeToExpireAfterAccess.HasValue => CreateExpireAfterAccess>(info), + LruInfo i when i.WithMetrics && i.ExpireAfter != null => CreateExpireAfter>(info), + LruInfo i when i.ExpireAfter != null => CreateExpireAfter>(info), _ => new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer), }; } @@ -58,6 +66,20 @@ private static ICache CreateExpireAfterAccess(LruInfo info) where T { return new ConcurrentLruCore, AfterAccessLongTicksPolicy, TP>( info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new AfterAccessLongTicksPolicy(info.TimeToExpireAfterAccess.Value), default); + } + + private static ICache CreateExpireAfter(LruInfo info) where TP : struct, ITelemetryPolicy + { + if (info.ExpireAfter is not IExpiry) + { + Throw.InvalidOp($"Expiry must be of type {typeof(IExpiry)}."); + } + + var expiry = info.ExpireAfter as IExpiry; + + return new ConcurrentLruCore, CustomExpiryPolicy, TP>( + info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new CustomExpiryPolicy(expiry), default); + } } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index b3b75593..b41fe755 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; @@ -379,7 +379,7 @@ public bool TryUpdate(K key, V value) V oldValue = existing.Value; existing.Value = value; this.itemPolicy.Update(existing); -// backcompat: remove conditional compile + // backcompat: remove conditional compile #if NETCOREAPP3_0_OR_GREATER this.telemetryPolicy.OnItemUpdated(existing.Key, oldValue, existing.Value); #endif @@ -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; } @@ -558,7 +558,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) { @@ -777,15 +777,20 @@ 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()); + } + + if (typeof(P) == typeof(CustomExpiryPolicy)) + { + return new CachePolicy(new Optional(p), Optional.None(), Optional.None(), new Optional(new VariableTimeProxy(lru))); } - return new CachePolicy(new Optional(p), lru.itemPolicy.CanDiscard() ? new Optional(p) : Optional.None()); + return new CachePolicy(new Optional(p), lru.itemPolicy.CanDiscard() ? new Optional(p) : Optional.None()); } private static Optional CreateMetrics(ConcurrentLruCore lru) @@ -840,7 +845,7 @@ public Proxy(ConcurrentLruCore lru) public long Evicted => lru.telemetryPolicy.Evicted; -// backcompat: remove conditional compile + // backcompat: remove conditional compile #if NETCOREAPP3_0_OR_GREATER public long Updated => lru.telemetryPolicy.Updated; #endif @@ -854,7 +859,7 @@ public event EventHandler> ItemRemoved remove { this.lru.telemetryPolicy.ItemRemoved -= value; } } -// backcompat: remove conditional compile + // backcompat: remove conditional compile #if NETCOREAPP3_0_OR_GREATER public event EventHandler> ItemUpdated { @@ -872,5 +877,38 @@ public void TrimExpired() lru.TrimExpired(); } } + + private class VariableTimeProxy : IVariableTimePolicy + { + private readonly ConcurrentLruCore lru; + + public VariableTimeProxy(ConcurrentLruCore lru) + { + this.lru = lru; + } + + public void TrimExpired() + { + lru.TrimExpired(); + } + + public bool TryGetTimeToLive(object key, out TimeSpan timeToLive) + { + if (key is K k) + { + if (lru.dictionary.TryGetValue(k, out var item)) + { + if (item is LongTickCountLruItem tickItem) + { + timeToLive = TimeSpan.FromTicks(tickItem.TickCount); + return true; + } + } + } + + timeToLive = default; + return false; + } + } } } diff --git a/BitFaster.Caching/Lru/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs new file mode 100644 index 00000000..eeff624c --- /dev/null +++ b/BitFaster.Caching/Lru/CustomPolicy.cs @@ -0,0 +1,265 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitFaster.Caching.Lru +{ + public interface IExpiry + { + Func GetExpireAfterCreate { get; } + + Func GetExpireAfterRead { get; } + + Func GetExpireAfterUpdate { get; } + } + + public readonly struct Expiry : IExpiry + { + private readonly Func expireAfterCreate; + private readonly Func expireAfterRead; + private readonly Func expireAfterUpdate; + + public Expiry(Func expireAfterCreate) + { + this.expireAfterCreate = expireAfterCreate; + this.expireAfterRead = expireAfterCreate; + this.expireAfterUpdate = expireAfterCreate; + } + + public Expiry(Func expireAfterCreate, Func expireAfterRead, Func expireAfterUpdate) + { + this.expireAfterCreate = expireAfterCreate; + this.expireAfterRead = expireAfterRead; + this.expireAfterUpdate = expireAfterUpdate; + } + + public Func GetExpireAfterCreate => expireAfterCreate; + + public Func GetExpireAfterRead => expireAfterRead; + + public Func GetExpireAfterUpdate => expireAfterUpdate; + } + +#if NETCOREAPP3_0_OR_GREATER + internal readonly struct CustomExpiryPolicy : IItemPolicy> + { + private readonly IExpiry expiry; + private readonly Time time; + + public TimeSpan TimeToLive => TimeSpan.Zero; + + public CustomExpiryPolicy(IExpiry expiry) + { + this.expiry = expiry; + this.time = new Time(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LongTickCountLruItem CreateItem(K key, V value) + { + var ttl = expiry.GetExpireAfterCreate(key, value); + return new LongTickCountLruItem(key, value, ttl.Ticks + Environment.TickCount64); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Touch(LongTickCountLruItem item) + { + var ttl = expiry.GetExpireAfterRead(item.Key, item.Value); + item.TickCount = this.time.Last + ttl.Ticks; + item.WasAccessed = true; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(LongTickCountLruItem item) + { + var ttl = expiry.GetExpireAfterUpdate(item.Key, item.Value); + item.TickCount = Environment.TickCount64 + ttl.Ticks; + } + + /// + [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; + } + } +#else + // TODO: this should use stopwatch timing + internal readonly struct CustomExpiryPolicy : IItemPolicy> + { + private readonly IExpiry expiry; + private readonly Time time; + + public TimeSpan TimeToLive => TimeSpan.Zero; + + public CustomExpiryPolicy(IExpiry expiry) + { + this.expiry = expiry; + this.time = new Time(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LongTickCountLruItem CreateItem(K key, V value) + { + var ttl = expiry.GetExpireAfterCreate(key, value); + return new LongTickCountLruItem(key, value, ttl.Ticks + Environment.TickCount); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Touch(LongTickCountLruItem item) + { + var ttl = expiry.GetExpireAfterRead(item.Key, item.Value); + item.TickCount = this.time.Last + ttl.Ticks; + item.WasAccessed = true; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(LongTickCountLruItem item) + { + var ttl = expiry.GetExpireAfterUpdate(item.Key, item.Value); + item.TickCount = Environment.TickCount + ttl.Ticks; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldDiscard(LongTickCountLruItem item) + { + this.time.Last = Environment.TickCount; + 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 +} From 5ef0e14613f5f8426352f971a4ff3a222dec6dcf Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 16:51:10 -0800 Subject: [PATCH 02/32] disallow --- .../Lru/Builder/AsyncConcurrentLruBuilder.cs | 6 ++ .../Lru/Builder/LruBuilderBase.cs | 6 +- BitFaster.Caching/Lru/Builder/LruInfo.cs | 92 +++++++++---------- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 6 ++ 4 files changed, 59 insertions(+), 51 deletions(-) diff --git a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs index db39e689..ca0e53b7 100644 --- a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs @@ -13,6 +13,12 @@ internal AsyncConcurrentLruBuilder(LruInfo info) { } + public AsyncConcurrentLruBuilder WithExpireAfter(IExpiry expiry) + { + this.info.ExpireAfter = expiry; + return this; + } + /// public override IAsyncCache Build() { diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs index d3ad651a..93883108 100644 --- a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -98,11 +98,7 @@ public TBuilder WithExpireAfterAccess(TimeSpan expiration) return this as TBuilder; } - public TBuilder WithExpireAfter(IExpiry expiry) - { - this.info.ExpireAfter = expiry; - return this as TBuilder; - } + /// /// Builds a cache configured via the method calls invoked on the builder instance. diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index fc3ebbec..37c0cbeb 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -1,48 +1,48 @@ -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. - /// +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 the custom expire after. - /// - public object ExpireAfter { 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; - } -} + /// + /// Gets or sets the custom expire after. + /// + public object ExpireAfter { 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; + } +} diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 6b70fc32..74bf49ce 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -35,6 +35,12 @@ public ConcurrentLruBuilder() internal ConcurrentLruBuilder(LruInfo info) : base(info) { + } + + public ConcurrentLruBuilder WithExpireAfter(IExpiry expiry) + { + this.info.ExpireAfter = expiry; + return this; } /// From 01aa2091a0f0d2ea8602156768f4bf0df2736fa3 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 17:00:11 -0800 Subject: [PATCH 03/32] guards --- .../Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs | 2 ++ BitFaster.Caching/Lru/Builder/AtomicConcurrentLruBuilder.cs | 2 ++ .../Lru/Builder/AtomicScopedAsyncConcurrentLruBuilder.cs | 2 ++ .../Lru/Builder/AtomicScopedConcurrentLruBuilder.cs | 2 ++ BitFaster.Caching/Lru/Builder/LruInfo.cs | 6 ++++++ .../Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs | 2 ++ BitFaster.Caching/Lru/Builder/ScopedConcurrentLruBuilder.cs | 2 ++ 7 files changed, 18 insertions(+) diff --git a/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs index d0930c97..0ee3039a 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.ThrowIfExpireAfterSpecified("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..d6e10ad8 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.ThrowIfExpireAfterSpecified("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..49a2a01a 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.ThrowIfExpireAfterSpecified("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..5651317c 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.ThrowIfExpireAfterSpecified("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 37c0cbeb..be1f2251 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -44,5 +44,11 @@ public sealed class LruInfo /// Gets or sets the KeyComparer. /// public IEqualityComparer KeyComparer { get; set; } = EqualityComparer.Default; + + internal void ThrowIfExpireAfterSpecified(string builderType) + { + if (this.ExpireAfter != null) + Throw.InvalidOp("ExpireAfter is not compatible with " + builderType); + } } } diff --git a/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs index 7b6140b3..3d945838 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.ThrowIfExpireAfterSpecified("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..c90e8a8f 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.ThrowIfExpireAfterSpecified("AsScoped"); + // this is a legal type conversion due to the generic constraint on W var scopedInnerCache = inner.Build() as ICache>; From 4d6d3cddfa89ea0ecd18667e56cdcadbffb4e537 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 17:07:47 -0800 Subject: [PATCH 04/32] rename --- .../Lru/ConcurrentLruBuilderTests.cs | 2 +- .../Lru/Builder/AsyncConcurrentLruBuilder.cs | 9 ++++++-- .../Lru/Builder/LruBuilderBase.cs | 2 -- BitFaster.Caching/Lru/Builder/LruInfo.cs | 10 ++++----- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 21 ++++++++++++------- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 90d3e406..535ccfb2 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -182,7 +182,7 @@ public void TestExpireAfterReadAndExpireAfterWriteThrows() public void TestExpireAfter() { ICache expireAfter = new ConcurrentLruBuilder() - .WithExpireAfter(new Expiry((k, v) => TimeSpan.FromMinutes(5))) + .WithExpiry(new Expiry((k, v) => TimeSpan.FromMinutes(5))) .Build(); expireAfter.Metrics.HasValue.Should().BeFalse(); diff --git a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs index ca0e53b7..198b55d2 100644 --- a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs @@ -13,9 +13,14 @@ internal AsyncConcurrentLruBuilder(LruInfo info) { } - public AsyncConcurrentLruBuilder WithExpireAfter(IExpiry expiry) + /// + /// Evict after a variable duration specified by an IExpiry instance. + /// + /// The expiry that determines item time to live. + /// A AsyncConcurrentLruBuilder + public AsyncConcurrentLruBuilder WithExpiry(IExpiry expiry) { - this.info.ExpireAfter = expiry; + this.info.Expiry = expiry; return this; } diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs index 93883108..77834fac 100644 --- a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -98,8 +98,6 @@ public TBuilder WithExpireAfterAccess(TimeSpan expiration) return this as TBuilder; } - - /// /// Builds a cache configured via the method calls invoked on the builder instance. /// diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index be1f2251..ca5562a7 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -31,9 +31,9 @@ public sealed class LruInfo public TimeSpan? TimeToExpireAfterAccess { get; set; } = null; /// - /// Gets or sets the custom expire after. + /// Gets or sets the custom expiry. /// - public object ExpireAfter { get; set; } = null; + public object Expiry { get; set; } = null; /// /// Gets or sets a value indicating whether to use metrics. @@ -45,10 +45,10 @@ public sealed class LruInfo /// public IEqualityComparer KeyComparer { get; set; } = EqualityComparer.Default; - internal void ThrowIfExpireAfterSpecified(string builderType) + internal void ThrowIfExpireAfterSpecified(string extensionName) { - if (this.ExpireAfter != null) - Throw.InvalidOp("ExpireAfter is not compatible with " + builderType); + if (this.Expiry != null) + Throw.InvalidOp("WithExpiry is not compatible with " + extensionName); } } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 74bf49ce..7b33938d 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -37,9 +37,14 @@ internal ConcurrentLruBuilder(LruInfo info) { } - public ConcurrentLruBuilder WithExpireAfter(IExpiry expiry) + /// + /// Evict after a variable duration specified by an IExpiry instance. + /// + /// The expiry that determines item time to live. + /// A ConcurrentLruBuilder + public ConcurrentLruBuilder WithExpiry(IExpiry expiry) { - this.info.ExpireAfter = expiry; + this.info.Expiry = expiry; return this; } @@ -49,10 +54,10 @@ public override ICache Build() if (info.TimeToExpireAfterWrite.HasValue && info.TimeToExpireAfterAccess.HasValue) Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfterAccess is not supported."); - if (info.TimeToExpireAfterWrite.HasValue && info.ExpireAfter != null) + if (info.TimeToExpireAfterWrite.HasValue && info.Expiry != null) Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfter is not supported."); - if (info.TimeToExpireAfterAccess.HasValue && info.ExpireAfter != null) + if (info.TimeToExpireAfterAccess.HasValue && info.Expiry != null) Throw.InvalidOp("Specifying both ExpireAfterAccess and ExpireAfter is not supported."); return info switch @@ -62,8 +67,8 @@ public override ICache Build() LruInfo i when i.TimeToExpireAfterWrite.HasValue && !i.TimeToExpireAfterAccess.HasValue => new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value), LruInfo i when i.WithMetrics && !i.TimeToExpireAfterWrite.HasValue && i.TimeToExpireAfterAccess.HasValue => CreateExpireAfterAccess>(info), LruInfo i when !i.TimeToExpireAfterWrite.HasValue && i.TimeToExpireAfterAccess.HasValue => CreateExpireAfterAccess>(info), - LruInfo i when i.WithMetrics && i.ExpireAfter != null => CreateExpireAfter>(info), - LruInfo i when i.ExpireAfter != null => CreateExpireAfter>(info), + LruInfo i when i.WithMetrics && i.Expiry != null => CreateExpireAfter>(info), + LruInfo i when i.Expiry != null => CreateExpireAfter>(info), _ => new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer), }; } @@ -76,12 +81,12 @@ private static ICache CreateExpireAfterAccess(LruInfo info) where T private static ICache CreateExpireAfter(LruInfo info) where TP : struct, ITelemetryPolicy { - if (info.ExpireAfter is not IExpiry) + if (info.Expiry is not IExpiry) { Throw.InvalidOp($"Expiry must be of type {typeof(IExpiry)}."); } - var expiry = info.ExpireAfter as IExpiry; + var expiry = info.Expiry as IExpiry; return new ConcurrentLruCore, CustomExpiryPolicy, TP>( info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new CustomExpiryPolicy(expiry), default); From ee69a0ff3c3785ed2ebb070671698cc1f8e4b555 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 17:15:38 -0800 Subject: [PATCH 05/32] docs --- BitFaster.Caching/Lru/CustomPolicy.cs | 36 ++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/BitFaster.Caching/Lru/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs index eeff624c..a824127f 100644 --- a/BitFaster.Caching/Lru/CustomPolicy.cs +++ b/BitFaster.Caching/Lru/CustomPolicy.cs @@ -3,28 +3,53 @@ namespace BitFaster.Caching.Lru { + /// + /// Defines a policy for determining the expiry of an item. + /// public interface IExpiry { + /// + /// Gets the expiry for an item after it is created. + /// Func GetExpireAfterCreate { get; } + /// + /// Gets the expiry for an item after it is read. + /// Func GetExpireAfterRead { get; } + /// + /// Gets the expiry for an item after it is updated. + /// Func GetExpireAfterUpdate { get; } } + /// + /// Defines a policy for determining the expiry of an item using function delegates. + /// public readonly struct Expiry : IExpiry { private readonly Func expireAfterCreate; private readonly Func expireAfterRead; private readonly Func expireAfterUpdate; - public Expiry(Func expireAfterCreate) + /// + /// Initializes a new instance of the Expiry class. + /// + /// The delegate that computes the item time to live. + public Expiry(Func expireAfter) { - this.expireAfterCreate = expireAfterCreate; - this.expireAfterRead = expireAfterCreate; - this.expireAfterUpdate = expireAfterCreate; + this.expireAfterCreate = expireAfter; + this.expireAfterRead = expireAfter; + this.expireAfterUpdate = expireAfter; } + /// + /// Initializes a new instance of the Expiry class. + /// + /// The delegate that computes the item time to live at creation. + /// The delegate that computes the item time to live after a read operation. + /// The delegate that computes the item time to live after an update operation. public Expiry(Func expireAfterCreate, Func expireAfterRead, Func expireAfterUpdate) { this.expireAfterCreate = expireAfterCreate; @@ -32,10 +57,13 @@ public Expiry(Func expireAfterCreate, Func expir this.expireAfterUpdate = expireAfterUpdate; } + /// public Func GetExpireAfterCreate => expireAfterCreate; + /// public Func GetExpireAfterRead => expireAfterRead; + /// public Func GetExpireAfterUpdate => expireAfterUpdate; } From 2c13c5355cf84f4cdb604b186d7218662976feec Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 17:28:03 -0800 Subject: [PATCH 06/32] generic method --- .../Lru/Builder/AsyncConcurrentLruBuilder.cs | 2 +- BitFaster.Caching/Lru/Builder/LruInfo.cs | 6 +++++- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 21 +++++++------------ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs index 198b55d2..1727a4e9 100644 --- a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs @@ -20,7 +20,7 @@ internal AsyncConcurrentLruBuilder(LruInfo info) /// A AsyncConcurrentLruBuilder public AsyncConcurrentLruBuilder WithExpiry(IExpiry expiry) { - this.info.Expiry = expiry; + this.info.SetExpiry(expiry); return this; } diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index ca5562a7..d5cc3ccf 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -33,7 +33,11 @@ public sealed class LruInfo /// /// Gets or sets the custom expiry. /// - public object Expiry { get; set; } = null; + private object Expiry { get; set; } = null; + + public void SetExpiry(IExpiry expiry) => Expiry = expiry; + + public IExpiry GetExpiry() => this.Expiry as IExpiry; /// /// Gets or sets a value indicating whether to use metrics. diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 7b33938d..4a6075cc 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -44,7 +44,7 @@ internal ConcurrentLruBuilder(LruInfo info) /// A ConcurrentLruBuilder public ConcurrentLruBuilder WithExpiry(IExpiry expiry) { - this.info.Expiry = expiry; + this.info.SetExpiry(expiry); return this; } @@ -54,10 +54,12 @@ public override ICache Build() if (info.TimeToExpireAfterWrite.HasValue && info.TimeToExpireAfterAccess.HasValue) Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfterAccess is not supported."); - if (info.TimeToExpireAfterWrite.HasValue && info.Expiry != null) + var expiry = info.GetExpiry(); + + if (info.TimeToExpireAfterWrite.HasValue && expiry != null) Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfter is not supported."); - if (info.TimeToExpireAfterAccess.HasValue && info.Expiry != null) + if (info.TimeToExpireAfterAccess.HasValue && expiry != null) Throw.InvalidOp("Specifying both ExpireAfterAccess and ExpireAfter is not supported."); return info switch @@ -67,8 +69,8 @@ public override ICache Build() LruInfo i when i.TimeToExpireAfterWrite.HasValue && !i.TimeToExpireAfterAccess.HasValue => new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value), LruInfo i when i.WithMetrics && !i.TimeToExpireAfterWrite.HasValue && i.TimeToExpireAfterAccess.HasValue => CreateExpireAfterAccess>(info), LruInfo i when !i.TimeToExpireAfterWrite.HasValue && i.TimeToExpireAfterAccess.HasValue => CreateExpireAfterAccess>(info), - LruInfo i when i.WithMetrics && i.Expiry != null => CreateExpireAfter>(info), - LruInfo i when i.Expiry != null => CreateExpireAfter>(info), + LruInfo i when i.WithMetrics && expiry != null => CreateExpireAfter>(info, expiry), + LruInfo _ when expiry != null => CreateExpireAfter>(info, expiry), _ => new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer), }; } @@ -79,15 +81,8 @@ private static ICache CreateExpireAfterAccess(LruInfo info) where T info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new AfterAccessLongTicksPolicy(info.TimeToExpireAfterAccess.Value), default); } - private static ICache CreateExpireAfter(LruInfo info) where TP : struct, ITelemetryPolicy + private static ICache CreateExpireAfter(LruInfo info, IExpiry expiry) where TP : struct, ITelemetryPolicy { - if (info.Expiry is not IExpiry) - { - Throw.InvalidOp($"Expiry must be of type {typeof(IExpiry)}."); - } - - var expiry = info.Expiry as IExpiry; - return new ConcurrentLruCore, CustomExpiryPolicy, TP>( info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new CustomExpiryPolicy(expiry), default); From 14e9e56f31fc75a59c89e67a878196740452b4ad Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 17:34:40 -0800 Subject: [PATCH 07/32] generic methods --- BitFaster.Caching/ITimePolicy.cs | 62 +++++++++++----------- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 13 ++--- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/BitFaster.Caching/ITimePolicy.cs b/BitFaster.Caching/ITimePolicy.cs index 5a1e1c5a..fc617c13 100644 --- a/BitFaster.Caching/ITimePolicy.cs +++ b/BitFaster.Caching/ITimePolicy.cs @@ -1,40 +1,40 @@ -using System; - -namespace BitFaster.Caching -{ - /// - /// Represents a fixed 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 live for items in the cache. + /// + TimeSpan TimeToLive { get; } + + /// + /// Remove all expired items from the cache. + /// + void TrimExpired(); } - /// - /// Represents a variable time based cache policy. - /// - /// backcompat: this should be generic in terms of Key to avoid object arg. + /// + /// Represents a variable time based cache policy. + /// + /// backcompat: this should be generic in terms of Key to avoid object arg. public interface IVariableTimePolicy { - /// - /// Gets the time to live for an item in the cache. + /// + /// 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 TryGetTimeToLive(object key, out TimeSpan timeToLive); + bool TryGetTimeToLive(K key, out TimeSpan timeToLive); - /// - /// Remove all expired items from the cache. - /// - void TrimExpired(); - } -} + /// + /// Remove all expired items from the cache. + /// + void TrimExpired(); + } +} diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index b41fe755..07cc6c8a 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -892,17 +892,14 @@ public void TrimExpired() lru.TrimExpired(); } - public bool TryGetTimeToLive(object key, out TimeSpan timeToLive) + public bool TryGetTimeToLive(TKey key, out TimeSpan timeToLive) { - if (key is K k) + if (key is K k && lru.dictionary.TryGetValue(k, out var item)) { - if (lru.dictionary.TryGetValue(k, out var item)) + if (item is LongTickCountLruItem tickItem) { - if (item is LongTickCountLruItem tickItem) - { - timeToLive = TimeSpan.FromTicks(tickItem.TickCount); - return true; - } + timeToLive = TimeSpan.FromTicks(tickItem.TickCount); + return true; } } From f7feba8bb120004c228b7596194cb718e63f74f0 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 17:41:03 -0800 Subject: [PATCH 08/32] generics --- BitFaster.Caching/Lru/Builder/LruInfo.cs | 17 +++++++++++------ BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index d5cc3ccf..b1b73a68 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -10,6 +10,8 @@ namespace BitFaster.Caching.Lru.Builder // backcompat: make class internal public sealed class LruInfo { + private object expiry = null; + /// /// Gets or sets the capacity partition. /// @@ -31,13 +33,16 @@ public sealed class LruInfo public TimeSpan? TimeToExpireAfterAccess { get; set; } = null; /// - /// Gets or sets the custom expiry. + /// Set the custom expiry. /// - private object Expiry { get; set; } = null; - - public void SetExpiry(IExpiry expiry) => Expiry = expiry; + /// The expiry + public void SetExpiry(IExpiry expiry) => this.expiry = expiry; - public IExpiry GetExpiry() => this.Expiry as IExpiry; + /// + /// Get the custom expiry. + /// + /// The expiry. + public IExpiry GetExpiry() => this.expiry as IExpiry; /// /// Gets or sets a value indicating whether to use metrics. @@ -51,7 +56,7 @@ public sealed class LruInfo internal void ThrowIfExpireAfterSpecified(string extensionName) { - if (this.Expiry != null) + if (this.expiry != null) Throw.InvalidOp("WithExpiry is not compatible with " + extensionName); } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 4a6075cc..a917dd61 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -54,7 +54,7 @@ public override ICache Build() if (info.TimeToExpireAfterWrite.HasValue && info.TimeToExpireAfterAccess.HasValue) Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfterAccess is not supported."); - var expiry = info.GetExpiry(); + var expiry = info.GetExpiry(); if (info.TimeToExpireAfterWrite.HasValue && expiry != null) Throw.InvalidOp("Specifying both ExpireAfterWrite and ExpireAfter is not supported."); From 56b9de10ce90abb65620a6e4935075e9fe6fae8b Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 17:46:18 -0800 Subject: [PATCH 09/32] rename --- .../Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs | 2 +- BitFaster.Caching/Lru/Builder/AtomicConcurrentLruBuilder.cs | 2 +- .../Lru/Builder/AtomicScopedAsyncConcurrentLruBuilder.cs | 2 +- .../Lru/Builder/AtomicScopedConcurrentLruBuilder.cs | 2 +- BitFaster.Caching/Lru/Builder/LruInfo.cs | 2 +- .../Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs | 2 +- BitFaster.Caching/Lru/Builder/ScopedConcurrentLruBuilder.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs index 0ee3039a..2f4ba57e 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicAsyncConcurrentLruBuilder.cs @@ -20,7 +20,7 @@ internal AtomicAsyncConcurrentLruBuilder(ConcurrentLruBuilder public override IAsyncCache Build() { - info.ThrowIfExpireAfterSpecified("AsAtomic"); + 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 d6e10ad8..81dcf4ae 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicConcurrentLruBuilder.cs @@ -20,7 +20,7 @@ internal AtomicConcurrentLruBuilder(ConcurrentLruBuilder> /// public override ICache Build() { - info.ThrowIfExpireAfterSpecified("AsAtomic"); + 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 49a2a01a..f359a98c 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicScopedAsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicScopedAsyncConcurrentLruBuilder.cs @@ -21,7 +21,7 @@ internal AtomicScopedAsyncConcurrentLruBuilder(AsyncConcurrentLruBuilder public override IScopedAsyncCache Build() { - info.ThrowIfExpireAfterSpecified("AsAtomic or AsScoped"); + 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 5651317c..eff590f4 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicScopedConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicScopedConcurrentLruBuilder.cs @@ -21,7 +21,7 @@ internal AtomicScopedConcurrentLruBuilder(ConcurrentLruBuilder public override IScopedCache Build() { - info.ThrowIfExpireAfterSpecified("AsAtomic or AsScoped"); + 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 b1b73a68..e5b0573f 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -54,7 +54,7 @@ public sealed class LruInfo /// public IEqualityComparer KeyComparer { get; set; } = EqualityComparer.Default; - internal void ThrowIfExpireAfterSpecified(string extensionName) + internal void ThrowIfExpirySpecified(string extensionName) { if (this.expiry != null) Throw.InvalidOp("WithExpiry is not compatible with " + extensionName); diff --git a/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs index 3d945838..98325447 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedAsyncConcurrentLruBuilder.cs @@ -20,7 +20,7 @@ internal ScopedAsyncConcurrentLruBuilder(AsyncConcurrentLruBuilder> /// public override IScopedAsyncCache Build() { - info.ThrowIfExpireAfterSpecified("AsScoped"); + 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 c90e8a8f..5bc24073 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedConcurrentLruBuilder.cs @@ -21,7 +21,7 @@ internal ScopedConcurrentLruBuilder(ConcurrentLruBuilder inner) /// public override IScopedCache Build() { - info.ThrowIfExpireAfterSpecified("AsScoped"); + info.ThrowIfExpirySpecified("AsScoped"); // this is a legal type conversion due to the generic constraint on W var scopedInnerCache = inner.Build() as ICache>; From f57d8c7389e1d21e8af242f9417888ec108be89a Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 17:51:15 -0800 Subject: [PATCH 10/32] stopwatch timing --- .../Lru/AfterReadStopwatchPolicy.cs | 250 +++++++++--------- BitFaster.Caching/Lru/CustomPolicy.cs | 10 +- .../Lru/StopwatchTickConverter.cs | 3 + 3 files changed, 133 insertions(+), 130 deletions(-) diff --git a/BitFaster.Caching/Lru/AfterReadStopwatchPolicy.cs b/BitFaster.Caching/Lru/AfterReadStopwatchPolicy.cs index fcb0a793..68c683f3 100644 --- a/BitFaster.Caching/Lru/AfterReadStopwatchPolicy.cs +++ b/BitFaster.Caching/Lru/AfterReadStopwatchPolicy.cs @@ -1,127 +1,127 @@ -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace BitFaster.Caching.Lru -{ -#if !NETCOREAPP3_0_OR_GREATER - /// - /// Implement an expire after access policy. - /// - /// - /// This class measures time using Stopwatch.GetTimestamp() with a resolution of ~1us. - /// - public readonly struct AfterAccessLongTicksPolicy : IItemPolicy> - { - private readonly long timeToLive; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace BitFaster.Caching.Lru +{ +#if !NETCOREAPP3_0_OR_GREATER + /// + /// Implement an expire after access policy. + /// + /// + /// This class measures time using Stopwatch.GetTimestamp() with a resolution of ~1us. + /// + public readonly struct AfterAccessLongTicksPolicy : IItemPolicy> + { + private readonly long timeToLive; private readonly Time time; - /// - public TimeSpan TimeToLive => StopwatchTickConverter.FromTicks(timeToLive); - - /// - /// Initializes a new instance of the TLruLongTicksPolicy class with the specified time to live. - /// - /// The time to live. - public AfterAccessLongTicksPolicy(TimeSpan timeToLive) - { - this.timeToLive = StopwatchTickConverter.ToTicks(timeToLive); - this.time = new Time(); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public LongTickCountLruItem CreateItem(K key, V value) - { - return new LongTickCountLruItem(key, value, Stopwatch.GetTimestamp()); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Touch(LongTickCountLruItem item) - { - item.TickCount = this.time.Last; - item.WasAccessed = true; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(LongTickCountLruItem item) - { - item.TickCount = Stopwatch.GetTimestamp(); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ShouldDiscard(LongTickCountLruItem item) - { - this.time.Last = Stopwatch.GetTimestamp(); - if (this.time.Last - item.TickCount > this.timeToLive) - { - 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 -} + /// + public TimeSpan TimeToLive => StopwatchTickConverter.FromTicks(timeToLive); + + /// + /// Initializes a new instance of the TLruLongTicksPolicy class with the specified time to live. + /// + /// The time to live. + public AfterAccessLongTicksPolicy(TimeSpan timeToLive) + { + this.timeToLive = StopwatchTickConverter.ToTicks(timeToLive); + this.time = new Time(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LongTickCountLruItem CreateItem(K key, V value) + { + return new LongTickCountLruItem(key, value, Stopwatch.GetTimestamp()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Touch(LongTickCountLruItem item) + { + item.TickCount = this.time.Last; + item.WasAccessed = true; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(LongTickCountLruItem item) + { + item.TickCount = Stopwatch.GetTimestamp(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldDiscard(LongTickCountLruItem item) + { + this.time.Last = Stopwatch.GetTimestamp(); + if (this.time.Last - item.TickCount > this.timeToLive) + { + 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/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs index a824127f..babbd74b 100644 --- a/BitFaster.Caching/Lru/CustomPolicy.cs +++ b/BitFaster.Caching/Lru/CustomPolicy.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; namespace BitFaster.Caching.Lru @@ -178,7 +179,6 @@ public ItemDestination RouteCold(LongTickCountLruItem item) } } #else - // TODO: this should use stopwatch timing internal readonly struct CustomExpiryPolicy : IItemPolicy> { private readonly IExpiry expiry; @@ -197,7 +197,7 @@ public CustomExpiryPolicy(IExpiry expiry) public LongTickCountLruItem CreateItem(K key, V value) { var ttl = expiry.GetExpireAfterCreate(key, value); - return new LongTickCountLruItem(key, value, ttl.Ticks + Environment.TickCount); + return new LongTickCountLruItem(key, value, StopwatchTickConverter.ToTicks(ttl) + Stopwatch.GetTimestamp()); } /// @@ -205,7 +205,7 @@ public LongTickCountLruItem CreateItem(K key, V value) public void Touch(LongTickCountLruItem item) { var ttl = expiry.GetExpireAfterRead(item.Key, item.Value); - item.TickCount = this.time.Last + ttl.Ticks; + item.TickCount = this.time.Last + StopwatchTickConverter.ToTicks(ttl); item.WasAccessed = true; } @@ -214,14 +214,14 @@ public void Touch(LongTickCountLruItem item) public void Update(LongTickCountLruItem item) { var ttl = expiry.GetExpireAfterUpdate(item.Key, item.Value); - item.TickCount = Environment.TickCount + ttl.Ticks; + item.TickCount = Stopwatch.GetTimestamp() + StopwatchTickConverter.ToTicks(ttl); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ShouldDiscard(LongTickCountLruItem item) { - this.time.Last = Environment.TickCount; + this.time.Last = Stopwatch.GetTimestamp(); if (this.time.Last > item.TickCount) { return true; diff --git a/BitFaster.Caching/Lru/StopwatchTickConverter.cs b/BitFaster.Caching/Lru/StopwatchTickConverter.cs index 8930a727..f2a20705 100644 --- a/BitFaster.Caching/Lru/StopwatchTickConverter.cs +++ b/BitFaster.Caching/Lru/StopwatchTickConverter.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace BitFaster.Caching.Lru { @@ -8,6 +9,7 @@ internal static class StopwatchTickConverter // On some platforms (e.g. MacOS), stopwatch and timespan have different resolution private static readonly double stopwatchAdjustmentFactor = Stopwatch.Frequency / (double)TimeSpan.TicksPerSecond; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static long ToTicks(TimeSpan timespan) { // mac adjustment factor is 100, giving lowest maximum TTL on mac platform - use same upper limit on all platforms for consistency @@ -20,6 +22,7 @@ internal static long ToTicks(TimeSpan timespan) return (long)(timespan.Ticks * stopwatchAdjustmentFactor); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static TimeSpan FromTicks(long ticks) { return TimeSpan.FromTicks((long)(ticks / stopwatchAdjustmentFactor)); From 5f15026c82cdb6399cfd34dfc68e9ebfbebe3a90 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 18:24:15 -0800 Subject: [PATCH 11/32] rename --- BitFaster.Caching/CachePolicy.cs | 4 ++-- BitFaster.Caching/ITimePolicy.cs | 5 ++--- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/BitFaster.Caching/CachePolicy.cs b/BitFaster.Caching/CachePolicy.cs index 8c8ed746..0c2323c7 100644 --- a/BitFaster.Caching/CachePolicy.cs +++ b/BitFaster.Caching/CachePolicy.cs @@ -38,7 +38,7 @@ public CachePolicy(Optional eviction, Optional expi /// The expire after write policy. /// The expire after access policy. /// The expire after policy. - public CachePolicy(Optional eviction, Optional expireAfterWrite, Optional expireAfterAccess, Optional expireAfter) + public CachePolicy(Optional eviction, Optional expireAfterWrite, Optional expireAfterAccess, Optional expireAfter) { this.Eviction = eviction; this.ExpireAfterWrite = expireAfterWrite; @@ -68,7 +68,7 @@ public CachePolicy(Optional eviction, Optional expi /// Gets the expire after policy, if any. This policy evicts items after a /// variable time to live computed from the key and value. /// - public Optional ExpireAfter { get; } + public Optional ExpireAfter { get; } public bool TryTrimExpired() { diff --git a/BitFaster.Caching/ITimePolicy.cs b/BitFaster.Caching/ITimePolicy.cs index fc617c13..eb70a13c 100644 --- a/BitFaster.Caching/ITimePolicy.cs +++ b/BitFaster.Caching/ITimePolicy.cs @@ -19,10 +19,9 @@ public interface ITimePolicy } /// - /// Represents a variable time based cache policy. + /// Represents a per item time based cache policy. /// - /// backcompat: this should be generic in terms of Key to avoid object arg. - public interface IVariableTimePolicy + public interface IDiscreteTimePolicy { /// /// Gets the time to live for an item in the cache. diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 07cc6c8a..925e5184 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -782,12 +782,12 @@ private static CachePolicy CreatePolicy(ConcurrentLruCore lru) if (typeof(P) == typeof(AfterAccessLongTicksPolicy)) { - return new CachePolicy(new Optional(p), Optional.None(), new Optional(p), Optional.None()); + return new CachePolicy(new Optional(p), Optional.None(), new Optional(p), Optional.None()); } if (typeof(P) == typeof(CustomExpiryPolicy)) { - return new CachePolicy(new Optional(p), Optional.None(), Optional.None(), new Optional(new VariableTimeProxy(lru))); + return new CachePolicy(new Optional(p), Optional.None(), Optional.None(), new Optional(new VariableTimeProxy(lru))); } return new CachePolicy(new Optional(p), lru.itemPolicy.CanDiscard() ? new Optional(p) : Optional.None()); @@ -878,7 +878,7 @@ public void TrimExpired() } } - private class VariableTimeProxy : IVariableTimePolicy + private class VariableTimeProxy : IDiscreteTimePolicy { private readonly ConcurrentLruCore lru; From 797ffcb940f4bb72acf020a07acef0e35a375999 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 18:25:36 -0800 Subject: [PATCH 12/32] rename --- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 925e5184..4b0d67bc 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -787,7 +787,7 @@ private static CachePolicy CreatePolicy(ConcurrentLruCore lru) if (typeof(P) == typeof(CustomExpiryPolicy)) { - return new CachePolicy(new Optional(p), Optional.None(), Optional.None(), new Optional(new VariableTimeProxy(lru))); + return new CachePolicy(new Optional(p), Optional.None(), Optional.None(), new Optional(new DiscreteTimeProxy(lru))); } return new CachePolicy(new Optional(p), lru.itemPolicy.CanDiscard() ? new Optional(p) : Optional.None()); @@ -878,11 +878,11 @@ public void TrimExpired() } } - private class VariableTimeProxy : IDiscreteTimePolicy + private class DiscreteTimeProxy : IDiscreteTimePolicy { private readonly ConcurrentLruCore lru; - public VariableTimeProxy(ConcurrentLruCore lru) + public DiscreteTimeProxy(ConcurrentLruCore lru) { this.lru = lru; } From 395847dbf956a3bc46ba14c879013d4fd7ba6b84 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 18:37:39 -0800 Subject: [PATCH 13/32] ctor --- BitFaster.Caching/CachePolicy.cs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/BitFaster.Caching/CachePolicy.cs b/BitFaster.Caching/CachePolicy.cs index 0c2323c7..f4a37085 100644 --- a/BitFaster.Caching/CachePolicy.cs +++ b/BitFaster.Caching/CachePolicy.cs @@ -13,23 +13,9 @@ public class CachePolicy /// The eviction policy. /// The expire after write policy. public CachePolicy(Optional eviction, Optional expireAfterWrite) + : this(eviction, expireAfterWrite, Optional.None(), Optional.None()) { - this.Eviction = eviction; - this.ExpireAfterWrite = expireAfterWrite; } - - /// - /// 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; - } /// /// Initializes a new instance of the CachePolicy class with the specified policies. From c7178b3fd53942f695797c8dcbb3c9f1f12a7c42 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 18:43:38 -0800 Subject: [PATCH 14/32] policy tests --- BitFaster.Caching.UnitTests/CachePolicyTests.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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(); } From 7a5c39bb6f595201971afd8bf5c4d7ef21ebdc94 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 18:52:44 -0800 Subject: [PATCH 15/32] direct methods --- BitFaster.Caching/Lru/CustomPolicy.cs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/BitFaster.Caching/Lru/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs index babbd74b..07a47a0f 100644 --- a/BitFaster.Caching/Lru/CustomPolicy.cs +++ b/BitFaster.Caching/Lru/CustomPolicy.cs @@ -12,17 +12,17 @@ public interface IExpiry /// /// Gets the expiry for an item after it is created. /// - Func GetExpireAfterCreate { get; } + TimeSpan GetExpireAfterCreate(K key, V value); /// /// Gets the expiry for an item after it is read. /// - Func GetExpireAfterRead { get; } - + TimeSpan GetExpireAfterRead(K key, V value); + /// /// Gets the expiry for an item after it is updated. /// - Func GetExpireAfterUpdate { get; } + TimeSpan GetExpireAfterUpdate(K key, V value); } /// @@ -59,13 +59,25 @@ public Expiry(Func expireAfterCreate, Func expir } /// - public Func GetExpireAfterCreate => expireAfterCreate; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TimeSpan GetExpireAfterCreate(K key, V value) + { + return this.expireAfterCreate(key, value); + } /// - public Func GetExpireAfterRead => expireAfterRead; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TimeSpan GetExpireAfterRead(K key, V value) + { + return this.expireAfterRead(key, value); + } /// - public Func GetExpireAfterUpdate => expireAfterUpdate; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TimeSpan GetExpireAfterUpdate(K key, V value) + { + return this.expireAfterUpdate(key, value); + } } #if NETCOREAPP3_0_OR_GREATER From 3ab486cdfffd3568b8d670c8e7aa586fb04fdddf Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 19:08:08 -0800 Subject: [PATCH 16/32] docs --- BitFaster.Caching/Lru/CustomPolicy.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/BitFaster.Caching/Lru/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs index 07a47a0f..ed98dca2 100644 --- a/BitFaster.Caching/Lru/CustomPolicy.cs +++ b/BitFaster.Caching/Lru/CustomPolicy.cs @@ -1,32 +1,31 @@ using System; -using System.Diagnostics; using System.Runtime.CompilerServices; namespace BitFaster.Caching.Lru { /// - /// Defines a policy for determining the expiry of an item. + /// Defines a mechanism to calculate when cache entries expire. /// public interface IExpiry { /// - /// Gets the expiry for an item after it is created. + /// Specify the inital time to live after an entry is created. /// TimeSpan GetExpireAfterCreate(K key, V value); /// - /// Gets the expiry for an item after it is read. + /// Specify the time to live after an entry is read. /// TimeSpan GetExpireAfterRead(K key, V value); /// - /// Gets the expiry for an item after it is updated. + /// Specify the time to live after an entry is updated. /// TimeSpan GetExpireAfterUpdate(K key, V value); } /// - /// Defines a policy for determining the expiry of an item using function delegates. + /// Defines a mechanism to determine the time to live for a cache item using function delegates. /// public readonly struct Expiry : IExpiry { From 5cefc93fc4bab93af9a4d440223b64525fe58887 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 19:18:22 -0800 Subject: [PATCH 17/32] diag --- BitFaster.Caching/Lru/CustomPolicy.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/BitFaster.Caching/Lru/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs index ed98dca2..155dfa3c 100644 --- a/BitFaster.Caching/Lru/CustomPolicy.cs +++ b/BitFaster.Caching/Lru/CustomPolicy.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; namespace BitFaster.Caching.Lru From 9be04699f6486001e19b0cbf290c1222ee9d7a78 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 19:52:07 -0800 Subject: [PATCH 18/32] calc --- .../Lru/ConcurrentLruBuilderTests.cs | 2 +- BitFaster.Caching/CachePolicy.cs | 4 +- .../Lru/Builder/AsyncConcurrentLruBuilder.cs | 2 +- BitFaster.Caching/Lru/Builder/LruInfo.cs | 4 +- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 8 +-- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 8 +-- BitFaster.Caching/Lru/CustomPolicy.cs | 56 +++++++++++-------- 7 files changed, 46 insertions(+), 38 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 535ccfb2..450e2e17 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -182,7 +182,7 @@ public void TestExpireAfterReadAndExpireAfterWriteThrows() public void TestExpireAfter() { ICache expireAfter = new ConcurrentLruBuilder() - .WithExpiry(new Expiry((k, v) => TimeSpan.FromMinutes(5))) + .WithExpiry(new ExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) .Build(); expireAfter.Metrics.HasValue.Should().BeFalse(); diff --git a/BitFaster.Caching/CachePolicy.cs b/BitFaster.Caching/CachePolicy.cs index 99890eed..dbe4583c 100644 --- a/BitFaster.Caching/CachePolicy.cs +++ b/BitFaster.Caching/CachePolicy.cs @@ -51,8 +51,8 @@ public CachePolicy(Optional eviction, Optional expi public Optional ExpireAfterAccess { get; } /// - /// Gets the expire after policy, if any. This policy evicts items after a - /// variable time to live computed from the key and value. + /// Gets the expire after policy, if any. This policy evicts items based on + /// a time to live computed from the key and value. /// public Optional ExpireAfter { get; } diff --git a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs index 1727a4e9..a411703e 100644 --- a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs @@ -18,7 +18,7 @@ internal AsyncConcurrentLruBuilder(LruInfo info) /// /// The expiry that determines item time to live. /// A AsyncConcurrentLruBuilder - public AsyncConcurrentLruBuilder WithExpiry(IExpiry expiry) + public AsyncConcurrentLruBuilder WithExpiry(IExpiryCalculator expiry) { this.info.SetExpiry(expiry); return this; diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index e5b0573f..0c011a8a 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -36,13 +36,13 @@ public sealed class LruInfo /// Set the custom expiry. /// /// The expiry - public void SetExpiry(IExpiry expiry) => this.expiry = expiry; + public void SetExpiry(IExpiryCalculator expiry) => this.expiry = expiry; /// /// Get the custom expiry. /// /// The expiry. - public IExpiry GetExpiry() => this.expiry as IExpiry; + public IExpiryCalculator GetExpiry() => this.expiry as IExpiryCalculator; /// /// Gets or sets a value indicating whether to use metrics. diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index a917dd61..08d667c3 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -42,7 +42,7 @@ internal ConcurrentLruBuilder(LruInfo info) /// /// The expiry that determines item time to live. /// A ConcurrentLruBuilder - public ConcurrentLruBuilder WithExpiry(IExpiry expiry) + public ConcurrentLruBuilder WithExpiry(IExpiryCalculator expiry) { this.info.SetExpiry(expiry); return this; @@ -81,10 +81,10 @@ private static ICache CreateExpireAfterAccess(LruInfo info) where T info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new AfterAccessLongTicksPolicy(info.TimeToExpireAfterAccess.Value), default); } - private static ICache CreateExpireAfter(LruInfo info, IExpiry expiry) where TP : struct, ITelemetryPolicy + private static ICache CreateExpireAfter(LruInfo info, IExpiryCalculator expiry) where TP : struct, ITelemetryPolicy { - return new ConcurrentLruCore, CustomExpiryPolicy, TP>( - info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new CustomExpiryPolicy(expiry), default); + return new ConcurrentLruCore, DiscreteExpiryPolicy, TP>( + info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new DiscreteExpiryPolicy(expiry), default); } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 4b0d67bc..69840d22 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -785,9 +785,9 @@ private static CachePolicy CreatePolicy(ConcurrentLruCore lru) return new CachePolicy(new Optional(p), Optional.None(), new Optional(p), Optional.None()); } - if (typeof(P) == typeof(CustomExpiryPolicy)) + if (typeof(P) == typeof(DiscreteExpiryPolicy)) { - return new CachePolicy(new Optional(p), Optional.None(), Optional.None(), new Optional(new DiscreteTimeProxy(lru))); + 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()); @@ -878,11 +878,11 @@ public void TrimExpired() } } - private class DiscreteTimeProxy : IDiscreteTimePolicy + private class DiscreteExpiryProxy : IDiscreteTimePolicy { private readonly ConcurrentLruCore lru; - public DiscreteTimeProxy(ConcurrentLruCore lru) + public DiscreteExpiryProxy(ConcurrentLruCore lru) { this.lru = lru; } diff --git a/BitFaster.Caching/Lru/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs index 155dfa3c..faa47189 100644 --- a/BitFaster.Caching/Lru/CustomPolicy.cs +++ b/BitFaster.Caching/Lru/CustomPolicy.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Lru /// /// Defines a mechanism to calculate when cache entries expire. /// - public interface IExpiry + public interface IExpiryCalculator { /// /// Specify the inital time to live after an entry is created. @@ -15,20 +15,22 @@ public interface IExpiry TimeSpan GetExpireAfterCreate(K key, V value); /// - /// Specify the time to live after an entry is read. + /// Specify the time to live after an entry is read. The current TTL may be + /// be returned to not modify the expiration time. /// - TimeSpan GetExpireAfterRead(K key, V value); + TimeSpan GetExpireAfterRead(K key, V value, TimeSpan currentTtl); /// - /// Specify the time to live after an entry is updated. + /// Specify the time to live after an entry is updated.The current TTL may be + /// be returned to not modify the expiration time. /// - TimeSpan GetExpireAfterUpdate(K key, V value); + TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan currentTtl); } /// /// Defines a mechanism to determine the time to live for a cache item using function delegates. /// - public readonly struct Expiry : IExpiry + public readonly struct ExpiryCalculator : IExpiryCalculator { private readonly Func expireAfterCreate; private readonly Func expireAfterRead; @@ -38,7 +40,7 @@ public interface IExpiry /// Initializes a new instance of the Expiry class. /// /// The delegate that computes the item time to live. - public Expiry(Func expireAfter) + public ExpiryCalculator(Func expireAfter) { this.expireAfterCreate = expireAfter; this.expireAfterRead = expireAfter; @@ -51,7 +53,7 @@ public Expiry(Func expireAfter) /// The delegate that computes the item time to live at creation. /// The delegate that computes the item time to live after a read operation. /// The delegate that computes the item time to live after an update operation. - public Expiry(Func expireAfterCreate, Func expireAfterRead, Func expireAfterUpdate) + public ExpiryCalculator(Func expireAfterCreate, Func expireAfterRead, Func expireAfterUpdate) { this.expireAfterCreate = expireAfterCreate; this.expireAfterRead = expireAfterRead; @@ -67,28 +69,28 @@ public TimeSpan GetExpireAfterCreate(K key, V value) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TimeSpan GetExpireAfterRead(K key, V value) + public TimeSpan GetExpireAfterRead(K key, V value, TimeSpan currentTtl) { return this.expireAfterRead(key, value); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TimeSpan GetExpireAfterUpdate(K key, V value) + public TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan currentTtl) { return this.expireAfterUpdate(key, value); } } #if NETCOREAPP3_0_OR_GREATER - internal readonly struct CustomExpiryPolicy : IItemPolicy> + internal readonly struct DiscreteExpiryPolicy : IItemPolicy> { - private readonly IExpiry expiry; + private readonly IExpiryCalculator expiry; private readonly Time time; public TimeSpan TimeToLive => TimeSpan.Zero; - public CustomExpiryPolicy(IExpiry expiry) + public DiscreteExpiryPolicy(IExpiryCalculator expiry) { this.expiry = expiry; this.time = new Time(); @@ -106,8 +108,9 @@ public LongTickCountLruItem CreateItem(K key, V value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Touch(LongTickCountLruItem item) { - var ttl = expiry.GetExpireAfterRead(item.Key, item.Value); - item.TickCount = this.time.Last + ttl.Ticks; + var currentTtl = TimeSpan.FromTicks(item.TickCount - this.time.Last); + var newTtl = expiry.GetExpireAfterRead(item.Key, item.Value, currentTtl); + item.TickCount = this.time.Last + newTtl.Ticks; item.WasAccessed = true; } @@ -115,8 +118,10 @@ public void Touch(LongTickCountLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(LongTickCountLruItem item) { - var ttl = expiry.GetExpireAfterUpdate(item.Key, item.Value); - item.TickCount = Environment.TickCount64 + ttl.Ticks; + var time = Environment.TickCount64; + var currentTtl = TimeSpan.FromTicks(item.TickCount - time); + var newTtl = expiry.GetExpireAfterUpdate(item.Key, item.Value, currentTtl); + item.TickCount = time + newTtl.Ticks; } /// @@ -191,14 +196,14 @@ public ItemDestination RouteCold(LongTickCountLruItem item) } } #else - internal readonly struct CustomExpiryPolicy : IItemPolicy> + internal readonly struct DiscreteExpiryPolicy : IItemPolicy> { - private readonly IExpiry expiry; + private readonly IExpiryCalculator expiry; private readonly Time time; public TimeSpan TimeToLive => TimeSpan.Zero; - public CustomExpiryPolicy(IExpiry expiry) + public DiscreteExpiryPolicy(IExpiryCalculator expiry) { this.expiry = expiry; this.time = new Time(); @@ -216,8 +221,9 @@ public LongTickCountLruItem CreateItem(K key, V value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Touch(LongTickCountLruItem item) { - var ttl = expiry.GetExpireAfterRead(item.Key, item.Value); - item.TickCount = this.time.Last + StopwatchTickConverter.ToTicks(ttl); + var currentTtl = StopwatchTickConverter.FromTicks(item.TickCount - this.time.Last); + var newTtl = expiry.GetExpireAfterRead(item.Key, item.Value, currentTtl); + item.TickCount = this.time.Last + StopwatchTickConverter.ToTicks(newTtl); item.WasAccessed = true; } @@ -225,8 +231,10 @@ public void Touch(LongTickCountLruItem item) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(LongTickCountLruItem item) { - var ttl = expiry.GetExpireAfterUpdate(item.Key, item.Value); - item.TickCount = Stopwatch.GetTimestamp() + StopwatchTickConverter.ToTicks(ttl); + var time = Stopwatch.GetTimestamp(); + var currentTtl = StopwatchTickConverter.FromTicks(item.TickCount - time); + var newTtl = expiry.GetExpireAfterUpdate(item.Key, item.Value, currentTtl); + item.TickCount = time + StopwatchTickConverter.ToTicks(newTtl); } /// From aa906c486dc7d1337ec0da227a0099579c7447b2 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 20:30:20 -0800 Subject: [PATCH 19/32] discrete interface --- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 4 ++-- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 14 +++++++++++--- BitFaster.Caching/Lru/CustomPolicy.cs | 14 ++++++++++---- BitFaster.Caching/Lru/IDiscreteItemPolicy.cs | 16 ++++++++++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 BitFaster.Caching/Lru/IDiscreteItemPolicy.cs diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 08d667c3..ee567cd6 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -83,8 +83,8 @@ private static ICache CreateExpireAfterAccess(LruInfo info) where T private static ICache CreateExpireAfter(LruInfo info, IExpiryCalculator expiry) where TP : struct, ITelemetryPolicy { - return new ConcurrentLruCore, DiscreteExpiryPolicy, TP>( - info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new DiscreteExpiryPolicy(expiry), default); + return new ConcurrentLruCore, DiscretePolicy, TP>( + info.ConcurrencyLevel, info.Capacity, info.KeyComparer, new DiscretePolicy(expiry), default); } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 69840d22..40478bca 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -785,7 +785,14 @@ private static CachePolicy CreatePolicy(ConcurrentLruCore lru) return new CachePolicy(new Optional(p), Optional.None(), new Optional(p), Optional.None()); } - if (typeof(P) == typeof(DiscreteExpiryPolicy)) + // TODO: how to bind to the interface? + //if (typeof(P) == typeof(DiscretePolicy)) + //{ + // return new CachePolicy(new Optional(p), Optional.None(), Optional.None(), new Optional(new DiscreteExpiryProxy(lru))); + //} + + // 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))); } @@ -896,9 +903,10 @@ public bool TryGetTimeToLive(TKey key, out TimeSpan timeToLive) { if (key is K k && lru.dictionary.TryGetValue(k, out var item)) { - if (item is LongTickCountLruItem tickItem) + // note: using the interface here will box the policy (since it is constrained to be a value type) + if (item is LongTickCountLruItem tickItem && lru.itemPolicy is IDiscreteItemPolicy policy) { - timeToLive = TimeSpan.FromTicks(tickItem.TickCount); + timeToLive = policy.ConvertTicks(tickItem.TickCount); return true; } } diff --git a/BitFaster.Caching/Lru/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs index faa47189..1a18bcf9 100644 --- a/BitFaster.Caching/Lru/CustomPolicy.cs +++ b/BitFaster.Caching/Lru/CustomPolicy.cs @@ -83,14 +83,17 @@ public TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan currentTtl) } #if NETCOREAPP3_0_OR_GREATER - internal readonly struct DiscreteExpiryPolicy : IItemPolicy> + internal readonly struct DiscretePolicy : IDiscreteItemPolicy { private readonly IExpiryCalculator expiry; private readonly Time time; public TimeSpan TimeToLive => TimeSpan.Zero; - public DiscreteExpiryPolicy(IExpiryCalculator expiry) + /// + public TimeSpan ConvertTicks(long ticks) => TimeSpan.FromTicks(ticks); + + public DiscretePolicy(IExpiryCalculator expiry) { this.expiry = expiry; this.time = new Time(); @@ -196,14 +199,17 @@ public ItemDestination RouteCold(LongTickCountLruItem item) } } #else - internal readonly struct DiscreteExpiryPolicy : IItemPolicy> + internal readonly struct DiscretePolicy : IDiscreteItemPolicy { private readonly IExpiryCalculator expiry; private readonly Time time; public TimeSpan TimeToLive => TimeSpan.Zero; - public DiscreteExpiryPolicy(IExpiryCalculator expiry) + /// + public TimeSpan ConvertTicks(long ticks) => StopwatchTickConverter.FromTicks(ticks); + + public DiscretePolicy(IExpiryCalculator expiry) { this.expiry = expiry; this.time = new Time(); diff --git a/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs b/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs new file mode 100644 index 00000000..56a175bf --- /dev/null +++ b/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BitFaster.Caching.Lru +{ + public interface IDiscreteItemPolicy : IItemPolicy> + { + /// + /// Convert ticks to a TimeSpan. + /// + /// The number of ticks to convert. + /// Ticks converted to a TimeSpan + TimeSpan ConvertTicks(long ticks); + } +} From 800826736c1113c52cfbf9ae2d87395003f2e459 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 20:36:01 -0800 Subject: [PATCH 20/32] args --- BitFaster.Caching/Lru/CustomPolicy.cs | 14 +++++++------- BitFaster.Caching/Lru/IDiscreteItemPolicy.cs | 7 +++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/BitFaster.Caching/Lru/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs index 1a18bcf9..b6ff78ec 100644 --- a/BitFaster.Caching/Lru/CustomPolicy.cs +++ b/BitFaster.Caching/Lru/CustomPolicy.cs @@ -33,8 +33,8 @@ public interface IExpiryCalculator public readonly struct ExpiryCalculator : IExpiryCalculator { private readonly Func expireAfterCreate; - private readonly Func expireAfterRead; - private readonly Func expireAfterUpdate; + private readonly Func expireAfterRead; + private readonly Func expireAfterUpdate; /// /// Initializes a new instance of the Expiry class. @@ -43,8 +43,8 @@ public interface IExpiryCalculator public ExpiryCalculator(Func expireAfter) { this.expireAfterCreate = expireAfter; - this.expireAfterRead = expireAfter; - this.expireAfterUpdate = expireAfter; + this.expireAfterRead = null; + this.expireAfterUpdate = null; } /// @@ -53,7 +53,7 @@ public ExpiryCalculator(Func expireAfter) /// The delegate that computes the item time to live at creation. /// The delegate that computes the item time to live after a read operation. /// The delegate that computes the item time to live after an update operation. - public ExpiryCalculator(Func expireAfterCreate, Func expireAfterRead, Func expireAfterUpdate) + public ExpiryCalculator(Func expireAfterCreate, Func expireAfterRead, Func expireAfterUpdate) { this.expireAfterCreate = expireAfterCreate; this.expireAfterRead = expireAfterRead; @@ -71,14 +71,14 @@ public TimeSpan GetExpireAfterCreate(K key, V value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public TimeSpan GetExpireAfterRead(K key, V value, TimeSpan currentTtl) { - return this.expireAfterRead(key, value); + return this.expireAfterRead == null ? this.expireAfterCreate(key, value) : this.expireAfterRead(key, value, currentTtl); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan currentTtl) { - return this.expireAfterUpdate(key, value); + return this.expireAfterUpdate == null ? this.expireAfterCreate(key, value) : this.expireAfterUpdate(key, value, currentTtl); } } diff --git a/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs b/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs index 56a175bf..da934cb3 100644 --- a/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs +++ b/BitFaster.Caching/Lru/IDiscreteItemPolicy.cs @@ -1,9 +1,12 @@ using System; -using System.Collections.Generic; -using System.Text; namespace BitFaster.Caching.Lru { + /// + /// A marker interface for discrete expiry policies. + /// + /// + /// public interface IDiscreteItemPolicy : IItemPolicy> { /// From 2c6a671a26ce3bcf7f20f07e4e17bfdff336dc3c Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 13 Nov 2023 22:59:51 -0800 Subject: [PATCH 21/32] line endings --- BitFaster.Caching/CachePolicy.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/BitFaster.Caching/CachePolicy.cs b/BitFaster.Caching/CachePolicy.cs index dbe4583c..00cb99de 100644 --- a/BitFaster.Caching/CachePolicy.cs +++ b/BitFaster.Caching/CachePolicy.cs @@ -13,8 +13,8 @@ public class CachePolicy /// The eviction policy. /// The expire after write policy. public CachePolicy(Optional eviction, Optional expireAfterWrite) - : this(eviction, expireAfterWrite, Optional.None(), Optional.None()) - { + : this(eviction, expireAfterWrite, Optional.None(), Optional.None()) + { } /// @@ -51,15 +51,15 @@ public CachePolicy(Optional eviction, Optional expi public Optional ExpireAfterAccess { get; } /// - /// Gets the expire after policy, if any. This policy evicts items based on - /// a time to live computed from the key and value. - /// - public Optional ExpireAfter { get; } + /// Gets the expire after policy, if any. This policy evicts items based on + /// a time to live 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. + /// True if expiry is supported and expired items were trimmed, otherwise false. public bool TryTrimExpired() { if (ExpireAfterWrite.HasValue) From b6d2af6469094fce7798d46e0f414f165e077edf Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 13 Nov 2023 23:27:19 -0800 Subject: [PATCH 22/32] factory --- BitFaster.Caching/Lru/LruFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching/Lru/LruFactory.cs b/BitFaster.Caching/Lru/LruFactory.cs index 129cd54b..f2c4e543 100644 --- a/BitFaster.Caching/Lru/LruFactory.cs +++ b/BitFaster.Caching/Lru/LruFactory.cs @@ -25,7 +25,7 @@ internal static ICache CreateConcurrent(LruInfo info) 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 + return (info.WithMetrics, info.TimeToExpireAfterWrite.HasValue, info.TimeToExpireAfterAccess.HasValue, expiry != null) switch { (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), From 8aba48a5f66af15ed86e2b5b1515e088652b5706 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 14 Nov 2023 10:40:21 -0800 Subject: [PATCH 23/32] renaming --- .../Lru/ConcurrentLruBuilderTests.cs | 2 +- .../TestExpiryCalculator.cs | 56 +++ BitFaster.Caching/CachePolicy.cs | 2 +- BitFaster.Caching/IExpiryCalculator.cs | 27 ++ BitFaster.Caching/ITimePolicy.cs | 6 +- .../Lru/Builder/AsyncConcurrentLruBuilder.cs | 2 +- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 2 +- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 2 +- BitFaster.Caching/Lru/CustomPolicy.cs | 319 ------------------ .../Lru/DiscreteStopwatchPolicy.cs | 124 +++++++ .../Lru/DiscreteTickCount64Policy.cs | 125 +++++++ 11 files changed, 340 insertions(+), 327 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/TestExpiryCalculator.cs create mode 100644 BitFaster.Caching/IExpiryCalculator.cs delete mode 100644 BitFaster.Caching/Lru/CustomPolicy.cs create mode 100644 BitFaster.Caching/Lru/DiscreteStopwatchPolicy.cs create mode 100644 BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 450e2e17..01a0ccd3 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -182,7 +182,7 @@ public void TestExpireAfterReadAndExpireAfterWriteThrows() public void TestExpireAfter() { ICache expireAfter = new ConcurrentLruBuilder() - .WithExpiry(new ExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) .Build(); expireAfter.Metrics.HasValue.Should().BeFalse(); diff --git a/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs b/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs new file mode 100644 index 00000000..f93163bc --- /dev/null +++ b/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs @@ -0,0 +1,56 @@ +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 + { + private readonly Func expireAfterCreate; + private readonly Func expireAfterRead; + private readonly Func expireAfterUpdate; + + /// + /// 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 00cb99de..c8dc7922 100644 --- a/BitFaster.Caching/CachePolicy.cs +++ b/BitFaster.Caching/CachePolicy.cs @@ -52,7 +52,7 @@ public CachePolicy(Optional eviction, Optional expi /// /// Gets the expire after policy, if any. This policy evicts items based on - /// a time to live computed from the key and value. + /// a time to expire computed from the key and value. /// public Optional ExpireAfter { get; } diff --git a/BitFaster.Caching/IExpiryCalculator.cs b/BitFaster.Caching/IExpiryCalculator.cs new file mode 100644 index 00000000..f4d0851d --- /dev/null +++ b/BitFaster.Caching/IExpiryCalculator.cs @@ -0,0 +1,27 @@ +using System; + +namespace BitFaster.Caching +{ + /// + /// Defines a mechanism to calculate when cache entries 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 TTL may be + /// be returned to not modify the expiration time. + /// + TimeSpan GetExpireAfterRead(K key, V value, TimeSpan currentTtl); + + /// + /// Specify the time to expire after an entry is updated.The current TTL may be + /// be returned to not modify the expiration time. + /// + TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan currentTtl); + } +} diff --git a/BitFaster.Caching/ITimePolicy.cs b/BitFaster.Caching/ITimePolicy.cs index eb70a13c..01e9e366 100644 --- a/BitFaster.Caching/ITimePolicy.cs +++ b/BitFaster.Caching/ITimePolicy.cs @@ -8,7 +8,7 @@ namespace BitFaster.Caching public interface ITimePolicy { /// - /// Gets the time to live for items in the cache. + /// Gets the time to expire for items in the cache. /// TimeSpan TimeToLive { get; } @@ -27,9 +27,9 @@ 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. + /// If the key exists, the time to live for the item with the specified key. /// True if the key exists, otherwise false. - bool TryGetTimeToLive(K key, out TimeSpan timeToLive); + bool TryGetTimeToExpire(K key, out TimeSpan timeToExpire); /// /// Remove all expired items from the cache. diff --git a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs index bfc52a3e..8afe9f27 100644 --- a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs @@ -16,7 +16,7 @@ internal AsyncConcurrentLruBuilder(LruInfo info) /// /// Evict after a variable duration specified by an IExpiry instance. /// - /// The expiry that determines item time to live. + /// The expiry that determines item time to expire. /// A AsyncConcurrentLruBuilder public AsyncConcurrentLruBuilder WithExpiry(IExpiryCalculator expiry) { diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index acaf30d7..2cee69b7 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -40,7 +40,7 @@ internal ConcurrentLruBuilder(LruInfo info) /// /// Evict after a variable duration specified by an IExpiry instance. /// - /// The expiry that determines item time to live. + /// The expiry that determines item time to expire. /// A ConcurrentLruBuilder public ConcurrentLruBuilder WithExpiry(IExpiryCalculator expiry) { diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 06bfa64a..fcefa7a0 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -909,7 +909,7 @@ public void TrimExpired() lru.TrimExpired(); } - public bool TryGetTimeToLive(TKey key, out TimeSpan timeToLive) + public bool TryGetTimeToExpire(TKey key, out TimeSpan timeToLive) { if (key is K k && lru.dictionary.TryGetValue(k, out var item)) { diff --git a/BitFaster.Caching/Lru/CustomPolicy.cs b/BitFaster.Caching/Lru/CustomPolicy.cs deleted file mode 100644 index b6ff78ec..00000000 --- a/BitFaster.Caching/Lru/CustomPolicy.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace BitFaster.Caching.Lru -{ - /// - /// Defines a mechanism to calculate when cache entries expire. - /// - public interface IExpiryCalculator - { - /// - /// Specify the inital time to live after an entry is created. - /// - TimeSpan GetExpireAfterCreate(K key, V value); - - /// - /// Specify the time to live after an entry is read. The current TTL may be - /// be returned to not modify the expiration time. - /// - TimeSpan GetExpireAfterRead(K key, V value, TimeSpan currentTtl); - - /// - /// Specify the time to live after an entry is updated.The current TTL may be - /// be returned to not modify the expiration time. - /// - TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan currentTtl); - } - - /// - /// Defines a mechanism to determine the time to live for a cache item using function delegates. - /// - public readonly struct ExpiryCalculator : IExpiryCalculator - { - private readonly Func expireAfterCreate; - private readonly Func expireAfterRead; - private readonly Func expireAfterUpdate; - - /// - /// Initializes a new instance of the Expiry class. - /// - /// The delegate that computes the item time to live. - public ExpiryCalculator(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 live at creation. - /// The delegate that computes the item time to live after a read operation. - /// The delegate that computes the item time to live after an update operation. - public ExpiryCalculator(Func expireAfterCreate, Func expireAfterRead, Func expireAfterUpdate) - { - this.expireAfterCreate = expireAfterCreate; - this.expireAfterRead = expireAfterRead; - this.expireAfterUpdate = expireAfterUpdate; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TimeSpan GetExpireAfterCreate(K key, V value) - { - return this.expireAfterCreate(key, value); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TimeSpan GetExpireAfterRead(K key, V value, TimeSpan currentTtl) - { - return this.expireAfterRead == null ? this.expireAfterCreate(key, value) : this.expireAfterRead(key, value, currentTtl); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan currentTtl) - { - return this.expireAfterUpdate == null ? this.expireAfterCreate(key, value) : this.expireAfterUpdate(key, value, currentTtl); - } - } - -#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) => TimeSpan.FromTicks(ticks); - - public DiscretePolicy(IExpiryCalculator expiry) - { - this.expiry = expiry; - this.time = new Time(); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public LongTickCountLruItem CreateItem(K key, V value) - { - var ttl = expiry.GetExpireAfterCreate(key, value); - return new LongTickCountLruItem(key, value, ttl.Ticks + Environment.TickCount64); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Touch(LongTickCountLruItem item) - { - var currentTtl = TimeSpan.FromTicks(item.TickCount - this.time.Last); - var newTtl = expiry.GetExpireAfterRead(item.Key, item.Value, currentTtl); - item.TickCount = this.time.Last + newTtl.Ticks; - item.WasAccessed = true; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(LongTickCountLruItem item) - { - var time = Environment.TickCount64; - var currentTtl = TimeSpan.FromTicks(item.TickCount - time); - var newTtl = expiry.GetExpireAfterUpdate(item.Key, item.Value, currentTtl); - item.TickCount = time + newTtl.Ticks; - } - - /// - [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; - } - } -#else - 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); - - public DiscretePolicy(IExpiryCalculator expiry) - { - this.expiry = expiry; - this.time = new Time(); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public LongTickCountLruItem CreateItem(K key, V value) - { - var ttl = expiry.GetExpireAfterCreate(key, value); - return new LongTickCountLruItem(key, value, StopwatchTickConverter.ToTicks(ttl) + Stopwatch.GetTimestamp()); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Touch(LongTickCountLruItem item) - { - var currentTtl = StopwatchTickConverter.FromTicks(item.TickCount - this.time.Last); - var newTtl = expiry.GetExpireAfterRead(item.Key, item.Value, currentTtl); - item.TickCount = this.time.Last + StopwatchTickConverter.ToTicks(newTtl); - item.WasAccessed = true; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Update(LongTickCountLruItem item) - { - var time = Stopwatch.GetTimestamp(); - var currentTtl = StopwatchTickConverter.FromTicks(item.TickCount - time); - var newTtl = expiry.GetExpireAfterUpdate(item.Key, item.Value, currentTtl); - item.TickCount = time + StopwatchTickConverter.ToTicks(newTtl); - } - - /// - [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/DiscreteStopwatchPolicy.cs b/BitFaster.Caching/Lru/DiscreteStopwatchPolicy.cs new file mode 100644 index 00000000..1a6eb2a0 --- /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); + + 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..3b3d6dbe --- /dev/null +++ b/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +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) => TimeSpan.FromTicks(ticks); + + 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, expiry.Ticks + Environment.TickCount64); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Touch(LongTickCountLruItem item) + { + var currentExpiry = TimeSpan.FromTicks(item.TickCount - this.time.Last); + var newExpiry = expiry.GetExpireAfterRead(item.Key, item.Value, currentExpiry); + item.TickCount = this.time.Last + newExpiry.Ticks; + item.WasAccessed = true; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(LongTickCountLruItem item) + { + var time = Environment.TickCount64; + var currentExpiry = TimeSpan.FromTicks(item.TickCount - time); + var newExpiry = expiry.GetExpireAfterUpdate(item.Key, item.Value, currentExpiry); + item.TickCount = time + newExpiry.Ticks; + } + + /// + [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 +} From 6b22762ec384ce5da799b9330a3946bd7068047c Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 14 Nov 2023 10:48:20 -0800 Subject: [PATCH 24/32] builder tests --- .../Lru/ConcurrentLruBuilderTests.cs | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 01a0ccd3..79a12c58 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -1,8 +1,4 @@ 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; @@ -190,6 +186,55 @@ public void TestExpireAfter() expireAfter.Policy.ExpireAfterAccess.HasValue.Should().BeFalse(); expireAfter.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); + } + + [Fact] + public void TestExpireAfterWithMetrics() + { + ICache expireAfter = new ConcurrentLruBuilder() + .WithExpiry(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 TestExpireAfterWriteAndExpireAfteThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestExpireAfterAccessAndExpireAfteThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) + .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestExpireAfterAccessAndWriteAndExpireAfteThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) + .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); + + Action act = () => builder.Build(); + act.Should().Throw(); } // There are 15 combinations to test: From d0ca874c631e3f3362f5c95956682cfef87ba93e Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 14 Nov 2023 12:43:43 -0800 Subject: [PATCH 25/32] policy test --- .../Lru/ConcurrentLruBuilderTests.cs | 69 ++++++- .../Lru/DiscretePolicyTests.cs | 193 ++++++++++++++++++ .../TestExpiryCalculator.cs | 27 +-- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 12 +- .../Lru/DiscreteTickCount64Policy.cs | 13 +- 5 files changed, 284 insertions(+), 30 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 77f03720..2e5a3cbc 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -188,6 +188,21 @@ public void TestExpireAfter() expireAfter.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); } + [Fact] + public void TestAsyncExpireAfter() + { + IAsyncCache expireAfter = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithExpiry(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() { @@ -204,7 +219,7 @@ public void TestExpireAfterWithMetrics() } [Fact] - public void TestExpireAfterWriteAndExpireAfteThrows() + public void TestExpireAfterWriteAndExpireAfterThrows() { var builder = new ConcurrentLruBuilder() .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) @@ -215,7 +230,7 @@ public void TestExpireAfterWriteAndExpireAfteThrows() } [Fact] - public void TestExpireAfterAccessAndExpireAfteThrows() + public void TestExpireAfterAccessAndExpireAfterThrows() { var builder = new ConcurrentLruBuilder() .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) @@ -226,7 +241,7 @@ public void TestExpireAfterAccessAndExpireAfteThrows() } [Fact] - public void TestExpireAfterAccessAndWriteAndExpireAfteThrows() + public void TestExpireAfterAccessAndWriteAndExpireAfterThrows() { var builder = new ConcurrentLruBuilder() .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) @@ -237,6 +252,54 @@ public void TestExpireAfterAccessAndWriteAndExpireAfteThrows() act.Should().Throw(); } + [Fact] + public void TestScopedWithExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .AsScopedCache(); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestScopedAtomicWithExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .AsScopedCache() + .WithAtomicGetOrAdd(); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestAsyncScopedWithExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .AsAsyncCache() + .AsScopedCache(); + + Action act = () => builder.Build(); + act.Should().Throw(); + } + + [Fact] + public void TestAsyncScopedAtomicWithExpireAfterThrows() + { + var builder = new ConcurrentLruBuilder() + .WithExpiry(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 diff --git a/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs new file mode 100644 index 00000000..e3fadf70 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs @@ -0,0 +1,193 @@ +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(); + + private static readonly TimeSpan defaultTimeToExpire = TimeSpan.FromSeconds(10); + + public DiscretePolicyTests() + { + expiryCalculator = new TestExpiryCalculator(); + expiryCalculator.ExpireAfterCreate = (_, _) => defaultTimeToExpire; + expiryCalculator.ExpireAfterRead = (_, _, _) => defaultTimeToExpire; + expiryCalculator.ExpireAfterUpdate = (_, _, _) => defaultTimeToExpire; + 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(defaultTimeToExpire)).Should().Be(defaultTimeToExpire); +#else + this.policy.ConvertTicks(defaultTimeToExpire.ToEnvTick64()).Should().Be(defaultTimeToExpire); +#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.FromSeconds(11)); +#else + item.TickCount = item.TickCount - TimeSpan.FromSeconds(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.FromSeconds(9)); +#else + item.TickCount = item.TickCount - (int)TimeSpan.FromSeconds(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.FromSeconds(11)); +#else + item.TickCount = item.TickCount - TimeSpan.FromSeconds(11).ToEnvTick64(); +#endif + } + + return item; + } + } +} diff --git a/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs b/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs index f93163bc..747b1763 100644 --- a/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs +++ b/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs @@ -7,9 +7,12 @@ namespace BitFaster.Caching.UnitTests /// public class TestExpiryCalculator : IExpiryCalculator { - private readonly Func expireAfterCreate; - private readonly Func expireAfterRead; - private readonly Func expireAfterUpdate; + public Func ExpireAfterCreate { get; set; } + public Func ExpireAfterRead { get; set; } + public Func ExpireAfterUpdate { get; set; } + + public TestExpiryCalculator() + { } /// /// Initializes a new instance of the Expiry class. @@ -17,9 +20,9 @@ public class TestExpiryCalculator : IExpiryCalculator /// The delegate that computes the item time to expire. public TestExpiryCalculator(Func expireAfter) { - this.expireAfterCreate = expireAfter; - this.expireAfterRead = null; - this.expireAfterUpdate = null; + this.ExpireAfterCreate = expireAfter; + this.ExpireAfterRead = null; + this.ExpireAfterUpdate = null; } /// @@ -30,27 +33,27 @@ public TestExpiryCalculator(Func expireAfter) /// 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; + this.ExpireAfterCreate = expireAfterCreate; + this.ExpireAfterRead = expireAfterRead; + this.ExpireAfterUpdate = expireAfterUpdate; } /// public TimeSpan GetExpireAfterCreate(K key, V value) { - return this.expireAfterCreate(key, 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); + 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); + return this.ExpireAfterUpdate == null ? this.ExpireAfterCreate(key, value) : this.ExpireAfterUpdate(key, value, currentTtl); } } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index fcefa7a0..5168c02e 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -379,7 +379,7 @@ public bool TryUpdate(K key, V value) V oldValue = existing.Value; existing.Value = value; this.itemPolicy.Update(existing); - // backcompat: remove conditional compile +// backcompat: remove conditional compile #if NETCOREAPP3_0_OR_GREATER this.telemetryPolicy.OnItemUpdated(existing.Key, oldValue, existing.Value); #endif @@ -795,12 +795,6 @@ private static CachePolicy CreatePolicy(ConcurrentLruCore lru) return new CachePolicy(new Optional(p), Optional.None(), new Optional(p), Optional.None()); } - // TODO: how to bind to the interface? - //if (typeof(P) == typeof(DiscretePolicy)) - //{ - // return new CachePolicy(new Optional(p), Optional.None(), Optional.None(), new Optional(new DiscreteExpiryProxy(lru))); - //} - // IsAssignableFrom is a jit intrinsic https://github.com/dotnet/runtime/issues/4920 if (typeof(IDiscreteItemPolicy).IsAssignableFrom(typeof(P))) { @@ -862,7 +856,7 @@ public Proxy(ConcurrentLruCore lru) public long Evicted => lru.telemetryPolicy.Evicted; - // backcompat: remove conditional compile +// backcompat: remove conditional compile #if NETCOREAPP3_0_OR_GREATER public long Updated => lru.telemetryPolicy.Updated; #endif @@ -876,7 +870,7 @@ public event EventHandler> ItemRemoved remove { this.lru.telemetryPolicy.ItemRemoved -= value; } } - // backcompat: remove conditional compile +// backcompat: remove conditional compile #if NETCOREAPP3_0_OR_GREATER public event EventHandler> ItemUpdated { diff --git a/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs b/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs index 3b3d6dbe..4f683060 100644 --- a/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs +++ b/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs @@ -6,6 +6,7 @@ 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; @@ -14,7 +15,7 @@ namespace BitFaster.Caching.Lru public TimeSpan TimeToLive => TimeSpan.Zero; /// - public TimeSpan ConvertTicks(long ticks) => TimeSpan.FromTicks(ticks); + public TimeSpan ConvertTicks(long ticks) => TimeSpan.FromMilliseconds(ticks); public DiscretePolicy(IExpiryCalculator expiry) { @@ -27,16 +28,16 @@ public DiscretePolicy(IExpiryCalculator expiry) public LongTickCountLruItem CreateItem(K key, V value) { var expiry = this.expiry.GetExpireAfterCreate(key, value); - return new LongTickCountLruItem(key, value, expiry.Ticks + Environment.TickCount64); + return new LongTickCountLruItem(key, value, (long)expiry.TotalMilliseconds + Environment.TickCount64); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Touch(LongTickCountLruItem item) { - var currentExpiry = TimeSpan.FromTicks(item.TickCount - this.time.Last); + var currentExpiry = TimeSpan.FromMilliseconds(item.TickCount - this.time.Last); var newExpiry = expiry.GetExpireAfterRead(item.Key, item.Value, currentExpiry); - item.TickCount = this.time.Last + newExpiry.Ticks; + item.TickCount = this.time.Last + (long)newExpiry.TotalMilliseconds; item.WasAccessed = true; } @@ -45,9 +46,9 @@ public void Touch(LongTickCountLruItem item) public void Update(LongTickCountLruItem item) { var time = Environment.TickCount64; - var currentExpiry = TimeSpan.FromTicks(item.TickCount - time); + var currentExpiry = TimeSpan.FromMilliseconds(item.TickCount - time); var newExpiry = expiry.GetExpireAfterUpdate(item.Key, item.Value, currentExpiry); - item.TickCount = time + newExpiry.Ticks; + item.TickCount = time + (long)newExpiry.TotalMilliseconds; } /// From 693f0b24121d43b3c6d8f5a71ec1e4bf3c1038c2 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 14 Nov 2023 13:24:03 -0800 Subject: [PATCH 26/32] basic e2e --- .../Lru/ConcurrentLruAfterDiscreteTests.cs | 64 +++++++++++++++++++ .../Lru/DiscretePolicyTests.cs | 4 +- .../Lru/DiscreteStopwatchPolicy.cs | 2 +- .../Lru/DiscreteTickCount64Policy.cs | 2 +- 4 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs new file mode 100644 index 00000000..4d011dcc --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs @@ -0,0 +1,64 @@ +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 TimeSpan defaultTimeToExpire = TimeSpan.FromMinutes(1); + 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() + { + expiryCalculator.ExpireAfterCreate = (_, _) => defaultTimeToExpire; + expiryCalculator.ExpireAfterRead = (_, _, _) => defaultTimeToExpire; + expiryCalculator.ExpireAfterUpdate = (_, _, _) => defaultTimeToExpire; + + lru = new ConcurrentLruBuilder() + .WithCapacity(capacity) + .WithExpiry(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(defaultTimeToExpire, delta); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs index e3fadf70..6529b46f 100644 --- a/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs @@ -36,9 +36,9 @@ public void TimeToLiveShouldBeZero() public void ConvertTicksReturnsTimeSpan() { #if NETFRAMEWORK - this.policy.ConvertTicks(StopwatchTickConverter.ToTicks(defaultTimeToExpire)).Should().Be(defaultTimeToExpire); + this.policy.ConvertTicks(StopwatchTickConverter.ToTicks(defaultTimeToExpire) + Stopwatch.GetTimestamp()).Should().BeCloseTo(defaultTimeToExpire, TimeSpan.FromMilliseconds(20)); #else - this.policy.ConvertTicks(defaultTimeToExpire.ToEnvTick64()).Should().Be(defaultTimeToExpire); + this.policy.ConvertTicks(defaultTimeToExpire.ToEnvTick64() + Environment.TickCount64).Should().BeCloseTo(defaultTimeToExpire, TimeSpan.FromMilliseconds(20)); #endif } diff --git a/BitFaster.Caching/Lru/DiscreteStopwatchPolicy.cs b/BitFaster.Caching/Lru/DiscreteStopwatchPolicy.cs index 1a6eb2a0..081550cc 100644 --- a/BitFaster.Caching/Lru/DiscreteStopwatchPolicy.cs +++ b/BitFaster.Caching/Lru/DiscreteStopwatchPolicy.cs @@ -13,7 +13,7 @@ namespace BitFaster.Caching.Lru public TimeSpan TimeToLive => TimeSpan.Zero; /// - public TimeSpan ConvertTicks(long ticks) => StopwatchTickConverter.FromTicks(ticks); + public TimeSpan ConvertTicks(long ticks) => StopwatchTickConverter.FromTicks(ticks - Stopwatch.GetTimestamp()); public DiscretePolicy(IExpiryCalculator expiry) { diff --git a/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs b/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs index 4f683060..f87c6dae 100644 --- a/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs +++ b/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs @@ -15,7 +15,7 @@ namespace BitFaster.Caching.Lru public TimeSpan TimeToLive => TimeSpan.Zero; /// - public TimeSpan ConvertTicks(long ticks) => TimeSpan.FromMilliseconds(ticks); + public TimeSpan ConvertTicks(long ticks) => TimeSpan.FromMilliseconds(ticks - Environment.TickCount64); public DiscretePolicy(IExpiryCalculator expiry) { From 13f645a59ad491c70e2ac4a34f4e63c0d5b28ef9 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 14 Nov 2023 16:46:00 -0800 Subject: [PATCH 27/32] more tests --- .../Lru/ConcurrentLruAfterDiscreteTests.cs | 201 +++++++++++++++++- .../Lru/DiscretePolicyTests.cs | 21 +- .../TestExpiryCalculator.cs | 6 +- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 14 +- .../Lru/DiscreteTickCount64Policy.cs | 2 - 5 files changed, 213 insertions(+), 31 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs index 4d011dcc..0ed5b59a 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs @@ -9,32 +9,26 @@ namespace BitFaster.Caching.UnitTests.Lru { public class ConcurrentLruAfterDiscreteTests { - private readonly TimeSpan defaultTimeToExpire = TimeSpan.FromMinutes(1); 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>(); + 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) + private void OnLruItemRemoved(object sender, ItemRemovedEventArgs e) { removedItems.Add(e); } public ConcurrentLruAfterDiscreteTests() { - expiryCalculator.ExpireAfterCreate = (_, _) => defaultTimeToExpire; - expiryCalculator.ExpireAfterRead = (_, _, _) => defaultTimeToExpire; - expiryCalculator.ExpireAfterUpdate = (_, _, _) => defaultTimeToExpire; - lru = new ConcurrentLruBuilder() .WithCapacity(capacity) .WithExpiry(expiryCalculator) @@ -58,7 +52,196 @@ public void WhenKeyExistsTryGetTimeToExpireReturnsExpiryTime() { lru.GetOrAdd(1, k => "1"); lru.Policy.ExpireAfter.Value.TryGetTimeToExpire(1, out var expiry).Should().BeTrue(); - expiry.Should().BeCloseTo(defaultTimeToExpire, delta); + 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() + { + var lru = new ConcurrentLruBuilder() + .WithCapacity(capacity) + .WithExpireAfterAccess(TimeSpan.FromMilliseconds(100)) + .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() + { + var lruEvents = new ConcurrentLruBuilder() + .WithCapacity(new EqualCapacityPartition(6)) + .WithExpiry(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/DiscretePolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs index 6529b46f..9fd99ea8 100644 --- a/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs @@ -15,14 +15,9 @@ public class DiscretePolicyTests private static readonly ulong stopwatchDelta = (ulong)StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(20)); private static readonly ulong tickCountDelta = (ulong)TimeSpan.FromMilliseconds(20).ToEnvTick64(); - private static readonly TimeSpan defaultTimeToExpire = TimeSpan.FromSeconds(10); - public DiscretePolicyTests() { expiryCalculator = new TestExpiryCalculator(); - expiryCalculator.ExpireAfterCreate = (_, _) => defaultTimeToExpire; - expiryCalculator.ExpireAfterRead = (_, _, _) => defaultTimeToExpire; - expiryCalculator.ExpireAfterUpdate = (_, _, _) => defaultTimeToExpire; policy = new DiscretePolicy(expiryCalculator); } @@ -36,9 +31,9 @@ public void TimeToLiveShouldBeZero() public void ConvertTicksReturnsTimeSpan() { #if NETFRAMEWORK - this.policy.ConvertTicks(StopwatchTickConverter.ToTicks(defaultTimeToExpire) + Stopwatch.GetTimestamp()).Should().BeCloseTo(defaultTimeToExpire, TimeSpan.FromMilliseconds(20)); + this.policy.ConvertTicks(StopwatchTickConverter.ToTicks(TestExpiryCalculator.DefaultTimeToExpire) + Stopwatch.GetTimestamp()).Should().BeCloseTo(TestExpiryCalculator.DefaultTimeToExpire, TimeSpan.FromMilliseconds(20)); #else - this.policy.ConvertTicks(defaultTimeToExpire.ToEnvTick64() + Environment.TickCount64).Should().BeCloseTo(defaultTimeToExpire, TimeSpan.FromMilliseconds(20)); + this.policy.ConvertTicks(TestExpiryCalculator.DefaultTimeToExpire.ToEnvTick64() + Environment.TickCount64).Should().BeCloseTo(TestExpiryCalculator.DefaultTimeToExpire, TimeSpan.FromMilliseconds(20)); #endif } @@ -108,9 +103,9 @@ public void WhenItemIsExpiredShouldDiscardIsTrue() var item = this.policy.CreateItem(1, 2); #if NETFRAMEWORK - item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromSeconds(11)); + item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(11)); #else - item.TickCount = item.TickCount - TimeSpan.FromSeconds(11).ToEnvTick64(); + item.TickCount = item.TickCount - TimeSpan.FromMilliseconds(11).ToEnvTick64(); #endif this.policy.ShouldDiscard(item).Should().BeTrue(); } @@ -121,9 +116,9 @@ public void WhenItemIsNotExpiredShouldDiscardIsFalse() var item = this.policy.CreateItem(1, 2); #if NETFRAMEWORK - item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromSeconds(9)); + item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(9)); #else - item.TickCount = item.TickCount - (int)TimeSpan.FromSeconds(9).ToEnvTick64(); + item.TickCount = item.TickCount - (int)TimeSpan.FromMilliseconds(9).ToEnvTick64(); #endif this.policy.ShouldDiscard(item).Should().BeFalse(); @@ -181,9 +176,9 @@ private LongTickCountLruItem CreateItem(bool wasAccessed, bool isExpir if (isExpired) { #if NETFRAMEWORK - item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromSeconds(11)); + item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(11)); #else - item.TickCount = item.TickCount - TimeSpan.FromSeconds(11).ToEnvTick64(); + item.TickCount = item.TickCount - TimeSpan.FromMilliseconds(11).ToEnvTick64(); #endif } diff --git a/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs b/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs index 747b1763..5ed5b35a 100644 --- a/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs +++ b/BitFaster.Caching.UnitTests/TestExpiryCalculator.cs @@ -7,12 +7,16 @@ namespace BitFaster.Caching.UnitTests /// 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. diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 5168c02e..1a3105dd 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -892,10 +892,15 @@ public void 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() @@ -907,12 +912,9 @@ public bool TryGetTimeToExpire(TKey key, out TimeSpan timeToLive) { if (key is K k && lru.dictionary.TryGetValue(k, out var item)) { - // note: using the interface here will box the policy (since it is constrained to be a value type) - if (item is LongTickCountLruItem tickItem && lru.itemPolicy is IDiscreteItemPolicy policy) - { - timeToLive = policy.ConvertTicks(tickItem.TickCount); - return true; - } + LongTickCountLruItem tickItem = item as LongTickCountLruItem; + timeToLive = policy.ConvertTicks(tickItem.TickCount); + return true; } timeToLive = default; diff --git a/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs b/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs index f87c6dae..f7daefed 100644 --- a/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs +++ b/BitFaster.Caching/Lru/DiscreteTickCount64Policy.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Text; namespace BitFaster.Caching.Lru { From 6b2407ae6b576cacd166941b485c0f99a895fab0 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 14 Nov 2023 17:04:50 -0800 Subject: [PATCH 28/32] fix tests --- .../Lru/ConcurrentLruAfterDiscreteTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs index 0ed5b59a..f40c59ec 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs @@ -95,9 +95,11 @@ public void WhenItemIsUpdatedTtlIsExtended() [Fact] public void WhenItemIsReadTtlIsExtended() { + expiryCalculator.ExpireAfterCreate = (_, _) => TimeSpan.FromMilliseconds(100); + var lru = new ConcurrentLruBuilder() .WithCapacity(capacity) - .WithExpireAfterAccess(TimeSpan.FromMilliseconds(100)) + .WithExpiry(expiryCalculator) .Build(); // execute the method to ensure it is always jitted @@ -128,6 +130,8 @@ public void WhenItemIsReadTtlIsExtended() [Fact] public void WhenValueEvictedItemRemovedEventIsFired() { + expiryCalculator.ExpireAfterCreate = (_, _) => TimeSpan.FromSeconds(10); + var lruEvents = new ConcurrentLruBuilder() .WithCapacity(new EqualCapacityPartition(6)) .WithExpiry(expiryCalculator) From d2ae6c090965366a87b24706c3585d69b03259df Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 14 Nov 2023 20:24:30 -0800 Subject: [PATCH 29/32] verify expiry type --- .../Lru/ConcurrentLruAfterDiscreteTests.cs | 6 ++-- .../Lru/ConcurrentLruBuilderTests.cs | 20 ++++++------- .../Lru/LruInfoTests.cs | 29 +++++++++++++++++++ BitFaster.Caching/IExpiryCalculator.cs | 3 +- .../Lru/Builder/AsyncConcurrentLruBuilder.cs | 8 ++--- BitFaster.Caching/Lru/Builder/LruInfo.cs | 17 ++++++++++- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 6 ++-- 7 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Lru/LruInfoTests.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs index f40c59ec..e64b513d 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruAfterDiscreteTests.cs @@ -31,7 +31,7 @@ public ConcurrentLruAfterDiscreteTests() { lru = new ConcurrentLruBuilder() .WithCapacity(capacity) - .WithExpiry(expiryCalculator) + .WithExpireAfter(expiryCalculator) .Build(); } @@ -99,7 +99,7 @@ public void WhenItemIsReadTtlIsExtended() var lru = new ConcurrentLruBuilder() .WithCapacity(capacity) - .WithExpiry(expiryCalculator) + .WithExpireAfter(expiryCalculator) .Build(); // execute the method to ensure it is always jitted @@ -134,7 +134,7 @@ public void WhenValueEvictedItemRemovedEventIsFired() var lruEvents = new ConcurrentLruBuilder() .WithCapacity(new EqualCapacityPartition(6)) - .WithExpiry(expiryCalculator) + .WithExpireAfter(expiryCalculator) .WithMetrics() .Build(); diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 2e5a3cbc..27bff8ce 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -178,7 +178,7 @@ public void TestExpireAfterReadAndExpireAfterWriteThrows() public void TestExpireAfter() { ICache expireAfter = new ConcurrentLruBuilder() - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) .Build(); expireAfter.Metrics.HasValue.Should().BeFalse(); @@ -193,7 +193,7 @@ public void TestAsyncExpireAfter() { IAsyncCache expireAfter = new ConcurrentLruBuilder() .AsAsyncCache() - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) .Build(); expireAfter.Metrics.HasValue.Should().BeFalse(); @@ -207,7 +207,7 @@ public void TestAsyncExpireAfter() public void TestExpireAfterWithMetrics() { ICache expireAfter = new ConcurrentLruBuilder() - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) .WithMetrics() .Build(); @@ -223,7 +223,7 @@ public void TestExpireAfterWriteAndExpireAfterThrows() { var builder = new ConcurrentLruBuilder() .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); Action act = () => builder.Build(); act.Should().Throw(); @@ -234,7 +234,7 @@ public void TestExpireAfterAccessAndExpireAfterThrows() { var builder = new ConcurrentLruBuilder() .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); Action act = () => builder.Build(); act.Should().Throw(); @@ -246,7 +246,7 @@ public void TestExpireAfterAccessAndWriteAndExpireAfterThrows() var builder = new ConcurrentLruBuilder() .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) .WithExpireAfterAccess(TimeSpan.FromSeconds(1)) - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))); Action act = () => builder.Build(); act.Should().Throw(); @@ -256,7 +256,7 @@ public void TestExpireAfterAccessAndWriteAndExpireAfterThrows() public void TestScopedWithExpireAfterThrows() { var builder = new ConcurrentLruBuilder() - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) .AsScopedCache(); Action act = () => builder.Build(); @@ -267,7 +267,7 @@ public void TestScopedWithExpireAfterThrows() public void TestScopedAtomicWithExpireAfterThrows() { var builder = new ConcurrentLruBuilder() - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) .AsScopedCache() .WithAtomicGetOrAdd(); @@ -279,7 +279,7 @@ public void TestScopedAtomicWithExpireAfterThrows() public void TestAsyncScopedWithExpireAfterThrows() { var builder = new ConcurrentLruBuilder() - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) .AsAsyncCache() .AsScopedCache(); @@ -291,7 +291,7 @@ public void TestAsyncScopedWithExpireAfterThrows() public void TestAsyncScopedAtomicWithExpireAfterThrows() { var builder = new ConcurrentLruBuilder() - .WithExpiry(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) + .WithExpireAfter(new TestExpiryCalculator((k, v) => TimeSpan.FromMinutes(5))) .AsAsyncCache() .AsScopedCache() .WithAtomicGetOrAdd(); 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/IExpiryCalculator.cs b/BitFaster.Caching/IExpiryCalculator.cs index f4d0851d..1570a3db 100644 --- a/BitFaster.Caching/IExpiryCalculator.cs +++ b/BitFaster.Caching/IExpiryCalculator.cs @@ -3,7 +3,8 @@ namespace BitFaster.Caching { /// - /// Defines a mechanism to calculate when cache entries expire. + /// Defines a mechanism to calculate when cache entries expire based on the item key, value + /// or existing time to expire. /// public interface IExpiryCalculator { diff --git a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs index f6d91db5..c7c2ce29 100644 --- a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs @@ -14,11 +14,11 @@ internal AsyncConcurrentLruBuilder(LruInfo info) } /// - /// Evict after a variable duration specified by an IExpiry instance. + /// Evict after a duration calculated for each item using the specified IExpiryCalculator. /// - /// The expiry that determines item time to expire. - /// A AsyncConcurrentLruBuilder - public AsyncConcurrentLruBuilder WithExpiry(IExpiryCalculator expiry) + /// The expiry calculator that determines item time to expire. + /// A ConcurrentLruBuilder + public AsyncConcurrentLruBuilder WithExpireAfter(IExpiryCalculator expiry) { this.info.SetExpiry(expiry); return this; diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index 0c011a8a..3e02f36f 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -42,7 +42,22 @@ public sealed class LruInfo /// Get the custom expiry. /// /// The expiry. - public IExpiryCalculator GetExpiry() => this.expiry as IExpiryCalculator; + 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. diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 0c00c206..e58c7cff 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -38,11 +38,11 @@ internal ConcurrentLruBuilder(LruInfo info) } /// - /// Evict after a variable duration specified by an IExpiry instance. + /// Evict after a duration calculated for each item using the specified IExpiryCalculator. /// - /// The expiry that determines item time to expire. + /// The expiry calculator that determines item time to expire. /// A ConcurrentLruBuilder - public ConcurrentLruBuilder WithExpiry(IExpiryCalculator expiry) + public ConcurrentLruBuilder WithExpireAfter(IExpiryCalculator expiry) { this.info.SetExpiry(expiry); return this; From 547467d0484467a73067c906cfb9a69a26021263 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 14 Nov 2023 21:14:57 -0800 Subject: [PATCH 30/32] rename --- BitFaster.Caching/IExpiryCalculator.cs | 8 ++++---- BitFaster.Caching/Lru/Builder/LruInfo.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BitFaster.Caching/IExpiryCalculator.cs b/BitFaster.Caching/IExpiryCalculator.cs index 1570a3db..c7a16c41 100644 --- a/BitFaster.Caching/IExpiryCalculator.cs +++ b/BitFaster.Caching/IExpiryCalculator.cs @@ -14,15 +14,15 @@ public interface IExpiryCalculator TimeSpan GetExpireAfterCreate(K key, V value); /// - /// Specify the time to expire after an entry is read. The current TTL may be + /// 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 currentTtl); + TimeSpan GetExpireAfterRead(K key, V value, TimeSpan current); /// - /// Specify the time to expire after an entry is updated.The current TTL may be + /// 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 currentTtl); + TimeSpan GetExpireAfterUpdate(K key, V value, TimeSpan current); } } diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index 3e02f36f..87f7c3e3 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -72,7 +72,7 @@ public IExpiryCalculator GetExpiry() internal void ThrowIfExpirySpecified(string extensionName) { if (this.expiry != null) - Throw.InvalidOp("WithExpiry is not compatible with " + extensionName); + Throw.InvalidOp("WithExpireAfter is not compatible with " + extensionName); } } } From 7a8b4b2f51e8046bf18cd7b66fd59fd7e1009056 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Wed, 15 Nov 2023 11:39:44 -0800 Subject: [PATCH 31/32] braces --- BitFaster.Caching/Lru/Builder/LruInfo.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index 87f7c3e3..411c4829 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -52,9 +52,7 @@ public IExpiryCalculator GetExpiry() 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; } From 6c7b37025a8f8597833b464bea7fc3b367937ba3 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Wed, 15 Nov 2023 12:06:33 -0800 Subject: [PATCH 32/32] file --- BitFaster.Caching/IDiscreteTimePolicy.cs | 23 +++++++++++++++++++++++ BitFaster.Caching/ITimePolicy.cs | 19 ------------------- 2 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 BitFaster.Caching/IDiscreteTimePolicy.cs 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/ITimePolicy.cs b/BitFaster.Caching/ITimePolicy.cs index 01e9e366..f698b1af 100644 --- a/BitFaster.Caching/ITimePolicy.cs +++ b/BitFaster.Caching/ITimePolicy.cs @@ -17,23 +17,4 @@ public interface ITimePolicy /// void TrimExpired(); } - - /// - /// 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(); - } }