1
0
mirror of https://github.com/bitwarden/server synced 2025-12-17 16:53:23 +00:00

Upgrade ExtendedCache to support non-Redis distributed cache (#6682)

* Upgrade ExtendedCache to support non-Redis distributed cache

* Update CACHING.md to use UseSharedDistributedCache setting

Updated documentation to reflect the setting rename from UseSharedRedisCache
to UseSharedDistributedCache in the ExtendedCache configuration examples.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Matt Bishop <withinfocus@users.noreply.github.com>
This commit is contained in:
Brant DeBow
2025-12-04 16:37:51 -05:00
committed by GitHub
parent 101ff9d6ed
commit 3605b4d2ff
4 changed files with 218 additions and 20 deletions

View File

@@ -732,7 +732,7 @@ public class GlobalSettings : IGlobalSettings
public class ExtendedCacheSettings public class ExtendedCacheSettings
{ {
public bool EnableDistributedCache { get; set; } = true; public bool EnableDistributedCache { get; set; } = true;
public bool UseSharedRedisCache { get; set; } = true; public bool UseSharedDistributedCache { get; set; } = true;
public IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings(); public IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings();
public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(30); public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(30);
public bool IsFailSafeEnabled { get; set; } = true; public bool IsFailSafeEnabled { get; set; } = true;

View File

@@ -140,7 +140,7 @@ services.AddExtendedCache("MyFeatureCache", globalSettings, new GlobalSettings.E
// Option 4: Isolated Redis for specialized features // Option 4: Isolated Redis for specialized features
services.AddExtendedCache("SpecializedCache", globalSettings, new GlobalSettings.ExtendedCacheSettings services.AddExtendedCache("SpecializedCache", globalSettings, new GlobalSettings.ExtendedCacheSettings
{ {
UseSharedRedisCache = false, UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings Redis = new GlobalSettings.ConnectionStringSettings
{ {
ConnectionString = "localhost:6379,ssl=false" ConnectionString = "localhost:6379,ssl=false"

View File

@@ -18,9 +18,12 @@ public static class ExtendedCacheServiceCollectionExtensions
/// Adds a new, named Fusion Cache <see href="https://github.com/ZiggyCreatures/FusionCache"/> to the service /// Adds a new, named Fusion Cache <see href="https://github.com/ZiggyCreatures/FusionCache"/> to the service
/// collection. If an existing cache of the same name is found, it will do nothing.<br/> /// collection. If an existing cache of the same name is found, it will do nothing.<br/>
/// <br/> /// <br/>
/// <b>Note</b>: When re-using the existing Redis cache, it is expected to call this method <b>after</b> calling /// <b>Note</b>: When re-using an existing distributed cache, it is expected to call this method <b>after</b> calling
/// <code>services.AddDistributedCache(globalSettings)</code><br />This ensures that DI correctly finds, /// <code>services.AddDistributedCache(globalSettings)</code><br />This ensures that DI correctly finds
/// configures, and re-uses all the shared Redis architecture. /// and re-uses the shared distributed cache infrastructure.<br />
/// <br />
/// <b>Backplane</b>: Cross-instance cache invalidation is only available when using Redis.
/// Non-Redis distributed caches operate with eventual consistency across multiple instances.
/// </summary> /// </summary>
public static IServiceCollection AddExtendedCache( public static IServiceCollection AddExtendedCache(
this IServiceCollection services, this IServiceCollection services,
@@ -72,12 +75,21 @@ public static class ExtendedCacheServiceCollectionExtensions
if (!settings.EnableDistributedCache) if (!settings.EnableDistributedCache)
return services; return services;
if (settings.UseSharedRedisCache) if (settings.UseSharedDistributedCache)
{ {
// Using Shared Redis, TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString)) if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString))
{
// Using Shared Non-Redis Distributed Cache:
// 1. Assume IDistributedCache is already registered (e.g., Cosmos, SQL Server)
// 2. Backplane not supported (Redis-only feature, requires pub/sub)
fusionCacheBuilder
.TryWithRegisteredDistributedCache();
return services; return services;
}
// Using Shared Redis, TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
services.TryAddSingleton<IConnectionMultiplexer>(sp => services.TryAddSingleton<IConnectionMultiplexer>(sp =>
CreateConnectionMultiplexer(sp, cacheName, globalSettings.DistributedCache.Redis.ConnectionString)); CreateConnectionMultiplexer(sp, cacheName, globalSettings.DistributedCache.Redis.ConnectionString));
@@ -92,13 +104,13 @@ public static class ExtendedCacheServiceCollectionExtensions
}); });
services.TryAddSingleton<IFusionCacheBackplane>(sp => services.TryAddSingleton<IFusionCacheBackplane>(sp =>
{
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
return new RedisBackplane(new RedisBackplaneOptions
{ {
var mux = sp.GetRequiredService<IConnectionMultiplexer>(); ConnectionMultiplexerFactory = () => Task.FromResult(mux)
return new RedisBackplane(new RedisBackplaneOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
});
}); });
});
fusionCacheBuilder fusionCacheBuilder
.WithRegisteredDistributedCache() .WithRegisteredDistributedCache()
@@ -107,10 +119,21 @@ public static class ExtendedCacheServiceCollectionExtensions
return services; return services;
} }
// Using keyed Redis / Distributed Cache. Create all pieces as keyed services. // Using keyed Distributed Cache. Create/Reuse all pieces as keyed services.
if (!CoreHelpers.SettingHasValue(settings.Redis.ConnectionString)) if (!CoreHelpers.SettingHasValue(settings.Redis.ConnectionString))
{
// Using Keyed Non-Redis Distributed Cache:
// 1. Assume IDistributedCache (e.g., Cosmos, SQL Server) is already registered with cacheName as key
// 2. Backplane not supported (Redis-only feature, requires pub/sub)
fusionCacheBuilder
.TryWithRegisteredKeyedDistributedCache(serviceKey: cacheName);
return services; return services;
}
// Using Keyed Redis: TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
services.TryAddKeyedSingleton<IConnectionMultiplexer>( services.TryAddKeyedSingleton<IConnectionMultiplexer>(
cacheName, cacheName,

View File

@@ -7,6 +7,7 @@ using NSubstitute;
using StackExchange.Redis; using StackExchange.Redis;
using Xunit; using Xunit;
using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Backplane;
namespace Bit.Core.Test.Utilities; namespace Bit.Core.Test.Utilities;
@@ -167,7 +168,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
var settings = CreateGlobalSettings(new() var settings = CreateGlobalSettings(new()
{ {
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }, { "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
{ "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedRedisCache", "true" } { "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedDistributedCache", "true" }
}); });
// Provide a multiplexer (shared) // Provide a multiplexer (shared)
@@ -187,7 +188,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
{ {
var settings = new GlobalSettings.ExtendedCacheSettings var settings = new GlobalSettings.ExtendedCacheSettings
{ {
UseSharedRedisCache = false, UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" } Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" }
}; };
@@ -242,7 +243,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
{ {
var settings = new GlobalSettings.ExtendedCacheSettings var settings = new GlobalSettings.ExtendedCacheSettings
{ {
UseSharedRedisCache = false, UseSharedDistributedCache = false,
// No Redis connection string // No Redis connection string
}; };
@@ -261,13 +262,13 @@ public class ExtendedCacheServiceCollectionExtensionsTests
var settingsA = new GlobalSettings.ExtendedCacheSettings var settingsA = new GlobalSettings.ExtendedCacheSettings
{ {
EnableDistributedCache = true, EnableDistributedCache = true,
UseSharedRedisCache = false, UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" } Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
}; };
var settingsB = new GlobalSettings.ExtendedCacheSettings var settingsB = new GlobalSettings.ExtendedCacheSettings
{ {
EnableDistributedCache = true, EnableDistributedCache = true,
UseSharedRedisCache = false, UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6380" } Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6380" }
}; };
@@ -294,7 +295,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
var settings = new GlobalSettings.ExtendedCacheSettings var settings = new GlobalSettings.ExtendedCacheSettings
{ {
UseSharedRedisCache = false, UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" } Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
}; };
@@ -306,6 +307,180 @@ public class ExtendedCacheServiceCollectionExtensionsTests
Assert.Same(existingCache, resolved); Assert.Same(existingCache, resolved);
} }
[Fact]
public void AddExtendedCache_SharedNonRedisCache_UsesDistributedCacheWithoutBackplane()
{
var settings = new GlobalSettings.ExtendedCacheSettings
{
UseSharedDistributedCache = true,
EnableDistributedCache = true,
// No Redis.ConnectionString
};
// Register non-Redis distributed cache
_services.AddSingleton(Substitute.For<IDistributedCache>());
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
using var provider = _services.BuildServiceProvider();
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
Assert.True(cache.HasDistributedCache);
Assert.False(cache.HasBackplane); // No backplane for non-Redis
}
[Fact]
public void AddExtendedCache_SharedRedisWithMockedMultiplexer_ReusesExistingMultiplexer()
{
// Override GlobalSettings to include Redis connection string
var globalSettings = CreateGlobalSettings(new()
{
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }
});
// Custom settings for this cache
var settings = new GlobalSettings.ExtendedCacheSettings
{
UseSharedDistributedCache = true,
EnableDistributedCache = true,
};
// Pre-register mocked multiplexer (simulates AddDistributedCache already called)
var mockMultiplexer = Substitute.For<IConnectionMultiplexer>();
_services.AddSingleton(mockMultiplexer);
_services.AddExtendedCache(_cacheName, globalSettings, settings);
using var provider = _services.BuildServiceProvider();
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
Assert.True(cache.HasDistributedCache);
Assert.True(cache.HasBackplane);
// Verify same multiplexer was reused (TryAdd didn't replace it)
var resolvedMux = provider.GetRequiredService<IConnectionMultiplexer>();
Assert.Same(mockMultiplexer, resolvedMux);
}
[Fact]
public void AddExtendedCache_KeyedNonRedisCache_UsesKeyedDistributedCacheWithoutBackplane()
{
var settings = new GlobalSettings.ExtendedCacheSettings
{
UseSharedDistributedCache = false,
EnableDistributedCache = true,
// No Redis.ConnectionString
};
// Register keyed non-Redis distributed cache
_services.AddKeyedSingleton(_cacheName, Substitute.For<IDistributedCache>());
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
using var provider = _services.BuildServiceProvider();
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
Assert.True(cache.HasDistributedCache);
Assert.False(cache.HasBackplane);
}
[Fact]
public void AddExtendedCache_KeyedRedisWithConnectionString_CreatesIsolatedInfrastructure()
{
var settings = new GlobalSettings.ExtendedCacheSettings
{
UseSharedDistributedCache = false,
EnableDistributedCache = true,
Redis = new GlobalSettings.ConnectionStringSettings
{
ConnectionString = "localhost:6379"
}
};
// Pre-register mocked keyed multiplexer to avoid connection attempt
_services.AddKeyedSingleton(_cacheName, Substitute.For<IConnectionMultiplexer>());
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
using var provider = _services.BuildServiceProvider();
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
Assert.True(cache.HasDistributedCache);
Assert.True(cache.HasBackplane);
// Verify keyed services exist
var keyedMux = provider.GetRequiredKeyedService<IConnectionMultiplexer>(_cacheName);
Assert.NotNull(keyedMux);
var keyedRedis = provider.GetRequiredKeyedService<IDistributedCache>(_cacheName);
Assert.NotNull(keyedRedis);
var keyedBackplane = provider.GetRequiredKeyedService<IFusionCacheBackplane>(_cacheName);
Assert.NotNull(keyedBackplane);
}
[Fact]
public void AddExtendedCache_NoDistributedCacheRegistered_WorksWithMemoryOnly()
{
var settings = new GlobalSettings.ExtendedCacheSettings
{
UseSharedDistributedCache = true,
EnableDistributedCache = true,
// No Redis connection string, no IDistributedCache registered
// This is technically a misconfiguration, but we handle it without failing
};
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
using var provider = _services.BuildServiceProvider();
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
Assert.False(cache.HasDistributedCache);
Assert.False(cache.HasBackplane);
// Verify L1 memory cache still works
cache.Set("key", "value");
var result = cache.GetOrDefault<string>("key");
Assert.Equal("value", result);
}
[Fact]
public void AddExtendedCache_MultipleKeyedCachesWithDifferentTypes_EachHasCorrectConfig()
{
var redisSettings = new GlobalSettings.ExtendedCacheSettings
{
UseSharedDistributedCache = false,
EnableDistributedCache = true,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
};
var nonRedisSettings = new GlobalSettings.ExtendedCacheSettings
{
UseSharedDistributedCache = false,
EnableDistributedCache = true,
// No Redis connection string
};
// Setup Cache1 (Redis)
_services.AddKeyedSingleton("Cache1", Substitute.For<IConnectionMultiplexer>());
_services.AddExtendedCache("Cache1", _globalSettings, redisSettings);
// Setup Cache2 (non-Redis)
_services.AddKeyedSingleton("Cache2", Substitute.For<IDistributedCache>());
_services.AddExtendedCache("Cache2", _globalSettings, nonRedisSettings);
using var provider = _services.BuildServiceProvider();
var cache1 = provider.GetRequiredKeyedService<IFusionCache>("Cache1");
var cache2 = provider.GetRequiredKeyedService<IFusionCache>("Cache2");
Assert.True(cache1.HasDistributedCache);
Assert.True(cache1.HasBackplane);
Assert.True(cache2.HasDistributedCache);
Assert.False(cache2.HasBackplane);
Assert.NotSame(cache1, cache2);
}
private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data) private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data)
{ {
var config = new ConfigurationBuilder() var config = new ConfigurationBuilder()