145 lines
6.1 KiB
C#
145 lines
6.1 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
using StackExchange.Redis;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Application.Tenancy;
|
|
using Tiku.Infrastructure;
|
|
using Tiku.Infrastructure.Caching;
|
|
using Tiku.Infrastructure.Tenancy;
|
|
using ZiggyCreatures.Caching.Fusion;
|
|
|
|
namespace Tiku.IntegrationTests;
|
|
|
|
public sealed class BusinessCachingTests
|
|
{
|
|
[Fact]
|
|
public async Task Local_cache_coalesces_concurrent_factories_and_uses_safe_defaults()
|
|
{
|
|
await using var provider = BuildLocalProvider("Integration Tests");
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(
|
|
BusinessCachingServiceCollectionExtensions.CacheName);
|
|
var options = provider.GetRequiredService<IOptionsMonitor<FusionCacheOptions>>()
|
|
.Get(BusinessCachingServiceCollectionExtensions.CacheName);
|
|
var calls = 0;
|
|
var releaseFactory = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
var tasks = Enumerable.Range(0, 16).Select(_ => cache.GetOrSetAsync(
|
|
"stampede",
|
|
async cancellationToken =>
|
|
{
|
|
Interlocked.Increment(ref calls);
|
|
await releaseFactory.Task.WaitAsync(cancellationToken);
|
|
return 42;
|
|
},
|
|
entry => entry.Duration = TimeSpan.FromMinutes(1))
|
|
.AsTask()).ToArray();
|
|
|
|
Assert.True(SpinWait.SpinUntil(() => Volatile.Read(ref calls) > 0, TimeSpan.FromSeconds(2)));
|
|
releaseFactory.SetResult();
|
|
|
|
Assert.All(await Task.WhenAll(tasks), value => Assert.Equal(42, value));
|
|
Assert.Equal(1, calls);
|
|
Assert.Equal(BusinessCachingServiceCollectionExtensions.CacheName, cache.CacheName);
|
|
Assert.Equal("tiku:integration-tests:business:v2:", options.CacheKeyPrefix);
|
|
Assert.Equal("tiku:integration-tests:business:v2", options.BackplaneChannelPrefix);
|
|
Assert.False(cache.DefaultEntryOptions.IsFailSafeEnabled);
|
|
Assert.False(cache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations);
|
|
Assert.False(cache.DefaultEntryOptions.AllowBackgroundBackplaneOperations);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unavailable_redis_falls_back_to_the_value_factory()
|
|
{
|
|
await using var provider = BuildRedisProvider(
|
|
"localhost:6399,connectTimeout=200,syncTimeout=200,asyncTimeout=200,abortConnect=false",
|
|
$"business-unavailable-{Guid.NewGuid():N}");
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(
|
|
BusinessCachingServiceCollectionExtensions.CacheName);
|
|
|
|
var value = await cache.GetOrSetAsync(
|
|
"fallback",
|
|
_ => Task.FromResult(42),
|
|
entry => entry.Duration = TimeSpan.FromMinutes(1));
|
|
|
|
Assert.Equal(42, value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Tenant_directory_caches_negative_results()
|
|
{
|
|
await using var factory = new Api.ApiTestFactory();
|
|
_ = factory.Services;
|
|
using var scope = factory.Services.CreateScope();
|
|
var directory = scope.ServiceProvider.GetRequiredService<ITenantDirectory>();
|
|
var cache = scope.ServiceProvider.GetRequiredKeyedService<IFusionCache>(
|
|
BusinessCachingServiceCollectionExtensions.CacheName);
|
|
var host = $"missing-{Guid.NewGuid():N}.example.test";
|
|
|
|
Assert.Null(await directory.FindByHostAsync(host));
|
|
Assert.Null(await directory.FindByHostAsync(host));
|
|
|
|
var cached = await cache.TryGetAsync<TenantDirectoryCacheEntry>(TenantDirectory.CacheKey("host", host));
|
|
Assert.True(cached.HasValue);
|
|
Assert.False(cached.Value.Found);
|
|
Assert.Null(cached.Value.Entry);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Redis_backplane_removes_another_nodes_local_entry()
|
|
{
|
|
var connectionString = Environment.GetEnvironmentVariable("TIKU_TEST_REDIS");
|
|
if (string.IsNullOrWhiteSpace(connectionString)) return;
|
|
|
|
var environment = $"business-backplane-{Guid.NewGuid():N}";
|
|
await using var first = BuildRedisProvider(connectionString, environment);
|
|
await using var second = BuildRedisProvider(connectionString, environment);
|
|
var firstCache = first.GetRequiredKeyedService<IFusionCache>(
|
|
BusinessCachingServiceCollectionExtensions.CacheName);
|
|
var secondCache = second.GetRequiredKeyedService<IFusionCache>(
|
|
BusinessCachingServiceCollectionExtensions.CacheName);
|
|
const string key = "shared-entry";
|
|
|
|
try
|
|
{
|
|
await firstCache.SetAsync(key, 1, entry => entry.Duration = TimeSpan.FromMinutes(1));
|
|
Assert.Equal(1, await secondCache.GetOrSetAsync(
|
|
key,
|
|
_ => Task.FromResult(99),
|
|
entry => entry.Duration = TimeSpan.FromMinutes(1)));
|
|
|
|
await firstCache.RemoveAsync(key);
|
|
var reloaded = await secondCache.GetOrSetAsync(
|
|
key,
|
|
_ => Task.FromResult(2),
|
|
entry => entry.Duration = TimeSpan.FromMinutes(1));
|
|
|
|
Assert.Equal(2, reloaded);
|
|
}
|
|
finally
|
|
{
|
|
var multiplexer = first.GetRequiredService<IConnectionMultiplexer>();
|
|
var server = multiplexer.GetServer(multiplexer.GetEndPoints().Single());
|
|
var keys = server.Keys(pattern: $"tiku:{environment}:business:v2:*").ToArray();
|
|
if (keys.Length > 0) await multiplexer.GetDatabase().KeyDeleteAsync(keys);
|
|
}
|
|
}
|
|
|
|
private static ServiceProvider BuildLocalProvider(string environment)
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddBusinessCaching(environment, false);
|
|
return services.BuildServiceProvider();
|
|
}
|
|
|
|
private static ServiceProvider BuildRedisProvider(string connectionString, string environment)
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.Configure<AuthorizationCacheOptions>(_ => { });
|
|
services.AddRedisSecurity(connectionString, environment);
|
|
services.AddBusinessCaching(environment, true);
|
|
return services.BuildServiceProvider();
|
|
}
|
|
}
|