mirror of
https://github.com/bitwarden/server
synced 2025-12-15 07:43:54 +00:00
Upgrade ExtendedCache with support for named caches (#6591)
* Upgrade ExtendedCache with support for named caches * Addressed Claude PR suggestions - defensive mux creation, defend empty cache name, added tests * Addressed PR suggestions; Fixed issue where IDistributedCache was missing when using the shared route; Added more unit tests * Revert to TryAdd, document expectation that AddDistributedCache is called first
This commit is contained in:
@@ -14,6 +14,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
|
||||
{
|
||||
private readonly IServiceCollection _services;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private const string _cacheName = "TestCache";
|
||||
|
||||
public ExtendedCacheServiceCollectionExtensionsTests()
|
||||
{
|
||||
@@ -33,129 +34,276 @@ public class ExtendedCacheServiceCollectionExtensionsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAddFusionCoreServices_CustomSettings_OverridesDefaults()
|
||||
public void AddExtendedCache_CustomSettings_OverridesDefaults()
|
||||
{
|
||||
var settings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
{ "GlobalSettings:DistributedCache:Duration", "00:12:00" },
|
||||
{ "GlobalSettings:DistributedCache:FailSafeMaxDuration", "01:30:00" },
|
||||
{ "GlobalSettings:DistributedCache:FailSafeThrottleDuration", "00:01:00" },
|
||||
{ "GlobalSettings:DistributedCache:EagerRefreshThreshold", "0.75" },
|
||||
{ "GlobalSettings:DistributedCache:FactorySoftTimeout", "00:00:00.020" },
|
||||
{ "GlobalSettings:DistributedCache:FactoryHardTimeout", "00:00:03" },
|
||||
{ "GlobalSettings:DistributedCache:DistributedCacheSoftTimeout", "00:00:00.500" },
|
||||
{ "GlobalSettings:DistributedCache:DistributedCacheHardTimeout", "00:00:01.500" },
|
||||
{ "GlobalSettings:DistributedCache:JitterMaxDuration", "00:00:05" },
|
||||
{ "GlobalSettings:DistributedCache:IsFailSafeEnabled", "false" },
|
||||
{ "GlobalSettings:DistributedCache:AllowBackgroundDistributedCacheOperations", "false" },
|
||||
Duration = TimeSpan.FromMinutes(12),
|
||||
FailSafeMaxDuration = TimeSpan.FromHours(1.5),
|
||||
FailSafeThrottleDuration = TimeSpan.FromMinutes(1),
|
||||
EagerRefreshThreshold = 0.75f,
|
||||
FactorySoftTimeout = TimeSpan.FromMilliseconds(20),
|
||||
FactoryHardTimeout = TimeSpan.FromSeconds(3),
|
||||
DistributedCacheSoftTimeout = TimeSpan.FromSeconds(0.5),
|
||||
DistributedCacheHardTimeout = TimeSpan.FromSeconds(1.5),
|
||||
JitterMaxDuration = TimeSpan.FromSeconds(5),
|
||||
IsFailSafeEnabled = false,
|
||||
AllowBackgroundDistributedCacheOperations = false,
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
var opt = cache.DefaultEntryOptions;
|
||||
|
||||
Assert.Equal(TimeSpan.FromMinutes(12), opt.Duration);
|
||||
Assert.False(opt.IsFailSafeEnabled);
|
||||
Assert.Equal(TimeSpan.FromHours(1.5), opt.FailSafeMaxDuration);
|
||||
Assert.Equal(TimeSpan.FromMinutes(1), opt.FailSafeThrottleDuration);
|
||||
Assert.Equal(0.75f, opt.EagerRefreshThreshold);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(20), opt.FactorySoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(3000), opt.FactoryHardTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(0.5), opt.DistributedCacheSoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(1.5), opt.DistributedCacheHardTimeout);
|
||||
Assert.False(opt.AllowBackgroundDistributedCacheOperations);
|
||||
Assert.Equal(TimeSpan.FromSeconds(5), opt.JitterMaxDuration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_DefaultSettings_ConfiguresExpectedValues()
|
||||
{
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
var opt = cache.DefaultEntryOptions;
|
||||
|
||||
Assert.Equal(TimeSpan.FromMinutes(30), opt.Duration);
|
||||
Assert.True(opt.IsFailSafeEnabled);
|
||||
Assert.Equal(TimeSpan.FromHours(2), opt.FailSafeMaxDuration);
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), opt.FailSafeThrottleDuration);
|
||||
Assert.Equal(0.9f, opt.EagerRefreshThreshold);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(100), opt.FactorySoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(1500), opt.FactoryHardTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(1), opt.DistributedCacheSoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(2), opt.DistributedCacheHardTimeout);
|
||||
Assert.True(opt.AllowBackgroundDistributedCacheOperations);
|
||||
Assert.Equal(TimeSpan.FromSeconds(2), opt.JitterMaxDuration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_DisabledDistributedCache_DoesNotRegisterBackplaneOrRedis()
|
||||
{
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
EnableDistributedCache = false,
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.False(cache.HasDistributedCache);
|
||||
Assert.False(cache.HasBackplane);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_EmptyCacheName_DoesNothing()
|
||||
{
|
||||
_services.AddExtendedCache(string.Empty, _globalSettings);
|
||||
|
||||
var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList();
|
||||
Assert.Empty(regs);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetKeyedService<IFusionCache>(_cacheName);
|
||||
Assert.Null(cache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_MultipleCalls_OnlyAddsOneCacheService()
|
||||
{
|
||||
var settings = CreateGlobalSettings(new()
|
||||
{
|
||||
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }
|
||||
});
|
||||
|
||||
_services.TryAddExtendedCacheServices(settings);
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||
var options = fusionCache.DefaultEntryOptions;
|
||||
// Provide a multiplexer (shared)
|
||||
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
||||
|
||||
Assert.Equal(TimeSpan.FromMinutes(12), options.Duration);
|
||||
Assert.False(options.IsFailSafeEnabled);
|
||||
Assert.Equal(TimeSpan.FromHours(1.5), options.FailSafeMaxDuration);
|
||||
Assert.Equal(TimeSpan.FromMinutes(1), options.FailSafeThrottleDuration);
|
||||
Assert.Equal(0.75f, options.EagerRefreshThreshold);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(20), options.FactorySoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(3000), options.FactoryHardTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(0.5), options.DistributedCacheSoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(1.5), options.DistributedCacheHardTimeout);
|
||||
Assert.False(options.AllowBackgroundDistributedCacheOperations);
|
||||
Assert.Equal(TimeSpan.FromSeconds(5), options.JitterMaxDuration);
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
|
||||
var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList();
|
||||
Assert.Single(regs);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
Assert.NotNull(cache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAddFusionCoreServices_DefaultSettings_ConfiguresExpectedValues()
|
||||
public void AddExtendedCache_MultipleDifferentCaches_AddsAll()
|
||||
{
|
||||
_services.TryAddExtendedCacheServices(_globalSettings);
|
||||
_services.AddExtendedCache("Cache1", _globalSettings);
|
||||
_services.AddExtendedCache("Cache2", _globalSettings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
|
||||
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||
var options = fusionCache.DefaultEntryOptions;
|
||||
var cache1 = provider.GetRequiredKeyedService<IFusionCache>("Cache1");
|
||||
var cache2 = provider.GetRequiredKeyedService<IFusionCache>("Cache2");
|
||||
|
||||
Assert.Equal(TimeSpan.FromMinutes(30), options.Duration);
|
||||
Assert.True(options.IsFailSafeEnabled);
|
||||
Assert.Equal(TimeSpan.FromHours(2), options.FailSafeMaxDuration);
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), options.FailSafeThrottleDuration);
|
||||
Assert.Equal(0.9f, options.EagerRefreshThreshold);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(100), options.FactorySoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(1500), options.FactoryHardTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(1), options.DistributedCacheSoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(2), options.DistributedCacheHardTimeout);
|
||||
Assert.True(options.AllowBackgroundDistributedCacheOperations);
|
||||
Assert.Equal(TimeSpan.FromSeconds(2), options.JitterMaxDuration);
|
||||
Assert.NotNull(cache1);
|
||||
Assert.NotNull(cache2);
|
||||
Assert.NotSame(cache1, cache2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAddFusionCoreServices_MultipleCalls_OnlyConfiguresOnce()
|
||||
public void AddExtendedCache_WithRedis_EnablesDistributedCacheAndBackplane()
|
||||
{
|
||||
var settings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
var settings = CreateGlobalSettings(new()
|
||||
{
|
||||
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
||||
{ "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedRedisCache", "true" }
|
||||
});
|
||||
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
||||
_services.TryAddExtendedCacheServices(settings);
|
||||
_services.TryAddExtendedCacheServices(settings);
|
||||
_services.TryAddExtendedCacheServices(settings);
|
||||
|
||||
var registrations = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList();
|
||||
Assert.Single(registrations);
|
||||
// Provide a multiplexer (shared)
|
||||
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
||||
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||
Assert.NotNull(fusionCache);
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.True(cache.HasDistributedCache);
|
||||
Assert.True(cache.HasBackplane);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAddFusionCoreServices_WithRedis_EnablesDistributedCacheAndBackplane()
|
||||
public void AddExtendedCache_InvalidRedisConnection_LogsAndThrows()
|
||||
{
|
||||
var settings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
||||
});
|
||||
UseSharedRedisCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" }
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
||||
_services.TryAddExtendedCacheServices(settings);
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
|
||||
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||
Assert.True(fusionCache.HasDistributedCache);
|
||||
Assert.True(fusionCache.HasBackplane);
|
||||
Assert.Throws<RedisConnectionException>(() =>
|
||||
{
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
// Trigger lazy initialization
|
||||
cache.GetOrDefault<string>("test");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAddFusionCoreServices_WithExistingRedis_EnablesDistributedCacheAndBackplane()
|
||||
public void AddExtendedCache_WithExistingRedis_UsesExistingDistributedCacheAndBackplane()
|
||||
{
|
||||
var settings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
var settings = CreateGlobalSettings(new()
|
||||
{
|
||||
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
||||
});
|
||||
|
||||
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
||||
_services.AddSingleton(Substitute.For<IDistributedCache>());
|
||||
_services.TryAddExtendedCacheServices(settings);
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
|
||||
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||
Assert.True(fusionCache.HasDistributedCache);
|
||||
Assert.True(fusionCache.HasBackplane);
|
||||
var distributedCache = provider.GetRequiredService<IDistributedCache>();
|
||||
Assert.NotNull(distributedCache);
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.True(cache.HasDistributedCache);
|
||||
Assert.True(cache.HasBackplane);
|
||||
|
||||
var existingCache = provider.GetRequiredService<IDistributedCache>();
|
||||
Assert.NotNull(existingCache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAddFusionCoreServices_WithoutRedis_DisablesDistributedCacheAndBackplane()
|
||||
public void AddExtendedCache_NoRedis_DisablesDistributedCacheAndBackplane()
|
||||
{
|
||||
_services.TryAddExtendedCacheServices(_globalSettings);
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings);
|
||||
|
||||
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||
Assert.False(fusionCache.HasDistributedCache);
|
||||
Assert.False(fusionCache.HasBackplane);
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.False(cache.HasDistributedCache);
|
||||
Assert.False(cache.HasBackplane);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_NoSharedRedisButNoConnectionString_DisablesDistributedCacheAndBackplane()
|
||||
{
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
// No Redis connection string
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.False(cache.HasDistributedCache);
|
||||
Assert.False(cache.HasBackplane);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_KeyedRedis_UsesSeparateMultiplexers()
|
||||
{
|
||||
var settingsA = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
EnableDistributedCache = true,
|
||||
UseSharedRedisCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
|
||||
};
|
||||
var settingsB = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
EnableDistributedCache = true,
|
||||
UseSharedRedisCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6380" }
|
||||
};
|
||||
|
||||
_services.AddKeyedSingleton("CacheA", Substitute.For<IConnectionMultiplexer>());
|
||||
_services.AddKeyedSingleton("CacheB", Substitute.For<IConnectionMultiplexer>());
|
||||
|
||||
_services.AddExtendedCache("CacheA", _globalSettings, settingsA);
|
||||
_services.AddExtendedCache("CacheB", _globalSettings, settingsB);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var muxA = provider.GetRequiredKeyedService<IConnectionMultiplexer>("CacheA");
|
||||
var muxB = provider.GetRequiredKeyedService<IConnectionMultiplexer>("CacheB");
|
||||
|
||||
Assert.NotNull(muxA);
|
||||
Assert.NotNull(muxB);
|
||||
Assert.NotSame(muxA, muxB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_WithExistingKeyedDistributedCache_ReusesIt()
|
||||
{
|
||||
var existingCache = Substitute.For<IDistributedCache>();
|
||||
_services.AddKeyedSingleton<IDistributedCache>(_cacheName, existingCache);
|
||||
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var resolved = provider.GetRequiredKeyedService<IDistributedCache>(_cacheName);
|
||||
|
||||
Assert.Same(existingCache, resolved);
|
||||
}
|
||||
|
||||
private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data)
|
||||
|
||||
Reference in New Issue
Block a user