diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..fa3cebc --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,24 @@ +name: CI + +on: [push] + +jobs: + build: + strategy: + matrix: + category: [Redis, Postgres, SqlServer, MySql] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Build + run: dotnet build src + + - name: Run Tests + run: dotnet test src --filter Name~${{ matrix.category }} diff --git a/docs/Developing DistributedLock.md b/docs/Developing DistributedLock.md index 380b848..de29dd5 100644 --- a/docs/Developing DistributedLock.md +++ b/docs/Developing DistributedLock.md @@ -51,23 +51,15 @@ If the Oracle tests fail with `ORA-12541: TNS:no listener`, you may have to star ### Postgres -You can install Postgres from [here](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads). - -In `C:\Program Files\PostgreSQL\\data\postgresql.conf`, update `max_connections` to 200. - -Add your username (e.g. postgres) and password to `DistributedLock.Tests/credentials/postgres.txt`, with the username on line 1 and the password on line 2. +Have docker installed, we are using https://testcontainers.com/modules/postgresql/ ### SQL Server -Download SQL developer edition from [here](https://www.microsoft.com/en-us/sql-server/sql-server-downloads). - -The tests connect via integrated security. +Have docker installed, we are using https://testcontainers.com/modules/mssql/ ### Redis -Install Redis locally. On Windows, install it via WSL as described [here](https://developer.redis.com/create/windows/). - -You do not need it running as a service: the tests will start and stop instances automatically. +Have docker installed, we are using https://testcontainers.com/modules/redis/ ### ZooKeeper diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 4d00b52..abfd565 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -22,6 +22,11 @@ + + + + + diff --git a/src/DistributedLock.Core/packages.lock.json b/src/DistributedLock.Core/packages.lock.json index 2f96fec..a861f7b 100644 --- a/src/DistributedLock.Core/packages.lock.json +++ b/src/DistributedLock.Core/packages.lock.json @@ -11,12 +11,6 @@ "System.Threading.Tasks.Extensions": "4.5.4" } }, - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", "requested": "[1.0.3, )", @@ -81,12 +75,6 @@ "System.Threading.Tasks.Extensions": "4.5.4" } }, - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -136,12 +124,6 @@ } }, ".NETStandard,Version=v2.1": { - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -164,12 +146,6 @@ } }, "net8.0": { - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", "requested": "[8.0.4, )", diff --git a/src/DistributedLock.Tests/AbstractTestCases/Data/ConnectionStringStrategyTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Data/ConnectionStringStrategyTestCases.cs index e9cc4ce..bf3a50d 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Data/ConnectionStringStrategyTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Data/ConnectionStringStrategyTestCases.cs @@ -10,8 +10,14 @@ public abstract class ConnectionStringStrategyTestCases this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); /// /// Tests that internally-owned connections are properly cleaned up by disposing the lock handle diff --git a/src/DistributedLock.Tests/AbstractTestCases/Data/DbSemaphoreTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Data/DbSemaphoreTestCases.cs index 91240ac..4c86b75 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Data/DbSemaphoreTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Data/DbSemaphoreTestCases.cs @@ -9,8 +9,14 @@ public abstract class DbSemaphoreTestCases { private TSemaphoreProvider _semaphoreProvider = default!; - [SetUp] public void SetUp() => this._semaphoreProvider = new TSemaphoreProvider(); - [TearDown] public void TearDown() => this._semaphoreProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._semaphoreProvider = new TSemaphoreProvider(); + await this._semaphoreProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._semaphoreProvider.DisposeAsync(); /// /// This case and several that follow test "self-deadlock", where a semaphore acquire cannot possibly succeed because diff --git a/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalConnectionOrTransactionStrategyTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalConnectionOrTransactionStrategyTestCases.cs index 3a0e097..db5ebc6 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalConnectionOrTransactionStrategyTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalConnectionOrTransactionStrategyTestCases.cs @@ -12,8 +12,14 @@ public abstract class ExternalConnectionOrTransactionStrategyTestCases this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); [Test] [NonParallelizable, Retry(tryCount: 3)] // timing sensitive for SqlSemaphore (see comment in that file regarding the 32ms wait) diff --git a/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalConnectionStrategyTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalConnectionStrategyTestCases.cs index cd42e91..fb554e4 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalConnectionStrategyTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalConnectionStrategyTestCases.cs @@ -8,8 +8,14 @@ public abstract class ExternalConnectionStrategyTestCases { private TLockProvider _lockProvider = default!; - [SetUp] public void SetUp() => this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); [Test] public void TestCloseLockOnClosedConnection() diff --git a/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalTransactionStrategyTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalTransactionStrategyTestCases.cs index ea4595a..2c5233a 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalTransactionStrategyTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Data/ExternalTransactionStrategyTestCases.cs @@ -10,11 +10,17 @@ public abstract class ExternalTransactionStrategyTestCases { private TLockProvider _lockProvider = default!; - [SetUp] public void SetUp() => this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); [Test] - public void TestScopedToTransactionOnly() + public async Task TestScopedToTransactionOnly() { this._lockProvider.Strategy.StartAmbient(); @@ -24,7 +30,7 @@ public void TestScopedToTransactionOnly() Assert.That(this._lockProvider.CreateLock(nameof(TestScopedToTransactionOnly)).IsHeld(), Is.True); // create a lock of the same type on the underlying connection of the ambient transaction - using dynamic specificConnectionProvider = Activator.CreateInstance( + await using dynamic specificConnectionProvider = Activator.CreateInstance( ReplaceGenericParameter(typeof(TLockProvider), this._lockProvider.Strategy.GetType(), typeof(SpecificConnectionStrategy)) )!; specificConnectionProvider.Strategy.Test = this; @@ -50,6 +56,11 @@ static Type ReplaceGenericParameter(Type type, Type old, Type @new) /// private class SpecificConnectionStrategy : TestingDbSynchronizationStrategy { + public SpecificConnectionStrategy() + { + Console.WriteLine("SpecificConnectionStrategy created"); + } + public ExternalTransactionStrategyTestCases? Test { get; set; } public override TestingDbConnectionOptions GetConnectionOptions() => diff --git a/src/DistributedLock.Tests/AbstractTestCases/Data/MultiplexingConnectionStrategyTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Data/MultiplexingConnectionStrategyTestCases.cs index 6f9f8d2..edfc225 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Data/MultiplexingConnectionStrategyTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Data/MultiplexingConnectionStrategyTestCases.cs @@ -11,8 +11,14 @@ public abstract class MultiplexingConnectionStrategyTestCases this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); /// /// Similar to but demonstrates diff --git a/src/DistributedLock.Tests/AbstractTestCases/Data/OwnedConnectionStrategyTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Data/OwnedConnectionStrategyTestCases.cs index 1e25fb3..bdf7ba0 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Data/OwnedConnectionStrategyTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Data/OwnedConnectionStrategyTestCases.cs @@ -10,8 +10,14 @@ public abstract class OwnedConnectionStrategyTestCases { private TLockProvider _lockProvider = default!; - [SetUp] public void SetUp() => this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); /// /// Tests that our idle session killer works, therefore validating our other tests that use it. diff --git a/src/DistributedLock.Tests/AbstractTestCases/Data/OwnedTransactionStrategyTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Data/OwnedTransactionStrategyTestCases.cs index 31fc912..a66d67c 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Data/OwnedTransactionStrategyTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Data/OwnedTransactionStrategyTestCases.cs @@ -9,8 +9,14 @@ public abstract class OwnedTransactionStrategyTestCases { private TLockProvider _lockProvider = default!; - [SetUp] public void SetUp() => this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); /// /// Validates that we use the default isolation level to avoid the problem described diff --git a/src/DistributedLock.Tests/AbstractTestCases/Data/UpgradeableReaderWriterLockConnectionStringStrategyTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Data/UpgradeableReaderWriterLockConnectionStringStrategyTestCases.cs index c66540e..22480be 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Data/UpgradeableReaderWriterLockConnectionStringStrategyTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Data/UpgradeableReaderWriterLockConnectionStringStrategyTestCases.cs @@ -9,8 +9,14 @@ public abstract class UpgradeableReaderWriterLockConnectionStringStrategyTestCas { private TLockProvider _lockProvider = default!; - [SetUp] public void SetUp() => this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); /// /// Tests the logic where upgrading a connection stops and restarts the keepalive diff --git a/src/DistributedLock.Tests/AbstractTestCases/DistributedLockCoreTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/DistributedLockCoreTestCases.cs index e48bf31..d3aff6b 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/DistributedLockCoreTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/DistributedLockCoreTestCases.cs @@ -14,14 +14,19 @@ public abstract class DistributedLockCoreTestCases private TLockProvider _lockProvider = default!; private readonly List _cleanupActions = []; - [SetUp] public void SetUp() => this._lockProvider = new TLockProvider(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } [TearDown] - public void TearDown() + public async Task TearDown() { this._cleanupActions.ForEach(a => a()); this._cleanupActions.Clear(); - this._lockProvider.Dispose(); + await this._lockProvider.DisposeAsync(); } [Test] @@ -390,7 +395,7 @@ public async Task TestLockAbandonment() public void TestCrossProcess() { var lockName = this._lockProvider.GetUniqueSafeName(); - var command = this.RunLockTaker(this._lockProvider, this._lockProvider.GetCrossProcessLockType(), lockName); + var command = this.RunLockTaker(this._lockProvider, this._lockProvider.GetCrossProcessLockType(), lockName, this._lockProvider.GetConnectionStringForCrossProcessTest()); Assert.That(command.StandardOutput.ReadLineAsync().Wait(TimeSpan.FromSeconds(10)), Is.True); Assert.That(command.Task.Wait(TimeSpan.FromSeconds(.1)), Is.False); @@ -421,7 +426,7 @@ public void TestCrossProcessAbandonmentWithKill() private void CrossProcessAbandonmentHelper(bool asyncWait, bool kill) { var name = this._lockProvider.GetUniqueSafeName($"cpl-{asyncWait}-{kill}"); - var command = this.RunLockTaker(this._lockProvider, this._lockProvider.GetCrossProcessLockType(), name); + var command = this.RunLockTaker(this._lockProvider, this._lockProvider.GetCrossProcessLockType(), name, this._lockProvider.GetConnectionStringForCrossProcessTest()); Assert.That(command.StandardOutput.ReadLineAsync().Wait(TimeSpan.FromSeconds(10)), Is.True); Assert.That(command.Task.IsCompleted, Is.False); diff --git a/src/DistributedLock.Tests/AbstractTestCases/DistributedReaderWriterLockCoreTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/DistributedReaderWriterLockCoreTestCases.cs index fcfb868..bab2435 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/DistributedReaderWriterLockCoreTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/DistributedReaderWriterLockCoreTestCases.cs @@ -8,8 +8,15 @@ public abstract class DistributedReaderWriterLockCoreTestCases this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); [Test] public async Task TestMultipleReadersSingleWriter() diff --git a/src/DistributedLock.Tests/AbstractTestCases/DistributedSemaphoreCoreTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/DistributedSemaphoreCoreTestCases.cs index c0c934a..3caf336 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/DistributedSemaphoreCoreTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/DistributedSemaphoreCoreTestCases.cs @@ -10,8 +10,14 @@ public abstract class DistributedSemaphoreCoreTestCases this._semaphoreProvider = new TSemaphoreProvider(); - [TearDown] public void TearDown() => this._semaphoreProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._semaphoreProvider = new TSemaphoreProvider(); + await this._semaphoreProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._semaphoreProvider.DisposeAsync(); [Test] public void TestMaxCount() @@ -46,7 +52,7 @@ public void TestConcurrencyHandling() Thread.Sleep(10); Interlocked.Decrement(ref counter); } - }, + }, TaskCreationOptions.LongRunning // dedicated thread )) .ToArray(); diff --git a/src/DistributedLock.Tests/AbstractTestCases/DistributedUpgradeableReaderWriterLockCoreTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/DistributedUpgradeableReaderWriterLockCoreTestCases.cs index dd31642..191d6eb 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/DistributedUpgradeableReaderWriterLockCoreTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/DistributedUpgradeableReaderWriterLockCoreTestCases.cs @@ -8,8 +8,14 @@ public abstract class DistributedUpgradeableReaderWriterLockCoreTestCases this._lockProvider = new TLockProvider(); - [TearDown] public void TearDown() => this._lockProvider.Dispose(); + [SetUp] + public async Task SetUp() + { + this._lockProvider = new TLockProvider(); + await this._lockProvider.SetupAsync(); + } + [TearDown] + public async Task TearDown() => await this._lockProvider.DisposeAsync(); [Test] public void TestMultipleReadersSingleWriter() diff --git a/src/DistributedLock.Tests/AbstractTestCases/Redis/RedisExtensionTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Redis/RedisExtensionTestCases.cs index 913fb91..cfbf14b 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Redis/RedisExtensionTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Redis/RedisExtensionTestCases.cs @@ -9,10 +9,13 @@ public abstract class RedisExtensionTestCases private TLockProvider _provider = default!; [SetUp] - public void SetUp() => this._provider = new TLockProvider(); - + public async Task SetUp() + { + this._provider = new TLockProvider(); + await this._provider.SetupAsync(); + } [TearDown] - public void TearDown() => this._provider.Dispose(); + public async Task TearDown() => await this._provider.DisposeAsync(); [Test] [NonParallelizable, Retry(tryCount: 3)] // timing-sensitive diff --git a/src/DistributedLock.Tests/AbstractTestCases/Redis/RedisSynchronizationCoreTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/Redis/RedisSynchronizationCoreTestCases.cs index 1cc5333..6379d65 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/Redis/RedisSynchronizationCoreTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/Redis/RedisSynchronizationCoreTestCases.cs @@ -14,12 +14,16 @@ public abstract class RedisSynchronizationCoreTestCases private TLockProvider _provider = default!; [SetUp] - public void SetUp() => this._provider = new TLockProvider(); - + public async Task SetUp() + { + this._provider = new TLockProvider(); + await this._provider.SetupAsync(); + } [TearDown] - public void TearDown() => this._provider.Dispose(); + public async Task TearDown() => await this._provider.DisposeAsync(); [Test] + [Retry(tryCount: 3)] // unstable in CI public void TestMajorityFaultingDatabasesCauseAcquireToThrow() { var databases = Enumerable.Range(0, 3).Select(_ => CreateDatabaseMock()).ToArray(); @@ -67,7 +71,8 @@ public void TestMajorityFaultingDatabasesCauseReleaseToThrow() new List { 1, 2, 4 }.ForEach(i => MockDatabase(databases[i], () => throw new DataMisalignedException())); var aggregateException = Assert.Throws(() => handle.Dispose())!; - Assert.That(aggregateException.InnerException, Is.InstanceOf()); + Assert.That(aggregateException.InnerExceptions, Has.Count.EqualTo(3)); + Assert.That(aggregateException.InnerExceptions, Is.All.InstanceOf()); } [Test] @@ -104,7 +109,7 @@ public async Task TestFailedAcquireReleasesWhatHasAlreadyBeenAcquired() var failDatabase = CreateDatabaseMock(); MockDatabase(failDatabase, () => { @event.Wait(); return false; }); - this._provider.Strategy.DatabaseProvider.Databases = new[] { RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase(), failDatabase.Object }; + this._provider.Strategy.DatabaseProvider.Databases = new[] { RedisServer.CreateDatabase(_provider.Strategy.DatabaseProvider.Redis), failDatabase.Object }; var @lock = this._provider.CreateLock("lock"); var acquireTask = @lock.TryAcquireAsync().AsTask(); @@ -112,7 +117,7 @@ public async Task TestFailedAcquireReleasesWhatHasAlreadyBeenAcquired() @event.Set(); Assert.That(await acquireTask, Is.Null); - this._provider.Strategy.DatabaseProvider.Databases = new[] { RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase() }; + this._provider.Strategy.DatabaseProvider.Databases = new[] { RedisServer.CreateDatabase(_provider.Strategy.DatabaseProvider.Redis) }; var singleDatabaseLock = this._provider.CreateLock("lock"); using var handle = await singleDatabaseLock.TryAcquireAsync(); Assert.That(handle, Is.Not.Null); @@ -135,9 +140,9 @@ public void TestAcquireWithLockPrefix() using var explicitPrefixHandle = explicitPrefixLock.TryAcquire(); Assert.That(explicitPrefixHandle, Is.Null); - static IDatabase CreateDatabase(string? keyPrefix = null) + IDatabase CreateDatabase(string? keyPrefix = null) { - var database = RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase(); + var database = RedisServer.CreateDatabase(_provider.Strategy.DatabaseProvider.Redis); return keyPrefix is null ? database : database.WithKeyPrefix(keyPrefix); } } diff --git a/src/DistributedLock.Tests/AbstractTestCases/ZooKeeper/ZooKeeperSynchronizationCoreTestCases.cs b/src/DistributedLock.Tests/AbstractTestCases/ZooKeeper/ZooKeeperSynchronizationCoreTestCases.cs index b6aa642..5aa46c8 100644 --- a/src/DistributedLock.Tests/AbstractTestCases/ZooKeeper/ZooKeeperSynchronizationCoreTestCases.cs +++ b/src/DistributedLock.Tests/AbstractTestCases/ZooKeeper/ZooKeeperSynchronizationCoreTestCases.cs @@ -13,10 +13,13 @@ public abstract class ZooKeeperSynchronizationCoreTestCases private TLockProvider _provider = default!; [SetUp] - public void SetUp() => this._provider = new TLockProvider(); - + public async Task SetUp() + { + this._provider = new TLockProvider(); + await this._provider.SetupAsync(); + } [TearDown] - public void TearDown() => this._provider.Dispose(); + public async Task TearDown() => await this._provider.DisposeAsync(); [Test] public async Task TestDoesNotAttemptToCreateOrDeleteExistingNode() diff --git a/src/DistributedLock.Tests/DistributedLock.Tests.csproj b/src/DistributedLock.Tests/DistributedLock.Tests.csproj index be82219..1115a38 100644 --- a/src/DistributedLock.Tests/DistributedLock.Tests.csproj +++ b/src/DistributedLock.Tests/DistributedLock.Tests.csproj @@ -27,6 +27,11 @@ + + + + + diff --git a/src/DistributedLock.Tests/Infrastructure/Azure/TestingAzureBlobLeaseSynchronizationStrategy.cs b/src/DistributedLock.Tests/Infrastructure/Azure/TestingAzureBlobLeaseSynchronizationStrategy.cs index eaedd1a..9c550de 100644 --- a/src/DistributedLock.Tests/Infrastructure/Azure/TestingAzureBlobLeaseSynchronizationStrategy.cs +++ b/src/DistributedLock.Tests/Infrastructure/Azure/TestingAzureBlobLeaseSynchronizationStrategy.cs @@ -43,10 +43,11 @@ public override void PrepareForHighContention(ref int maxConcurrentAcquires) this.CreateBlobBeforeLockIsCreated = true; } - public override void Dispose() + public override ValueTask DisposeAsync() { try { this._disposables.Dispose(); } - finally { base.Dispose(); } + finally { base.DisposeAsync(); } + return ValueTask.CompletedTask; } private class HandleLostScope : IDisposable diff --git a/src/DistributedLock.Tests/Infrastructure/Data/TestingDb.cs b/src/DistributedLock.Tests/Infrastructure/Data/TestingDb.cs index 2cbe9e6..52e7c19 100644 --- a/src/DistributedLock.Tests/Infrastructure/Data/TestingDb.cs +++ b/src/DistributedLock.Tests/Infrastructure/Data/TestingDb.cs @@ -58,6 +58,9 @@ public void ClearPool(DbConnection connection) public abstract IsolationLevel GetIsolationLevel(DbConnection connection); public virtual void PrepareForHighContention(ref int maxConcurrentAcquires) { } + + public virtual ValueTask SetupAsync() => ValueTask.CompletedTask; + public virtual ValueTask DisposeAsync() => ValueTask.CompletedTask; } public enum TransactionSupport diff --git a/src/DistributedLock.Tests/Infrastructure/Data/TestingDbSynchronizationStrategy.cs b/src/DistributedLock.Tests/Infrastructure/Data/TestingDbSynchronizationStrategy.cs index 91e8bc0..76f10a3 100644 --- a/src/DistributedLock.Tests/Infrastructure/Data/TestingDbSynchronizationStrategy.cs +++ b/src/DistributedLock.Tests/Infrastructure/Data/TestingDbSynchronizationStrategy.cs @@ -18,6 +18,11 @@ protected TestingDbSynchronizationStrategy(TestingDb db) public override void PrepareForHighContention(ref int maxConcurrentAcquires) => this.Db.PrepareForHighContention(ref maxConcurrentAcquires); + + public override ValueTask SetupAsync() => this.Db.SetupAsync(); + public override ValueTask DisposeAsync() => this.Db.DisposeAsync(); + + public override string GetConnectionStringForCrossProcessTest() => this.Db.ConnectionString; } public abstract class TestingDbSynchronizationStrategy : TestingDbSynchronizationStrategy @@ -26,19 +31,6 @@ public abstract class TestingDbSynchronizationStrategy : TestingDbSynchroni protected TestingDbSynchronizationStrategy() : base(new TDb()) { } public new TDb Db => (TDb)base.Db; - - public override void Dispose() - { - // if we have a uniquely-named connection, clear it's pool to avoid "leaking" connections into pools we'll never - // use again - if (!Equals(this.Db.ApplicationName, new TDb().ApplicationName)) - { - using var connection = this.Db.CreateConnection(); - this.Db.ClearPool(connection); - } - - base.Dispose(); - } } public abstract class TestingConnectionStringSynchronizationStrategy : TestingDbSynchronizationStrategy @@ -167,10 +159,10 @@ public override TestingDbConnectionOptions GetConnectionOptions() return new TestingDbConnectionOptions { Connection = connection }; } - public override void Dispose() + public override ValueTask DisposeAsync() { this._disposables.Dispose(); - base.Dispose(); + return base.DisposeAsync(); } } @@ -211,9 +203,9 @@ public override TestingDbConnectionOptions GetConnectionOptions() return new TestingDbConnectionOptions { Transaction = transaction }; } - public override void Dispose() + public override ValueTask DisposeAsync() { this._disposables.Dispose(); - base.Dispose(); + return base.DisposeAsync(); } } diff --git a/src/DistributedLock.Tests/Infrastructure/MySql/TestingMySqlDb.cs b/src/DistributedLock.Tests/Infrastructure/MySql/TestingMySqlDb.cs index 827e293..477c19b 100644 --- a/src/DistributedLock.Tests/Infrastructure/MySql/TestingMySqlDb.cs +++ b/src/DistributedLock.Tests/Infrastructure/MySql/TestingMySqlDb.cs @@ -1,6 +1,5 @@ using Medallion.Threading.Tests.Data; using MySqlConnector; -using NUnit.Framework; using System.Data; using System.Data.Common; @@ -12,7 +11,7 @@ public class TestingMySqlDb : TestingPrimaryClientDb private readonly MySqlConnectionStringBuilder _connectionStringBuilder; public TestingMySqlDb() - : this(MySqlCredentials.GetConnectionString(TestContext.CurrentContext.TestDirectory)) + : this(MySqlSetUpFixture.MySql.GetConnectionString()) { } @@ -88,7 +87,7 @@ JOIN information_schema.processlist p public sealed class TestingMariaDbDb : TestingMySqlDb { - public TestingMariaDbDb() : base(MariaDbCredentials.GetConnectionString(TestContext.CurrentContext.TestDirectory)) { } + public TestingMariaDbDb() : base(MySqlSetUpFixture.MariaDb.GetConnectionString()) { } protected override string IsolationLevelVariableName => "tx_isolation"; } diff --git a/src/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresDb.cs b/src/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresDb.cs index 46a137a..fc007e4 100644 --- a/src/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresDb.cs +++ b/src/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresDb.cs @@ -2,19 +2,19 @@ using Medallion.Threading.Tests.Data; using Npgsql; using NpgsqlTypes; -using NUnit.Framework; using System.Data; using System.Data.Common; +using Testcontainers.PostgreSql; namespace Medallion.Threading.Tests.Postgres; public sealed class TestingPostgresDb : TestingPrimaryClientDb { - internal static readonly string DefaultConnectionString = PostgresCredentials.GetConnectionString(TestContext.CurrentContext.TestDirectory); + private readonly PostgreSqlContainer _container = new PostgreSqlBuilder().Build(); - private readonly NpgsqlConnectionStringBuilder _connectionStringBuilder = new(DefaultConnectionString); + private NpgsqlConnectionStringBuilder? _connectionStringBuilder; - public override DbConnectionStringBuilder ConnectionStringBuilder => this._connectionStringBuilder; + public override DbConnectionStringBuilder ConnectionStringBuilder => this._connectionStringBuilder!; // https://til.hashrocket.com/posts/8f87c65a0a-postgresqls-max-identifier-length-is-63-bytes public override int MaxApplicationNameLength => 63; @@ -30,7 +30,7 @@ public override int CountActiveSessions(string applicationName) { Invariant.Require(applicationName.Length <= this.MaxApplicationNameLength); - using var connection = new NpgsqlConnection(DefaultConnectionString); + using var connection = new NpgsqlConnection(_container.GetConnectionString()); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT COUNT(*)::int FROM pg_stat_activity WHERE application_name = @applicationName"; @@ -50,7 +50,7 @@ public override IsolationLevel GetIsolationLevel(DbConnection connection) public override async Task KillSessionsAsync(string applicationName, DateTimeOffset? idleSince) { - using var connection = new NpgsqlConnection(DefaultConnectionString); + using var connection = new NpgsqlConnection(_container.GetConnectionString()); await connection.OpenAsync(); using var command = connection.CreateCommand(); // based on https://stackoverflow.com/questions/13236160/is-there-a-timeout-for-idle-postgresql-connections @@ -67,4 +67,12 @@ @idleSince IS NULL await command.ExecuteNonQueryAsync(); } + + public override async ValueTask SetupAsync() + { + await _container.StartAsync(); + this._connectionStringBuilder = new NpgsqlConnectionStringBuilder(_container.GetConnectionString()); + } + + public override async ValueTask DisposeAsync() => await _container.DisposeAsync(); } diff --git a/src/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresProviders.cs b/src/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresProviders.cs index 8d89e66..542fa7c 100644 --- a/src/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresProviders.cs +++ b/src/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresProviders.cs @@ -21,6 +21,8 @@ public override IDistributedLock CreateLockWithExactName(string name) => public override string GetSafeName(string name) => new PostgresAdvisoryLockKey(name, allowHashing: true).ToString(); + public override string GetConnectionStringForCrossProcessTest() => this.Strategy.Db.ConnectionString; + internal static Action ToPostgresOptions((bool useMultiplexing, bool useTransaction, TimeSpan? keepaliveCadence) options) => o => { o.UseMultiplexing(options.useMultiplexing); diff --git a/src/DistributedLock.Tests/Infrastructure/Redis/RedisServer.cs b/src/DistributedLock.Tests/Infrastructure/Redis/RedisServer.cs index d524205..31c639d 100644 --- a/src/DistributedLock.Tests/Infrastructure/Redis/RedisServer.cs +++ b/src/DistributedLock.Tests/Infrastructure/Redis/RedisServer.cs @@ -1,40 +1,13 @@ -using Medallion.Shell; -using StackExchange.Redis; +using StackExchange.Redis; +using Testcontainers.Redis; namespace Medallion.Threading.Tests.Redis; internal class RedisServer { - // redis default is 6379, so go one above that - private static readonly int MinDynamicPort = RedisPorts.DefaultPorts.Max() + 1, MaxDynamicPort = MinDynamicPort + 100; - - // it's important for this to be lazy because it doesn't work when running on Linux - private static readonly Lazy WslPath = new( - () => Directory.GetDirectories(@"C:\Windows\WinSxS") - .Select(d => Path.Combine(d, "wsl.exe")) - .Where(File.Exists) - .OrderByDescending(File.GetCreationTimeUtc) - .First() - ); - - private static readonly Dictionary ActiveServersByPort = []; - private static readonly RedisServer[] DefaultServers = new RedisServer[RedisPorts.DefaultPorts.Count]; - - private readonly Command _command; - - public RedisServer(bool allowAdmin = false) : this(null, allowAdmin) { } - - private RedisServer(int? port, bool allowAdmin) + public RedisServer(int port, bool allowAdmin) { - lock (ActiveServersByPort) - { - this.Port = port ?? Enumerable.Range(MinDynamicPort, count: MaxDynamicPort - MinDynamicPort + 1) - .First(p => !ActiveServersByPort.ContainsKey(p)); - this._command = Command.Run(WslPath.Value, ["redis-server", "--port", this.Port], options: o => o.StartInfo(si => si.RedirectStandardInput = false)) - .RedirectTo(Console.Out) - .RedirectStandardErrorTo(Console.Error); - ActiveServersByPort.Add(this.Port, this); - } + this.Port = port; this.Multiplexer = ConnectionMultiplexer.Connect($"localhost:{this.Port},abortConnect=false{(allowAdmin ? ",allowAdmin=true" : string.Empty)}"); // Clean the db to ensure it is empty. Running an arbitrary command also ensures that // the db successfully spun up before we proceed (Connect seemingly can complete before that happens). @@ -43,49 +16,11 @@ private RedisServer(int? port, bool allowAdmin) this.Multiplexer.GetDatabase().Execute("flushall", Array.Empty(), CommandFlags.DemandMaster); } - public int ProcessId => this._command.ProcessId; public int Port { get; } public ConnectionMultiplexer Multiplexer { get; } - - public static RedisServer GetDefaultServer(int index) - { - lock (DefaultServers) - { - return DefaultServers[index] ??= new RedisServer(RedisPorts.DefaultPorts[index], allowAdmin: false); - } - } - - public static void DisposeAll() - { - lock (ActiveServersByPort) - { - var shutdownTasks = ActiveServersByPort.Values - .Select(async server => - { - // When testing the case of a server outage, we'll have manually shut down some servers. - // In that case, we shouldn't attempt to connect to them since that will fail. - var isConnected = server.Multiplexer.GetServers().Any(s => s.IsConnected); - server.Multiplexer.Dispose(); - try - { - if (isConnected) - { - using var adminMultiplexer = await ConnectionMultiplexer.ConnectAsync($"localhost:{server.Port},allowAdmin=true"); - adminMultiplexer.GetServer("localhost", server.Port).Shutdown(ShutdownMode.Never); - } - } - finally - { - if (!await server._command.Task.TryWaitAsync(TimeSpan.FromSeconds(5))) - { - server._command.Kill(); - throw new InvalidOperationException("Forced to kill Redis server"); - } - } - }) - .ToArray(); - ActiveServersByPort.Clear(); - Task.WaitAll(shutdownTasks); - } - } + + public static RedisServer Create(RedisContainer container, bool allowAdmin = false) + => new RedisServer(container.GetMappedPublicPort(RedisBuilder.RedisPort), allowAdmin); + public static IDatabase CreateDatabase(RedisContainer container, bool allowAdmin = false) + => Create(container, allowAdmin).Multiplexer.GetDatabase(); } diff --git a/src/DistributedLock.Tests/Infrastructure/Redis/RedisSetUpFixture.cs b/src/DistributedLock.Tests/Infrastructure/Redis/RedisSetUpFixture.cs index 0226708..8147cfc 100644 --- a/src/DistributedLock.Tests/Infrastructure/Redis/RedisSetUpFixture.cs +++ b/src/DistributedLock.Tests/Infrastructure/Redis/RedisSetUpFixture.cs @@ -1,13 +1,20 @@ using NUnit.Framework; +using Testcontainers.Redis; namespace Medallion.Threading.Tests.Redis; [SetUpFixture] public class RedisSetUpFixture { + public static RedisContainer Redis; + [OneTimeSetUp] - public void OneTimeSetUp() { } + public static async Task OneTimeSetUp() + { + Redis = new RedisBuilder().Build(); + await Redis.StartAsync(); + } [OneTimeTearDown] - public void OneTimeTearDown() => RedisServer.DisposeAll(); + public static async Task OneTimeTearDown() => await Redis.DisposeAsync(); } diff --git a/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisDatabaseProvider.cs b/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisDatabaseProvider.cs index 875ec6c..a23be47 100644 --- a/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisDatabaseProvider.cs +++ b/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisDatabaseProvider.cs @@ -1,62 +1,110 @@ -using NUnit.Framework; -using StackExchange.Redis; +using StackExchange.Redis; using StackExchange.Redis.KeyspaceIsolation; -using System.Diagnostics; +using Testcontainers.Redis; namespace Medallion.Threading.Tests.Redis; public abstract class TestingRedisDatabaseProvider { - protected TestingRedisDatabaseProvider(IEnumerable databases) - { - this.Databases = databases.ToArray(); - } - - protected TestingRedisDatabaseProvider(int count) - : this(Enumerable.Range(0, count).Select(i => RedisServer.GetDefaultServer(i).Multiplexer.GetDatabase())) - { - } - // publicly settable so that callers can alter the dbs in use - public IReadOnlyList Databases { get; set; } + public IReadOnlyList Databases { get; set; } = default!; + public RedisContainer Redis { get; protected set; } = default!; + public abstract string ConnectionStrings { get; } public virtual string CrossProcessLockTypeSuffix => this.Databases.Count.ToString(); + + public abstract ValueTask SetupAsync(); + public abstract ValueTask DisposeAsync(); } public sealed class TestingRedisSingleDatabaseProvider : TestingRedisDatabaseProvider { - public TestingRedisSingleDatabaseProvider() : base(count: 1) { } + public override string ConnectionStrings => this.Redis.GetConnectionString(); + public override async ValueTask SetupAsync() + { + this.Redis = new RedisBuilder().Build(); + await this.Redis.StartAsync(); + + this.Databases = [RedisServer.CreateDatabase(this.Redis)]; + } + + public override ValueTask DisposeAsync() => this.Redis.DisposeAsync(); } public sealed class TestingRedisWithKeyPrefixSingleDatabaseProvider : TestingRedisDatabaseProvider { - public TestingRedisWithKeyPrefixSingleDatabaseProvider() - : base(new[] { RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase().WithKeyPrefix("distributed_locks:") }) { } - + public override string ConnectionStrings => this.Redis.GetConnectionString(); public override string CrossProcessLockTypeSuffix => "1WithPrefix"; + + public override async ValueTask SetupAsync() + { + this.Redis = new RedisBuilder().Build(); + await this.Redis.StartAsync(); + + this.Databases = [RedisServer.CreateDatabase(this.Redis).WithKeyPrefix("distributed_locks:")]; + } + + public override ValueTask DisposeAsync() => this.Redis.DisposeAsync(); } public sealed class TestingRedis3DatabaseProvider : TestingRedisDatabaseProvider { - public TestingRedis3DatabaseProvider() : base(count: 3) { } + private RedisContainer[] _redises = default!; + + public override string ConnectionStrings => string.Join("||", _redises.Select(x => x.GetConnectionString())); + + public override async ValueTask SetupAsync() + { + this._redises = Enumerable.Range(0, 3).Select(_ => new RedisBuilder().Build()).ToArray(); + await Task.WhenAll(this._redises.Select(redis => redis.StartAsync())); + + this.Redis = _redises[0]; + + this.Databases = this._redises.Select(redis => RedisServer.CreateDatabase(redis)).ToArray(); + } + + public override async ValueTask DisposeAsync() + { + foreach (var redis in this._redises) + { + await redis.DisposeAsync(); + } + } } public sealed class TestingRedis2x1DatabaseProvider : TestingRedisDatabaseProvider { - private static readonly IDatabase DeadDatabase; + private static IDatabase DeadDatabase; + private RedisContainer[] _redises = default!; + + public override string ConnectionStrings => string.Join("||", _redises.Select(x => x.GetConnectionString())); static TestingRedis2x1DatabaseProvider() { - var server = new RedisServer(allowAdmin: true); + var redis = new RedisBuilder().Build(); + redis.StartAsync().GetAwaiter().GetResult(); + var server = RedisServer.Create(redis, allowAdmin: true); DeadDatabase = server.Multiplexer.GetDatabase(); - using var process = Process.GetProcessById(server.ProcessId); server.Multiplexer.GetServer($"localhost:{server.Port}").Shutdown(ShutdownMode.Never); - Assert.That(process.WaitForExit(5000), Is.True); + redis.StopAsync().GetAwaiter().GetResult(); + } + + public override async ValueTask SetupAsync() + { + this._redises = Enumerable.Range(0, 2).Select(_ => new RedisBuilder().Build()).ToArray(); + await Task.WhenAll(this._redises.Select(redis => redis.StartAsync())); + + this.Redis = _redises[0]; + + Databases = this._redises.Select(redis => RedisServer.CreateDatabase(redis)).Append(DeadDatabase).ToArray(); } - public TestingRedis2x1DatabaseProvider() - : base(Enumerable.Range(0, 2).Select(i => RedisServer.GetDefaultServer(i).Multiplexer.GetDatabase()).Append(DeadDatabase)) + public override async ValueTask DisposeAsync() { + foreach (var redis in this._redises) + { + await redis.DisposeAsync(); + } } public override string CrossProcessLockTypeSuffix => "2x1"; diff --git a/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisProviders.cs b/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisProviders.cs index 10b5f14..a7ab9ca 100644 --- a/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisProviders.cs +++ b/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisProviders.cs @@ -19,6 +19,8 @@ public override IDistributedLock CreateLockWithExactName(string name) public override string GetSafeName(string name) => new RedisDistributedLock(name, this.Strategy.DatabaseProvider.Databases).Name; public override string GetCrossProcessLockType() => $"{nameof(RedisDistributedLock)}{this.Strategy.DatabaseProvider.CrossProcessLockTypeSuffix}"; + + public override string GetConnectionStringForCrossProcessTest() => this.Strategy.DatabaseProvider.ConnectionStrings; } public sealed class TestingRedisDistributedReaderWriterLockProvider : TestingReaderWriterLockProvider> diff --git a/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisSynchronizationStrategy.cs b/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisSynchronizationStrategy.cs index 041ac2d..7cfddcc 100644 --- a/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisSynchronizationStrategy.cs +++ b/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisSynchronizationStrategy.cs @@ -57,6 +57,10 @@ public void RegisterKillHandleAction(Action action) } } + public override string GetConnectionStringForCrossProcessTest() => this.DatabaseProvider.ConnectionStrings; + + public override ValueTask SetupAsync() => this.DatabaseProvider.SetupAsync(); + private class HandleLostScope : IDisposable { private TestingRedisSynchronizationStrategy? _strategy; diff --git a/src/DistributedLock.Tests/Infrastructure/Shared/MySqlCredentials.cs b/src/DistributedLock.Tests/Infrastructure/Shared/MySqlCredentials.cs deleted file mode 100644 index d2b9aa0..0000000 --- a/src/DistributedLock.Tests/Infrastructure/Shared/MySqlCredentials.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MySqlConnector; -using System; -using System.IO; - -namespace Medallion.Threading.Tests; - -internal static class MySqlCredentials -{ - private static (string username, string password) GetCredentials(string baseDirectory) - { - var file = Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "credentials", "mysql.txt")); - if (!File.Exists(file)) { throw new InvalidOperationException($"Unable to find mysql credentials file {file}"); } - var lines = File.ReadAllLines(file); - if (lines.Length != 2) { throw new FormatException($"{file} must contain exactly 2 lines of text"); } - return (lines[0], lines[1]); - } - - public static string GetConnectionString(string baseDirectory) - { - var (username, password) = GetCredentials(baseDirectory); - - return new MySqlConnectionStringBuilder - { - Port = 3307, - Server = "localhost", - Database = "mysql", - UserID = username, - Password = password, - PersistSecurityInfo = true, - // set a high pool size so that we don't empty the pool through things like lock abandonment tests - MaximumPoolSize = 500, - // workaround for https://github.com/mysql-net/MySqlConnector/issues/1448 - TlsVersion = "Tls12" - }.ConnectionString; - } -} diff --git a/src/DistributedLock.Tests/Infrastructure/Shared/PostgresCredentials.cs b/src/DistributedLock.Tests/Infrastructure/Shared/PostgresCredentials.cs deleted file mode 100644 index 3222829..0000000 --- a/src/DistributedLock.Tests/Infrastructure/Shared/PostgresCredentials.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Npgsql; -using System; -using System.IO; - -namespace Medallion.Threading.Tests; - -internal static class PostgresCredentials -{ - private static (string username, string password) GetCredentials(string baseDirectory) - { - var file = Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "credentials", "postgres.txt")); - if (!File.Exists(file)) { throw new InvalidOperationException($"Unable to find postgres credentials file {file}"); } - var lines = File.ReadAllLines(file); - if (lines.Length != 2) { throw new FormatException($"{file} must contain exactly 2 lines of text"); } - return (lines[0], lines[1]); - } - - public static string GetConnectionString(string baseDirectory) - { - var (username, password) = GetCredentials(baseDirectory); - - return new NpgsqlConnectionStringBuilder - { - Port = 5432, - Host = "localhost", - Database = "postgres", - Username = username, - Password = password, - PersistSecurityInfo = true, - ApplicationName = SqlServerCredentials.ApplicationName, - // set a high pool size so that we don't empty the pool through things like lock abandonment tests - MaxPoolSize = 500, - }.ConnectionString; - } -} diff --git a/src/DistributedLock.Tests/Infrastructure/Shared/RedisPorts.cs b/src/DistributedLock.Tests/Infrastructure/Shared/RedisPorts.cs deleted file mode 100644 index e970f9d..0000000 --- a/src/DistributedLock.Tests/Infrastructure/Shared/RedisPorts.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Medallion.Threading.Tests; - -internal static class RedisPorts -{ - // 6379 is the redis default, so don't use that - public static readonly IReadOnlyList DefaultPorts = Enumerable.Range(6380, count: 10).ToArray(); -} diff --git a/src/DistributedLock.Tests/Infrastructure/Shared/SqlServerCredentials.cs b/src/DistributedLock.Tests/Infrastructure/Shared/SqlServerCredentials.cs deleted file mode 100644 index d9598e3..0000000 --- a/src/DistributedLock.Tests/Infrastructure/Shared/SqlServerCredentials.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Medallion.Threading.Tests; - -internal static class SqlServerCredentials -{ - public static readonly string ApplicationName = $"{typeof(SqlServerCredentials).Assembly.GetName().Name} ({TargetFramework.Current})"; - - public static readonly string ConnectionString = new Microsoft.Data.SqlClient.SqlConnectionStringBuilder - { - DataSource = @"localhost", // localhost for SQL Developer, .\SQLEXPRESS for express - InitialCatalog = "master", - IntegratedSecurity = true, - ApplicationName = ApplicationName, - // set a high pool size so that we don't empty the pool through things like lock abandonment tests - MaxPoolSize = 10000, - // Allows us to connect to SQLExpress with Microsoft.Data.SqlClient while still being compatible - // with System.Data.SqlClient (alternative would be building the connection string with System.Data.SqlClient - // and doing TrustServerCertificate = true). - Encrypt = false, - } - .ConnectionString; -} diff --git a/src/DistributedLock.Tests/Infrastructure/SqlServer/TestingSqlServerDb.cs b/src/DistributedLock.Tests/Infrastructure/SqlServer/TestingSqlServerDb.cs index a7f8327..bbcfb32 100644 --- a/src/DistributedLock.Tests/Infrastructure/SqlServer/TestingSqlServerDb.cs +++ b/src/DistributedLock.Tests/Infrastructure/SqlServer/TestingSqlServerDb.cs @@ -2,6 +2,7 @@ using Medallion.Threading.Tests.Data; using System.Data; using System.Data.Common; +using Testcontainers.MsSql; namespace Medallion.Threading.Tests.SqlServer; @@ -9,10 +10,7 @@ public interface ITestingSqlServerDb { } public sealed class TestingSqlServerDb : TestingPrimaryClientDb, ITestingSqlServerDb { - internal static readonly string DefaultConnectionString = SqlServerCredentials.ConnectionString; - - private readonly Microsoft.Data.SqlClient.SqlConnectionStringBuilder _connectionStringBuilder = - new(DefaultConnectionString); + private Microsoft.Data.SqlClient.SqlConnectionStringBuilder _connectionStringBuilder; public override DbConnectionStringBuilder ConnectionStringBuilder => this._connectionStringBuilder; @@ -25,7 +23,7 @@ public override int CountActiveSessions(string applicationName) { Invariant.Require(applicationName.Length <= this.MaxApplicationNameLength); - using var connection = new Microsoft.Data.SqlClient.SqlConnection(DefaultConnectionString); + using var connection = new Microsoft.Data.SqlClient.SqlConnection(SqlServerSetUpFixture.SqlServer.GetConnectionString()); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = $@"SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE program_name = @applicationName"; @@ -54,7 +52,7 @@ FROM sys.dm_exec_sessions public override async Task KillSessionsAsync(string applicationName, DateTimeOffset? idleSince) { - using var connection = new Microsoft.Data.SqlClient.SqlConnection(DefaultConnectionString); + using var connection = new Microsoft.Data.SqlClient.SqlConnection(SqlServerSetUpFixture.SqlServer.GetConnectionString()); await connection.OpenAsync(); var findIdleSessionsCommand = connection.CreateCommand(); @@ -89,12 +87,17 @@ @idleSince IS NULL catch (Exception ex) { Console.WriteLine($"Failed to kill {spid}: {ex}"); } } } + + public override ValueTask SetupAsync() + { + _connectionStringBuilder = new(SqlServerSetUpFixture.SqlServer.GetConnectionString()); + return ValueTask.CompletedTask; + } } public sealed class TestingSystemDataSqlServerDb : TestingDb, ITestingSqlServerDb { - private readonly System.Data.SqlClient.SqlConnectionStringBuilder _connectionStringBuilder = - new(TestingSqlServerDb.DefaultConnectionString); + private System.Data.SqlClient.SqlConnectionStringBuilder _connectionStringBuilder; public override DbConnectionStringBuilder ConnectionStringBuilder => this._connectionStringBuilder; @@ -107,4 +110,10 @@ public sealed class TestingSystemDataSqlServerDb : TestingDb, ITestingSqlServerD public override IsolationLevel GetIsolationLevel(DbConnection connection) => new TestingSqlServerDb().GetIsolationLevel(connection); public override DbConnection CreateConnection() => new System.Data.SqlClient.SqlConnection(this.ConnectionStringBuilder.ConnectionString); + + public override ValueTask SetupAsync() + { + _connectionStringBuilder = new(SqlServerSetUpFixture.SqlServer.GetConnectionString()); + return ValueTask.CompletedTask; + } } diff --git a/src/DistributedLock.Tests/Infrastructure/TestingLockProvider.cs b/src/DistributedLock.Tests/Infrastructure/TestingLockProvider.cs index 407e04c..4c06930 100644 --- a/src/DistributedLock.Tests/Infrastructure/TestingLockProvider.cs +++ b/src/DistributedLock.Tests/Infrastructure/TestingLockProvider.cs @@ -1,6 +1,6 @@ namespace Medallion.Threading.Tests; -public abstract class TestingLockProvider : ITestingNameProvider, IDisposable +public abstract class TestingLockProvider : ITestingNameProvider, IAsyncDisposable where TStrategy : TestingSynchronizationStrategy, new() { private readonly Lazy _lazyStrategy = new(() => new TStrategy()); @@ -13,7 +13,11 @@ public abstract class TestingLockProvider : ITestingNameProvider, IDi public abstract string GetSafeName(string name); public virtual string GetCrossProcessLockType() => this.CreateLock(string.Empty).GetType().Name; - public virtual void Dispose() => this.Strategy.Dispose(); + + public virtual ValueTask SetupAsync() => this.Strategy.SetupAsync(); + public virtual ValueTask DisposeAsync() => this.Strategy.DisposeAsync(); + + public virtual string GetConnectionStringForCrossProcessTest() => this.Strategy.GetConnectionStringForCrossProcessTest(); /// /// Returns a lock whose name is based on diff --git a/src/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockAsMutexProvider.cs b/src/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockAsMutexProvider.cs index 87a542b..382228e 100644 --- a/src/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockAsMutexProvider.cs +++ b/src/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockAsMutexProvider.cs @@ -25,10 +25,10 @@ public override IDistributedLock CreateLockWithExactName(string name) => public override string GetCrossProcessLockType() => this._readerWriterLockProvider.GetCrossProcessLockType(ReaderWriterLockType.Write); - public override void Dispose() + public override async ValueTask DisposeAsync() { - this._readerWriterLockProvider.Dispose(); - base.Dispose(); + await this._readerWriterLockProvider.DisposeAsync(); + await base.DisposeAsync(); } private bool GetShouldUseUpgradeLock() diff --git a/src/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockProvider.cs b/src/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockProvider.cs index 8e137c7..468200e 100644 --- a/src/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockProvider.cs +++ b/src/DistributedLock.Tests/Infrastructure/TestingReaderWriterLockProvider.cs @@ -1,6 +1,6 @@ namespace Medallion.Threading.Tests; -public abstract class TestingReaderWriterLockProvider : ITestingNameProvider, IDisposable +public abstract class TestingReaderWriterLockProvider : ITestingNameProvider, IAsyncDisposable where TStrategy : TestingSynchronizationStrategy, new() { public TStrategy Strategy { get; } = new TStrategy(); @@ -17,7 +17,8 @@ public virtual string GetCrossProcessLockType(ReaderWriterLockType type) => public IDistributedReaderWriterLock CreateReaderWriterLock(string baseName) => this.CreateReaderWriterLockWithExactName(this.GetUniqueSafeName(baseName)); - public void Dispose() => this.Strategy.Dispose(); + public ValueTask DisposeAsync() => this.Strategy.DisposeAsync(); + public ValueTask SetupAsync() => this.Strategy.SetupAsync(); } public abstract class TestingUpgradeableReaderWriterLockProvider : TestingReaderWriterLockProvider diff --git a/src/DistributedLock.Tests/Infrastructure/TestingSemaphoreAsMutexProvider.cs b/src/DistributedLock.Tests/Infrastructure/TestingSemaphoreAsMutexProvider.cs index 0bb0399..b69d0f2 100644 --- a/src/DistributedLock.Tests/Infrastructure/TestingSemaphoreAsMutexProvider.cs +++ b/src/DistributedLock.Tests/Infrastructure/TestingSemaphoreAsMutexProvider.cs @@ -14,7 +14,6 @@ public abstract class TestingSemaphoreAsMutexProvider this._semaphoreProvider.Strategy; @@ -47,10 +46,11 @@ public override IDistributedLock CreateLockWithExactName(string name) public override string GetSafeName(string name) => this._semaphoreProvider.GetSafeName(name); - public override void Dispose() + public override async ValueTask DisposeAsync() { this._disposables.Dispose(); - base.Dispose(); + await this._semaphoreProvider.DisposeAsync(); + await base.DisposeAsync(); } private class SemaphoreAsMutex : IDistributedLock diff --git a/src/DistributedLock.Tests/Infrastructure/TestingSemaphoreProvider.cs b/src/DistributedLock.Tests/Infrastructure/TestingSemaphoreProvider.cs index febd92e..d0b3f43 100644 --- a/src/DistributedLock.Tests/Infrastructure/TestingSemaphoreProvider.cs +++ b/src/DistributedLock.Tests/Infrastructure/TestingSemaphoreProvider.cs @@ -1,6 +1,6 @@ namespace Medallion.Threading.Tests; -public abstract class TestingSemaphoreProvider : ITestingNameProvider, IDisposable +public abstract class TestingSemaphoreProvider : ITestingNameProvider, IAsyncDisposable where TStrategy : TestingSynchronizationStrategy, new() { public TStrategy Strategy { get; } = new TStrategy(); @@ -17,5 +17,6 @@ public virtual string GetCrossProcessLockType() => public IDistributedSemaphore CreateSemaphore(string baseName, int maxCount) => this.CreateSemaphoreWithExactName(this.GetUniqueSafeName(baseName), maxCount); - public void Dispose() => this.Strategy.Dispose(); + public ValueTask DisposeAsync() => this.Strategy.DisposeAsync(); + public ValueTask SetupAsync() => this.Strategy.SetupAsync(); } diff --git a/src/DistributedLock.Tests/Infrastructure/TestingSynchronizationStrategy.cs b/src/DistributedLock.Tests/Infrastructure/TestingSynchronizationStrategy.cs index 8a58ea0..e583073 100644 --- a/src/DistributedLock.Tests/Infrastructure/TestingSynchronizationStrategy.cs +++ b/src/DistributedLock.Tests/Infrastructure/TestingSynchronizationStrategy.cs @@ -4,7 +4,7 @@ /// Manages the underlying approach to synchronization. Having this class allows us to parameterize tests by /// synchronization strategy (e. g. only connection string-based strategies) /// -public abstract class TestingSynchronizationStrategy : IDisposable +public abstract class TestingSynchronizationStrategy : IAsyncDisposable { /// /// Whether or not abandoning a ticket held in another process will cause that ticket @@ -16,5 +16,7 @@ public virtual void PrepareForHandleAbandonment() { } public virtual void PerformAdditionalCleanupForHandleAbandonment() { } public virtual IDisposable? PrepareForHandleLost() => null; public virtual void PrepareForHighContention(ref int maxConcurrentAcquires) { } - public virtual void Dispose() { } + public virtual string GetConnectionStringForCrossProcessTest() => throw new NotSupportedException(""); + public virtual ValueTask SetupAsync() => ValueTask.CompletedTask; + public virtual ValueTask DisposeAsync() => ValueTask.CompletedTask; } diff --git a/src/DistributedLock.Tests/Tests/Azure/AzureBlobLeaseDistributedLockTest.cs b/src/DistributedLock.Tests/Tests/Azure/AzureBlobLeaseDistributedLockTest.cs index 409e475..9427ee6 100644 --- a/src/DistributedLock.Tests/Tests/Azure/AzureBlobLeaseDistributedLockTest.cs +++ b/src/DistributedLock.Tests/Tests/Azure/AzureBlobLeaseDistributedLockTest.cs @@ -49,7 +49,7 @@ public async Task TestLockOnDifferentBlobClientTypes( async ValueTask TestAsync() { - using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); + await using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); var name = provider.GetUniqueSafeName(); var client = CreateClient(type, name); @@ -71,7 +71,7 @@ async ValueTask TestAsync() [Test] public async Task TestWrapperCreateIfNotExists([Values] BlobClientType type) { - using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); + await using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); var name = provider.GetUniqueSafeName(); var client = CreateClient(type, name); var wrapper = new BlobClientWrapper(client); @@ -96,9 +96,9 @@ public async Task TestWrapperCreateIfNotExists([Values] BlobClientType type) } [Test] - public void TestCanUseLeaseIdForBlobOperations() + public async Task TestCanUseLeaseIdForBlobOperations() { - using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); + await using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); var name = provider.GetUniqueSafeName(); var client = new PageBlobClient(AzureCredentials.ConnectionString, AzureCredentials.DefaultBlobContainerName, name); const int BlobSize = 512; @@ -121,9 +121,9 @@ public void TestCanUseLeaseIdForBlobOperations() } [Test] - public void TestThrowsIfContainerDoesNotExist() + public async Task TestThrowsIfContainerDoesNotExist() { - using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); + await using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); provider.Strategy.ContainerName = "does-not-exist"; var @lock = provider.CreateLock(nameof(TestThrowsIfContainerDoesNotExist)); @@ -132,9 +132,9 @@ public void TestThrowsIfContainerDoesNotExist() } [Test] - public void TestCanAcquireIfContainerLeased() + public async Task TestCanAcquireIfContainerLeased() { - using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); + await using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); provider.Strategy.ContainerName = "leased-container" + TargetFramework.Current.Replace('.', '-'); var containerClient = new BlobContainerClient(AzureCredentials.ConnectionString, provider.Strategy.ContainerName); @@ -159,7 +159,7 @@ public void TestCanAcquireIfContainerLeased() [Test] public async Task TestSuccessfulRenewal() { - using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); + await using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); provider.Strategy.Options = o => o.RenewalCadence(TimeSpan.FromSeconds(.05)); var @lock = provider.CreateLock(nameof(TestSuccessfulRenewal)); @@ -170,9 +170,9 @@ public async Task TestSuccessfulRenewal() [Test] [NonParallelizable, Retry(tryCount: 3)] // timing-sensitive - public void TestTriggersHandleLostIfLeaseExpiresNaturally() + public async Task TestTriggersHandleLostIfLeaseExpiresNaturally() { - using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); + await using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); provider.Strategy.Options = o => o.RenewalCadence(Timeout.InfiniteTimeSpan).Duration(TimeSpan.FromSeconds(15)); var @lock = provider.CreateLock(nameof(TestTriggersHandleLostIfLeaseExpiresNaturally)); @@ -188,9 +188,9 @@ public void TestTriggersHandleLostIfLeaseExpiresNaturally() } [Test] - public void TestExitsDespiteLongSleepTime() + public async Task TestExitsDespiteLongSleepTime() { - using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); + await using var provider = new TestingAzureBlobLeaseDistributedLockProvider(); provider.Strategy.Options = o => o.BusyWaitSleepTime(TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(1)); var @lock = provider.CreateLock(nameof(TestExitsDespiteLongSleepTime)); diff --git a/src/DistributedLock.Tests/Tests/MySql/MySqlSetUpFixture.cs b/src/DistributedLock.Tests/Tests/MySql/MySqlSetUpFixture.cs new file mode 100644 index 0000000..5816ac9 --- /dev/null +++ b/src/DistributedLock.Tests/Tests/MySql/MySqlSetUpFixture.cs @@ -0,0 +1,28 @@ +using NUnit.Framework; +using Testcontainers.MariaDb; +using Testcontainers.MySql; + +namespace Medallion.Threading.Tests.MySql; + +[SetUpFixture] +public class MySqlSetUpFixture +{ + public static MySqlContainer MySql; + public static MariaDbContainer MariaDb; + + [OneTimeSetUp] + public static async Task OneTimeSetUp() + { + MySql = new MySqlBuilder().Build(); + MariaDb = new MariaDbBuilder().Build(); + + await Task.WhenAll(MySql.StartAsync(), MariaDb.StartAsync()); + } + + [OneTimeTearDown] + public static async Task OneTimeTearDown() + { + await MySql.DisposeAsync(); + await MariaDb.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/DistributedLock.Tests/Tests/Postgres/PostgresBehaviorTest.cs b/src/DistributedLock.Tests/Tests/Postgres/PostgresBehaviorTest.cs index 74ff8e6..710d9d6 100644 --- a/src/DistributedLock.Tests/Tests/Postgres/PostgresBehaviorTest.cs +++ b/src/DistributedLock.Tests/Tests/Postgres/PostgresBehaviorTest.cs @@ -18,7 +18,7 @@ public class PostgresBehaviorTest [Test] public async Task TestPostgresCommandAutomaticallyParticipatesInTransaction() { - using var connection = new NpgsqlConnection(TestingPostgresDb.DefaultConnectionString); + using var connection = new NpgsqlConnection(PostgresSetUpFixture.PostgreSql.GetConnectionString()); await connection.OpenAsync(); using var transaction = @@ -66,7 +66,7 @@ private async Task TestTransactionCancellationOrTimeoutRecovery(bool useTimeout) async Task RunTransactionWithAbortAsync(bool useSavePoint) { - using var connection = new NpgsqlConnection(TestingPostgresDb.DefaultConnectionString); + using var connection = new NpgsqlConnection(PostgresSetUpFixture.PostgreSql.GetConnectionString()); await connection.OpenAsync(); using (connection.BeginTransaction()) @@ -102,7 +102,7 @@ async Task RunTransactionWithAbortAsync(bool useSavePoint) [Test] public async Task TestCanDetectTransactionWithBeginTransactionException() { - using var connection = new NpgsqlConnection(TestingPostgresDb.DefaultConnectionString); + using var connection = new NpgsqlConnection(PostgresSetUpFixture.PostgreSql.GetConnectionString()); await connection.OpenAsync(); Assert.DoesNotThrow(() => connection.BeginTransaction().Dispose()); @@ -116,7 +116,7 @@ public async Task TestCanDetectTransactionWithBeginTransactionException() [Test] public async Task TestDoesNotDetectConnectionBreakViaState() { - using var connection = new NpgsqlConnection(TestingPostgresDb.DefaultConnectionString); + using var connection = new NpgsqlConnection(PostgresSetUpFixture.PostgreSql.GetConnectionString()); await connection.OpenAsync(); using var getPidCommand = connection.CreateCommand(); @@ -127,7 +127,7 @@ public async Task TestDoesNotDetectConnectionBreakViaState() connection.StateChange += (_, _2) => stateChangedEvent.Set(); // kill the connection from the back end - using var killingConnection = new NpgsqlConnection(TestingPostgresDb.DefaultConnectionString); + using var killingConnection = new NpgsqlConnection(PostgresSetUpFixture.PostgreSql.GetConnectionString()); await killingConnection.OpenAsync(); using var killCommand = killingConnection.CreateCommand(); killCommand.CommandText = $"SELECT pg_terminate_backend({pid})"; @@ -135,7 +135,7 @@ public async Task TestDoesNotDetectConnectionBreakViaState() Assert.That(stateChangedEvent.Wait(TimeSpan.FromSeconds(.1)), Is.False); - Assert.Throws(() => getPidCommand.ExecuteScalar()); + Assert.Throws(() => getPidCommand.ExecuteScalar()); Assert.That(stateChangedEvent.Wait(TimeSpan.FromSeconds(5)), Is.True); } @@ -145,7 +145,7 @@ public async Task TestExecutingQueryOnKilledConnectionFiresStateChanged() { using var stateChangedEvent = new ManualResetEventSlim(initialState: false); - using var connection = new NpgsqlConnection(TestingPostgresDb.DefaultConnectionString); + using var connection = new NpgsqlConnection(PostgresSetUpFixture.PostgreSql.GetConnectionString()); await connection.OpenAsync(); connection.StateChange += (o, e) => stateChangedEvent.Set(); @@ -156,7 +156,7 @@ public async Task TestExecutingQueryOnKilledConnectionFiresStateChanged() Assert.That(connection.State, Is.EqualTo(ConnectionState.Open)); // kill the connection from the back end - using var killingConnection = new NpgsqlConnection(TestingPostgresDb.DefaultConnectionString); + using var killingConnection = new NpgsqlConnection(PostgresSetUpFixture.PostgreSql.GetConnectionString()); await killingConnection.OpenAsync(); using var killCommand = killingConnection.CreateCommand(); killCommand.CommandText = $"SELECT pg_terminate_backend({pid})"; diff --git a/src/DistributedLock.Tests/Tests/Postgres/PostgresDistributedLockTest.cs b/src/DistributedLock.Tests/Tests/Postgres/PostgresDistributedLockTest.cs index c5abae7..f1bc81d 100644 --- a/src/DistributedLock.Tests/Tests/Postgres/PostgresDistributedLockTest.cs +++ b/src/DistributedLock.Tests/Tests/Postgres/PostgresDistributedLockTest.cs @@ -24,7 +24,7 @@ public void TestValidatesConstructorArguments() [Test] public void TestMultiplexingWithDbDataSourceThrowNotSupportedException() { - using var dataSource = new NpgsqlDataSourceBuilder(TestingPostgresDb.DefaultConnectionString).Build(); + using var dataSource = new NpgsqlDataSourceBuilder(PostgresSetUpFixture.PostgreSql.GetConnectionString()).Build(); Assert.Throws(() => new PostgresDistributedLock(new(0), dataSource, opt => opt.UseMultiplexing())); } @@ -33,7 +33,7 @@ public void TestMultiplexingWithDbDataSourceThrowNotSupportedException() [Test] public async Task TestDbDataSourceConstructorWorks() { - using var dataSource = new NpgsqlDataSourceBuilder(TestingPostgresDb.DefaultConnectionString).Build(); + using var dataSource = new NpgsqlDataSourceBuilder(PostgresSetUpFixture.PostgreSql.GetConnectionString()).Build(); PostgresDistributedLock @lock = new(new(5, 5), dataSource); await using (await @lock.AcquireAsync()) { @@ -46,7 +46,7 @@ public async Task TestDbDataSourceConstructorWorks() [Test] public async Task TestInt64AndInt32PairKeyNamespacesAreDifferent() { - var connectionString = TestingPostgresDb.DefaultConnectionString; + var connectionString = PostgresSetUpFixture.PostgreSql.GetConnectionString(); var key1 = new PostgresAdvisoryLockKey(0); var key2 = new PostgresAdvisoryLockKey(0, 0); var @lock1 = new PostgresDistributedLock(key1, connectionString); @@ -62,11 +62,11 @@ public async Task TestInt64AndInt32PairKeyNamespacesAreDifferent() [Test] public async Task TestWorksWithAmbientTransaction() { - using var connection = new NpgsqlConnection(TestingPostgresDb.DefaultConnectionString); + using var connection = new NpgsqlConnection(PostgresSetUpFixture.PostgreSql.GetConnectionString()); await connection.OpenAsync(); var connectionLock = new PostgresDistributedLock(new PostgresAdvisoryLockKey("AmbTrans"), connection); - var otherLock = new PostgresDistributedLock(connectionLock.Key, TestingPostgresDb.DefaultConnectionString); + var otherLock = new PostgresDistributedLock(connectionLock.Key, PostgresSetUpFixture.PostgreSql.GetConnectionString()); using var otherLockHandle = await otherLock.AcquireAsync(); using (var transaction = connection.BeginTransaction()) diff --git a/src/DistributedLock.Tests/Tests/Postgres/PostgresDistributedSynchronizationProviderTest.cs b/src/DistributedLock.Tests/Tests/Postgres/PostgresDistributedSynchronizationProviderTest.cs index 8b1deb6..f12fa9c 100644 --- a/src/DistributedLock.Tests/Tests/Postgres/PostgresDistributedSynchronizationProviderTest.cs +++ b/src/DistributedLock.Tests/Tests/Postgres/PostgresDistributedSynchronizationProviderTest.cs @@ -22,7 +22,7 @@ public void TestArgumentValidation() [Test] public async Task BasicTest() { - var provider = new PostgresDistributedSynchronizationProvider(TestingPostgresDb.DefaultConnectionString); + var provider = new PostgresDistributedSynchronizationProvider(PostgresSetUpFixture.PostgreSql.GetConnectionString()); const string LockName = TargetFramework.Current + "ProviderBasicTest"; await using (await provider.AcquireLockAsync(LockName)) diff --git a/src/DistributedLock.Tests/Tests/Postgres/PostgresSetUpFixture.cs b/src/DistributedLock.Tests/Tests/Postgres/PostgresSetUpFixture.cs new file mode 100644 index 0000000..b6ba456 --- /dev/null +++ b/src/DistributedLock.Tests/Tests/Postgres/PostgresSetUpFixture.cs @@ -0,0 +1,20 @@ +using NUnit.Framework; +using Testcontainers.PostgreSql; + +namespace Medallion.Threading.Tests.Postgres; + +[SetUpFixture] +public class PostgresSetUpFixture +{ + public static PostgreSqlContainer PostgreSql; + + [OneTimeSetUp] + public static async Task OneTimeSetUp() + { + PostgreSql = new PostgreSqlBuilder().Build(); + await PostgreSql.StartAsync(); + } + + [OneTimeTearDown] + public static async Task OneTimeTearDown() => await PostgreSql.DisposeAsync(); +} \ No newline at end of file diff --git a/src/DistributedLock.Tests/Tests/Redis/RedisDistributedLockTest.cs b/src/DistributedLock.Tests/Tests/Redis/RedisDistributedLockTest.cs index 6ea2d75..b55d7bf 100644 --- a/src/DistributedLock.Tests/Tests/Redis/RedisDistributedLockTest.cs +++ b/src/DistributedLock.Tests/Tests/Redis/RedisDistributedLockTest.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using StackExchange.Redis; using System.Globalization; +using Testcontainers.Redis; namespace Medallion.Threading.Tests.Redis; @@ -47,7 +48,7 @@ public async Task TestCanAcquireLockWhenCurrentCultureIsTurkishTurkey() CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR"); var @lock = new RedisDistributedLock( TestHelper.UniqueName, - RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase() + RedisServer.CreateDatabase(RedisSetUpFixture.Redis) ); await (await @lock.AcquireAsync()).DisposeAsync(); } diff --git a/src/DistributedLock.Tests/Tests/Redis/RedisDistributedReaderWriterLockTest.cs b/src/DistributedLock.Tests/Tests/Redis/RedisDistributedReaderWriterLockTest.cs index 0a8ea93..91e62c9 100644 --- a/src/DistributedLock.Tests/Tests/Redis/RedisDistributedReaderWriterLockTest.cs +++ b/src/DistributedLock.Tests/Tests/Redis/RedisDistributedReaderWriterLockTest.cs @@ -2,6 +2,7 @@ using Moq; using NUnit.Framework; using StackExchange.Redis; +using Testcontainers.Redis; namespace Medallion.Threading.Tests.Redis; @@ -36,7 +37,7 @@ public async Task TestCanExtendReadLock() { var @lock = new RedisDistributedReaderWriterLock( TestHelper.UniqueName, - RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase(), + RedisServer.CreateDatabase(RedisSetUpFixture.Redis), o => o.Expiry(TimeSpan.FromSeconds(0.3)).BusyWaitSleepTime(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(5)) ); @@ -57,7 +58,7 @@ public async Task TestReadLockAbandonment() { var @lock = new RedisDistributedReaderWriterLock( TestHelper.UniqueName, - RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase(), + RedisServer.CreateDatabase(RedisSetUpFixture.Redis), o => o.Expiry(TimeSpan.FromSeconds(1)) .ExtensionCadence(TimeSpan.FromSeconds(0.1)) .BusyWaitSleepTime(TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(50)) diff --git a/src/DistributedLock.Tests/Tests/Redis/RedisDistributedSynchronizationProviderTest.cs b/src/DistributedLock.Tests/Tests/Redis/RedisDistributedSynchronizationProviderTest.cs index 4cdd3f6..d0c04d9 100644 --- a/src/DistributedLock.Tests/Tests/Redis/RedisDistributedSynchronizationProviderTest.cs +++ b/src/DistributedLock.Tests/Tests/Redis/RedisDistributedSynchronizationProviderTest.cs @@ -1,6 +1,7 @@ using Medallion.Threading.Redis; using NUnit.Framework; using StackExchange.Redis; +using Testcontainers.Redis; namespace Medallion.Threading.Tests.Redis; @@ -18,7 +19,7 @@ public void TestArgumentValidation() [Test] public async Task BasicTest() { - var provider = new RedisDistributedSynchronizationProvider(RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase()); + var provider = new RedisDistributedSynchronizationProvider(RedisServer.CreateDatabase(RedisSetUpFixture.Redis)); const string LockName = TargetFramework.Current + "ProviderBasicTest"; await using (await provider.AcquireLockAsync(LockName)) diff --git a/src/DistributedLock.Tests/Tests/SqlServer/SqlDatabaseConnectionTest.cs b/src/DistributedLock.Tests/Tests/SqlServer/SqlDatabaseConnectionTest.cs index 6ec43b8..a9bbcbe 100644 --- a/src/DistributedLock.Tests/Tests/SqlServer/SqlDatabaseConnectionTest.cs +++ b/src/DistributedLock.Tests/Tests/SqlServer/SqlDatabaseConnectionTest.cs @@ -68,8 +68,8 @@ public async Task TestExecuteNonQueryCanCancel([Values] bool isAsync, [Values] b private static SqlDatabaseConnection CreateConnection(bool isSystemDataSqlClient) => new( isSystemDataSqlClient - ? new System.Data.SqlClient.SqlConnection(TestingSqlServerDb.DefaultConnectionString).As() - : new Microsoft.Data.SqlClient.SqlConnection(TestingSqlServerDb.DefaultConnectionString), + ? new System.Data.SqlClient.SqlConnection(SqlServerSetUpFixture.SqlServer.GetConnectionString()).As() + : new Microsoft.Data.SqlClient.SqlConnection(SqlServerSetUpFixture.SqlServer.GetConnectionString()), isExternallyOwned: false ); } diff --git a/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedLockTest.cs b/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedLockTest.cs index b3f3cf2..4d30faa 100644 --- a/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedLockTest.cs +++ b/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedLockTest.cs @@ -10,13 +10,13 @@ public class SqlDistributedLockTest [Test] public void TestBadConstructorArguments() { - Assert.Catch(() => new SqlDistributedLock(null!, TestingSqlServerDb.DefaultConnectionString)); - Assert.Catch(() => new SqlDistributedLock(null!, TestingSqlServerDb.DefaultConnectionString, exactName: true)); + Assert.Catch(() => new SqlDistributedLock(null!, SqlServerSetUpFixture.SqlServer.GetConnectionString())); + Assert.Catch(() => new SqlDistributedLock(null!, SqlServerSetUpFixture.SqlServer.GetConnectionString(), exactName: true)); Assert.Catch(() => new SqlDistributedLock("a", default(string)!)); Assert.Catch(() => new SqlDistributedLock("a", default(IDbTransaction)!)); Assert.Catch(() => new SqlDistributedLock("a", default(IDbConnection)!)); - Assert.Catch(() => new SqlDistributedLock(new string('a', SqlDistributedLock.MaxNameLength + 1), TestingSqlServerDb.DefaultConnectionString, exactName: true)); - Assert.DoesNotThrow(() => new SqlDistributedLock(new string('a', SqlDistributedLock.MaxNameLength), TestingSqlServerDb.DefaultConnectionString, exactName: true)); + Assert.Catch(() => new SqlDistributedLock(new string('a', SqlDistributedLock.MaxNameLength + 1), SqlServerSetUpFixture.SqlServer.GetConnectionString(), exactName: true)); + Assert.DoesNotThrow(() => new SqlDistributedLock(new string('a', SqlDistributedLock.MaxNameLength), SqlServerSetUpFixture.SqlServer.GetConnectionString(), exactName: true)); } [Test] @@ -38,7 +38,7 @@ public void TestGetSafeLockNameCompat() [Test] public async Task TestSqlCommandMustParticipateInTransaction() { - using var connection = new SqlConnection(TestingSqlServerDb.DefaultConnectionString); + using var connection = new SqlConnection(SqlServerSetUpFixture.SqlServer.GetConnectionString()); await connection.OpenAsync(); using var transaction = connection.BeginTransaction(); diff --git a/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedReaderWriterLockTest.cs b/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedReaderWriterLockTest.cs index 461b435..13cd0a7 100644 --- a/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedReaderWriterLockTest.cs +++ b/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedReaderWriterLockTest.cs @@ -9,13 +9,13 @@ public sealed class SqlDistributedReaderWriterLockTest [Test] public void TestBadConstructorArguments() { - Assert.Catch(() => new SqlDistributedReaderWriterLock(null!, TestingSqlServerDb.DefaultConnectionString)); - Assert.Catch(() => new SqlDistributedReaderWriterLock(null!, TestingSqlServerDb.DefaultConnectionString, exactName: true)); + Assert.Catch(() => new SqlDistributedReaderWriterLock(null!, SqlServerSetUpFixture.SqlServer.GetConnectionString())); + Assert.Catch(() => new SqlDistributedReaderWriterLock(null!, SqlServerSetUpFixture.SqlServer.GetConnectionString(), exactName: true)); Assert.Catch(() => new SqlDistributedReaderWriterLock("a", default(string)!)); Assert.Catch(() => new SqlDistributedReaderWriterLock("a", default(DbTransaction)!)); Assert.Catch(() => new SqlDistributedReaderWriterLock("a", default(DbConnection)!)); - Assert.Catch(() => new SqlDistributedReaderWriterLock(new string('a', SqlDistributedReaderWriterLock.MaxNameLength + 1), TestingSqlServerDb.DefaultConnectionString, exactName: true)); - Assert.DoesNotThrow(() => new SqlDistributedReaderWriterLock(new string('a', SqlDistributedReaderWriterLock.MaxNameLength), TestingSqlServerDb.DefaultConnectionString, exactName: true)); + Assert.Catch(() => new SqlDistributedReaderWriterLock(new string('a', SqlDistributedReaderWriterLock.MaxNameLength + 1), SqlServerSetUpFixture.SqlServer.GetConnectionString(), exactName: true)); + Assert.DoesNotThrow(() => new SqlDistributedReaderWriterLock(new string('a', SqlDistributedReaderWriterLock.MaxNameLength), SqlServerSetUpFixture.SqlServer.GetConnectionString(), exactName: true)); } [Test] diff --git a/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedSemaphoreTest.cs b/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedSemaphoreTest.cs index f13a315..ab43e9e 100644 --- a/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedSemaphoreTest.cs +++ b/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedSemaphoreTest.cs @@ -12,9 +12,9 @@ public sealed class SqlDistributedSemaphoreTest [Test] public void TestBadConstructorArguments() { - Assert.Catch(() => new SqlDistributedSemaphore(null!, 1, TestingSqlServerDb.DefaultConnectionString)); - Assert.Catch(() => new SqlDistributedSemaphore("a", -1, TestingSqlServerDb.DefaultConnectionString)); - Assert.Catch(() => new SqlDistributedSemaphore("a", 0, TestingSqlServerDb.DefaultConnectionString)); + Assert.Catch(() => new SqlDistributedSemaphore(null!, 1, SqlServerSetUpFixture.SqlServer.GetConnectionString())); + Assert.Catch(() => new SqlDistributedSemaphore("a", -1, SqlServerSetUpFixture.SqlServer.GetConnectionString())); + Assert.Catch(() => new SqlDistributedSemaphore("a", 0, SqlServerSetUpFixture.SqlServer.GetConnectionString())); Assert.Catch(() => new SqlDistributedSemaphore("a", 1, default(string)!)); Assert.Catch(() => new SqlDistributedSemaphore("a", 1, default(IDbConnection)!)); Assert.Catch(() => new SqlDistributedSemaphore("a", 1, default(IDbTransaction)!)); @@ -22,7 +22,7 @@ public void TestBadConstructorArguments() var random = new Random(1234); var bytes = new byte[10000]; random.NextBytes(bytes); - Assert.DoesNotThrow(() => new SqlDistributedSemaphore(Encoding.UTF8.GetString(bytes), int.MaxValue, TestingSqlServerDb.DefaultConnectionString)); + Assert.DoesNotThrow(() => new SqlDistributedSemaphore(Encoding.UTF8.GetString(bytes), int.MaxValue, SqlServerSetUpFixture.SqlServer.GetConnectionString())); } [Test] @@ -70,7 +70,7 @@ public void TestNameManglingCompatibility() [Test] public void TestTicketsTakenOnBothConnectionAndTransactionForThatConnection() { - using var connection = new SqlConnection(TestingSqlServerDb.DefaultConnectionString); + using var connection = new SqlConnection(SqlServerSetUpFixture.SqlServer.GetConnectionString()); connection.Open(); var semaphore1 = new SqlDistributedSemaphore( diff --git a/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedSynchronizationProviderTest.cs b/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedSynchronizationProviderTest.cs index 473e7a4..c7a8283 100644 --- a/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedSynchronizationProviderTest.cs +++ b/src/DistributedLock.Tests/Tests/SqlServer/SqlDistributedSynchronizationProviderTest.cs @@ -17,7 +17,7 @@ public void TestArgumentValidation() [Test] public async Task BasicTest() { - var provider = new SqlDistributedSynchronizationProvider(TestingSqlServerDb.DefaultConnectionString); + var provider = new SqlDistributedSynchronizationProvider(SqlServerSetUpFixture.SqlServer.GetConnectionString()); const string LockName = TargetFramework.Current + "ProviderBasicTest"; await using (await provider.AcquireLockAsync(LockName)) diff --git a/src/DistributedLock.Tests/Tests/SqlServer/SqlServerSetUpFixture.cs b/src/DistributedLock.Tests/Tests/SqlServer/SqlServerSetUpFixture.cs new file mode 100644 index 0000000..0fd621a --- /dev/null +++ b/src/DistributedLock.Tests/Tests/SqlServer/SqlServerSetUpFixture.cs @@ -0,0 +1,20 @@ +using NUnit.Framework; +using Testcontainers.MsSql; + +namespace Medallion.Threading.Tests.SqlServer; + +[SetUpFixture] +public class SqlServerSetUpFixture +{ + public static MsSqlContainer SqlServer; + + [OneTimeSetUp] + public static async Task OneTimeSetUp() + { + SqlServer = new MsSqlBuilder().Build(); + await SqlServer.StartAsync(); + } + + [OneTimeTearDown] + public static async Task OneTimeTearDown() => await SqlServer.DisposeAsync(); +} \ No newline at end of file diff --git a/src/DistributedLock.Tests/packages.lock.json b/src/DistributedLock.Tests/packages.lock.json index 318e9e0..c199282 100644 --- a/src/DistributedLock.Tests/packages.lock.json +++ b/src/DistributedLock.Tests/packages.lock.json @@ -1,7 +1,7 @@ { "version": 2, "dependencies": { - ".NETFramework,Version=v4.7.2": { + "net8.0": { "MedallionShell.StrongName": { "type": "Direct", "requested": "[1.6.2, )", @@ -14,7 +14,8 @@ "resolved": "17.9.0", "contentHash": "7GUNAUbJYn644jzwLm5BD3a2p9C1dmP8Hr6fDPDxgItQk9hBs1Svdxzz07KQ/UphMSmgza9AbijBJGmw5D658A==", "dependencies": { - "Microsoft.CodeCoverage": "17.9.0" + "Microsoft.CodeCoverage": "17.9.0", + "Microsoft.TestPlatform.TestHost": "17.9.0" } }, "Moq": { @@ -23,15 +24,17 @@ "resolved": "4.20.70", "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", "dependencies": { - "Castle.Core": "5.1.1", - "System.Threading.Tasks.Extensions": "4.5.4" + "Castle.Core": "5.1.1" } }, "NUnit": { "type": "Direct", "requested": "[3.14.0, )", "resolved": "3.14.0", - "contentHash": "R7iPwD7kbOaP3o2zldWJbWeMQAvDKD0uld27QvA3PAALl1unl7x0v2J7eGiJOYjimV/BuGT4VJmr45RjS7z4LA==" + "contentHash": "R7iPwD7kbOaP3o2zldWJbWeMQAvDKD0uld27QvA3PAALl1unl7x0v2J7eGiJOYjimV/BuGT4VJmr45RjS7z4LA==", + "dependencies": { + "NETStandard.Library": "2.0.0" + } }, "NUnit.Analyzers": { "type": "Direct", @@ -594,53 +597,38 @@ }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[17.9.0, )", - "resolved": "17.9.0", - "contentHash": "7GUNAUbJYn644jzwLm5BD3a2p9C1dmP8Hr6fDPDxgItQk9hBs1Svdxzz07KQ/UphMSmgza9AbijBJGmw5D658A==", + "requested": "[3.8.0, )", + "resolved": "3.8.0", + "contentHash": "4SUnPddk4VbXDZkeSxyCSQA1pFlvlMh5KA0iYXiRykG71F2+F0fyweogoTnD/dVU2NDMVwxJSzqz6SN1wzPAZw==", "dependencies": { - "Microsoft.CodeCoverage": "17.9.0", - "Microsoft.TestPlatform.TestHost": "17.9.0" + "Testcontainers": "3.8.0" } }, - "Moq": { + "Testcontainers.MySql": { "type": "Direct", - "requested": "[4.20.70, )", - "resolved": "4.20.70", - "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", + "requested": "[3.8.0, )", + "resolved": "3.8.0", + "contentHash": "q72RjwMoivYhGjTlf0Id48IwXfR1Hx/NqwKN+b3WVoseA5tHxMnZV3tjb6vEyEwzwKAaUeWDEauyvqqptaDWSw==", "dependencies": { - "Castle.Core": "5.1.1" + "Testcontainers": "3.8.0" } }, - "NUnit": { + "Testcontainers.PostgreSql": { "type": "Direct", - "requested": "[3.14.0, )", - "resolved": "3.14.0", - "contentHash": "R7iPwD7kbOaP3o2zldWJbWeMQAvDKD0uld27QvA3PAALl1unl7x0v2J7eGiJOYjimV/BuGT4VJmr45RjS7z4LA==", + "requested": "[3.8.0, )", + "resolved": "3.8.0", + "contentHash": "5sKpj6z4+Z92BFBILvjHqDry4OH9xyngqkeSM3ZrW5wTOJfu8GL0z5p/ZwYBQySPHQBbDa5et5r51Ga1i02ahQ==", "dependencies": { - "NETStandard.Library": "2.0.0" + "Testcontainers": "3.8.0" } }, - "NUnit.Analyzers": { + "Testcontainers.Redis": { "type": "Direct", - "requested": "[4.1.0, )", - "resolved": "4.1.0", - "contentHash": "Odd1RusSMnfswIiCPbokAqmlcCCXjQ20poaXWrw+CWDnBY1vQ/x6ZGqgyJXpebPq5Uf8uEBe5iOAySsCdSrWdQ==" - }, - "NUnit3TestAdapter": { - "type": "Direct", - "requested": "[4.5.0, )", - "resolved": "4.5.0", - "contentHash": "s8JpqTe9bI2f49Pfr3dFRfoVSuFQyraTj68c3XXjIS/MRGvvkLnrg6RLqnTjdShX+AdFUCCU/4Xex58AdUfs6A==" - }, - "System.Data.SqlClient": { - "type": "Direct", - "requested": "[4.8.6, )", - "resolved": "4.8.6", - "contentHash": "2Ij/LCaTQRyAi5lAv7UUTV9R2FobC8xN9mE0fXBZohum/xLl8IZVmE98Rq5ugQHjCgTBRKqpXRb4ORulRdA6Ig==", + "requested": "[3.8.0, )", + "resolved": "3.8.0", + "contentHash": "sPPSCuxLH/7ycjJUOLkIqspwFiU1eVZhxWAJ2JROZlYDscOP94Lm4IgNg/sMeSQI4VJ7ptcW/B/Sjw5tAnG6Ww==", "dependencies": { - "Microsoft.Win32.Registry": "4.7.0", - "System.Security.Principal.Windows": "4.7.0", - "runtime.native.System.Data.SqlClient.sni": "4.7.0" + "Testcontainers": "3.8.0" } }, "Azure.Core": { @@ -689,6 +677,24 @@ "System.Diagnostics.EventLog": "6.0.0" } }, + "Docker.DotNet": { + "type": "Transitive", + "resolved": "3.125.15", + "contentHash": "XN8FKxVv8Mjmwu104/Hl9lM61pLY675s70gzwSj8KR5pwblo8HfWLcCuinh9kYsqujBkMH4HVRCEcRuU6al4BQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.1", + "System.Buffers": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Docker.DotNet.X509": { + "type": "Transitive", + "resolved": "3.125.15", + "contentHash": "ONQN7ImrL3tHStUUCCPHwrFFQVpIpE+7L6jaDAMwSF+yTEmeWBmRARQZDRuvfj/+WtB8RR0oTW0tT3qQMSyHOw==", + "dependencies": { + "Docker.DotNet": "3.125.15" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "1.1.1", @@ -1082,6 +1088,18 @@ "System.Drawing.Common": "6.0.0" } }, + "Testcontainers": { + "type": "Transitive", + "resolved": "3.8.0", + "contentHash": "R0+4VTGtFm+Q50+dP7Rm+ZB6thjKdBF3mGYbh5dEf/qt+r35MdFx8tMsE0VIbtaqgJMsOSD06CUZB2wPqBe2cw==", + "dependencies": { + "Docker.DotNet": "3.125.15", + "Docker.DotNet.X509": "3.125.15", + "Microsoft.Extensions.Logging.Abstractions": "6.0.4", + "SSH.NET": "2023.0.0", + "SharpZipLib": "1.4.2" + } + }, "distributedlock": { "type": "Project", "dependencies": { diff --git a/src/DistributedLockTaker/Program.cs b/src/DistributedLockTaker/Program.cs index c15d0a5..f34bf76 100644 --- a/src/DistributedLockTaker/Program.cs +++ b/src/DistributedLockTaker/Program.cs @@ -24,32 +24,34 @@ public static int Main(string[] args) { var type = args[0]; var name = args[1]; + var connectionString = args[2]; + var connectionStrings = connectionString.Split(new[] { "||" }, StringSplitOptions.None); IDisposable? handle; switch (type) { case nameof(SqlDistributedLock): - handle = new SqlDistributedLock(name, SqlServerCredentials.ConnectionString).Acquire(); + handle = new SqlDistributedLock(name, connectionString).Acquire(); break; case "Write" + nameof(SqlDistributedReaderWriterLock): - handle = new SqlDistributedReaderWriterLock(name, SqlServerCredentials.ConnectionString).AcquireWriteLock(); + handle = new SqlDistributedReaderWriterLock(name, connectionString).AcquireWriteLock(); break; case nameof(SqlDistributedSemaphore) + "1AsMutex": - handle = new SqlDistributedSemaphore(name, maxCount: 1, connectionString: SqlServerCredentials.ConnectionString).Acquire(); + handle = new SqlDistributedSemaphore(name, maxCount: 1, connectionString: connectionString).Acquire(); break; case nameof(SqlDistributedSemaphore) + "5AsMutex": - handle = new SqlDistributedSemaphore(name, maxCount: 5, connectionString: SqlServerCredentials.ConnectionString).Acquire(); + handle = new SqlDistributedSemaphore(name, maxCount: 5, connectionString: connectionString).Acquire(); break; case nameof(PostgresDistributedLock): - handle = new PostgresDistributedLock(new PostgresAdvisoryLockKey(name), PostgresCredentials.GetConnectionString(Environment.CurrentDirectory)).Acquire(); + handle = new PostgresDistributedLock(new PostgresAdvisoryLockKey(name), connectionString).Acquire(); break; case "Write" + nameof(PostgresDistributedReaderWriterLock): - handle = new PostgresDistributedReaderWriterLock(new PostgresAdvisoryLockKey(name), PostgresCredentials.GetConnectionString(Environment.CurrentDirectory)).AcquireWriteLock(); + handle = new PostgresDistributedReaderWriterLock(new PostgresAdvisoryLockKey(name), connectionString).AcquireWriteLock(); break; case nameof(MySqlDistributedLock): - handle = new MySqlDistributedLock(name, MySqlCredentials.GetConnectionString(Environment.CurrentDirectory)).Acquire(); + handle = new MySqlDistributedLock(name, connectionString).Acquire(); break; case "MariaDB" + nameof(MySqlDistributedLock): - handle = new MySqlDistributedLock(name, MariaDbCredentials.GetConnectionString(Environment.CurrentDirectory)).Acquire(); + handle = new MySqlDistributedLock(name, connectionString).Acquire(); break; case nameof(OracleDistributedLock): handle = new OracleDistributedLock(name, OracleCredentials.GetConnectionString(Environment.CurrentDirectory)).Acquire(); @@ -77,38 +79,47 @@ public static int Main(string[] args) handle = new FileDistributedLock(new FileInfo(name)).Acquire(); break; case nameof(RedisDistributedLock) + "1": - handle = AcquireRedisLock(name, serverCount: 1); + ValidateConnectionStringCount(connectionStrings, 1); + handle = AcquireRedisLock(name, serverCount: 1, connectionStrings); break; case nameof(RedisDistributedLock) + "3": - handle = AcquireRedisLock(name, serverCount: 3); + ValidateConnectionStringCount(connectionStrings, 3); + handle = AcquireRedisLock(name, serverCount: 3, connectionStrings); break; case nameof(RedisDistributedLock) + "2x1": - handle = AcquireRedisLock(name, serverCount: 2); // we know the last will fail; don't bother (we also don't know its port) + ValidateConnectionStringCount(connectionStrings, 2); + handle = AcquireRedisLock(name, serverCount: 2, connectionStrings); // we know the last will fail; don't bother (we also don't know its port) break; case nameof(RedisDistributedLock) + "1WithPrefix": - handle = AcquireRedisLock("distributed_locks:" + name, serverCount: 1); + ValidateConnectionStringCount(connectionStrings, 1); + handle = AcquireRedisLock("distributed_locks:" + name, serverCount: 1, connectionStrings); break; case "Write" + nameof(RedisDistributedReaderWriterLock) + "1": - handle = AcquireRedisWriteLock(name, serverCount: 1); + ValidateConnectionStringCount(connectionStrings, 1); + handle = AcquireRedisWriteLock(name, serverCount: 1, connectionStrings); break; case "Write" + nameof(RedisDistributedReaderWriterLock) + "3": - handle = AcquireRedisWriteLock(name, serverCount: 3); + ValidateConnectionStringCount(connectionStrings, 3); + handle = AcquireRedisWriteLock(name, serverCount: 3, connectionStrings); break; case "Write" + nameof(RedisDistributedReaderWriterLock) + "2x1": - handle = AcquireRedisWriteLock(name, serverCount: 2); // we know the last will fail; don't bother (we also don't know its port) + ValidateConnectionStringCount(connectionStrings, 2); + handle = AcquireRedisWriteLock(name, serverCount: 2, connectionStrings); // we know the last will fail; don't bother (we also don't know its port) break; case "Write" + nameof(RedisDistributedReaderWriterLock) + "1WithPrefix": - handle = AcquireRedisWriteLock("distributed_locks:" + name, serverCount: 1); + ValidateConnectionStringCount(connectionStrings, 1); + handle = AcquireRedisWriteLock("distributed_locks:" + name, serverCount: 1,connectionStrings); break; case string _ when type.StartsWith(nameof(RedisDistributedSemaphore)): { + ValidateConnectionStringCount(connectionStrings, 1); var maxCount = type.EndsWith("1AsMutex") ? 1 : type.EndsWith("5AsMutex") ? 5 : throw new ArgumentException(type); handle = new RedisDistributedSemaphore( name, maxCount, - GetRedisDatabases(serverCount: 1).Single(), + GetRedisDatabases(serverCount: 1, connectionStrings).Single(), // in order to see abandonment work in a reasonable timeframe, use very short expiry options => options.Expiry(TimeSpan.FromSeconds(1)) .BusyWaitSleepTime(TimeSpan.FromSeconds(.1), TimeSpan.FromSeconds(.3)) @@ -150,14 +161,19 @@ public static int Main(string[] args) return 0; } - private static IDistributedSynchronizationHandle AcquireRedisLock(string name, int serverCount) => - new RedisDistributedLock(name, GetRedisDatabases(serverCount), RedisOptions).Acquire(); + private static void ValidateConnectionStringCount(string[] connectionStrings, int expectedCount) + { + if (connectionStrings.Length != expectedCount) throw new ArgumentOutOfRangeException("Invalid connection strings count"); + } + + private static IDistributedSynchronizationHandle AcquireRedisLock(string name, int serverCount, string[] connectionStrings) => + new RedisDistributedLock(name, GetRedisDatabases(serverCount, connectionStrings), RedisOptions).Acquire(); - private static IDistributedSynchronizationHandle AcquireRedisWriteLock(string name, int serverCount) => - new RedisDistributedReaderWriterLock(name, GetRedisDatabases(serverCount), RedisOptions).AcquireWriteLock(); + private static IDistributedSynchronizationHandle AcquireRedisWriteLock(string name, int serverCount, string[] connectionStrings) => + new RedisDistributedReaderWriterLock(name, GetRedisDatabases(serverCount, connectionStrings), RedisOptions).AcquireWriteLock(); - private static IEnumerable GetRedisDatabases(int serverCount) => RedisPorts.DefaultPorts.Take(serverCount) - .Select(port => ConnectionMultiplexer.Connect($"localhost:{port}").GetDatabase()); + private static IEnumerable GetRedisDatabases(int serverCount, string[] connectionStrings) => Enumerable.Range(0, serverCount) + .Select(i => ConnectionMultiplexer.Connect(connectionStrings[i]).GetDatabase()); private static void RedisOptions(RedisDistributedSynchronizationOptionsBuilder options) => options.Expiry(TimeSpan.FromSeconds(.5)) // short expiry for abandonment testing