mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
[PM-5518] Sql-backed IDistributedCache (#3791)
* Sql-backed IDistributedCache * sqlserver cache table * remove unused using * setup EF entity * cache indexes * add back cipher * revert SetupEntityFramework change * ef cache * EntityFrameworkCache * IServiceScopeFactory for db context * implement EntityFrameworkCache * move to _serviceScopeFactory * move to config file * ef migrations * fixes * datetime and error codes * revert migrations * migrations * format * static and namespace fix * use time provider * Move SQL migration and remove EF one for the moment * Add clean migration of just the new table * Formatting * Test Custom `IDistributedCache` Implementation * Add Back Logging * Remove Double Logging * Skip Test When Not EntityFrameworkCache * Format --------- Co-authored-by: Matt Bishop <mbishop@bitwarden.com> Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
@ -3,9 +3,11 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Infrastructure.Dapper;
|
||||
using Bit.Infrastructure.EntityFramework;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest;
|
||||
@ -13,6 +15,7 @@ namespace Bit.Infrastructure.IntegrationTest;
|
||||
public class DatabaseDataAttribute : DataAttribute
|
||||
{
|
||||
public bool SelfHosted { get; set; }
|
||||
public bool UseFakeTimeProvider { get; set; }
|
||||
|
||||
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
|
||||
{
|
||||
@ -52,7 +55,7 @@ public class DatabaseDataAttribute : DataAttribute
|
||||
if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf)
|
||||
{
|
||||
var dapperSqlServerCollection = new ServiceCollection();
|
||||
dapperSqlServerCollection.AddLogging(configureLogging);
|
||||
AddCommonServices(dapperSqlServerCollection, configureLogging);
|
||||
dapperSqlServerCollection.AddDapperRepositories(SelfHosted);
|
||||
var globalSettings = new GlobalSettings
|
||||
{
|
||||
@ -65,19 +68,35 @@ public class DatabaseDataAttribute : DataAttribute
|
||||
dapperSqlServerCollection.AddSingleton(globalSettings);
|
||||
dapperSqlServerCollection.AddSingleton<IGlobalSettings>(globalSettings);
|
||||
dapperSqlServerCollection.AddSingleton(database);
|
||||
dapperSqlServerCollection.AddDataProtection();
|
||||
dapperSqlServerCollection.AddDistributedSqlServerCache((o) =>
|
||||
{
|
||||
o.ConnectionString = database.ConnectionString;
|
||||
o.SchemaName = "dbo";
|
||||
o.TableName = "Cache";
|
||||
});
|
||||
yield return dapperSqlServerCollection.BuildServiceProvider();
|
||||
}
|
||||
else
|
||||
{
|
||||
var efCollection = new ServiceCollection();
|
||||
efCollection.AddLogging(configureLogging);
|
||||
AddCommonServices(efCollection, configureLogging);
|
||||
efCollection.SetupEntityFramework(database.ConnectionString, database.Type);
|
||||
efCollection.AddPasswordManagerEFRepositories(SelfHosted);
|
||||
efCollection.AddSingleton(database);
|
||||
efCollection.AddDataProtection();
|
||||
efCollection.AddSingleton<IDistributedCache, EntityFrameworkCache>();
|
||||
yield return efCollection.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCommonServices(IServiceCollection services, Action<ILoggingBuilder> configureLogging)
|
||||
{
|
||||
services.AddLogging(configureLogging);
|
||||
services.AddDataProtection();
|
||||
|
||||
if (UseFakeTimeProvider)
|
||||
{
|
||||
services.AddSingleton<TimeProvider, FakeTimeProvider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
71
test/Infrastructure.IntegrationTest/DistributedCacheTests.cs
Normal file
71
test/Infrastructure.IntegrationTest/DistributedCacheTests.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using Bit.Infrastructure.EntityFramework;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest;
|
||||
|
||||
public class DistributedCacheTests
|
||||
{
|
||||
[DatabaseTheory, DatabaseData(UseFakeTimeProvider = true)]
|
||||
public async Task Simple_NotExpiredItem_StartsScan(IDistributedCache cache, TimeProvider timeProvider)
|
||||
{
|
||||
if (cache is not EntityFrameworkCache efCache)
|
||||
{
|
||||
// We don't write the SqlServer cache implementation so we don't need to test it
|
||||
// also it doesn't use TimeProvider under the hood so we'd have to delay the test
|
||||
// for 30 minutes to get it to work. So just skip it.
|
||||
return;
|
||||
}
|
||||
|
||||
var fakeTimeProvider = (FakeTimeProvider)timeProvider;
|
||||
|
||||
cache.Set("test-key", "some-value"u8.ToArray(), new DistributedCacheEntryOptions
|
||||
{
|
||||
SlidingExpiration = TimeSpan.FromMinutes(20),
|
||||
});
|
||||
|
||||
// Should have expired and not be returned
|
||||
var firstValue = cache.Get("test-key");
|
||||
|
||||
// Scan for expired items is supposed to run every 30 minutes
|
||||
fakeTimeProvider.Advance(TimeSpan.FromMinutes(31));
|
||||
|
||||
var secondValue = cache.Get("test-key");
|
||||
|
||||
// This should have forced the EF cache to start a scan task
|
||||
Assert.NotNull(efCache.scanTask);
|
||||
// We don't want the scan task to throw an exception, unwrap it.
|
||||
await efCache.scanTask;
|
||||
|
||||
Assert.NotNull(firstValue);
|
||||
Assert.Null(secondValue);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData(UseFakeTimeProvider = true)]
|
||||
public async Task ParallelReadsAndWrites_Work(IDistributedCache cache, TimeProvider timeProvider)
|
||||
{
|
||||
var fakeTimeProvider = (FakeTimeProvider)timeProvider;
|
||||
|
||||
await Parallel.ForEachAsync(Enumerable.Range(1, 100), async (index, _) =>
|
||||
{
|
||||
await cache.SetAsync($"test-{index}", "some-value"u8.ToArray(), new DistributedCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(index),
|
||||
});
|
||||
});
|
||||
|
||||
await Parallel.ForEachAsync(Enumerable.Range(1, 100), async (index, _) =>
|
||||
{
|
||||
var value = await cache.GetAsync($"test-{index}");
|
||||
Assert.NotNull(value);
|
||||
});
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task MultipleWritesOnSameKey_ShouldNotThrow(IDistributedCache cache)
|
||||
{
|
||||
await cache.SetAsync("test-duplicate", "some-value"u8.ToArray());
|
||||
await cache.SetAsync("test-duplicate", "some-value"u8.ToArray());
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
|
BIN
test/Infrastructure.IntegrationTest/test.db
Normal file
BIN
test/Infrastructure.IntegrationTest/test.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user