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:
@@ -732,7 +732,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public class ExtendedCacheSettings
|
||||
{
|
||||
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 TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(30);
|
||||
public bool IsFailSafeEnabled { get; set; } = true;
|
||||
|
||||
@@ -140,7 +140,7 @@ services.AddExtendedCache("MyFeatureCache", globalSettings, new GlobalSettings.E
|
||||
// Option 4: Isolated Redis for specialized features
|
||||
services.AddExtendedCache("SpecializedCache", globalSettings, new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
UseSharedDistributedCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings
|
||||
{
|
||||
ConnectionString = "localhost:6379,ssl=false"
|
||||
|
||||
@@ -18,9 +18,12 @@ public static class ExtendedCacheServiceCollectionExtensions
|
||||
/// 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/>
|
||||
/// <br/>
|
||||
/// <b>Note</b>: When re-using the existing Redis cache, it is expected to call this method <b>after</b> calling
|
||||
/// <code>services.AddDistributedCache(globalSettings)</code><br />This ensures that DI correctly finds,
|
||||
/// configures, and re-uses all the shared Redis architecture.
|
||||
/// <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
|
||||
/// 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>
|
||||
public static IServiceCollection AddExtendedCache(
|
||||
this IServiceCollection services,
|
||||
@@ -72,12 +75,21 @@ public static class ExtendedCacheServiceCollectionExtensions
|
||||
if (!settings.EnableDistributedCache)
|
||||
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))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Using Shared Redis, TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
|
||||
|
||||
services.TryAddSingleton<IConnectionMultiplexer>(sp =>
|
||||
CreateConnectionMultiplexer(sp, cacheName, globalSettings.DistributedCache.Redis.ConnectionString));
|
||||
@@ -107,10 +119,21 @@ public static class ExtendedCacheServiceCollectionExtensions
|
||||
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))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Using Keyed Redis: TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
|
||||
|
||||
services.TryAddKeyedSingleton<IConnectionMultiplexer>(
|
||||
cacheName,
|
||||
|
||||
@@ -7,6 +7,7 @@ using NSubstitute;
|
||||
using StackExchange.Redis;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
using ZiggyCreatures.Caching.Fusion.Backplane;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
@@ -167,7 +168,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
|
||||
var settings = CreateGlobalSettings(new()
|
||||
{
|
||||
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
||||
{ "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedRedisCache", "true" }
|
||||
{ "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedDistributedCache", "true" }
|
||||
});
|
||||
|
||||
// Provide a multiplexer (shared)
|
||||
@@ -187,7 +188,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
|
||||
{
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
UseSharedDistributedCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" }
|
||||
};
|
||||
|
||||
@@ -242,7 +243,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
|
||||
{
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
UseSharedDistributedCache = false,
|
||||
// No Redis connection string
|
||||
};
|
||||
|
||||
@@ -261,13 +262,13 @@ public class ExtendedCacheServiceCollectionExtensionsTests
|
||||
var settingsA = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
EnableDistributedCache = true,
|
||||
UseSharedRedisCache = false,
|
||||
UseSharedDistributedCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
|
||||
};
|
||||
var settingsB = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
EnableDistributedCache = true,
|
||||
UseSharedRedisCache = false,
|
||||
UseSharedDistributedCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6380" }
|
||||
};
|
||||
|
||||
@@ -294,7 +295,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
|
||||
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
UseSharedDistributedCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
|
||||
};
|
||||
|
||||
@@ -306,6 +307,180 @@ public class ExtendedCacheServiceCollectionExtensionsTests
|
||||
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)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
|
||||
Reference in New Issue
Block a user