diff --git a/Akka.Persistence.SqlServer.sln b/Akka.Persistence.SqlServer.sln index 3675782..abdfbb2 100644 --- a/Akka.Persistence.SqlServer.sln +++ b/Akka.Persistence.SqlServer.sln @@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.Persistence.SqlServer" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.Persistence.SqlServer.Tests", "src\Akka.Persistence.SqlServer.Tests\Akka.Persistence.SqlServer.Tests.csproj", "{DEAC600E-5B71-4124-BE4B-5BF26C21575E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.SqlServer.Performance.Tests", "src\Akka.Persistence.SqlServer.Performance.Tests\Akka.Persistence.SqlServer.Performance.Tests.csproj", "{5B792D05-2CCE-4513-BAAD-100394E9E161}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {DEAC600E-5B71-4124-BE4B-5BF26C21575E}.Debug|Any CPU.Build.0 = Debug|Any CPU {DEAC600E-5B71-4124-BE4B-5BF26C21575E}.Release|Any CPU.ActiveCfg = Release|Any CPU {DEAC600E-5B71-4124-BE4B-5BF26C21575E}.Release|Any CPU.Build.0 = Release|Any CPU + {5B792D05-2CCE-4513-BAAD-100394E9E161}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B792D05-2CCE-4513-BAAD-100394E9E161}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B792D05-2CCE-4513-BAAD-100394E9E161}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B792D05-2CCE-4513-BAAD-100394E9E161}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build.fsx b/build.fsx index 94bec65..7db2f42 100644 --- a/build.fsx +++ b/build.fsx @@ -95,7 +95,9 @@ Target "RunTests" (fun _ -> let projects = match (isWindows) with | true -> !! "./src/**/*.Tests.csproj" + -- "./src/**/*.Performance.Tests.csproj" | _ -> !! "./src/**/*.Tests.csproj" // if you need to filter specs for Linux vs. Windows, do it here + -- "./src/**/*.Performance.Tests.csproj" let runSingleProject project = let arguments = diff --git a/src/Akka.Persistence.SqlServer.Performance.Tests/Akka.Persistence.SqlServer.Performance.Tests.csproj b/src/Akka.Persistence.SqlServer.Performance.Tests/Akka.Persistence.SqlServer.Performance.Tests.csproj new file mode 100644 index 0000000..4b7efaa --- /dev/null +++ b/src/Akka.Persistence.SqlServer.Performance.Tests/Akka.Persistence.SqlServer.Performance.Tests.csproj @@ -0,0 +1,25 @@ + + + $(NetFrameworkTestVersion);$(NetCoreTestVersion) + false + + + + + $(NetCoreTestVersion) + false + + + + + + + + + + + + + + + diff --git a/src/Akka.Persistence.SqlServer.Tests/Batching/BatchingSqlServerJournalPerfSpec.cs b/src/Akka.Persistence.SqlServer.Performance.Tests/BatchingSqlServerJournalPerfSpec.cs similarity index 97% rename from src/Akka.Persistence.SqlServer.Tests/Batching/BatchingSqlServerJournalPerfSpec.cs rename to src/Akka.Persistence.SqlServer.Performance.Tests/BatchingSqlServerJournalPerfSpec.cs index 170a3f9..febe5c4 100644 --- a/src/Akka.Persistence.SqlServer.Tests/Batching/BatchingSqlServerJournalPerfSpec.cs +++ b/src/Akka.Persistence.SqlServer.Performance.Tests/BatchingSqlServerJournalPerfSpec.cs @@ -10,7 +10,7 @@ using Xunit; using Xunit.Abstractions; -namespace Akka.Persistence.SqlServer.Tests +namespace Akka.Persistence.SqlServer.Performance.Tests { [Collection("SqlServerSpec")] public class BatchingSqlServerJournalPerfSpec : JournalPerfSpec, IDisposable diff --git a/src/Akka.Persistence.SqlServer.Performance.Tests/DbUtils.cs b/src/Akka.Persistence.SqlServer.Performance.Tests/DbUtils.cs new file mode 100644 index 0000000..537e9ce --- /dev/null +++ b/src/Akka.Persistence.SqlServer.Performance.Tests/DbUtils.cs @@ -0,0 +1,84 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013 - 2023 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.IO; +using Microsoft.Data.SqlClient; + +namespace Akka.Persistence.SqlServer.Performance.Tests +{ + public static class DbUtils + { + private static SqlConnectionStringBuilder _builder; + public static string ConnectionString => _builder.ToString(); + + public static void Initialize(string connectionString) + { + _builder = new SqlConnectionStringBuilder(connectionString); + var databaseName = $"akka_persistence_tests_{Guid.NewGuid()}"; + _builder.InitialCatalog = databaseName; + + var connectionBuilder = new SqlConnectionStringBuilder(connectionString) + { + InitialCatalog = "master" + }; + + using (var conn = new SqlConnection(connectionBuilder.ToString())) + { + conn.Open(); + + using (var cmd = new SqlCommand()) + { + cmd.CommandText = $@" +IF db_id('{databaseName}') IS NULL +BEGIN + CREATE DATABASE [{databaseName}]; +END"; + cmd.Connection = conn; + cmd.ExecuteScalar(); + } + } + + // Delete local snapshot flat file database + var path = "./snapshots"; + if (Directory.Exists(path)) + Directory.Delete(path, true); + } + + public static void Clean() + { + var databaseName = $"akka_persistence_tests_{Guid.NewGuid()}"; + _builder.InitialCatalog = databaseName; + + var connectionBuilder = new SqlConnectionStringBuilder(ConnectionString) + { + InitialCatalog = "master" + }; + + using (var conn = new SqlConnection(connectionBuilder.ToString())) + { + conn.Open(); + + using (var cmd = new SqlCommand()) + { + cmd.CommandText = $@" +IF db_id('{databaseName}') IS NULL +BEGIN + CREATE DATABASE [{databaseName}]; +END +"; + cmd.Connection = conn; + cmd.ExecuteScalar(); + } + } + + // Delete local snapshot flat file database + var path = "./snapshots"; + if (Directory.Exists(path)) + Directory.Delete(path, true); + } + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.SqlServer.Performance.Tests/SqlServerFixture.cs b/src/Akka.Persistence.SqlServer.Performance.Tests/SqlServerFixture.cs new file mode 100644 index 0000000..13230cf --- /dev/null +++ b/src/Akka.Persistence.SqlServer.Performance.Tests/SqlServerFixture.cs @@ -0,0 +1,186 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013 - 2023 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Akka.Util; +using Docker.DotNet; +using Docker.DotNet.Models; +using Xunit; +using Xunit.Sdk; + +namespace Akka.Persistence.SqlServer.Performance.Tests +{ + [CollectionDefinition("SqlServerSpec")] + public sealed class SqlServerSpecsFixture : ICollectionFixture + { + } + + /// + /// Fixture used to run SQL Server + /// + public class SqlServerFixture : IAsyncLifetime + { + protected readonly string SqlContainerName = $"sqlserver-{Guid.NewGuid():N}"; + protected DockerClient Client; + + public SqlServerFixture() + { + DockerClientConfiguration config; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + config = new DockerClientConfiguration(new Uri("unix://var/run/docker.sock")); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + config = new DockerClientConfiguration(new Uri("npipe://./pipe/docker_engine")); + else + throw new NotSupportedException($"Unsupported OS [{RuntimeInformation.OSDescription}]"); + + Client = config.CreateClient(); + } + + protected string ImageName => "mcr.microsoft.com/mssql/server"; + protected string Tag => "2019-latest"; + + protected string SqlServerImageName => $"{ImageName}:{Tag}"; + + public string ConnectionString { get; private set; } + + public async Task InitializeAsync() + { + var sysInfo = await Client.System.GetSystemInfoAsync(); + if (sysInfo.OSType.ToLowerInvariant() != "linux") + throw new TestClassException("MSSQL docker image only available for linux containers"); + + var images = await Client.Images.ListImagesAsync(new ImagesListParameters + { + Filters = new Dictionary> + { + { + "reference", + new Dictionary + { + { SqlServerImageName, true } + } + } + } + }); + + if (images.Count == 0) + await Client.Images.CreateImageAsync( + new ImagesCreateParameters { FromImage = ImageName, Tag = Tag }, null, + new Progress(message => + { + Console.WriteLine(!string.IsNullOrEmpty(message.ErrorMessage) + ? message.ErrorMessage + : $"{message.ID} {message.Status} {message.ProgressMessage}"); + })); + + var sqlServerHostPort = ThreadLocalRandom.Current.Next(9000, 10000); + + // create the container + await Client.Containers.CreateContainerAsync(new CreateContainerParameters + { + Image = SqlServerImageName, + Name = SqlContainerName, + Tty = true, + ExposedPorts = new Dictionary + { + { "1433/tcp", new EmptyStruct() } + }, + HostConfig = new HostConfig + { + PortBindings = new Dictionary> + { + { + "1433/tcp", + new List + { + new PortBinding + { + HostPort = $"{sqlServerHostPort}" + } + } + } + } + }, + Env = new[] + { + "ACCEPT_EULA=Y", + "MSSQL_SA_PASSWORD=l0l!Th1sIsOpenSource" + } + }); + + // start the container + await Client.Containers.StartContainerAsync(SqlContainerName, new ContainerStartParameters()); + + // Wait until MSSQL is completely ready + var logStream = await Client.Containers.GetContainerLogsAsync(SqlContainerName, new ContainerLogsParameters + { + Follow = true, + ShowStdout = true, + ShowStderr = true + }); + + string line = null; + var timeoutInMilis = 60000; + using (var reader = new StreamReader(logStream)) + { + var stopwatch = Stopwatch.StartNew(); + while (stopwatch.ElapsedMilliseconds < timeoutInMilis && (line = await reader.ReadLineAsync()) != null) + { + if (!string.IsNullOrWhiteSpace(line)) + Console.WriteLine(line); + if (line.Contains("SQL Server is now ready for client connections.")) break; + } + + stopwatch.Stop(); + } +#if NETCOREAPP3_1_OR_GREATER + await logStream.DisposeAsync(); +#else + logStream.Dispose(); +#endif + if (!line?.Contains("SQL Server is now ready for client connections.") ?? false) + throw new Exception("MSSQL docker image failed to run."); + + var connectionString = new DbConnectionStringBuilder + { + ["Server"] = $"localhost,{sqlServerHostPort}", + ["Database"] = "akka_persistence_tests", + ["User Id"] = "sa", + ["Password"] = "l0l!Th1sIsOpenSource" + }; + + ConnectionString = connectionString.ToString(); + Console.WriteLine($"Connection string: [{ConnectionString}]"); + + await Task.Delay(10000); + } + + public async Task DisposeAsync() + { + if (Client != null) + try + { + await Client.Containers.StopContainerAsync(SqlContainerName, new ContainerStopParameters()); + await Client.Containers.RemoveContainerAsync(SqlContainerName, + new ContainerRemoveParameters { Force = true }); + } + catch (DockerContainerNotFoundException) + { + // no-op + } + finally + { + Client.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.SqlServer.Tests/SqlServerJournalPerfSpec.cs b/src/Akka.Persistence.SqlServer.Performance.Tests/SqlServerJournalPerfSpec.cs similarity index 97% rename from src/Akka.Persistence.SqlServer.Tests/SqlServerJournalPerfSpec.cs rename to src/Akka.Persistence.SqlServer.Performance.Tests/SqlServerJournalPerfSpec.cs index d4f8f47..c70e60f 100644 --- a/src/Akka.Persistence.SqlServer.Tests/SqlServerJournalPerfSpec.cs +++ b/src/Akka.Persistence.SqlServer.Performance.Tests/SqlServerJournalPerfSpec.cs @@ -10,7 +10,7 @@ using Xunit; using Xunit.Abstractions; -namespace Akka.Persistence.SqlServer.Tests +namespace Akka.Persistence.SqlServer.Performance.Tests { [Collection("SqlServerSpec")] public class SqlServerJournalPerfSpec : JournalPerfSpec, IDisposable diff --git a/src/Akka.Persistence.SqlServer.Tests/Akka.Persistence.SqlServer.Tests.csproj b/src/Akka.Persistence.SqlServer.Tests/Akka.Persistence.SqlServer.Tests.csproj index 877aad1..8901b40 100644 --- a/src/Akka.Persistence.SqlServer.Tests/Akka.Persistence.SqlServer.Tests.csproj +++ b/src/Akka.Persistence.SqlServer.Tests/Akka.Persistence.SqlServer.Tests.csproj @@ -2,23 +2,25 @@ $(NetFrameworkTestVersion);$(NetCoreTestVersion) + false - $(NetCoreTestVersion) + $(NetCoreTestVersion) + false - - - - - + + + + + - +