Permalink
Browse files

Added memory cache resolver options to control instance eviction and …

…disposal.
  • Loading branch information...
Ben Foster
Ben Foster committed Jul 13, 2016
1 parent 6081b85 commit b3bfbb19229ed139905cb0aaa3397f1d0b64b73c
@@ -13,35 +13,22 @@ public abstract class MemoryCacheTenantResolver<TTenant> : ITenantResolver<TTena
{
protected readonly IMemoryCache cache;
protected readonly ILogger log;
+ protected readonly MemoryCacheTenantResolverOptions options;
public MemoryCacheTenantResolver(IMemoryCache cache, ILoggerFactory loggerFactory)
+ : this(cache, loggerFactory, new MemoryCacheTenantResolverOptions())
+ {
+ }
+
+ public MemoryCacheTenantResolver(IMemoryCache cache, ILoggerFactory loggerFactory, MemoryCacheTenantResolverOptions options)
{
Ensure.Argument.NotNull(cache, nameof(cache));
Ensure.Argument.NotNull(loggerFactory, nameof(loggerFactory));
+ Ensure.Argument.NotNull(options, nameof(options));
this.cache = cache;
this.log = loggerFactory.CreateLogger<MemoryCacheTenantResolver<TTenant>>();
- }
-
- protected virtual MemoryCacheEntryOptions GetCacheEntryOptions()
- {
- var cacheEntryOptions = CreateCacheEntryOptions();
-
- if (DisposeTenantOnExpiration)
- {
- var tokenSource = new CancellationTokenSource();
-
- cacheEntryOptions
- .RegisterPostEvictionCallback(
- (key, value, reason, state) =>
- {
- DisposeTenantContext(key, value as TenantContext<TTenant>);
- tokenSource.Cancel();
- })
- .AddExpirationToken(new CancellationChangeToken(tokenSource.Token));
- }
-
- return cacheEntryOptions;
+ this.options = options;
}
protected virtual MemoryCacheEntryOptions CreateCacheEntryOptions()
@@ -50,8 +37,6 @@ protected virtual MemoryCacheEntryOptions CreateCacheEntryOptions()
.SetSlidingExpiration(new TimeSpan(1, 0, 0));
}
- protected virtual bool DisposeTenantOnExpiration => true;
-
protected virtual void DisposeTenantContext(object cacheKey, TenantContext<TTenant> tenantContext)
{
if (tenantContext != null)
@@ -108,5 +93,35 @@ protected virtual void DisposeTenantContext(object cacheKey, TenantContext<TTena
return tenantContext;
}
+
+ private MemoryCacheEntryOptions GetCacheEntryOptions()
+ {
+ var cacheEntryOptions = CreateCacheEntryOptions();
+
+ if (options.EvictAllEntriesOnExpiry)
+ {
+ var tokenSource = new CancellationTokenSource();
+
+ cacheEntryOptions
+ .RegisterPostEvictionCallback(
+ (key, value, reason, state) =>
+ {
+ tokenSource.Cancel();
+ })
+ .AddExpirationToken(new CancellationChangeToken(tokenSource.Token));
+ }
+
+ if (options.DisposeOnEviction)
+ {
+ cacheEntryOptions
+ .RegisterPostEvictionCallback(
+ (key, value, reason, state) =>
+ {
+ DisposeTenantContext(key, value as TenantContext<TTenant>);
+ });
+ }
+
+ return cacheEntryOptions;
+ }
}
}
@@ -0,0 +1,29 @@
+namespace SaasKit.Multitenancy
+{
+ /// <summary>
+ /// Configuration options for <see cref="MemoryCacheTenantResolver{TTenant}"/>.
+ /// </summary>
+ public class MemoryCacheTenantResolverOptions
+ {
+ /// <summary>
+ /// Creates a new <see cref="MemoryCacheTenantResolverOptions"/> instance.
+ /// </summary>
+ public MemoryCacheTenantResolverOptions()
+ {
+ EvictAllEntriesOnExpiry = true;
+ DisposeOnEviction = true;
+ }
+
+ /// <summary>
+ /// Gets or sets a setting that determines whether all cache entries for a <see cref="TenantContext{TTenant}"/>
+ /// instance should be evicted when any of the entries expire. Default: True.
+ /// </summary>
+ public bool EvictAllEntriesOnExpiry { get; set; }
+
+ /// <summary>
+ /// Gets or sets a setting that determines whether cached tenant context instances should be disposed
+ /// when upon eviction from the cache. Default: True.
+ /// </summary>
+ public bool DisposeOnEviction { get; set; }
+ }
+}
@@ -22,7 +22,7 @@ private HttpContext CreateContext(string requestPath)
}
[Fact]
- public async Task Can_retrieve_tenant_from_resolver()
+ public async Task Can_resolve_tenant_context()
{
var harness = new TestHarness();
var context = CreateContext("/apple");
@@ -35,7 +35,7 @@ private HttpContext CreateContext(string requestPath)
[Fact]
- public async Task Can_retrieve_tenant_from_cache()
+ public async Task Can_retrieve_tenant_context_from_cache()
{
var harness = new TestHarness();
var context = CreateContext("/apple");
@@ -50,7 +50,7 @@ private HttpContext CreateContext(string requestPath)
}
[Fact]
- public async Task Can_retrieve_tenant_from_cache_with_different_key()
+ public async Task Can_retrieve_tenant_context_from_cache_using_linked_identifier()
{
var harness = new TestHarness();
var context = CreateContext("/apple");
@@ -65,9 +65,9 @@ private HttpContext CreateContext(string requestPath)
}
[Fact]
- public async Task Tenant_expires_from_cache()
+ public async Task Should_dispose_tenant_on_eviction_from_cache_by_default()
{
- var harness = new TestHarness(cacheExpirationInSeconds: 1, disposeOnExpiration: true);
+ var harness = new TestHarness(cacheExpirationInSeconds: 1);
var context = CreateContext("/apple");
var tenantContext = await harness.Resolver.ResolveAsync(context);
@@ -77,16 +77,50 @@ private HttpContext CreateContext(string requestPath)
Thread.Sleep(3 * 1000);
Assert.False(harness.Cache.TryGetValue("/pear", out cachedTenant));
-
+ Assert.True(tenantContext.Tenant.Disposed);
Assert.Null(cachedTenant);
}
+ [Fact]
+ public async Task Should_evict_all_cache_entries_of_tenant_context_by_default()
+ {
+ var harness = new TestHarness(cacheExpirationInSeconds: 10);
+
+ // first request for apple
+ var tenantContext = await harness.Resolver.ResolveAsync(CreateContext("/apple"));
+
+ // cache should have all 3 entries
+ Assert.NotNull(harness.Cache.Get("/apple"));
+ Assert.NotNull(harness.Cache.Get("/pear"));
+ Assert.NotNull(harness.Cache.Get("/grape"));
+
+ TenantContext<TestTenant> cachedTenant;
+
+ // expire apple
+ harness.Cache.Remove("/apple");
+
+ // look it up again so it registers
+ harness.Cache.TryGetValue("/apple", out cachedTenant);
+
+ // need to spin up a new task as "long running"
+ // so that MemoryCache can fire the eviction callbacks first
+ await Task.Factory.StartNew(state =>
+ {
+ Thread.Sleep(500);
+
+ // pear is expired - because apple is
+ Assert.False(harness.Cache.TryGetValue("/pear", out cachedTenant), "Pear Exists");
+ // should also expire tenant context by default
+ Assert.True(tenantContext.Tenant.Disposed);
+
+ }, this, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.FromCurrentSynchronizationContext());
+ }
[Fact]
- public async Task Tenant_expires_from_cache_for_only_its_identifier()
+ public async Task Can_evict_single_cache_entry_of_tenant_context()
{
TenantContext<TestTenant> cachedTenant;
- var harness = new TestHarness(cacheExpirationInSeconds: 2, disposeOnExpiration: false);
+ var harness = new TestHarness(cacheExpirationInSeconds: 2, evictAllOnExpiry: false);
var context = CreateContext("/apple");
// first request for apple
@@ -108,42 +142,23 @@ private HttpContext CreateContext(string requestPath)
Assert.True(harness.Cache.TryGetValue("/pear", out cachedTenant), "Pear Does Not Exist");
}
-
[Fact]
- public async Task Tenant_expires_from_cache_for_all_of_its_identifiers_start()
+ public async Task Can_not_dispose_on_eviction()
{
- var harness = new TestHarness(cacheExpirationInSeconds: 10, disposeOnExpiration: true);
-
- // first request for apple
- await harness.Resolver.ResolveAsync(CreateContext("/apple"));
-
- // cache should have all 3 entries
- Assert.NotNull(harness.Cache.Get("/apple"));
- Assert.NotNull(harness.Cache.Get("/pear"));
- Assert.NotNull(harness.Cache.Get("/grape"));
-
- TenantContext<TestTenant> cachedTenant;
-
- // expire apple
- harness.Cache.Remove("/apple");
+ var harness = new TestHarness(cacheExpirationInSeconds: 1, disposeOnEviction: false);
+ var context = CreateContext("/apple");
- // look it up again so it registers
- harness.Cache.TryGetValue("/apple", out cachedTenant);
+ var tenantContext = await harness.Resolver.ResolveAsync(context);
- // need to spin up a new task as "long running"
- // so that MemoryCache can fire the eviction callbacks first
- await Task.Factory.StartNew(state =>
- {
- Thread.Sleep(500);
+ Thread.Sleep(3 * 1000);
- // pear is expired - because apple is
- Assert.False(harness.Cache.TryGetValue("/pear", out cachedTenant), "Pear Exists");
- }, this, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.FromCurrentSynchronizationContext());
+ Assert.False(tenantContext.Tenant.Disposed);
}
+
class TestTenant : IDisposable
{
- private bool disposed;
+ public bool Disposed { get; set; }
public string Id { get; set; }
@@ -159,7 +174,7 @@ public void Dispose()
protected virtual void Dispose(bool disposing)
{
- if (disposed)
+ if (Disposed)
{
return;
}
@@ -169,7 +184,7 @@ protected virtual void Dispose(bool disposing)
Cts.Cancel();
}
- disposed = true;
+ Disposed = true;
}
}
@@ -183,15 +198,12 @@ class TestTenantMemoryCacheResolver : MemoryCacheTenantResolver<TestTenant>
private readonly int cacheExpirationInSeconds;
- public TestTenantMemoryCacheResolver(IMemoryCache cache, ILoggerFactory loggerFactory, bool disposeOnExpiration = true, int cacheExpirationInSeconds = 10)
- : base(cache, loggerFactory)
+ public TestTenantMemoryCacheResolver(IMemoryCache cache, ILoggerFactory loggerFactory, MemoryCacheTenantResolverOptions options, int cacheExpirationInSeconds = 10)
+ : base(cache, loggerFactory, options)
{
- this.DisposeTenantOnExpiration = disposeOnExpiration;
this.cacheExpirationInSeconds = cacheExpirationInSeconds;
}
- protected override bool DisposeTenantOnExpiration { get; }
-
protected override MemoryCacheEntryOptions CreateCacheEntryOptions()
{
return new MemoryCacheEntryOptions()
@@ -231,9 +243,10 @@ class TestHarness
Clock = new SystemClock()
});
- public TestHarness(bool disposeOnExpiration = false, int cacheExpirationInSeconds = 10)
+ public TestHarness(bool disposeOnEviction = true, int cacheExpirationInSeconds = 10, bool evictAllOnExpiry = true)
{
- Resolver = new TestTenantMemoryCacheResolver(Cache, loggerFactory, disposeOnExpiration, cacheExpirationInSeconds);
+ var options = new MemoryCacheTenantResolverOptions { DisposeOnEviction = disposeOnEviction, EvictAllEntriesOnExpiry = evictAllOnExpiry };
+ Resolver = new TestTenantMemoryCacheResolver(Cache, loggerFactory, options, cacheExpirationInSeconds);
}
public ITenantResolver<TestTenant> Resolver { get; }

0 comments on commit b3bfbb1

Please sign in to comment.