mirror of
https://github.com/bitwarden/server.git
synced 2025-12-11 13:53:48 -06:00
* 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>
495 lines
18 KiB
C#
495 lines
18 KiB
C#
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;
|
|
using ZiggyCreatures.Caching.Fusion.Backplane;
|
|
|
|
namespace Bit.Core.Test.Utilities;
|
|
|
|
public class ExtendedCacheServiceCollectionExtensionsTests
|
|
{
|
|
private readonly IServiceCollection _services;
|
|
private readonly GlobalSettings _globalSettings;
|
|
private const string _cacheName = "TestCache";
|
|
|
|
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 AddExtendedCache_CustomSettings_OverridesDefaults()
|
|
{
|
|
var settings = new GlobalSettings.ExtendedCacheSettings
|
|
{
|
|
Duration = TimeSpan.FromMinutes(12),
|
|
FailSafeMaxDuration = TimeSpan.FromHours(1.5),
|
|
FailSafeThrottleDuration = TimeSpan.FromMinutes(1),
|
|
EagerRefreshThreshold = 0.75f,
|
|
FactorySoftTimeout = TimeSpan.FromMilliseconds(20),
|
|
FactoryHardTimeout = TimeSpan.FromSeconds(3),
|
|
DistributedCacheSoftTimeout = TimeSpan.FromSeconds(0.5),
|
|
DistributedCacheHardTimeout = TimeSpan.FromSeconds(1.5),
|
|
JitterMaxDuration = TimeSpan.FromSeconds(5),
|
|
IsFailSafeEnabled = false,
|
|
AllowBackgroundDistributedCacheOperations = false,
|
|
};
|
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
|
var opt = cache.DefaultEntryOptions;
|
|
|
|
Assert.Equal(TimeSpan.FromMinutes(12), opt.Duration);
|
|
Assert.False(opt.IsFailSafeEnabled);
|
|
Assert.Equal(TimeSpan.FromHours(1.5), opt.FailSafeMaxDuration);
|
|
Assert.Equal(TimeSpan.FromMinutes(1), opt.FailSafeThrottleDuration);
|
|
Assert.Equal(0.75f, opt.EagerRefreshThreshold);
|
|
Assert.Equal(TimeSpan.FromMilliseconds(20), opt.FactorySoftTimeout);
|
|
Assert.Equal(TimeSpan.FromMilliseconds(3000), opt.FactoryHardTimeout);
|
|
Assert.Equal(TimeSpan.FromSeconds(0.5), opt.DistributedCacheSoftTimeout);
|
|
Assert.Equal(TimeSpan.FromSeconds(1.5), opt.DistributedCacheHardTimeout);
|
|
Assert.False(opt.AllowBackgroundDistributedCacheOperations);
|
|
Assert.Equal(TimeSpan.FromSeconds(5), opt.JitterMaxDuration);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_DefaultSettings_ConfiguresExpectedValues()
|
|
{
|
|
_services.AddExtendedCache(_cacheName, _globalSettings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
|
var opt = cache.DefaultEntryOptions;
|
|
|
|
Assert.Equal(TimeSpan.FromMinutes(30), opt.Duration);
|
|
Assert.True(opt.IsFailSafeEnabled);
|
|
Assert.Equal(TimeSpan.FromHours(2), opt.FailSafeMaxDuration);
|
|
Assert.Equal(TimeSpan.FromSeconds(30), opt.FailSafeThrottleDuration);
|
|
Assert.Equal(0.9f, opt.EagerRefreshThreshold);
|
|
Assert.Equal(TimeSpan.FromMilliseconds(100), opt.FactorySoftTimeout);
|
|
Assert.Equal(TimeSpan.FromMilliseconds(1500), opt.FactoryHardTimeout);
|
|
Assert.Equal(TimeSpan.FromSeconds(1), opt.DistributedCacheSoftTimeout);
|
|
Assert.Equal(TimeSpan.FromSeconds(2), opt.DistributedCacheHardTimeout);
|
|
Assert.True(opt.AllowBackgroundDistributedCacheOperations);
|
|
Assert.Equal(TimeSpan.FromSeconds(2), opt.JitterMaxDuration);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_DisabledDistributedCache_DoesNotRegisterBackplaneOrRedis()
|
|
{
|
|
var settings = new GlobalSettings.ExtendedCacheSettings
|
|
{
|
|
EnableDistributedCache = false,
|
|
};
|
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
|
|
|
Assert.False(cache.HasDistributedCache);
|
|
Assert.False(cache.HasBackplane);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_EmptyCacheName_DoesNothing()
|
|
{
|
|
_services.AddExtendedCache(string.Empty, _globalSettings);
|
|
|
|
var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList();
|
|
Assert.Empty(regs);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var cache = provider.GetKeyedService<IFusionCache>(_cacheName);
|
|
Assert.Null(cache);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_MultipleCalls_OnlyAddsOneCacheService()
|
|
{
|
|
var settings = CreateGlobalSettings(new()
|
|
{
|
|
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }
|
|
});
|
|
|
|
// Provide a multiplexer (shared)
|
|
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
|
|
|
_services.AddExtendedCache(_cacheName, settings);
|
|
_services.AddExtendedCache(_cacheName, settings);
|
|
_services.AddExtendedCache(_cacheName, settings);
|
|
|
|
var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList();
|
|
Assert.Single(regs);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
|
Assert.NotNull(cache);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_MultipleDifferentCaches_AddsAll()
|
|
{
|
|
_services.AddExtendedCache("Cache1", _globalSettings);
|
|
_services.AddExtendedCache("Cache2", _globalSettings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
|
|
var cache1 = provider.GetRequiredKeyedService<IFusionCache>("Cache1");
|
|
var cache2 = provider.GetRequiredKeyedService<IFusionCache>("Cache2");
|
|
|
|
Assert.NotNull(cache1);
|
|
Assert.NotNull(cache2);
|
|
Assert.NotSame(cache1, cache2);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_WithRedis_EnablesDistributedCacheAndBackplane()
|
|
{
|
|
var settings = CreateGlobalSettings(new()
|
|
{
|
|
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
|
{ "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedDistributedCache", "true" }
|
|
});
|
|
|
|
// Provide a multiplexer (shared)
|
|
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
|
|
|
_services.AddExtendedCache(_cacheName, settings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
|
|
|
Assert.True(cache.HasDistributedCache);
|
|
Assert.True(cache.HasBackplane);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_InvalidRedisConnection_LogsAndThrows()
|
|
{
|
|
var settings = new GlobalSettings.ExtendedCacheSettings
|
|
{
|
|
UseSharedDistributedCache = false,
|
|
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" }
|
|
};
|
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
Assert.Throws<RedisConnectionException>(() =>
|
|
{
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
|
// Trigger lazy initialization
|
|
cache.GetOrDefault<string>("test");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_WithExistingRedis_UsesExistingDistributedCacheAndBackplane()
|
|
{
|
|
var settings = CreateGlobalSettings(new()
|
|
{
|
|
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
|
});
|
|
|
|
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
|
_services.AddSingleton(Substitute.For<IDistributedCache>());
|
|
|
|
_services.AddExtendedCache(_cacheName, settings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
|
|
|
Assert.True(cache.HasDistributedCache);
|
|
Assert.True(cache.HasBackplane);
|
|
|
|
var existingCache = provider.GetRequiredService<IDistributedCache>();
|
|
Assert.NotNull(existingCache);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_NoRedis_DisablesDistributedCacheAndBackplane()
|
|
{
|
|
_services.AddExtendedCache(_cacheName, _globalSettings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
|
|
|
Assert.False(cache.HasDistributedCache);
|
|
Assert.False(cache.HasBackplane);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_NoSharedRedisButNoConnectionString_DisablesDistributedCacheAndBackplane()
|
|
{
|
|
var settings = new GlobalSettings.ExtendedCacheSettings
|
|
{
|
|
UseSharedDistributedCache = false,
|
|
// No Redis connection string
|
|
};
|
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
|
|
|
Assert.False(cache.HasDistributedCache);
|
|
Assert.False(cache.HasBackplane);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_KeyedRedis_UsesSeparateMultiplexers()
|
|
{
|
|
var settingsA = new GlobalSettings.ExtendedCacheSettings
|
|
{
|
|
EnableDistributedCache = true,
|
|
UseSharedDistributedCache = false,
|
|
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
|
|
};
|
|
var settingsB = new GlobalSettings.ExtendedCacheSettings
|
|
{
|
|
EnableDistributedCache = true,
|
|
UseSharedDistributedCache = false,
|
|
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6380" }
|
|
};
|
|
|
|
_services.AddKeyedSingleton("CacheA", Substitute.For<IConnectionMultiplexer>());
|
|
_services.AddKeyedSingleton("CacheB", Substitute.For<IConnectionMultiplexer>());
|
|
|
|
_services.AddExtendedCache("CacheA", _globalSettings, settingsA);
|
|
_services.AddExtendedCache("CacheB", _globalSettings, settingsB);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var muxA = provider.GetRequiredKeyedService<IConnectionMultiplexer>("CacheA");
|
|
var muxB = provider.GetRequiredKeyedService<IConnectionMultiplexer>("CacheB");
|
|
|
|
Assert.NotNull(muxA);
|
|
Assert.NotNull(muxB);
|
|
Assert.NotSame(muxA, muxB);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddExtendedCache_WithExistingKeyedDistributedCache_ReusesIt()
|
|
{
|
|
var existingCache = Substitute.For<IDistributedCache>();
|
|
_services.AddKeyedSingleton<IDistributedCache>(_cacheName, existingCache);
|
|
|
|
var settings = new GlobalSettings.ExtendedCacheSettings
|
|
{
|
|
UseSharedDistributedCache = false,
|
|
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
|
|
};
|
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
|
|
|
using var provider = _services.BuildServiceProvider();
|
|
var resolved = provider.GetRequiredKeyedService<IDistributedCache>(_cacheName);
|
|
|
|
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()
|
|
.AddInMemoryCollection(data)
|
|
.Build();
|
|
|
|
var settings = new GlobalSettings();
|
|
config.GetSection("GlobalSettings").Bind(settings);
|
|
return settings;
|
|
}
|
|
}
|