Skip to content

Commit

Permalink
feat: allow passing MemoryCacheEntryOptions at cache insertion time a…
Browse files Browse the repository at this point in the history
…nd add immediate cache removal

- Add options for expiration:
    - ExpirationMode.ImmediateExpiration which uses a timer to remove items from the cache as soon as they expire (more resource intensive)
    - ExpirationMode.LazyExpiration (existing default) which removes expired cache items when they are next accessed if they have expired.
- Fix #96 AddExpirationToken with CancellationChangeToken is not being honored
- Allow callers to pass MemoryCacheEntryOptions that is used at cache insertion time. This allows users to wire up callbacks and expiration tokens that fire at the correct time
  • Loading branch information
alastairtree committed Sep 19, 2020
1 parent 0b32080 commit 218bd23
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 21 deletions.
79 changes: 79 additions & 0 deletions LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,85 @@ public async Task GetOrAddAsyncWithCancellationExpiryBasedOnTimerInTheDelegateDo
Assert.That(expiredResult, Is.Null);
}

[Test]
public void GetOrAddWithCancellationExpiryBasedOnTimerAndCallbackInTheDelegateDoesExpireItemsAndFireTheCallback()
{
var millisecondsCacheDuration = 100;
var callbackHasFired = false;
var tokenSource = new CancellationTokenSource(millisecondsCacheDuration);
var expireToken = new CancellationChangeToken(tokenSource.Token);
var validResult = sut.GetOrAdd(
TestKey,
entry =>
{
entry.RegisterPostEvictionCallback((key, value, reason, state) => callbackHasFired = true);
return new ComplexTestObject();
}, new MemoryCacheEntryOptions()
.AddExpirationToken(expireToken));
// trigger expiry
Thread.Sleep(TimeSpan.FromMilliseconds(millisecondsCacheDuration + 50));

Assert.That(validResult, Is.Not.Null);
Assert.That(callbackHasFired, Is.True);
}

[Test]
public void GetOrAddWithImmediateExpirationAndCallbackInTheDelegateDoesExpireItemsAndFireTheCallback()
{
var millisecondsCacheDuration = 100;
var callbackHasFired = false;
var validResult = sut.GetOrAdd(
TestKey,
entry =>
{
entry.RegisterPostEvictionCallback((key, value, reason, state) => callbackHasFired = true);
return new ComplexTestObject();
}, LazyCacheEntryOptions.WithImmediateAbsoluteExpiration(TimeSpan.FromMilliseconds(millisecondsCacheDuration)));
// trigger expiry
Thread.Sleep(TimeSpan.FromMilliseconds(millisecondsCacheDuration + 50));

Assert.That(validResult, Is.Not.Null);
Assert.That(callbackHasFired, Is.True);
}

[Test]
public async Task GetOrAddAsyncWithImmediateExpirationAndCallbackInTheDelegateDoesExpireItemsAndFireTheCallback()
{
var millisecondsCacheDuration = 100;
var callbackHasFired = false;
var validResult = await sut.GetOrAddAsync(
TestKey,
entry =>
{
entry.RegisterPostEvictionCallback((key, value, reason, state) => callbackHasFired = true);
return Task.FromResult(new ComplexTestObject());
}, LazyCacheEntryOptions.WithImmediateAbsoluteExpiration(TimeSpan.FromMilliseconds(millisecondsCacheDuration)));
// trigger expiry
Thread.Sleep(TimeSpan.FromMilliseconds(millisecondsCacheDuration + 50));

Assert.That(validResult, Is.Not.Null);
Assert.That(callbackHasFired, Is.True);
}

[Test]
public async Task GetOrAddAsyncWithImmediateExpirationDoesExpireItems()
{
var millisecondsCacheDuration = 100;
var validResult = await sut.GetOrAddAsync(
TestKey,
() =>
{
return Task.FromResult(new ComplexTestObject());
}, DateTimeOffset.UtcNow.AddMilliseconds(millisecondsCacheDuration), ExpirationMode.ImmediateExpiration);
// trigger expiry
Thread.Sleep(TimeSpan.FromMilliseconds(millisecondsCacheDuration + 50));

var actual = sut.Get<ComplexTestObject>(TestKey);

Assert.That(validResult, Is.Not.Null);
Assert.That(actual, Is.Null);
}

[Test]
public void GetOrAddWithPolicyAndThenGetObjectReturnsCorrectType()
{
Expand Down
46 changes: 35 additions & 11 deletions LazyCache/AppCacheExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;

namespace LazyCache
{
Expand Down Expand Up @@ -41,6 +43,21 @@ public static T GetOrAdd<T>(this IAppCache cache, string key, Func<T> addItemFac
return cache.GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires});
}

public static T GetOrAdd<T>(this IAppCache cache, string key, Func<T> addItemFactory, DateTimeOffset expires, ExpirationMode mode)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

