Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -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<int, int>()
.Build();

lru.Should().BeOfType<FastConcurrentLru<int, int>>();
}

[Fact]
public void TestMetricsLru()
{
var lru = new ConcurrentLruBuilder<int, int>()
.WithMetrics()
.Build();

lru.Should().BeOfType<ConcurrentLru<int, int>>();
}

[Fact]
public void TestFastTLru()
{
var lru = new ConcurrentLruBuilder<int, int>()
.WithExpireAfterWrite(TimeSpan.FromSeconds(1))
.Build();

lru.Should().BeOfType<FastConcurrentTLru<int, int>>();
}

[Fact]
public void TestMetricsTLru()
{
var lru = new ConcurrentLruBuilder<int, int>()
.WithExpireAfterWrite(TimeSpan.FromSeconds(1))
.WithMetrics()
.Build();

lru.Should().BeOfType<ConcurrentTLru<int, int>>();
lru.Capacity.Should().Be(128);
}

[Fact]
public void TestScoped()
{
var lru = new ConcurrentLruBuilder<int, Disposable>()
.WithScopedValues()
.WithCapacity(3)
.WithExpireAfterWrite(TimeSpan.FromMinutes(1))
.Build();

lru.Should().BeOfType<ScopedCache<int, Disposable>>();
lru.Capacity.Should().Be(3);
}

