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
20 changes: 20 additions & 0 deletions BitFaster.Caching.UnitTests/Disposable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;

namespace BitFaster.Caching.UnitTests
{
public class Disposable : IDisposable
{
public bool IsDisposed { get; set; }

public void Dispose()
{
this.IsDisposed.Should().BeFalse();
IsDisposed = true;
}
}
}
18 changes: 18 additions & 0 deletions BitFaster.Caching.UnitTests/DisposableValueFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BitFaster.Caching.UnitTests
{
public class DisposableValueFactory
{
public Disposable Disposable { get; } = new Disposable();

public Scoped<Disposable> Create(int key)
{
return new Scoped<Disposable>(this.Disposable);
}
}
}
204 changes: 204 additions & 0 deletions BitFaster.Caching.UnitTests/ScopedCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
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
{
public class ScopedCacheTests
{
private const int capacity = 6;
private readonly ScopedCache<int, Disposable> cache = new (new ConcurrentLru<int, Scoped<Disposable>>(capacity));

[Fact]
public void WhenInnerCacheIsNullCtorThrows()
{
Action constructor = () => { var x = new ScopedCache<int, Disposable>(null); };

constructor.Should().Throw<ArgumentNullException>();
}

[Fact]
public void WhenCreatedCapacityPropertyWrapsInnerCache()
{
this.cache.Capacity.Should().Be(capacity);
}

[Fact]
public void WhenItemIsAddedCountIsCorrect()
{
this.cache.Count.Should().Be(0);

this.cache.AddOrUpdate(1, new Disposable());

this.cache.Count.Should().Be(1);
}

[Fact]
public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem()
{
var d = new Disposable();
this.cache.AddOrUpdate(1, d);

this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
lifetime.Value.Should().Be(d);
}

[Fact]
public void WhenKeyExistsAddOrUpdateUpdatesExistingItem()
{
var d1 = new Disposable();
var d2 = new Disposable();
this.cache.AddOrUpdate(1, d1);
this.cache.AddOrUpdate(1, d2);

this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
lifetime.Value.Should().Be(d2);
}

[Fact]
public void WhenItemUpdatedOldValueIsAliveUntilLifetimeCompletes()
{
var d1 = new Disposable();
var d2 = new Disposable();

// start a lifetime on 1
this.cache.AddOrUpdate(1, d1);
this.cache.ScopedTryGet(1, out var lifetime1).Should().BeTrue();

using (lifetime1)
{
// replace 1
this.cache.AddOrUpdate(1, d2);

// cache reflects replacement
this.cache.ScopedTryGet(1, out var lifetime2).Should().BeTrue();
lifetime2.Value.Should().Be(d2);

d1.IsDisposed.Should().BeFalse();
}

d1.IsDisposed.Should().BeTrue();
}

[Fact]
public void WhenClearedItemsAreDisposed()
{
var d = new Disposable();
this.cache.AddOrUpdate(1, d);

this.cache.Clear();

d.IsDisposed.Should().BeTrue();
}

[Fact]
public void WhenItemExistsTryGetReturnsLifetime()
{
this.cache.AddOrUpdate(1, new Disposable());
this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();

lifetime.Should().NotBeNull();
}

[Fact]
public void WhenItemDoesNotExistTryGetReturnsFalse()
{
this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse();
}

[Fact]
public void WhenScopeIsDisposedTryGetReturnsFalse()
{
var scope = new Scoped<Disposable>(new Disposable());

this.cache.ScopedGetOrAdd(1, k => scope);

scope.Dispose();

this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse();
}

[Fact]
public void WhenKeyDoesNotExistGetOrAddAddsValue()
{
this.cache.ScopedGetOrAdd(1, k => new Scoped<Disposable>(new Disposable()));

this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
}

[Fact]
public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue()
{
await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped<Disposable>(new Disposable())));

this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
}

[Fact]
public void GetOrAddDisposedScopeThrows()
{
var scope = new Scoped<Disposable>(new Disposable());
scope.Dispose();


Action getOrAdd = () => { this.cache.ScopedGetOrAdd(1, k => scope); };

getOrAdd.Should().Throw<InvalidOperationException>();
}

[Fact]
public void GetOrAddAsyncDisposedScopeThrows()
{
var scope = new Scoped<Disposable>(new Disposable());
scope.Dispose();

Func<Task> getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); };

getOrAdd.Should().ThrowAsync<InvalidOperationException>();
}

[Fact]
public void WhenCacheContainsValuesTrim1RemovesColdestValue()
{
this.cache.AddOrUpdate(0, new Disposable());
this.cache.AddOrUpdate(1, new Disposable());
this.cache.AddOrUpdate(2, new Disposable());

this.cache.Trim(1);

this.cache.ScopedTryGet(0, out var lifetime).Should().BeFalse();
}

[Fact]
public void WhenKeyDoesNotExistTryRemoveReturnsFalse()
{
this.cache.TryRemove(1).Should().BeFalse();
}