switch (mode)
{
case ExpirationMode.LazyExpiration:
return cache.GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions { AbsoluteExpiration = expires });
case ExpirationMode.ImmediateExpiration:
return cache.GetOrAdd(key, addItemFactory, LazyCacheEntryOptions.WithImmediateAbsoluteExpiration(expires));
default:
throw new ArgumentOutOfRangeException(nameof(mode), mode, null);
}
}

public static T GetOrAdd<T>(this IAppCache cache, string key, Func<T> addItemFactory,
TimeSpan slidingExpiration)
{
Expand All @@ -53,11 +70,7 @@ public static T GetOrAdd<T>(this IAppCache cache, string key, Func<T> addItemFac
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

return cache.GetOrAdd(key, entry =>
{
entry.SetOptions(policy);
return addItemFactory();
});
return cache.GetOrAdd(key, _=> addItemFactory(), policy);
}

public static Task<T> GetOrAddAsync<T>(this IAppCache cache, string key, Func<Task<T>> addItemFactory)
Expand All @@ -67,7 +80,6 @@ public static Task<T> GetOrAddAsync<T>(this IAppCache cache, string key, Func<Ta
return cache.GetOrAddAsync(key, addItemFactory, cache.DefaultCachePolicy.BuildOptions());
}


public static Task<T> GetOrAddAsync<T>(this IAppCache cache, string key, Func<Task<T>> addItemFactory,
DateTimeOffset expires)
{
Expand All @@ -76,6 +88,22 @@ public static Task<T> GetOrAddAsync<T>(this IAppCache cache, string key, Func<Ta
return cache.GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires});
}

public static Task<T> GetOrAddAsync<T>(this IAppCache cache, string key, Func<Task<T>> addItemFactory,
DateTimeOffset expires, ExpirationMode mode)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

switch (mode)
{
case ExpirationMode.LazyExpiration:
return cache.GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions { AbsoluteExpiration = expires });
case ExpirationMode.ImmediateExpiration:
return cache.GetOrAddAsync(key, addItemFactory, new LazyCacheEntryOptions().SetAbsoluteExpiration(expires,mode));
default:
throw new ArgumentOutOfRangeException(nameof(mode), mode, null);
}
}

public static Task<T> GetOrAddAsync<T>(this IAppCache cache, string key, Func<Task<T>> addItemFactory,
TimeSpan slidingExpiration)
{
Expand All @@ -90,11 +118,7 @@ public static Task<T> GetOrAddAsync<T>(this IAppCache cache, string key, Func<Ta
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

return cache.GetOrAddAsync(key, entry =>
{
entry.SetOptions(policy);
return addItemFactory();
});
return cache.GetOrAddAsync(key, _=> addItemFactory(), policy);
}
}
}
17 changes: 14 additions & 3 deletions LazyCache/CachingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using LazyCache.Providers;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;

namespace LazyCache
{
Expand Down Expand Up @@ -84,6 +85,11 @@ public virtual Task<T> GetAsync<T>(string key)
}

public virtual T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory)
{
return GetOrAdd(key, addItemFactory, null);
}

public virtual T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory, MemoryCacheEntryOptions policy)
{
ValidateKey(key);

Expand All @@ -101,7 +107,7 @@ object CacheFactory(ICacheEntry entry) =>
locker.Wait(); //TODO: do we really need this? Could we just lock on the key? like this? https://github.com/zkSNACKs/WalletWasabi/blob/7780db075685d2dc13620e0bcf6cc07578b627c2/WalletWasabi/Extensions/MemoryExtensions.cs
try
{
cacheItem = CacheProvider.GetOrCreate<object>(key, CacheFactory);
cacheItem = CacheProvider.GetOrCreate<object>(key, policy, CacheFactory);
}
finally
{
Expand All @@ -110,7 +116,7 @@ object CacheFactory(ICacheEntry entry) =>

try
{
var result = GetValueFromLazy<T>(cacheItem, out var valueHasChangedType);
var result = GetValueFromLazy<T>(cacheItem, out var valueHasChangedType);

// if we get a cache hit but for something with the wrong type we need to evict it, start again and cache the new item instead
if (valueHasChangedType)
Expand Down Expand Up @@ -156,6 +162,11 @@ public virtual void Remove(string key)

public virtual async Task<T> GetOrAddAsync<T>(string key, Func<ICacheEntry, Task<T>> addItemFactory)
{
return await GetOrAddAsync(key, addItemFactory, null);
}
public virtual async Task<T> GetOrAddAsync<T>(string key, Func<ICacheEntry, Task<T>> addItemFactory,
MemoryCacheEntryOptions policy)
{
ValidateKey(key);

object cacheItem;
Expand All @@ -180,7 +191,7 @@ object CacheFactory(ICacheEntry entry) =>

try
{
cacheItem = CacheProvider.GetOrCreate<object>(key, CacheFactory);
cacheItem = CacheProvider.GetOrCreate<object>(key, policy, CacheFactory);
}
finally
{
Expand Down
20 changes: 20 additions & 0 deletions LazyCache/ExpirationMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace LazyCache
{
public enum ExpirationMode
{
/// <summary>
/// This is the default for Memory cache - expired items are removed from the cache
/// the next time that key is accessed. This is the most performant, and so the default,
/// because no timers are required to removed expired items, but it does mean that
/// PostEvictionCallbacks may fire later than expected, or not at all.
/// </summary>
LazyExpiration,

/// <summary>
/// Use a timer to force eviction of expired items from the cache as soon as they expire.
/// This will then trigger PostEvictionCallbacks at the expected time. This uses more resources
/// than LazyExpiration.
/// </summary>
ImmediateExpiration
}
}
10 changes: 3 additions & 7 deletions LazyCache/IAppCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,13 @@ public interface IAppCache
/// Define the number of seconds to cache objects for by default
/// </summary>
CacheDefaults DefaultCachePolicy { get; }

void Add<T>(string key, T item, MemoryCacheEntryOptions policy);

T Get<T>(string key);

T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory);

Task<T> GetAsync<T>(string key);

T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory);
T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory, MemoryCacheEntryOptions policy);
Task<T> GetOrAddAsync<T>(string key, Func<ICacheEntry, Task<T>> addItemFactory);

Task<T> GetOrAddAsync<T>(string key, Func<ICacheEntry, Task<T>> addItemFactory, MemoryCacheEntryOptions policy);
void Remove(string key);
}
}
1 change: 1 addition & 0 deletions LazyCache/ICacheProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public interface ICacheProvider : IDisposable
void Set(string key, object item, MemoryCacheEntryOptions policy);
object Get(string key);
object GetOrCreate<T>(string key, Func<ICacheEntry, T> func);
object GetOrCreate<T>(string key, MemoryCacheEntryOptions policy, Func<ICacheEntry, T> func);
void Remove(string key);
Task<T> GetOrCreateAsync<T>(string key, Func<ICacheEntry, Task<T>> func);
}
Expand Down
57 changes: 57 additions & 0 deletions LazyCache/MemoryCacheEntryOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using Microsoft.Extensions.Caching.Memory;

