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 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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user