[Fact]
public void TestComparer()
{
var fastLru = new ConcurrentLruBuilder<string, int>()
.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<int, int>()
.WithConcurrencyLevel(-1);

Action constructor = () => { var x = b.Build(); };

constructor.Should().Throw<ArgumentOutOfRangeException>();
}
}
}
15 changes: 15 additions & 0 deletions BitFaster.Caching/IScoped.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BitFaster.Caching
{
/// <summary>
/// A marker interface for scopes to enable type constraints.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IScoped<T> where T : IDisposable
{ }
}
82 changes: 82 additions & 0 deletions BitFaster.Caching/Lru/Builder/LruBuilderBase.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Recursive generic base class enables builder inheritance.
/// </summary>
public abstract class LruBuilderBase<K, V, TBuilder, TCacheReturn> where TBuilder : LruBuilderBase<K, V, TBuilder, TCacheReturn>
{
internal readonly LruInfo<K> info;

protected LruBuilderBase(LruInfo<K> info)
{
this.info = info;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="capacity">The maximum number of values to keep in the cache.</param>
/// <returns>A ConcurrentLruBuilder</returns>
public TBuilder WithCapacity(int capacity)
{
this.info.Capacity = capacity;
return this as TBuilder;
}

/// <summary>
/// Use the specified concurrency level.
/// </summary>
/// <param name="concurrencyLevel">The estimated number of threads that will update the cache concurrently.</param>
/// <returns>A ConcurrentLruBuilder</returns>
public TBuilder WithConcurrencyLevel(int concurrencyLevel)
{
this.info.ConcurrencyLevel = concurrencyLevel;
return this as TBuilder;
}

/// <summary>
/// Use the specified equality comparison implementation to compare keys.
/// </summary>
/// <param name="comparer">The equality comparison implementation to use when comparing keys.</param>
/// <returns>A ConcurrentLruBuilder</returns>
public TBuilder WithKeyComparer(IEqualityComparer<K> comparer)
{
this.info.KeyComparer = comparer;
return this as TBuilder;
}

/// <summary>
/// Collect cache metrics, such as Hit rate. Metrics have a small performance penalty.
/// </summary>
/// <returns>A ConcurrentLruBuilder</returns>
public TBuilder WithMetrics()
{
this.info.WithMetrics = true;
return this as TBuilder;
}

/// <summary>
/// Evict after a fixed duration since an entry's creation or most recent replacement.
/// </summary>
/// <param name="expiration">The length of time before an entry is automatically removed.</param>
/// <returns>A ConcurrentLruBuilder</returns>
public TBuilder WithExpireAfterWrite(TimeSpan expiration)
{
this.info.TimeToExpireAfterWrite = expiration;
return this as TBuilder;
}

/// <summary>
/// Builds a cache configured via the method calls invoked on the builder instance.
/// </summary>
/// <returns>A cache.</returns>
public abstract TCacheReturn Build();
}
}
21 changes: 21 additions & 0 deletions BitFaster.Caching/Lru/Builder/LruInfo.cs
Original file line number Diff line number Diff line change
@@ -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<K>
{
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<K> KeyComparer { get; set; } = EqualityComparer<K>.Default;
}
}
28 changes: 28 additions & 0 deletions BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<K, V, W> : LruBuilderBase<K, V, ScopedLruBuilder<K, V, W>, IScopedCache<K, V>> where V : IDisposable where W : IScoped<V>
{
private readonly ConcurrentLruBuilder<K, W> inner;

internal ScopedLruBuilder(ConcurrentLruBuilder<K, W> inner)
: base(inner.info)
{
this.inner = inner;
}

///<inheritdoc/>
public override IScopedCache<K, V> Build()
{
// this is a legal type conversion due to the generic constraint on W
var scopedInnerCache = inner.Build() as ICache<K, Scoped<V>>;

return new ScopedCache<K, V>(scopedInnerCache);
}
}
}
55 changes: 55 additions & 0 deletions BitFaster.Caching/Lru/ConcurrentLruBuilder.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="K">The type of keys in the cache.</typeparam>
/// <typeparam name="V">The type of values in the cache.</typeparam>
public sealed class ConcurrentLruBuilder<K, V> : LruBuilderBase<K, V, ConcurrentLruBuilder<K, V>, ICache<K, V>>
{
/// <summary>
/// Creates a ConcurrentLruBuilder.
/// </summary>
public ConcurrentLruBuilder()
: base(new LruInfo<K>())
{
}

internal ConcurrentLruBuilder(LruInfo<K> info)
: base(info)
{
}

///<inheritdoc/>
public override ICache<K, V> Build()
{
switch (info)
{
case LruInfo<K> i when i.WithMetrics && !i.TimeToExpireAfterWrite.HasValue:
return new ConcurrentLru<K, V>(info.ConcurrencyLevel, info.Capacity, info.KeyComparer);
case LruInfo<K> i when i.WithMetrics && i.TimeToExpireAfterWrite.HasValue:
return new ConcurrentTLru<K, V>(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value);
case LruInfo<K> i when i.TimeToExpireAfterWrite.HasValue:
return new FastConcurrentTLru<K, V>(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value);
default:
return new FastConcurrentLru<K, V>(info.ConcurrencyLevel, info.Capacity, info.KeyComparer);
}
}
}
}
26 changes: 26 additions & 0 deletions BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Wrap IDisposable values in a lifetime scope. Scoped caches return lifetimes that prevent
/// values from being disposed until the calling code completes.
/// </summary>
/// <typeparam name="K">The type of keys in the cache.</typeparam>
/// <typeparam name="V">The type of values in the cache.</typeparam>
/// <param name="builder">The ConcurrentLruBuilder to chain method calls onto.</param>
/// <returns>A ScopedLruBuilder</returns>
public static ScopedLruBuilder<K, V, Scoped<V>> WithScopedValues<K, V>(this ConcurrentLruBuilder<K, V> builder) where V : IDisposable
{
var scoped = new ConcurrentLruBuilder<K, Scoped<V>>(builder.info);
return new ScopedLruBuilder<K, V, Scoped<V>>(scoped);
}
}
}
3 changes: 2 additions & 1 deletion BitFaster.Caching/Scoped.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Text;
using System.Threading;
using BitFaster.Caching.Lru;

namespace BitFaster.Caching
{
Expand All @@ -11,7 +12,7 @@ namespace BitFaster.Caching
/// the wrapped object from being diposed until the calling code completes.
/// </summary>
/// <typeparam name="T">The type of scoped value.</typeparam>
public sealed class Scoped<T> : IDisposable where T : IDisposable
public sealed class Scoped<T> : IScoped<T>, IDisposable where T : IDisposable
{
private ReferenceCount<T> refCount;
private bool isDisposed;
Expand Down