namespace LazyCache
{
public class LazyCacheEntryOptions : MemoryCacheEntryOptions
{
public ExpirationMode ExpirationMode { get; set; }
public TimeSpan ImmediateAbsoluteExpirationRelativeToNow { get; set; }

public static LazyCacheEntryOptions WithImmediateAbsoluteExpiration(DateTimeOffset absoluteExpiration)
{
var delay = absoluteExpiration.Subtract(DateTimeOffset.UtcNow);
return new LazyCacheEntryOptions
{
AbsoluteExpiration = absoluteExpiration,
ExpirationMode = ExpirationMode.ImmediateExpiration,
ImmediateAbsoluteExpirationRelativeToNow = delay
};
}

public static LazyCacheEntryOptions WithImmediateAbsoluteExpiration(TimeSpan absoluteExpiration)
{
return new LazyCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = absoluteExpiration,
ExpirationMode = ExpirationMode.ImmediateExpiration,
ImmediateAbsoluteExpirationRelativeToNow = absoluteExpiration
};
}
}

public static class LazyCacheEntryOptionsExtension {
public static LazyCacheEntryOptions SetAbsoluteExpiration(this LazyCacheEntryOptions option, DateTimeOffset absoluteExpiration,
ExpirationMode mode)
{
if (option == null) throw new ArgumentNullException(nameof(option));

var delay = absoluteExpiration.Subtract(DateTimeOffset.UtcNow);
option.AbsoluteExpiration = absoluteExpiration;
option.ExpirationMode = mode;
option.ImmediateAbsoluteExpirationRelativeToNow = delay;
return option;
}

public static LazyCacheEntryOptions SetAbsoluteExpiration(this LazyCacheEntryOptions option, TimeSpan absoluteExpiration,
ExpirationMode mode)
{
if (option == null) throw new ArgumentNullException(nameof(option));

option.AbsoluteExpirationRelativeToNow = absoluteExpiration;
option.ExpirationMode = mode;
option.ImmediateAbsoluteExpirationRelativeToNow = absoluteExpiration;
return option;
}
}
}
5 changes: 5 additions & 0 deletions LazyCache/Mocks/MockCacheProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public object GetOrCreate<T>(string key, Func<ICacheEntry, T> func)
return func(null);
}

public object GetOrCreate<T>(string key, MemoryCacheEntryOptions policy, Func<ICacheEntry, T> func)
{
return func(null);
}

public void Remove(string key)
{
}
Expand Down
11 changes: 11 additions & 0 deletions LazyCache/Mocks/MockCachingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ public T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory)
return addItemFactory(new MockCacheEntry(key));
}

public T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory, MemoryCacheEntryOptions policy)
{
return addItemFactory(new MockCacheEntry(key));
}

public Task<T> GetOrAddAsync<T>(string key, Func<ICacheEntry, Task<T>> addItemFactory,
MemoryCacheEntryOptions policy)
{
return addItemFactory(new MockCacheEntry(key));
}

public void Remove(string key)
{
}
Expand Down
Loading

0 comments on commit 218bd23

Please sign in to comment.