[Fact]
public void WhenKeyExistsTryRemoveReturnsTrue()
{
this.cache.AddOrUpdate(1, new Disposable());
this.cache.TryRemove(1).Should().BeTrue();
}

[Fact]
public void WhenKeyDoesNotExistTryUpdateReturnsFalse()
{
this.cache.TryUpdate(1, new Disposable()).Should().BeFalse();
}

[Fact]
public void WhenKeyExistsTryUpdateReturnsTrue()
{
this.cache.AddOrUpdate(1, new Disposable());

this.cache.TryUpdate(1, new Disposable()).Should().BeTrue();
}
}
}
21 changes: 0 additions & 21 deletions BitFaster.Caching.UnitTests/ScopedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,5 @@ public void WhenScopedIsCreatedFromCacheItemHasExpectedLifetime()

valueFactory.Disposable.IsDisposed.Should().BeTrue();
}

private class DisposableValueFactory
{
public Disposable Disposable { get; } = new Disposable();

public Scoped<Disposable> Create(int key)
{
return new Scoped<Disposable>(this.Disposable);
}
}

private class Disposable : IDisposable
{
public bool IsDisposed { get; set; }

public void Dispose()
{
this.IsDisposed.Should().BeFalse();
IsDisposed = true;
}
}
}
}
2 changes: 1 addition & 1 deletion BitFaster.Caching/ICache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public interface ICache<K, V>
/// <param name="key">The key of the element to add.</param>
/// <param name="valueFactory">The factory function used to generate a value for the key.</param>
/// <returns>The value for the key. This will be either the existing value for the key if the key is already
/// in the cache, or the new value if the key was not in the dictionary.</returns>
/// in the cache, or the new value if the key was not in the cache.</returns>
V GetOrAdd(K key, Func<K, V> valueFactory);

/// <summary>
Expand Down
87 changes: 87 additions & 0 deletions BitFaster.Caching/IScopedCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BitFaster.Caching
{
/// <summary>
/// Represents a generic cache of key/scoped IDisposable value pairs.
/// </summary>
/// <typeparam name="K">The type of keys in the cache.</typeparam>
/// <typeparam name="V">The type of values in the cache.</typeparam>
public interface IScopedCache<K, V> where V : IDisposable
{
/// <summary>
/// Gets the total number of items that can be stored in the cache.
/// </summary>
int Capacity { get; }

/// <summary>
/// Gets the number of items currently held in the cache.
/// </summary>
int Count { get; }

/// <summary>
/// Attempts to create a lifetime for the value associated with the specified key from the cache
/// </summary>
/// <param name="key">The key of the value to get.</param>
/// <param name="lifetime">When this method returns, contains a lifetime for the object from the cache that has the specified key, or the default value of the type if the operation failed.</param>
/// <returns>true if the key was found in the cache; otherwise, false.</returns>
bool ScopedTryGet(K key, out Lifetime<V> lifetime);

/// <summary>
/// Adds a key/scoped value pair to the cache if the key does not already exist. Returns a lifetime for either the new value, or the
/// existing value if the key already exists.
/// </summary>
/// <param name="key">The key of the element to add.</param>
/// <param name="valueFactory">The factory function used to generate a scoped value for the key.</param>
/// <returns>The lifetime for the value associated with the key. The lifetime will be either reference the existing value for the key if the key is already
/// in the cache, or the new value if the key was not in the cache.</returns>
Lifetime<V> ScopedGetOrAdd(K key, Func<K, Scoped<V>> valueFactory);

/// <summary>
/// Adds a key/scoped value pair to the cache if the key does not already exist. Returns a lifetime for either the new value, or the
/// existing value if the key already exists.
/// </summary>
/// <param name="key">The key of the element to add.</param>
/// <param name="valueFactory">The factory function used to asynchronously generate a scoped value for the key.</param>
/// <returns>A task that represents the asynchronous ScopedGetOrAdd operation.</returns>
Task<Lifetime<V>> ScopedGetOrAddAsync(K key, Func<K, Task<Scoped<V>>> valueFactory);

/// <summary>
/// Attempts to remove the value that has the specified key.
/// </summary>
/// <param name="key">The key of the element to remove.</param>
/// <returns>true if the object was removed successfully; otherwise, false.</returns>
bool TryRemove(K key);

/// <summary>
/// Attempts to update the value that has the specified key.
/// </summary>
/// <param name="key">The key of the element to update.</param>
/// <param name="value">The new value.</param>
/// <returns>true if the object was updated successfully; otherwise, false.</returns>
bool TryUpdate(K key, V value);

/// <summary>
/// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the
/// key already exists.
/// </summary>
/// <param name="key">The key of the element to update.</param>
/// <param name="value">The new value.</param>
void AddOrUpdate(K key, V value);

/// <summary>
/// Removes all keys and values from the cache.
/// </summary>
void Clear();

/// <summary>
/// Trim the specified number of items from the cache.
/// </summary>
/// <param name="itemCount">The number of items to remove.</param>
void Trim(int itemCount);
}
}
Loading