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
21 changes: 21 additions & 0 deletions BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ public void WhenKeyIsRequestedItIsCreatedAndCached()
result1.Should().Be(result2);
}


[Fact]
public void WhenKeyIsRequestedWithArgItIsCreatedAndCached()
{
var result1 = lru.GetOrAdd(1, valueFactory.Create, "x");
var result2 = lru.GetOrAdd(1, valueFactory.Create, "y");

valueFactory.timesCalled.Should().Be(1);
result1.Should().Be(result2);
}

[Fact]
public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync()
{
Expand All @@ -191,6 +202,16 @@ public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync()
result1.Should().Be(result2);
}

[Fact]
public async Task WhenKeyIsRequestedWithArgItIsCreatedAndCachedAsync()
{
var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "x").ConfigureAwait(false);
var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "y").ConfigureAwait(false);

valueFactory.timesCalled.Should().Be(1);
result1.Should().Be(result2);
}

[Fact]
public void WhenDifferentKeysAreRequestedValueIsCreatedForEach()
{
Expand Down
110 changes: 74 additions & 36 deletions BitFaster.Caching/Lru/ClassicLru.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,9 @@ public bool TryGet(K key, out V value)
return false;
}

///<inheritdoc/>
public V GetOrAdd(K key, Func<K, V> valueFactory)
private bool TryAdd(K key, V value)
{
if (this.TryGet(key, out var value))
{
return value;
}

var node = new LinkedListNode<LruItem>(new LruItem(key, valueFactory(key)));
var node = new LinkedListNode<LruItem>(new LruItem(key, value));

if (this.dictionary.TryAdd(key, node))
{
Expand Down Expand Up @@ -152,57 +146,101 @@ public V GetOrAdd(K key, Func<K, V> valueFactory)
Disposer<V>.Dispose(removed.Value.Value);
}

return node.Value.Value;
return true;
}

return this.GetOrAdd(key, valueFactory);
return false;
}

///<inheritdoc/>
public async ValueTask<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
public V GetOrAdd(K key, Func<K, V> valueFactory)
{
if (this.TryGet(key, out var value))
{
return value;
}

var node = new LinkedListNode<LruItem>(new LruItem(key, await valueFactory(key)));
value = valueFactory(key);

if (this.dictionary.TryAdd(key, node))
if (TryAdd(key, value))
{
LinkedListNode<LruItem> first = null;
return value;
}

lock (this.linkedList)
{
if (linkedList.Count >= capacity)
{
first = linkedList.First;
linkedList.RemoveFirst();
}
return this.GetOrAdd(key, valueFactory);
}

linkedList.AddLast(node);
}
/// <summary>
/// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
/// existing value if the key already exists.
/// </summary>
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
/// <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>
/// <param name="factoryArgument">An argument value to pass into valueFactory.</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 cache.</returns>
public V GetOrAdd<TArg>(K key, Func<K, TArg, V> valueFactory, TArg factoryArgument)
{
if (this.TryGet(key, out var value))
{
return value;
}

// Remove from the dictionary outside the lock. This means that the dictionary at this moment
// contains an item that is not in the linked list. If another thread fetches this item,
// LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an
// item just as it was about to move to the back of the LRU list and be preserved. The next request
// for the same key will be a miss. Dictionary and list are eventually consistent.
// However, all operations inside the lock are extremely fast, so contention is minimized.
if (first != null)
{
dictionary.TryRemove(first.Value.Key, out var removed);
value = valueFactory(key, factoryArgument);

Interlocked.Increment(ref this.metrics.evictedCount);
Disposer<V>.Dispose(removed.Value.Value);
}
if (TryAdd(key, value))
{
return value;
}

return this.GetOrAdd(key, valueFactory, factoryArgument);
}

///<inheritdoc/>
public async ValueTask<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
{
if (this.TryGet(key, out var value))
{
return value;
}

value = await valueFactory(key);

return node.Value.Value;
if (TryAdd(key, value))
{
return value;
}

return await this.GetOrAddAsync(key, valueFactory);
}

/// <summary>
/// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
/// existing value if the key already exists.
/// </summary>
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
/// <param name="key">The key of the element to add.</param>
/// <param name="valueFactory">The factory function used to asynchronously generate a value for the key.</param>
/// <param name="factoryArgument">An argument value to pass into valueFactory.</param>
/// <returns>A task that represents the asynchronous GetOrAdd operation.</returns>
public async ValueTask<V> GetOrAddAsync<TArg>(K key, Func<K, TArg, Task<V>> valueFactory, TArg factoryArgument)
{
if (this.TryGet(key, out var value))
{
return value;
}

value = await valueFactory(key, factoryArgument);

if (TryAdd(key, value))
{
return value;
}

return await this.GetOrAddAsync(key, valueFactory, factoryArgument);
}

///<inheritdoc/>
public bool TryRemove(K key)
{
Expand Down