mirror of
https://github.com/bitwarden/server
synced 2026-01-02 16:43:25 +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:
@@ -3,6 +3,7 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Caching.StackExchangeRedis;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
using ZiggyCreatures.Caching.Fusion.Backplane;
|
||||
@@ -14,77 +15,149 @@ namespace Microsoft.Extensions.DependencyInjection;
|
||||
public static class ExtendedCacheServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add Fusion Cache <see href="https://github.com/ZiggyCreatures/FusionCache"/> to the service
|
||||
/// collection.<br/>
|
||||
/// 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/>
|
||||
/// If Redis is configured, it uses Redis for an L2 cache and backplane. If not, it simply uses in-memory caching.
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public static IServiceCollection TryAddExtendedCacheServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
public static IServiceCollection AddExtendedCache(
|
||||
this IServiceCollection services,
|
||||
string cacheName,
|
||||
GlobalSettings globalSettings,
|
||||
GlobalSettings.ExtendedCacheSettings? settings = null)
|
||||
{
|
||||
if (services.Any(s => s.ServiceType == typeof(IFusionCache)))
|
||||
settings ??= globalSettings.DistributedCache.DefaultExtendedCache;
|
||||
if (settings is null || string.IsNullOrEmpty(cacheName))
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
var fusionCacheBuilder = services.AddFusionCache()
|
||||
.WithOptions(options =>
|
||||
// If a cache already exists with this key, do nothing
|
||||
if (services.Any(s => s.ServiceType == typeof(IFusionCache) &&
|
||||
s.ServiceKey?.Equals(cacheName) == true))
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
if (services.All(s => s.ServiceType != typeof(FusionCacheSystemTextJsonSerializer)))
|
||||
{
|
||||
services.AddFusionCacheSystemTextJsonSerializer();
|
||||
}
|
||||
var fusionCacheBuilder = services
|
||||
.AddFusionCache(cacheName)
|
||||
.WithCacheKeyPrefix($"{cacheName}:")
|
||||
.AsKeyedServiceByCacheName()
|
||||
.WithOptions(opt =>
|
||||
{
|
||||
options.DistributedCacheCircuitBreakerDuration = globalSettings.DistributedCache.DistributedCacheCircuitBreakerDuration;
|
||||
opt.DistributedCacheCircuitBreakerDuration = settings.DistributedCacheCircuitBreakerDuration;
|
||||
})
|
||||
.WithDefaultEntryOptions(new FusionCacheEntryOptions
|
||||
{
|
||||
Duration = globalSettings.DistributedCache.Duration,
|
||||
IsFailSafeEnabled = globalSettings.DistributedCache.IsFailSafeEnabled,
|
||||
FailSafeMaxDuration = globalSettings.DistributedCache.FailSafeMaxDuration,
|
||||
FailSafeThrottleDuration = globalSettings.DistributedCache.FailSafeThrottleDuration,
|
||||
EagerRefreshThreshold = globalSettings.DistributedCache.EagerRefreshThreshold,
|
||||
FactorySoftTimeout = globalSettings.DistributedCache.FactorySoftTimeout,
|
||||
FactoryHardTimeout = globalSettings.DistributedCache.FactoryHardTimeout,
|
||||
DistributedCacheSoftTimeout = globalSettings.DistributedCache.DistributedCacheSoftTimeout,
|
||||
DistributedCacheHardTimeout = globalSettings.DistributedCache.DistributedCacheHardTimeout,
|
||||
AllowBackgroundDistributedCacheOperations = globalSettings.DistributedCache.AllowBackgroundDistributedCacheOperations,
|
||||
JitterMaxDuration = globalSettings.DistributedCache.JitterMaxDuration
|
||||
Duration = settings.Duration,
|
||||
IsFailSafeEnabled = settings.IsFailSafeEnabled,
|
||||
FailSafeMaxDuration = settings.FailSafeMaxDuration,
|
||||
FailSafeThrottleDuration = settings.FailSafeThrottleDuration,
|
||||
EagerRefreshThreshold = settings.EagerRefreshThreshold,
|
||||
FactorySoftTimeout = settings.FactorySoftTimeout,
|
||||
FactoryHardTimeout = settings.FactoryHardTimeout,
|
||||
DistributedCacheSoftTimeout = settings.DistributedCacheSoftTimeout,
|
||||
DistributedCacheHardTimeout = settings.DistributedCacheHardTimeout,
|
||||
AllowBackgroundDistributedCacheOperations = settings.AllowBackgroundDistributedCacheOperations,
|
||||
JitterMaxDuration = settings.JitterMaxDuration
|
||||
})
|
||||
.WithSerializer(
|
||||
new FusionCacheSystemTextJsonSerializer()
|
||||
);
|
||||
.WithRegisteredSerializer();
|
||||
|
||||
if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString))
|
||||
{
|
||||
if (!settings.EnableDistributedCache)
|
||||
return services;
|
||||
}
|
||||
|
||||
services.TryAddSingleton<IConnectionMultiplexer>(sp =>
|
||||
ConnectionMultiplexer.Connect(globalSettings.DistributedCache.Redis.ConnectionString));
|
||||
if (settings.UseSharedRedisCache)
|
||||
{
|
||||
// Using Shared Redis, TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
|
||||
|
||||
fusionCacheBuilder
|
||||
.WithDistributedCache(sp =>
|
||||
if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString))
|
||||
return services;
|
||||
|
||||
services.TryAddSingleton<IConnectionMultiplexer>(sp =>
|
||||
CreateConnectionMultiplexer(sp, cacheName, globalSettings.DistributedCache.Redis.ConnectionString));
|
||||
|
||||
services.TryAddSingleton<IDistributedCache>(sp =>
|
||||
{
|
||||
var cache = sp.GetService<IDistributedCache>();
|
||||
if (cache is not null)
|
||||
{
|
||||
return cache;
|
||||
}
|
||||
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
|
||||
return new RedisCache(new RedisCacheOptions
|
||||
{
|
||||
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
|
||||
});
|
||||
})
|
||||
.WithBackplane(sp =>
|
||||
{
|
||||
var backplane = sp.GetService<IFusionCacheBackplane>();
|
||||
if (backplane is not null)
|
||||
});
|
||||
|
||||
services.TryAddSingleton<IFusionCacheBackplane>(sp =>
|
||||
{
|
||||
return backplane;
|
||||
}
|
||||
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
|
||||
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
|
||||
return new RedisBackplane(new RedisBackplaneOptions
|
||||
{
|
||||
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
|
||||
});
|
||||
});
|
||||
|
||||
fusionCacheBuilder
|
||||
.WithRegisteredDistributedCache()
|
||||
.WithRegisteredBackplane();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
// Using keyed Redis / Distributed Cache. Create all pieces as keyed services.
|
||||
|
||||
if (!CoreHelpers.SettingHasValue(settings.Redis.ConnectionString))
|
||||
return services;
|
||||
|
||||
services.TryAddKeyedSingleton<IConnectionMultiplexer>(
|
||||
cacheName,
|
||||
(sp, _) => CreateConnectionMultiplexer(sp, cacheName, settings.Redis.ConnectionString)
|
||||
);
|
||||
services.TryAddKeyedSingleton<IDistributedCache>(
|
||||
cacheName,
|
||||
(sp, _) =>
|
||||
{
|
||||
var mux = sp.GetRequiredKeyedService<IConnectionMultiplexer>(cacheName);
|
||||
return new RedisCache(new RedisCacheOptions
|
||||
{
|
||||
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
|
||||
});
|
||||
}
|
||||
);
|
||||
services.TryAddKeyedSingleton<IFusionCacheBackplane>(
|
||||
cacheName,
|
||||
(sp, _) =>
|
||||
{
|
||||
var mux = sp.GetRequiredKeyedService<IConnectionMultiplexer>(cacheName);
|
||||
return new RedisBackplane(new RedisBackplaneOptions
|
||||
{
|
||||
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
fusionCacheBuilder
|
||||
.WithRegisteredKeyedDistributedCacheByCacheName()
|
||||
.WithRegisteredKeyedBackplaneByCacheName();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static ConnectionMultiplexer CreateConnectionMultiplexer(IServiceProvider sp, string cacheName,
|
||||
string connectionString)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ConnectionMultiplexer.Connect(connectionString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = sp.GetService<ILogger>();
|
||||
logger?.LogError(ex, "Failed to connect to Redis for cache {CacheName}", cacheName);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user