diff --git a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs new file mode 100644 index 00000000..95d7f601 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lru +{ + public class LruBuilderTests + { + [Fact] + public void TestFastLru() + { + var lru = new ConcurrentLruBuilder() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestMetricsLru() + { + var lru = new ConcurrentLruBuilder() + .WithMetrics() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestFastTLru() + { + var lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestMetricsTLru() + { + var lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .WithMetrics() + .Build(); + + lru.Should().BeOfType>(); + lru.Capacity.Should().Be(128); + } + + [Fact] + public void TestScoped() + { + var lru = new ConcurrentLruBuilder() + .WithScopedValues() + .WithCapacity(3) + .WithExpireAfterWrite(TimeSpan.FromMinutes(1)) + .Build(); + + lru.Should().BeOfType>(); + lru.Capacity.Should().Be(3); + } + + [Fact] + public void TestComparer() + { + var fastLru = new ConcurrentLruBuilder() + .WithKeyComparer(StringComparer.OrdinalIgnoreCase) + .Build(); + + fastLru.GetOrAdd("a", k => 1); + fastLru.TryGet("A", out var value).Should().BeTrue(); + } + + [Fact] + public void TestConcurrencyLevel() + { + var b = new ConcurrentLruBuilder() + .WithConcurrencyLevel(-1); + + Action constructor = () => { var x = b.Build(); }; + + constructor.Should().Throw(); + } + } +} diff --git a/BitFaster.Caching/IScoped.cs b/BitFaster.Caching/IScoped.cs new file mode 100644 index 00000000..5a760055 --- /dev/null +++ b/BitFaster.Caching/IScoped.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + /// + /// A marker interface for scopes to enable type constraints. + /// + /// + public interface IScoped where T : IDisposable + { } +} diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs new file mode 100644 index 00000000..bfd8c253 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + /// + /// Recursive generic base class enables builder inheritance. + /// + public abstract class LruBuilderBase where TBuilder : LruBuilderBase + { + internal readonly LruInfo info; + + protected LruBuilderBase(LruInfo info) + { + this.info = info; + } + + /// + /// Set the maximum number of values to keep in the cache. If more items than this are added, + /// the cache eviction policy will determine which values to remove. + /// + /// The maximum number of values to keep in the cache. + /// A ConcurrentLruBuilder + public TBuilder WithCapacity(int capacity) + { + this.info.Capacity = capacity; + return this as TBuilder; + } + + /// + /// Use the specified concurrency level. + /// + /// The estimated number of threads that will update the cache concurrently. + /// A ConcurrentLruBuilder + public TBuilder WithConcurrencyLevel(int concurrencyLevel) + { + this.info.ConcurrencyLevel = concurrencyLevel; + return this as TBuilder; + } + + /// + /// Use the specified equality comparison implementation to compare keys. + /// + /// The equality comparison implementation to use when comparing keys. + /// A ConcurrentLruBuilder + public TBuilder WithKeyComparer(IEqualityComparer comparer) + { + this.info.KeyComparer = comparer; + return this as TBuilder; + } + + /// + /// Collect cache metrics, such as Hit rate. Metrics have a small performance penalty. + /// + /// A ConcurrentLruBuilder + public TBuilder WithMetrics() + { + this.info.WithMetrics = true; + return this as TBuilder; + } + + /// + /// Evict after a fixed duration since an entry's creation or most recent replacement. + /// + /// The length of time before an entry is automatically removed. + /// A ConcurrentLruBuilder + public TBuilder WithExpireAfterWrite(TimeSpan expiration) + { + this.info.TimeToExpireAfterWrite = expiration; + return this as TBuilder; + } + + /// + /// Builds a cache configured via the method calls invoked on the builder instance. + /// + /// A cache. + public abstract TCacheReturn Build(); + } +} diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs new file mode 100644 index 00000000..42581963 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public sealed class LruInfo + { + public int Capacity { get; set; } = 128; + + public int ConcurrencyLevel { get; set; } = Defaults.ConcurrencyLevel; + + public TimeSpan? TimeToExpireAfterWrite { get; set; } = null; + + public bool WithMetrics { get; set; } = false; + + public IEqualityComparer KeyComparer { get; set; } = EqualityComparer.Default; + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs new file mode 100644 index 00000000..e445de71 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public sealed class ScopedLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped + { + private readonly ConcurrentLruBuilder inner; + + internal ScopedLruBuilder(ConcurrentLruBuilder inner) + : base(inner.info) + { + this.inner = inner; + } + + /// + public override IScopedCache Build() + { + // this is a legal type conversion due to the generic constraint on W + var scopedInnerCache = inner.Build() as ICache>; + + return new ScopedCache(scopedInnerCache); + } + } +} diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs new file mode 100644 index 00000000..91b3431d --- /dev/null +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru.Builder; + +namespace BitFaster.Caching.Lru +{ + /// + /// A builder of ICache and IScopedCache instances with the following configuration + /// settings: + /// - The maximum size. + /// - The concurrency level. + /// - The key comparer. + /// + /// The following features can be selected which change the underlying cache implementation: + /// - Collect metrics (e.g. hit rate). Small perf penalty. + /// - Time based expiration, measured since write. + /// - Scoped IDisposable values. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public sealed class ConcurrentLruBuilder : LruBuilderBase, ICache> + { + /// + /// Creates a ConcurrentLruBuilder. + /// + public ConcurrentLruBuilder() + : base(new LruInfo()) + { + } + + internal ConcurrentLruBuilder(LruInfo info) + : base(info) + { + } + + /// + public override ICache Build() + { + switch (info) + { + case LruInfo i when i.WithMetrics && !i.TimeToExpireAfterWrite.HasValue: + return new ConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); + case LruInfo i when i.WithMetrics && i.TimeToExpireAfterWrite.HasValue: + return new ConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); + case LruInfo i when i.TimeToExpireAfterWrite.HasValue: + return new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); + default: + return new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); + } + } + } +} diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs new file mode 100644 index 00000000..0e3dd1c8 --- /dev/null +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru.Builder; + +namespace BitFaster.Caching.Lru +{ + public static class ConcurrentLruBuilderExtensions + { + /// + /// Wrap IDisposable values in a lifetime scope. Scoped caches return lifetimes that prevent + /// values from being disposed until the calling code completes. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + /// The ConcurrentLruBuilder to chain method calls onto. + /// A ScopedLruBuilder + public static ScopedLruBuilder> WithScopedValues(this ConcurrentLruBuilder builder) where V : IDisposable + { + var scoped = new ConcurrentLruBuilder>(builder.info); + return new ScopedLruBuilder>(scoped); + } + } +} diff --git a/BitFaster.Caching/Scoped.cs b/BitFaster.Caching/Scoped.cs index dad6ee46..69b41236 100644 --- a/BitFaster.Caching/Scoped.cs +++ b/BitFaster.Caching/Scoped.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Threading; +using BitFaster.Caching.Lru; namespace BitFaster.Caching { @@ -11,7 +12,7 @@ namespace BitFaster.Caching /// the wrapped object from being diposed until the calling code completes. /// /// The type of scoped value. - public sealed class Scoped : IDisposable where T : IDisposable + public sealed class Scoped : IScoped, IDisposable where T : IDisposable { private ReferenceCount refCount; private bool isDisposed;