diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f4c2be4a2..a3c7df2df6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: matrix: os: [windows-latest, ubuntu-latest] # Categories are declared by the property in each test project. - test-category: [ DefaultCore, DefaultAudit, DefaultMonitoring, SqlServer, SqlServerPersistence, AzureServiceBus, RabbitMQClassicConventional, RabbitMQClassicDirect, RabbitMQQuorumConventional, RabbitMQQuorumDirect, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSql, PostgreSqlPersistence, IBMMQ ] + test-category: [ DefaultCore, DefaultAudit, DefaultMonitoring, SqlServer, SqlServerPersistence, AzureServiceBus, RabbitMQClassicConventional, RabbitMQClassicDirect, RabbitMQQuorumConventional, RabbitMQQuorumDirect, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimarySqlServerAcceptance, PrimaryPostgreSqlAcceptance, PrimaryRavenPersistence, PostgreSql, PostgreSqlPersistence, IBMMQ ] include: - os: windows-latest os-name: Windows @@ -56,7 +56,7 @@ jobs: # run in on Windows. A no-op on Linux, but still run there so those actions see the same # environment variables on both runners. - name: Setup WSL - if: startsWith(matrix.test-category, 'RabbitMQ') || contains(fromJSON('["SqlServer", "SqlServerPersistence", "PostgreSql", "PostgreSqlPersistence", "IBMMQ"]'), matrix.test-category) + if: startsWith(matrix.test-category, 'RabbitMQ') || contains(fromJSON('["SqlServer", "SqlServerPersistence", "PrimarySqlServerAcceptance", "PostgreSql", "PostgreSqlPersistence", "PrimaryPostgreSqlAcceptance", "IBMMQ"]'), matrix.test-category) uses: Particular/setup-wsl-action@v1.2.0 with: # The action defaults to 4GB. The runner has 16GB and the build runs concurrently with the @@ -88,7 +88,7 @@ jobs: catalog: nservicebus - name: Setup SQL Server persistence uses: Particular/install-sql-server-action@v3.0.0 - if: matrix.test-category == 'SqlServerPersistence' + if: matrix.test-category == 'SqlServerPersistence' || matrix.test-category == 'PrimarySqlServerAcceptance' with: connection-string-env-var: ServiceControl_Persistence_SqlServer_ConnectionString catalog: ServiceControl @@ -102,7 +102,7 @@ jobs: registry-password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup PostgreSQL persistence uses: Particular/setup-postgres-action@v3.0.0 - if: matrix.test-category == 'PostgreSqlPersistence' + if: matrix.test-category == 'PostgreSqlPersistence' || matrix.test-category == 'PrimaryPostgreSqlAcceptance' with: connection-string-name: ServiceControl_Persistence_PostgreSql_ConnectionString registry-username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/src/ServiceControl.AcceptanceTests.PostgreSql/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.AcceptanceTests.PostgreSql/AcceptanceTestStorageConfiguration.cs new file mode 100644 index 0000000000..eb22d27223 --- /dev/null +++ b/src/ServiceControl.AcceptanceTests.PostgreSql/AcceptanceTestStorageConfiguration.cs @@ -0,0 +1,98 @@ +namespace ServiceControl.AcceptanceTests.PostgreSql; + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Npgsql; +using ServiceBus.Management.Infrastructure.Settings; +using ServiceControl.AcceptanceTests.TestSupport; +using ServiceControl.Persistence.EFCore.Abstractions; +using ServiceControl.Persistence.EFCore.PostgreSql; +using ServiceControl.Persistence.Tests; + +public class AcceptanceTestStorageConfiguration : IAcceptanceTestStorageConfiguration +{ + public string PersistenceType { get; } = "PostgreSQL"; + + public async Task CustomizeSettings(Settings settings, CancellationToken cancellationToken = default) + { + databaseName = $"sc_at_{Guid.NewGuid():n}"; + serverConnectionString = await PostgreSqlSharedContainer.GetConnectionStringAsync(cancellationToken).ConfigureAwait(false); + + var connectionStringBuilder = new NpgsqlConnectionStringBuilder(serverConnectionString) + { + Database = databaseName + }; + + bodyStoragePath = Directory.CreateTempSubdirectory("sc_at_bodies_").FullName; + + settings.PersisterSpecificSettings = new PostgreSqlPersisterSettings + { + ConnectionString = connectionStringBuilder.ConnectionString, + ErrorRetentionPeriod = TimeSpan.FromDays(10), + BodyStorage = new FileSystemBodyStorageSettings { StoragePath = bodyStoragePath } + }; + } + + public async Task Cleanup(CancellationToken cancellationToken = default) + { + if (Interlocked.Exchange(ref cleanupStarted, 1) != 0) + { + return; + } + + try + { + if (serverConnectionString == null || databaseName == null) + { + return; + } + + var connection = new NpgsqlConnection(serverConnectionString); + await using (connection.ConfigureAwait(false)) + { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + var command = connection.CreateCommand(); + await using (command.ConfigureAwait(false)) + { + command.CommandText = $"DROP DATABASE IF EXISTS \"{databaseName}\" WITH (FORCE)"; + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + } + } + finally + { + if (bodyStoragePath != null) + { + try + { + Directory.Delete(bodyStoragePath, recursive: true); + } + catch (DirectoryNotFoundException) + { + } + } + } + } + + public Task UseDatabaseLifecycleLock(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(NoOpDisposable.Instance); + } + + sealed class NoOpDisposable : IDisposable + { + public static readonly NoOpDisposable Instance = new(); + + public void Dispose() + { + } + } + + string serverConnectionString; + string databaseName; + string bodyStoragePath; + int cleanupStarted; +} \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests.PostgreSql/ServiceControl.AcceptanceTests.PostgreSql.csproj b/src/ServiceControl.AcceptanceTests.PostgreSql/ServiceControl.AcceptanceTests.PostgreSql.csproj new file mode 100644 index 0000000000..d5ac47492d --- /dev/null +++ b/src/ServiceControl.AcceptanceTests.PostgreSql/ServiceControl.AcceptanceTests.PostgreSql.csproj @@ -0,0 +1,75 @@ + + + + net10.0 + PrimaryPostgreSqlAcceptance + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs index e480fe43cc..462c2ab607 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/AcceptanceTestStorageConfiguration.cs @@ -5,18 +5,19 @@ using System.Threading; using System.Threading.Tasks; using ServiceBus.Management.Infrastructure.Settings; +using ServiceControl.AcceptanceTests.TestSupport; using ServiceControl.Persistence.Tests; using ServiceControl.RavenDB; -public class AcceptanceTestStorageConfiguration +public class AcceptanceTestStorageConfiguration : IAcceptanceTestStorageConfiguration { public string PersistenceType { get; } = "RavenDB"; - public async Task CustomizeSettings(Settings settings) + public async Task CustomizeSettings(Settings settings, CancellationToken cancellationToken = default) { databaseName = Guid.NewGuid().ToString("n"); - databaseInstance = await SharedEmbeddedServer.GetInstance(); + databaseInstance = await SharedEmbeddedServer.GetInstance(cancellationToken); settings.PersisterSpecificSettings = new RavenPersisterSettings { @@ -26,13 +27,13 @@ public async Task CustomizeSettings(Settings settings) }; } - public async Task Cleanup() + public async Task Cleanup(CancellationToken cancellationToken = default) { if (databaseInstance == null) { return; } - using var _ = await UseDatabaseLifecycleLock(); + using var _ = await UseDatabaseLifecycleLock(cancellationToken); await databaseInstance.DeleteDatabase(databaseName); } @@ -40,7 +41,7 @@ public async Task Cleanup() /// The shared server cannot perform database lifecycle operations in parallel, take this lock when you /// need to do one of these operations in a test. /// - public static async Task UseDatabaseLifecycleLock(CancellationToken cancellationToken = default) + public async Task UseDatabaseLifecycleLock(CancellationToken cancellationToken = default) { await databaseLifecycleLock.WaitAsync(cancellationToken); return Disposable.Create(() => databaseLifecycleLock.Release()); diff --git a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs similarity index 98% rename from src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs rename to src/ServiceControl.AcceptanceTests.RavenDB/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs index 55c1d2b072..6067ea881e 100644 --- a/src/ServiceControl.AcceptanceTests/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Monitoring/CustomChecks/When_critical_storage_threshold_reached.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.AcceptanceTests.Monitoring.CustomChecks +namespace ServiceControl.AcceptanceTests.RavenDB.Monitoring.CustomChecks { using System; using System.Linq; @@ -97,4 +97,4 @@ public class MyMessageHandler : IHandleMessages public class MyMessage : ICommand; } -} \ No newline at end of file +} diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj b/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj index 26650ebe99..93d3a06ba3 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj +++ b/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj @@ -5,6 +5,10 @@ PrimaryRavenAcceptance + + + + diff --git a/src/ServiceControl.AcceptanceTests.SqlServer/AcceptanceTestStorageConfiguration.cs b/src/ServiceControl.AcceptanceTests.SqlServer/AcceptanceTestStorageConfiguration.cs new file mode 100644 index 0000000000..4cf4c442c9 --- /dev/null +++ b/src/ServiceControl.AcceptanceTests.SqlServer/AcceptanceTestStorageConfiguration.cs @@ -0,0 +1,104 @@ +namespace ServiceControl.AcceptanceTests.SqlServer; + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; +using ServiceBus.Management.Infrastructure.Settings; +using ServiceControl.AcceptanceTests.TestSupport; +using ServiceControl.Persistence.EFCore.Abstractions; +using ServiceControl.Persistence.EFCore.SqlServer; +using ServiceControl.Persistence.Tests; + +public class AcceptanceTestStorageConfiguration : IAcceptanceTestStorageConfiguration +{ + public string PersistenceType { get; } = "SQLServer"; + + public async Task CustomizeSettings(Settings settings, CancellationToken cancellationToken = default) + { + databaseName = $"sc_at_{Guid.NewGuid():n}"; + serverConnectionString = await SqlServerSharedContainer.GetConnectionStringAsync(cancellationToken).ConfigureAwait(false); + + var connectionStringBuilder = new SqlConnectionStringBuilder(serverConnectionString) + { + InitialCatalog = databaseName + }; + + bodyStoragePath = Directory.CreateTempSubdirectory("sc_at_bodies_").FullName; + + settings.PersisterSpecificSettings = new SqlServerPersisterSettings + { + ConnectionString = connectionStringBuilder.ConnectionString, + ErrorRetentionPeriod = TimeSpan.FromDays(10), + BodyStorage = new FileSystemBodyStorageSettings { StoragePath = bodyStoragePath } + }; + } + + public async Task Cleanup(CancellationToken cancellationToken = default) + { + if (Interlocked.Exchange(ref cleanupStarted, 1) != 0) + { + return; + } + + try + { + if (serverConnectionString == null || databaseName == null) + { + return; + } + + var connection = new SqlConnection(serverConnectionString); + await using (connection.ConfigureAwait(false)) + { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + var command = connection.CreateCommand(); + await using (command.ConfigureAwait(false)) + { + command.CommandText = $""" + IF DB_ID('{databaseName}') IS NOT NULL + BEGIN + ALTER DATABASE [{databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE [{databaseName}]; + END + """; + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + } + } + finally + { + if (bodyStoragePath != null) + { + try + { + Directory.Delete(bodyStoragePath, recursive: true); + } + catch (DirectoryNotFoundException) + { + } + } + } + } + + public Task UseDatabaseLifecycleLock(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(NoOpDisposable.Instance); + } + + sealed class NoOpDisposable : IDisposable + { + public static readonly NoOpDisposable Instance = new(); + + public void Dispose() + { + } + } + + string serverConnectionString; + string databaseName; + string bodyStoragePath; + int cleanupStarted; +} diff --git a/src/ServiceControl.AcceptanceTests.SqlServer/ServiceControl.AcceptanceTests.SqlServer.csproj b/src/ServiceControl.AcceptanceTests.SqlServer/ServiceControl.AcceptanceTests.SqlServer.csproj new file mode 100644 index 0000000000..cecd9a36e8 --- /dev/null +++ b/src/ServiceControl.AcceptanceTests.SqlServer/ServiceControl.AcceptanceTests.SqlServer.csproj @@ -0,0 +1,72 @@ + + + + net10.0 + PrimarySqlServerAcceptance + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs index 30ad9075b1..f83968857d 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs @@ -14,7 +14,6 @@ namespace ServiceControl.AcceptanceTests using NServiceBus.AcceptanceTesting; using NServiceBus.AcceptanceTesting.Support; using NUnit.Framework; - using RavenDB; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.AcceptanceTesting.InfrastructureConfig; using TestSupport; @@ -95,7 +94,7 @@ protected void ExecuteWhen(Func execute, Func action, protected Action SetSettings = _ => { }; protected Action CustomizeHostBuilder = _ => { }; protected ITransportIntegration TransportIntegration; - protected AcceptanceTestStorageConfiguration StorageConfiguration; + protected IAcceptanceTestStorageConfiguration StorageConfiguration; ServiceControlComponentBehavior serviceControlRunnerBehavior; TextWriterTraceListener textWriterTraceListener; diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/HttpClientServiceCollectionExtensions.cs b/src/ServiceControl.AcceptanceTests/TestSupport/HttpClientServiceCollectionExtensions.cs index d82ed852a3..9b1b012e0f 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/HttpClientServiceCollectionExtensions.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/HttpClientServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.AcceptanceTests.RavenDB.Shared; +namespace ServiceControl.AcceptanceTests.TestSupport; using System; using System.Net.Http; diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/IAcceptanceTestStorageConfiguration.cs b/src/ServiceControl.AcceptanceTests/TestSupport/IAcceptanceTestStorageConfiguration.cs new file mode 100644 index 0000000000..b79507a129 --- /dev/null +++ b/src/ServiceControl.AcceptanceTests/TestSupport/IAcceptanceTestStorageConfiguration.cs @@ -0,0 +1,17 @@ +namespace ServiceControl.AcceptanceTests.TestSupport; + +using System; +using System.Threading; +using System.Threading.Tasks; +using ServiceBus.Management.Infrastructure.Settings; + +public interface IAcceptanceTestStorageConfiguration +{ + string PersistenceType { get; } + + Task CustomizeSettings(Settings settings, CancellationToken cancellationToken = default); + + Task Cleanup(CancellationToken cancellationToken = default); + + Task UseDatabaseLifecycleLock(CancellationToken cancellationToken = default); +} diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs index 912db9e850..b41a353ec9 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentBehavior.cs @@ -9,12 +9,11 @@ namespace ServiceControl.AcceptanceTests.TestSupport using Microsoft.Extensions.Hosting; using NServiceBus; using NServiceBus.AcceptanceTesting.Support; - using RavenDB; using ServiceBus.Management.Infrastructure.Settings; class ServiceControlComponentBehavior : IComponentBehavior, IAcceptanceTestInfrastructureProvider { - public ServiceControlComponentBehavior(ITransportIntegration transportToUse, AcceptanceTestStorageConfiguration persistenceToUse, Action setSettings, Action customConfiguration, Action hostBuilderCustomization) + public ServiceControlComponentBehavior(ITransportIntegration transportToUse, IAcceptanceTestStorageConfiguration persistenceToUse, Action setSettings, Action customConfiguration, Action hostBuilderCustomization) { this.customConfiguration = customConfiguration; this.persistenceToUse = persistenceToUse; @@ -36,7 +35,7 @@ public async Task CreateRunner(RunDescriptor run) } readonly ITransportIntegration transportIntegration; - readonly AcceptanceTestStorageConfiguration persistenceToUse; + readonly IAcceptanceTestStorageConfiguration persistenceToUse; readonly Action setSettings; readonly Action customConfiguration; readonly Action hostBuilderCustomization; diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index e3a5761b78..f7890a32df 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -26,15 +26,13 @@ using Particular.ServiceControl; using Particular.ServiceControl.Hosting; using Persistence.Tests; - using RavenDB; - using RavenDB.Shared; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Infrastructure; using LogLevel = Microsoft.Extensions.Logging.LogLevel; public class ServiceControlComponentRunner : ComponentRunner, IAcceptanceTestInfrastructureProvider { - public ServiceControlComponentRunner(ITransportIntegration transportToUse, AcceptanceTestStorageConfiguration persistenceToUse, Action setSettings, Action customConfiguration, Action hostBuilderCustomization) + public ServiceControlComponentRunner(ITransportIntegration transportToUse, IAcceptanceTestStorageConfiguration persistenceToUse, Action setSettings, Action customConfiguration, Action hostBuilderCustomization) { this.transportToUse = transportToUse; this.persistenceToUse = persistenceToUse; @@ -51,7 +49,7 @@ public ServiceControlComponentRunner(ITransportIntegration transportToUse, Accep public async Task Initialize(RunDescriptor run) { - using var _ = await AcceptanceTestStorageConfiguration.UseDatabaseLifecycleLock(); + using var _ = await persistenceToUse.UseDatabaseLifecycleLock(); await InitializeServiceControlCore(run.ScenarioContext); } @@ -162,13 +160,13 @@ public override async Task Stop(CancellationToken cancellationToken = default) await host.StopAsync(cancellationToken); HttpClient.Dispose(); await host.DisposeAsync(); - await persistenceToUse.Cleanup(); + await persistenceToUse.Cleanup(cancellationToken); } } WebApplication host; readonly ITransportIntegration transportToUse; - readonly AcceptanceTestStorageConfiguration persistenceToUse; + readonly IAcceptanceTestStorageConfiguration persistenceToUse; readonly Action setSettings; readonly Action customConfiguration; readonly Action hostBuilderCustomization; diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/WebApplicationBuilderExtensions.cs b/src/ServiceControl.AcceptanceTests/TestSupport/WebApplicationBuilderExtensions.cs index 7c5aa29a1b..70f291151f 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/WebApplicationBuilderExtensions.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/WebApplicationBuilderExtensions.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.AcceptanceTests.RavenDB.Shared; +namespace ServiceControl.AcceptanceTests.TestSupport; using System; using Microsoft.AspNetCore.Builder; diff --git a/src/ServiceControl.slnx b/src/ServiceControl.slnx index b59518a697..622050f094 100644 --- a/src/ServiceControl.slnx +++ b/src/ServiceControl.slnx @@ -48,7 +48,9 @@ + + diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index 2223cb11af..0eb6cee74e 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -58,7 +58,9 @@ + +