Skip to content
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 @@ async Task<TenantContext<TTenant>> ITenantResolver<TTenant>.ResolveAsync(HttpCon

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 @@ public async Task Can_retrieve_tenant_from_resolver()


[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 @@ public async Task Can_retrieve_tenant_from_cache()
}

[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 @@ public async Task Can_retrieve_tenant_from_cache_with_different_key()
}

[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 @@ public async Task Tenant_expires_from_cache()
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 @@ public async Task Tenant_expires_from_cache_for_only_its_identifier()
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.
You can’t perform that action at this time.