Files
tiku-backend.net/Tiku.IntegrationTests/BusinessCachingTests.cs
xiong 603bc24c26
Some checks are pending
ci / release-gate (push) Waiting to run
feat(cache): adopt FusionCache for business caching
2026-08-05 09:30:48 +08:00

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();
}
}