mirror of
https://github.com/bitwarden/server
synced 2025-12-10 13:23:27 +00:00
Add FusionCache to service collection (#6575)
* Add FusionCache to service collection * Refactored to it's own service collection extension, added full unit tests, added TryAdd style support * Move to ExtendedCache instead of FusionCache, re-use exsting DistributedCache if present, expose backplane to DI * Reworked builders to reuse multiplexer if present
This commit is contained in:
@@ -68,6 +68,9 @@
|
|||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
|
||||||
<PackageReference Include="RabbitMQ.Client" Version="7.1.2" />
|
<PackageReference Include="RabbitMQ.Client" Version="7.1.2" />
|
||||||
|
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.0.2" />
|
||||||
|
<PackageReference Include="ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis" Version="2.0.2" />
|
||||||
|
<PackageReference Include="ZiggyCreatures.FusionCache.Serialization.SystemTextJson" Version="2.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Label="Pinned transitive dependencies">
|
<ItemGroup Label="Pinned transitive dependencies">
|
||||||
|
|||||||
@@ -783,6 +783,19 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
{
|
{
|
||||||
public virtual IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings();
|
public virtual IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings();
|
||||||
public virtual IConnectionStringSettings Cosmos { get; set; } = new ConnectionStringSettings();
|
public virtual IConnectionStringSettings Cosmos { get; set; } = new ConnectionStringSettings();
|
||||||
|
|
||||||
|
public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(30);
|
||||||
|
public bool IsFailSafeEnabled { get; set; } = true;
|
||||||
|
public TimeSpan FailSafeMaxDuration { get; set; } = TimeSpan.FromHours(2);
|
||||||
|
public TimeSpan FailSafeThrottleDuration { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
public float? EagerRefreshThreshold { get; set; } = 0.9f;
|
||||||
|
public TimeSpan FactorySoftTimeout { get; set; } = TimeSpan.FromMilliseconds(100);
|
||||||
|
public TimeSpan FactoryHardTimeout { get; set; } = TimeSpan.FromMilliseconds(1500);
|
||||||
|
public TimeSpan DistributedCacheSoftTimeout { get; set; } = TimeSpan.FromSeconds(1);
|
||||||
|
public TimeSpan DistributedCacheHardTimeout { get; set; } = TimeSpan.FromSeconds(2);
|
||||||
|
public bool AllowBackgroundDistributedCacheOperations { get; set; } = true;
|
||||||
|
public TimeSpan JitterMaxDuration { get; set; } = TimeSpan.FromSeconds(2);
|
||||||
|
public TimeSpan DistributedCacheCircuitBreakerDuration { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WebPushSettings : IWebPushSettings
|
public class WebPushSettings : IWebPushSettings
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Caching.StackExchangeRedis;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using ZiggyCreatures.Caching.Fusion;
|
||||||
|
using ZiggyCreatures.Caching.Fusion.Backplane;
|
||||||
|
using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
|
||||||
|
using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
public static class ExtendedCacheServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Add Fusion Cache <see href="https://github.com/ZiggyCreatures/FusionCache"/> to the service
|
||||||
|
/// collection.<br/>
|
||||||
|
/// <br/>
|
||||||
|
/// If Redis is configured, it uses Redis for an L2 cache and backplane. If not, it simply uses in-memory caching.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection TryAddExtendedCacheServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
if (services.Any(s => s.ServiceType == typeof(IFusionCache)))
|
||||||
|
{
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fusionCacheBuilder = services.AddFusionCache()
|
||||||
|
.WithOptions(options =>
|
||||||
|
{
|
||||||
|
options.DistributedCacheCircuitBreakerDuration = globalSettings.DistributedCache.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
|
||||||
|
})
|
||||||
|
.WithSerializer(
|
||||||
|
new FusionCacheSystemTextJsonSerializer()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString))
|
||||||
|
{
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
services.TryAddSingleton<IConnectionMultiplexer>(sp =>
|
||||||
|
ConnectionMultiplexer.Connect(globalSettings.DistributedCache.Redis.ConnectionString));
|
||||||
|
|
||||||
|
fusionCacheBuilder
|
||||||
|
.WithDistributedCache(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)
|
||||||
|
{
|
||||||
|
return backplane;
|
||||||
|
}
|
||||||
|
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
|
||||||
|
return new RedisBackplane(new RedisBackplaneOptions
|
||||||
|
{
|
||||||
|
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
using Bit.Core.Settings;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using NSubstitute;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using Xunit;
|
||||||
|
using ZiggyCreatures.Caching.Fusion;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Utilities;
|
||||||
|
|
||||||
|
public class ExtendedCacheServiceCollectionExtensionsTests
|
||||||
|
{
|
||||||
|
private readonly IServiceCollection _services;
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
|
public ExtendedCacheServiceCollectionExtensionsTests()
|
||||||
|
{
|
||||||
|
_services = new ServiceCollection();
|
||||||
|
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(new Dictionary<string, string?>())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_globalSettings = new GlobalSettings();
|
||||||
|
config.GetSection("GlobalSettings").Bind(_globalSettings);
|
||||||
|
|
||||||
|
_services.TryAddSingleton(config);
|
||||||
|
_services.TryAddSingleton(_globalSettings);
|
||||||
|
_services.TryAddSingleton<IGlobalSettings>(_globalSettings);
|
||||||
|
_services.AddLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryAddFusionCoreServices_CustomSettings_OverridesDefaults()
|
||||||
|
{
|
||||||
|
var settings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
{ "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" },
|
||||||
|
});
|
||||||
|
|
||||||
|
_services.TryAddExtendedCacheServices(settings);
|
||||||
|
using var provider = _services.BuildServiceProvider();
|
||||||
|
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||||
|
var options = fusionCache.DefaultEntryOptions;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryAddFusionCoreServices_DefaultSettings_ConfiguresExpectedValues()
|
||||||
|
{
|
||||||
|
_services.TryAddExtendedCacheServices(_globalSettings);
|
||||||
|
using var provider = _services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||||
|
var options = fusionCache.DefaultEntryOptions;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryAddFusionCoreServices_MultipleCalls_OnlyConfiguresOnce()
|
||||||
|
{
|
||||||
|
var settings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
||||||
|
});
|
||||||
|
_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);
|
||||||
|
|
||||||
|
using var provider = _services.BuildServiceProvider();
|
||||||
|
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||||
|
Assert.NotNull(fusionCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryAddFusionCoreServices_WithRedis_EnablesDistributedCacheAndBackplane()
|
||||||
|
{
|
||||||
|
var settings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
||||||
|
});
|
||||||
|
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryAddFusionCoreServices_WithExistingRedis_EnablesDistributedCacheAndBackplane()
|
||||||
|
{
|
||||||
|
var settings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
{ "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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryAddFusionCoreServices_WithoutRedis_DisablesDistributedCacheAndBackplane()
|
||||||
|
{
|
||||||
|
_services.TryAddExtendedCacheServices(_globalSettings);
|
||||||
|
using var provider = _services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var fusionCache = provider.GetRequiredService<IFusionCache>();
|
||||||
|
Assert.False(fusionCache.HasDistributedCache);
|
||||||
|
Assert.False(fusionCache.HasBackplane);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data)
|
||||||
|
{
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(data)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var settings = new GlobalSettings();
|
||||||
|
config.GetSection("GlobalSettings").Bind(settings);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user