From ebbfeea002f48b42620c84b269e7f5b2ef7d74e2 Mon Sep 17 00:00:00 2001 From: saibulusu Date: Mon, 1 Jun 2026 23:45:06 -0700 Subject: [PATCH 1/4] copied over mongo db files from internal --- VERSION | 2 +- .../MongoDB/YCSBMongoDBOutputExample.txt | 36 + .../MongoDB/MongoDBClientExecutorTests.cs | 1036 +++++++++++++++++ .../MongoDB/MongoDBExecutorTests.cs | 386 ++++++ .../MongoDB/MongoDBMetricsParserTests.cs | 237 ++++ .../MongoDB/MongoDBServerExecutorTests.cs | 510 ++++++++ .../MongoDB/MongoDBClientExecutor.cs | 584 ++++++++++ .../MongoDB/MongoDBExecutor.cs | 124 ++ .../MongoDB/MongoDBMetricsParser.cs | 346 ++++++ .../MongoDB/MongoDBServerExecutor.cs | 431 +++++++ website/docs/workloads/mongodb/YCSB.md | 150 +++ .../workloads/mongodb/mongodb-profiles.md | 118 ++ .../docs/workloads/mongodb/testing_scripts.md | 201 ++++ 13 files changed, 4160 insertions(+), 1 deletion(-) create mode 100644 src/VirtualClient/VirtualClient.Actions.UnitTests/Examples/MongoDB/YCSBMongoDBOutputExample.txt create mode 100644 src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBClientExecutorTests.cs create mode 100644 src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBExecutorTests.cs create mode 100644 src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBMetricsParserTests.cs create mode 100644 src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBServerExecutorTests.cs create mode 100644 src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBClientExecutor.cs create mode 100644 src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBExecutor.cs create mode 100644 src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBMetricsParser.cs create mode 100644 src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBServerExecutor.cs create mode 100644 website/docs/workloads/mongodb/YCSB.md create mode 100644 website/docs/workloads/mongodb/mongodb-profiles.md create mode 100644 website/docs/workloads/mongodb/testing_scripts.md diff --git a/VERSION b/VERSION index 619b537668..a0891f563f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.3 +3.3.4 diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/Examples/MongoDB/YCSBMongoDBOutputExample.txt b/src/VirtualClient/VirtualClient.Actions.UnitTests/Examples/MongoDB/YCSBMongoDBOutputExample.txt new file mode 100644 index 0000000000..31ab2c44ef --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/Examples/MongoDB/YCSBMongoDBOutputExample.txt @@ -0,0 +1,36 @@ +/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/microsoft-jdk-21.0.1/linux-x64/bin/java -classpath /home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/conf:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/lib/HdrHistogram-2.1.4.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/lib/core-0.17.0.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/lib/htrace-core4-4.1.0-incubating.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/lib/jackson-core-asl-1.9.4.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/lib/jackson-mapper-asl-1.9.4.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/mongodb-binding/lib/logback-classic-1.1.2.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/mongodb-binding/lib/logback-core-1.1.2.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/mongodb-binding/lib/mongo-java-driver-3.8.0.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/mongodb-binding/lib/mongodb-async-driver-2.0.1.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/mongodb-binding/lib/mongodb-binding-0.17.0.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/mongodb-binding/lib/slf4j-api-1.7.25.jar:/home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/mongodb-binding/lib/snappy-java-1.1.7.1.jar site.ycsb.Client -t -db site.ycsb.db.MongoDbClient -s -p maxexecutiontime=300 -p operationcount=5000000 -p recordcount=500000 -threads 8 -p fieldcount=64 -p fieldlength=128 -P /home/azureuser/VirtualClientScheduler/VirtualClient.2.1.3203.1966/content/linux-x64/packages/ycsb-0.17.0/ycsb-0.17.0/workloads/workloada +mongo client connection created with mongodb://localhost:27017/ycsb?w=1 +[OVERALL], RunTime(ms), 177015 +[OVERALL], Throughput(ops/sec), 28246.19382538203 +[TOTAL_GCS_G1_Young_Generation], Count, 330 +[TOTAL_GC_TIME_G1_Young_Generation], Time(ms), 326 +[TOTAL_GC_TIME_%_G1_Young_Generation], Time(%), 0.18416518374149085 +[TOTAL_GCS_G1_Concurrent_GC], Count, 0 +[TOTAL_GC_TIME_G1_Concurrent_GC], Time(ms), 0 +[TOTAL_GC_TIME_%_G1_Concurrent_GC], Time(%), 0.0 +[TOTAL_GCS_G1_Old_Generation], Count, 0 +[TOTAL_GC_TIME_G1_Old_Generation], Time(ms), 0 +[TOTAL_GC_TIME_%_G1_Old_Generation], Time(%), 0.0 +[TOTAL_GCs], Count, 330 +[TOTAL_GC_TIME], Time(ms), 326 +[TOTAL_GC_TIME_%], Time(%), 0.18416518374149085 +[READ], Operations, 2498425 +[READ], AverageLatency(us), 202.70430571259894 +[READ], MinLatency(us), 129 +[READ], MaxLatency(us), 1556479 +[READ], 95thPercentileLatency(us), 231 +[READ], 99thPercentileLatency(us), 256 +[READ], Return=OK, 2498425 +[CLEANUP], Operations, 8 +[CLEANUP], AverageLatency(us), 184.875 +[CLEANUP], MinLatency(us), 1 +[CLEANUP], MaxLatency(us), 1464 +[CLEANUP], 95thPercentileLatency(us), 1464 +[CLEANUP], 99thPercentileLatency(us), 1464 +[UPDATE], Operations, 2501575 +[UPDATE], AverageLatency(us), 354.05011122992516 +[UPDATE], MinLatency(us), 147 +[UPDATE], MaxLatency(us), 3090431 +[UPDATE], 95thPercentileLatency(us), 243 +[UPDATE], 99thPercentileLatency(us), 294 +[UPDATE], Return=OK, 2501575 diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBClientExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBClientExecutorTests.cs new file mode 100644 index 0000000000..bf04fc4c1c --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBClientExecutorTests.cs @@ -0,0 +1,1036 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient.Actions.UnitTests.MongoDB +{ + using VirtualClient.Actions.MongoDB; + using System; + using System.Collections.Generic; + using System.IO; + using System.Net.Http; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Moq; + using NUnit.Framework; + using Polly; + using VirtualClient; + using VirtualClient.Common.Telemetry; + using VirtualClient.Contracts; + + /// + /// Comprehensive unit tests for MongoDBClientExecutor covering all functions and lines. + /// Follows the test pattern established in CassandraClientExecutorTests. + /// + [TestFixture] + [Category("Unit")] + public class MongoDBClientExecutorTests + { + private MockFixture mockFixture; + private DependencyPath mockJdkPackage; + private DependencyPath mockYcsbPackage; + private string mockWorkloadOutput; + + [SetUp] + public void SetupDefaults() + { + this.mockFixture = new MockFixture(); + this.mockFixture.Setup(PlatformID.Unix); + + this.mockJdkPackage = new DependencyPath("jdk", this.mockFixture.PlatformSpecifics.GetPackagePath("jdk")); + this.mockYcsbPackage = new DependencyPath("ycsb-0.17.0", this.mockFixture.PlatformSpecifics.GetPackagePath("ycsb-0.17.0")); + + this.mockFixture.Parameters = new Dictionary + { + ["Scenario"] = "runworkload", + ["JdkPackageName"] = this.mockJdkPackage.Name, + ["YCSBPackageName"] = this.mockYcsbPackage.Name, + ["WorkloadName"] = "workloada", + ["RunCommand"] = "run mongodb -s {ServerIP}:{Port} -threads 100 -recordcount 1000", + ["LoadCommand"] = "load mongodb -s {ServerIP}:{Port} -recordcount 50000", + ["Port"] = 27017 + }; + + // Setup package manager mocks + this.mockFixture.PackageManager + .Setup(mgr => mgr.GetPackageAsync(this.mockJdkPackage.Name, It.IsAny())) + .ReturnsAsync(this.mockJdkPackage); + + this.mockFixture.PackageManager + .Setup(mgr => mgr.GetPackageAsync(this.mockYcsbPackage.Name, It.IsAny())) + .ReturnsAsync(this.mockYcsbPackage); + + // Setup process mock + this.mockFixture.ProcessManager.OnCreateProcess = (exe, args, workDir) => + { + return this.mockFixture.Process; + }; + + // Setup system management + string agentId = $"{Environment.MachineName}"; + this.mockFixture.SystemManagement.SetupGet(obj => obj.AgentId).Returns(agentId); + + // Setup API client manager + this.mockFixture.ApiClientManager.Setup(mgr => mgr.GetOrCreateApiClient(It.IsAny(), It.IsAny())) + .Returns(this.mockFixture.ApiClient.Object); + + // Setup file system mocks for file existence checks + this.mockFixture.File.Setup(f => f.Exists(It.IsAny())) + .Returns(true); + + this.mockFixture.Directory.Setup(d => d.Exists(It.IsAny())) + .Returns(true); + + // Setup workload output + this.mockWorkloadOutput = "[OVERALL], RunTime(ms), 120000\n" + + "[OVERALL], Throughput(ops/sec), 8333\n" + + "[READ], Operations, 500000\n" + + "[READ], AverageLatency(us), 1200\n" + + "[WRITE], Operations, 500000\n" + + "[WRITE], AverageLatency(us), 1100\n"; + + this.mockFixture.File.Setup(f => f.ReadAllTextAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(this.mockWorkloadOutput); + } + + [TearDown] + public void Teardown() + { + // Cleanup if needed + } + + /// + /// Testable derived class to expose protected members for testing. + /// Follows the pattern established by TestCassandraClientExecutor. + /// + private class TestMongoDBClientExecutor : MongoDBClientExecutor + { + public TestMongoDBClientExecutor(MockFixture fixture) + : base(fixture.Dependencies, fixture.Parameters) + { + } + + // Setters for protected properties (for test setup) + public void SetYcsbPackagePath(DependencyPath packagePath) + { + this.YcsbPackagePath = packagePath.Path; + } + + public void SetJdkPackagePath(DependencyPath packagePath) + { + this.JDKPackagePath = packagePath.Path; + } + + public void SetYcsbExecutablePath(string path) + { + this.YcsbExecutablePath = path; + } + + public void SetYcsbSetEnvPath(string path) + { + this.YcsbSetEnvPath = path; + } + + public void SetJavaExportString(string exportString) + { + this.JavaExportString = exportString; + } + + public void SetServerApiClient(IApiClient mockApiClient) + { + this.ServerApiClient = mockApiClient; + } + + // Expose protected methods + public new Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + return base.InitializeAsync(telemetryContext, cancellationToken); + } + + public new Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + return base.ExecuteAsync(telemetryContext, cancellationToken); + } + + // Expose private methods via reflection for testing + public Task CallCheckDatabaseExistsAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + var method = typeof(MongoDBClientExecutor).GetMethod( + "CheckDatabaseExistsAsync", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + var task = (Task)method.Invoke(this, new object[] { telemetryContext, cancellationToken }); + return task; + } + + public async Task CallDropDatabaseAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + var method = typeof(MongoDBClientExecutor).GetMethod( + "DropDatabaseAsync", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + var task = (Task)method.Invoke(this, new object[] { telemetryContext, cancellationToken }); + await task; + } + + // Getters for protected properties (for test verification) + public string GetYcsbPackagePath() => this.YcsbPackagePath; + public string GetJdkPackagePath() => this.JDKPackagePath; + public string GetYcsbExecutablePath() => this.YcsbExecutablePath; + public string GetYcsbSetEnvPath() => this.YcsbSetEnvPath; + public string GetJavaExportString() => this.JavaExportString; + public string GetYCSBFolderName() => this.YCSBFolderName; + public int GetPort() => this.Port; + public string GetScenario() => this.Scenario; + } + + #region Constructor and Property Tests + + [Test] + public void MongoDBClientExecutor_Constructor_InitializesWithParameters() + { + // ACT + using (var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + // ASSERT + Assert.IsNotNull(executor); + Assert.AreEqual(this.mockJdkPackage.Name, executor.JdkPackageName); + Assert.AreEqual(this.mockYcsbPackage.Name, executor.YCSBPackageName); + } + } + + [Test] + public void MongoDBClientExecutor_YCSBPackageName_ReturnsParameterValue() + { + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual(this.mockYcsbPackage.Name, executor.YCSBPackageName); + } + + [Test] + public void MongoDBClientExecutor_JdkPackageName_ReturnsParameterValue() + { + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual(this.mockJdkPackage.Name, executor.JdkPackageName); + } + + [Test] + public void MongoDBClientExecutor_RunCommand_ReturnsParameterValue() + { + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual("run mongodb -s {ServerIP}:{Port} -threads 100 -recordcount 1000", executor.RunCommand); + } + + [Test] + public void MongoDBClientExecutor_LoadCommand_ReturnsParameterValue() + { + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual("load mongodb -s {ServerIP}:{Port} -recordcount 50000", executor.LoadCommand); + } + + [Test] + public void MongoDBClientExecutor_WorkloadName_ReturnsParameterValue() + { + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual("workloada", executor.WorkloadName); + } + + [Test] + public void MongoDBClientExecutor_RunCommand_WithEmptyValue_ReturnsEmptyString() + { + // ARRANGE + this.mockFixture.Parameters["RunCommand"] = string.Empty; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual(string.Empty, executor.RunCommand); + } + + [Test] + public void MongoDBClientExecutor_LoadCommand_WithEmptyValue_ReturnsEmptyString() + { + // ARRANGE + this.mockFixture.Parameters["LoadCommand"] = string.Empty; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual(string.Empty, executor.LoadCommand); + } + + #endregion + + #region Command Placeholder Tests + + [Test] + public void MongoDBClientExecutor_RunCommand_ContainsExpectedPlaceholders() + { + // ARRANGE + this.mockFixture.Parameters["RunCommand"] = "run mongodb -s {ServerIP}:{Port} -recordcount 1000"; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + string command = executor.RunCommand; + + // ASSERT + Assert.IsTrue(command.Contains("{ServerIP}"), "Command should contain ServerIP placeholder"); + Assert.IsTrue(command.Contains("{Port}"), "Command should contain Port placeholder"); + } + + [Test] + public void MongoDBClientExecutor_LoadCommand_ContainsExpectedPlaceholders() + { + // ARRANGE + this.mockFixture.Parameters["LoadCommand"] = "load mongodb -s {ServerIP}:{Port} -recordcount 50000"; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + string command = executor.LoadCommand; + + // ASSERT + Assert.IsTrue(command.Contains("{ServerIP}"), "Command should contain ServerIP placeholder"); + Assert.IsTrue(command.Contains("{Port}"), "Command should contain Port placeholder"); + } + + #endregion + + #region Initialization Tests + + [Test] + public async Task MongoDBClientExecutor_InitializeAsync_InitializesSuccessfully() + { + // ARRANGE + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + int commandsExecuted = 0; + + // Mock server online status check + this.mockFixture.ApiClient + .Setup(client => client.GetServerOnlineStatusAsync(It.IsAny(), It.IsAny>())) + .ReturnsAsync(this.mockFixture.CreateHttpResponse(System.Net.HttpStatusCode.OK)); + + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + commandsExecuted++; + return this.mockFixture.Process; + }; + + // ACT + await testInstance.InitializeAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsTrue(commandsExecuted >= 2, "Expected at least 2 commands (chmod and echo export)"); + Assert.IsNotNull(testInstance.GetYcsbPackagePath()); + Assert.IsNotNull(testInstance.GetJdkPackagePath()); + } + } + + [Test] + public void MongoDBClientExecutor_InitializeAsync_WithMissingJdkPackage_ThrowsDependencyException() + { + // ARRANGE + this.mockFixture.PackageManager + .Setup(mgr => mgr.GetPackageAsync(this.mockJdkPackage.Name, It.IsAny())) + .ReturnsAsync(null as DependencyPath); + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + // ACT & ASSERT + DependencyException exception = Assert.ThrowsAsync( + async () => await testInstance.InitializeAsync(EventContext.None, CancellationToken.None)); + + Assert.IsTrue(exception.Message.Contains(this.mockJdkPackage.Name)); + Assert.AreEqual(ErrorReason.WorkloadDependencyMissing, exception.Reason); + } + } + + [Test] + public void MongoDBClientExecutor_InitializeAsync_WithMissingYcsbPackage_ThrowsDependencyException() + { + // ARRANGE + this.mockFixture.PackageManager + .Setup(mgr => mgr.GetPackageAsync(this.mockYcsbPackage.Name, It.IsAny())) + .ReturnsAsync(null as DependencyPath); + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + // ACT & ASSERT + DependencyException exception = Assert.ThrowsAsync( + async () => await testInstance.InitializeAsync(EventContext.None, CancellationToken.None)); + + Assert.IsTrue(exception.Message.Contains(this.mockYcsbPackage.Name)); + Assert.AreEqual(ErrorReason.WorkloadDependencyMissing, exception.Reason); + } + } + + #endregion + + #region RunWorkload Scenario Tests + + [Test] + public async Task MongoDBClientExecutor_ExecuteAsync_WithRunWorkloadScenario_ExecutesSuccessfully() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "runworkload"; + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + string ycsbExecutable = this.mockFixture.PlatformSpecifics.Combine( + this.mockYcsbPackage.Path, "ycsb-0.17.0", "bin", "ycsb.sh"); + + testInstance.SetYcsbPackagePath(this.mockYcsbPackage); + testInstance.SetJdkPackagePath(this.mockJdkPackage); + testInstance.SetYcsbExecutablePath(ycsbExecutable); + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + bool commandExecuted = false; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + if (arguments.Contains("run mongodb")) + { + commandExecuted = true; + this.mockFixture.Process.StandardOutput.Append(this.mockWorkloadOutput); + } + return this.mockFixture.Process; + }; + + // ACT + await testInstance.ExecuteAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsTrue(commandExecuted, "Run workload command should have been executed"); + } + } + + [Test] + public async Task MongoDBClientExecutor_ExecuteAsync_WithRunScenario_ReplacesServerIPPlaceholder() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "runworkload"; + this.mockFixture.Parameters["RunCommand"] = "run mongodb -s {ServerIP}:{Port}"; + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + string ycsbExecutable = this.mockFixture.PlatformSpecifics.Combine( + this.mockYcsbPackage.Path, "ycsb-0.17.0", "bin", "ycsb.sh"); + + testInstance.SetYcsbPackagePath(this.mockYcsbPackage); + testInstance.SetJdkPackagePath(this.mockJdkPackage); + testInstance.SetYcsbExecutablePath(ycsbExecutable); + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + string capturedArguments = null; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + // Capture all arguments to inspect + if (string.IsNullOrEmpty(capturedArguments)) + { + capturedArguments = arguments; + } + if (arguments.Contains("run mongodb")) + { + this.mockFixture.Process.StandardOutput.Append(this.mockWorkloadOutput); + } + return this.mockFixture.Process; + }; + + // ACT + await testInstance.ExecuteAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsNotNull(capturedArguments, "Process should have been created"); + // The actual command might be in exe or arguments depending on how it's invoked + // Just verify the replacement happened by checking for IP address in captured args or in all process calls + bool hasServerIP = capturedArguments.Contains("1.2.3.5") || capturedArguments.Contains("1.2.3.4"); + Assert.IsTrue(hasServerIP || !capturedArguments.Contains("{ServerIP}"), + "ServerIP placeholder should be replaced with actual IP"); + } + } + + [Test] + public async Task MongoDBClientExecutor_ExecuteAsync_WithRunScenario_ReplacesPortPlaceholder() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "runworkload"; + this.mockFixture.Parameters["RunCommand"] = "run mongodb -s {ServerIP}:{Port}"; + this.mockFixture.Parameters["Port"] = 27019; + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + string ycsbExecutable = this.mockFixture.PlatformSpecifics.Combine( + this.mockYcsbPackage.Path, "ycsb-0.17.0", "bin", "ycsb.sh"); + + testInstance.SetYcsbPackagePath(this.mockYcsbPackage); + testInstance.SetJdkPackagePath(this.mockJdkPackage); + testInstance.SetYcsbExecutablePath(ycsbExecutable); + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + string capturedArguments = null; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + if (arguments.Contains("run mongodb")) + { + capturedArguments = arguments; + this.mockFixture.Process.StandardOutput.Append(this.mockWorkloadOutput); + } + return this.mockFixture.Process; + }; + + // ACT + await testInstance.ExecuteAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsNotNull(capturedArguments); + Assert.IsFalse(capturedArguments.Contains("{Port}"), "Port placeholder should be replaced"); + Assert.IsTrue(capturedArguments.Contains("27019"), "Should contain actual port number"); + } + } + + #endregion + + #region LoadDatabase Scenario Tests + + [Test] + public async Task MongoDBClientExecutor_ExecuteAsync_WithLoadDatabaseScenario_ExecutesSuccessfully() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "loaddatabase"; + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + string ycsbExecutable = this.mockFixture.PlatformSpecifics.Combine( + this.mockYcsbPackage.Path, "ycsb-0.17.0", "bin", "ycsb.sh"); + + testInstance.SetYcsbPackagePath(this.mockYcsbPackage); + testInstance.SetJdkPackagePath(this.mockJdkPackage); + testInstance.SetYcsbExecutablePath(ycsbExecutable); + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + bool loadCommandExecuted = false; + bool statsCommandExecuted = false; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + if (arguments.Contains("load mongodb")) + { + loadCommandExecuted = true; + this.mockFixture.Process.StandardOutput.Append(this.mockWorkloadOutput); + } + if (arguments.Contains("db.stats()")) + { + statsCommandExecuted = true; + } + return this.mockFixture.Process; + }; + + // ACT + await testInstance.ExecuteAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsTrue(loadCommandExecuted, "Load database command should have been executed"); + Assert.IsTrue(statsCommandExecuted, "Database stats command should have been executed"); + } + } + + [Test] + public async Task MongoDBClientExecutor_ExecuteAsync_WithLoadDatabaseScenario_ReplacesPlaceholders() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "loaddatabase"; + this.mockFixture.Parameters["LoadCommand"] = "load mongodb -s {ServerIP}:{Port}"; + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + string ycsbExecutable = this.mockFixture.PlatformSpecifics.Combine( + this.mockYcsbPackage.Path, "ycsb-0.17.0", "bin", "ycsb.sh"); + + testInstance.SetYcsbPackagePath(this.mockYcsbPackage); + testInstance.SetJdkPackagePath(this.mockJdkPackage); + testInstance.SetYcsbExecutablePath(ycsbExecutable); + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + string capturedArguments = null; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + // Capture the first command's arguments + if (string.IsNullOrEmpty(capturedArguments)) + { + capturedArguments = arguments; + } + + if (arguments.Contains("load mongodb")) + { + this.mockFixture.Process.StandardOutput.Append(this.mockWorkloadOutput); + } + + return this.mockFixture.Process; + }; + + // ACT + await testInstance.ExecuteAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsNotNull(capturedArguments, "Process should have been created"); + // Verify placeholders are not present (they should be replaced) + bool hasPlaceholders = capturedArguments.Contains("{ServerIP}") || capturedArguments.Contains("{Port}"); + Assert.IsFalse(hasPlaceholders, "Placeholders should be replaced with actual values"); + } + } + + [Test] + public async Task MongoDBClientExecutor_ExecuteAsync_WithLoadDatabaseVariantScenario_ExecutesLoadLogic() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "loaddatabase_inbetween"; + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + string ycsbExecutable = this.mockFixture.PlatformSpecifics.Combine( + this.mockYcsbPackage.Path, "ycsb-0.17.0", "bin", "ycsb.sh"); + + testInstance.SetYcsbPackagePath(this.mockYcsbPackage); + testInstance.SetJdkPackagePath(this.mockJdkPackage); + testInstance.SetYcsbExecutablePath(ycsbExecutable); + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + bool loadCommandExecuted = false; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + if (arguments.Contains("load mongodb")) + { + loadCommandExecuted = true; + this.mockFixture.Process.StandardOutput.Append(this.mockWorkloadOutput); + } + + return this.mockFixture.Process; + }; + + // ACT + await testInstance.ExecuteAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsTrue(loadCommandExecuted, "Load database variant scenario should execute load logic"); + } + } + + #endregion + + [Test] + public async Task MongoDBClientExecutor_CheckDatabaseExistsAsync_WhenDatabaseExists_ReturnsTrue() + { + // ARRANGE + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + string capturedCommand = null; + string capturedArguments = null; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + capturedCommand = exe; + capturedArguments = arguments; + + // Simulate database exists in output + this.mockFixture.Process.StandardOutput.Append("admin 40.00 KiB\n"); + this.mockFixture.Process.StandardOutput.Append("config 60.00 KiB\n"); + this.mockFixture.Process.StandardOutput.Append("ycsb 100.00 MiB\n"); + this.mockFixture.Process.StandardOutput.Append("local 72.00 KiB\n"); + + return this.mockFixture.Process; + }; + + // ACT + bool result = await testInstance.CallCheckDatabaseExistsAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsTrue(result, "Should return true when database exists in output"); + // On Unix, CreateElevatedProcess wraps with sudo + Assert.IsTrue(capturedCommand == "sudo" || capturedCommand == "mongosh", "Command should be sudo or mongosh"); + Assert.IsTrue(capturedArguments.Contains("show dbs"), "Should execute 'show dbs' command"); + } + } + + [Test] + public async Task MongoDBClientExecutor_CheckDatabaseExistsAsync_WhenDatabaseNotExists_ReturnsFalse() + { + // ARRANGE + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + // Simulate database does not exist in output + this.mockFixture.Process.StandardOutput.Append("admin 40.00 KiB\n"); + this.mockFixture.Process.StandardOutput.Append("config 60.00 KiB\n"); + this.mockFixture.Process.StandardOutput.Append("local 72.00 KiB\n"); + // No 'ycsb' database + return this.mockFixture.Process; + }; + + // ACT + bool result = await testInstance.CallCheckDatabaseExistsAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsFalse(result, "Should return false when database does not exist in output"); + } + } + + [Test] + public async Task MongoDBClientExecutor_DropDatabaseAsync_ExecutesDropCommand() + { + // ARRANGE + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + string capturedCommand = null; + string capturedArguments = null; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + capturedCommand = exe; + capturedArguments = arguments; + this.mockFixture.Process.StandardOutput.Append("{ ok: 1 }"); + return this.mockFixture.Process; + }; + + // ACT + await testInstance.CallDropDatabaseAsync(EventContext.None, CancellationToken.None); + + // ASSERT + // On Unix, CreateElevatedProcess wraps with sudo + Assert.IsTrue(capturedCommand == "sudo" || capturedCommand == "mongosh", "Command should be sudo or mongosh"); + Assert.IsTrue(capturedArguments.Contains("db.dropDatabase()"), "Should execute dropDatabase command"); + Assert.IsTrue(capturedArguments.Contains("ycsb"), "Should target ycsb database"); + } + } + + [Test] + public async Task MongoDBClientExecutor_DropDatabaseAsync_WithNonZeroExitCode_LogsWarning() + { + // ARRANGE + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + // Simulate failure + this.mockFixture.Process.ExitCode = 1; + this.mockFixture.Process.StandardError.Append("Error: database not found"); + return this.mockFixture.Process; + }; + + // ACT & ASSERT - Should not throw, just log warning + await testInstance.CallDropDatabaseAsync(EventContext.None, CancellationToken.None); + + // Verify the process was created (no exception thrown) + Assert.Pass("DropDatabase completed without throwing exception despite non-zero exit code"); + } + } + + [Test] + public async Task MongoDBClientExecutor_ExecuteAsync_WithDropDatabaseScenario_ChecksDatabaseExists() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "dropdatabase"; + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + testInstance.SetYcsbPackagePath(this.mockYcsbPackage); + testInstance.SetJdkPackagePath(this.mockJdkPackage); + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + bool checkDbCalled = false; + bool dropDbCalled = false; + + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + if (arguments.Contains("show dbs")) + { + checkDbCalled = true; + this.mockFixture.Process.StandardOutput.Append("ycsb 100MB\n"); + } + if (arguments.Contains("db.dropDatabase()")) + { + dropDbCalled = true; + } + + return this.mockFixture.Process; + }; + + // ACT + await testInstance.ExecuteAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsTrue(checkDbCalled, "Should check if database exists"); + Assert.IsTrue(dropDbCalled, "Should drop database when it exists"); + } + } + + [Test] + public async Task MongoDBClientExecutor_ExecuteAsync_WithDropDatabaseScenario_DatabaseNotExists_DoesNotDrop() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "dropdatabase"; + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + testInstance.SetYcsbPackagePath(this.mockYcsbPackage); + testInstance.SetJdkPackagePath(this.mockJdkPackage); + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + bool checkDbCalled = false; + bool dropDbCalled = false; + + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + if (arguments.Contains("show dbs")) + { + checkDbCalled = true; + // Return output without ycsb database + this.mockFixture.Process.StandardOutput.Append("admin 40KB\nconfig 60KB\n"); + } + if (arguments.Contains("db.dropDatabase()")) + { + dropDbCalled = true; + } + + return this.mockFixture.Process; + }; + + // ACT + await testInstance.ExecuteAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsTrue(checkDbCalled, "Should check if database exists"); + Assert.IsFalse(dropDbCalled, "Should not drop database when it doesn't exist"); + } + } + + [Test] + public async Task MongoDBClientExecutor_ExecuteAsync_WithDropDatabaseVariantScenario_ExecutesDropLogic() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "dropdatabase_atlast"; + + using (TestMongoDBClientExecutor testInstance = new TestMongoDBClientExecutor(this.mockFixture)) + { + testInstance.SetYcsbPackagePath(this.mockYcsbPackage); + testInstance.SetJdkPackagePath(this.mockJdkPackage); + testInstance.SetServerApiClient(this.mockFixture.ApiClient.Object); + + bool checkDbCalled = false; + bool dropDbCalled = false; + + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDirectory) => + { + if (arguments.Contains("show dbs")) + { + checkDbCalled = true; + this.mockFixture.Process.StandardOutput.Append("ycsb 100MB\n"); + } + if (arguments.Contains("db.dropDatabase()")) + { + dropDbCalled = true; + } + + return this.mockFixture.Process; + }; + + // ACT + await testInstance.ExecuteAsync(EventContext.None, CancellationToken.None); + + // ASSERT + Assert.IsTrue(checkDbCalled, "Drop database variant scenario should check database existence"); + Assert.IsTrue(dropDbCalled, "Drop database variant scenario should execute drop logic"); + } + } + + [Test] + public void MongoDBClientExecutor_Scenario_LoadDatabase_ConfiguresCorrectly() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "loaddatabase"; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var scenario = this.mockFixture.Parameters["Scenario"]; + + // ASSERT + Assert.AreEqual("loaddatabase", scenario); + } + + [Test] + public void MongoDBClientExecutor_Scenario_DropDatabase_ConfiguresCorrectly() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "dropdatabase"; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var scenario = this.mockFixture.Parameters["Scenario"]; + + // ASSERT + Assert.AreEqual("dropdatabase", scenario); + } + + [Test] + public void MongoDBClientExecutor_Scenario_RunWorkload_ConfiguresCorrectly() + { + // ARRANGE + this.mockFixture.Parameters["Scenario"] = "runworkload"; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var scenario = this.mockFixture.Parameters["Scenario"]; + + // ASSERT + Assert.AreEqual("runworkload", scenario); + } + + [Test] + public void MongoDBClientExecutor_NormalizeMetricScenarioName_FormatsWorkloadPrefixAsExpected() + { + // ARRANGE + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var method = typeof(MongoDBClientExecutor).GetMethod( + "NormalizeMetricScenarioName", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + // ACT + string normalized = (string)method.Invoke( + executor, + new object[] { "workloada_read50_write50_operationcnt5000000_fieldcnt128_fieldlength128_th32" }); + + // ASSERT + Assert.AreEqual("workload_A_read50_write50_operationcnt5000000_fieldcnt128_fieldlength128_th32", normalized); + } + + [Test] + public void MongoDBClientExecutor_WithCustomPort_StoresConfiguration() + { + // ARRANGE + this.mockFixture.Parameters["Port"] = 27020; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual(27020, this.mockFixture.Parameters["Port"]); + } + + [Test] + public void MongoDBClientExecutor_WithCustomDatabase_StoresConfiguration() + { + // ARRANGE + this.mockFixture.Parameters["Database"] = "customdb"; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual("customdb", this.mockFixture.Parameters["Database"]); + } + + [Test] + public void MongoDBClientExecutor_WithDifferentJdkPackage_StoresConfiguration() + { + // ARRANGE + this.mockFixture.Parameters["JdkPackageName"] = "jdk-custom-11"; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual("jdk-custom-11", executor.JdkPackageName); + } + + [Test] + public void MongoDBClientExecutor_WithDifferentYCSBVersion_StoresConfiguration() + { + // ARRANGE + this.mockFixture.Parameters["YCSBPackageName"] = "ycsb-0.18.0"; + + // ACT + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT + Assert.AreEqual("ycsb-0.18.0", executor.YCSBPackageName); + } + + + [Test] + public void MongoDBClientExecutor_Dispose_CompletesSuccessfully() + { + // ACT & ASSERT + Assert.DoesNotThrow(() => + { + using (var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + // Executor should be disposable without errors + } + }); + } + + + [Test] + public void MongoDBClientExecutor_MultipleInstances_HaveIndependentConfiguration() + { + // ARRANGE + var params1 = new Dictionary(this.mockFixture.Parameters) + { + ["Port"] = 27001, + ["Database"] = "db1" + }; + var params2 = new Dictionary(this.mockFixture.Parameters) + { + ["Port"] = 27002, + ["Database"] = "db2" + }; + + // ACT + var executor1 = new MongoDBClientExecutor(this.mockFixture.Dependencies, params1); + var executor2 = new MongoDBClientExecutor(this.mockFixture.Dependencies, params2); + + // ASSERT + Assert.AreNotEqual(executor1.GetHashCode(), executor2.GetHashCode()); + } + + [Test] + public void MongoDBClientExecutor_Constructor_WithNullParameters_DoesNotThrow() + { + // ACT & ASSERT + Assert.DoesNotThrow(() => + new MongoDBClientExecutor(this.mockFixture.Dependencies, null)); + } + + [Test] + public void MongoDBClientExecutor_Constructor_WithEmptyParameters_DoesNotThrow() + { + // ARRANGE + var emptyParams = new Dictionary(); + + // ACT & ASSERT + Assert.DoesNotThrow(() => + new MongoDBClientExecutor(this.mockFixture.Dependencies, emptyParams)); + } + + } +} diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBExecutorTests.cs new file mode 100644 index 0000000000..96539ef57c --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBExecutorTests.cs @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient.Actions.UnitTests.MongoDB +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Runtime.InteropServices; + using System.Threading; + using System.Threading.Tasks; + using Moq; + using NUnit.Framework; + using VirtualClient.Actions.MongoDB; + using VirtualClient; + using VirtualClient.Actions; + using VirtualClient.Common; + using VirtualClient.Common.Telemetry; + using VirtualClient.Contracts; + using Microsoft.Extensions.DependencyInjection; + + [TestFixture] + [Category("Unit")] + public class MongoDBExecutorTests + { + private MockFixture mockFixture; + + [SetUp] + public void Setup() + { + this.mockFixture = new MockFixture(); + this.mockFixture.Setup(PlatformID.Unix, Architecture.X64); + } + + [Test] + public void MongoDBExecutor_PortParameter_ReturnsDefault27017() + { + // SETUP + this.mockFixture.Parameters = new Dictionary(); + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + int port = executor.Port; + + // ASSERT + Assert.AreEqual(27017, port); + } + + [Test] + public void MongoDBExecutor_PortParameter_ReturnsCustomValue() + { + // SETUP + this.mockFixture.Parameters = new Dictionary { { "Port", 27018 } }; + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + int port = executor.Port; + + // ASSERT + Assert.AreEqual(27018, port); + } + + [Test] + public void MongoDBExecutor_PortParameter_WithStringValue_ConvertsProperly() + { + // SETUP + this.mockFixture.Parameters = new Dictionary { { "Port", "27019" } }; + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + int port = executor.Port; + + // ASSERT + Assert.AreEqual(27019, port); + } + + [Test] + public void MongoDBExecutor_MultipleInstances_MaintainIndependentPorts() + { + // SETUP + var params1 = new Dictionary { { "Port", 27017 } }; + var params2 = new Dictionary { { "Port", 27018 } }; + + // ACT + var executor1 = new MongoDBClientExecutor(this.mockFixture.Dependencies, params1); + var executor2 = new MongoDBClientExecutor(this.mockFixture.Dependencies, params2); + + // ASSERT + Assert.AreEqual(27017, executor1.Port); + Assert.AreEqual(27018, executor2.Port); + } + + [Test] + public void MongoDBExecutor_LargePortNumber_ReturnsCorrectValue() + { + // SETUP + this.mockFixture.Parameters = new Dictionary { { "Port", 65535 } }; + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + int port = executor.Port; + + // ASSERT + Assert.AreEqual(65535, port); + } + + [Test] + public void MongoDBExecutor_MultiplePortRanges_HandledCorrectly() + { + // SETUP + var ports = new[] { 1024, 8080, 27017, 49152, 65535 }; + + // ACT & ASSERT + foreach (var port in ports) + { + this.mockFixture.Parameters = new Dictionary { { "Port", port } }; + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + Assert.AreEqual(port, executor.Port); + } + } + + [Test] + public void MongoDBExecutor_PortAsString_ConvertedToInt() + { + // SETUP + this.mockFixture.Parameters = new Dictionary { { "Port", "28000" } }; + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + int port = executor.Port; + + // ASSERT + Assert.AreEqual(28000, port); + Assert.IsInstanceOf(port); + } + + [Test] + public void MongoDBExecutor_NullPort_ReturnsDefault() + { + // SETUP + this.mockFixture.Parameters = new Dictionary(); + var executor = new MongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + int port = executor.Port; + + // ASSERT + Assert.AreEqual(27017, port); + } + + #region Protected Method Tests (Pattern 1: Derived Test Class) + + /// + /// Test helper class that exposes protected methods using Pattern 1. + /// Inherits from MongoDBClientExecutor and exposes protected members via 'public new' methods. + /// + private class TestableMongoDBClientExecutor : MongoDBClientExecutor + { + public TestableMongoDBClientExecutor(IServiceCollection dependencies, IDictionary parameters = null) + : base(dependencies, parameters) + { + } + + /// + /// Exposes the protected InitializeAsync method for testing. + /// + public new async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken) => await base.InitializeAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + + /// + /// Exposes the protected InitializeApiClients method for testing. + /// + public new void InitializeApiClients() => base.InitializeApiClients(); + + /// + /// Accessor for the ServerApiClient property. + /// + public IApiClient GetServerApiClient() => this.ServerApiClient; + + /// + /// Accessor for the ServerIpAddress property. + /// + public string GetServerIpAddress() => this.ServerIpAddress; + } + + /// + /// Test helper class for MongoDBServerExecutor protected methods. + /// + private class TestableMongoDBServerExecutor : MongoDBServerExecutor + { + public TestableMongoDBServerExecutor(IServiceCollection dependencies, IDictionary parameters = null) + : base(dependencies, parameters) + { + } + + /// + /// Exposes the protected InitializeAsync method for testing. + /// + public new async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken) => await base.InitializeAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + + /// + /// Exposes the protected InitializeApiClients method for testing. + /// + public new void InitializeApiClients() => base.InitializeApiClients(); + + /// + /// Accessor for the ServerApiClient property. + /// + public IApiClient GetServerApiClient() => this.ServerApiClient; + + /// + /// Accessor for the ServerIpAddress property. + /// + public string GetServerIpAddress() => this.ServerIpAddress; + } + + [Test] + public void MongoDBExecutor_InitializeApiClients_CreatesLoopbackClient_OnSingleVMLayout() + { + // SETUP: Single VM layout (no multi-role configuration) + this.mockFixture.Parameters = new Dictionary(); + var executor = new TestableMongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT: Initialize API clients for single VM (uses loopback) + executor.InitializeApiClients(); + + // ASSERT: ServerApiClient should be created for loopback address + IApiClient serverClient = executor.GetServerApiClient(); + Assert.IsNotNull(serverClient, "ServerApiClient should be initialized for single VM"); + } + + [Test] + public void MongoDBExecutor_InitializeApiClients_UsesLoopbackIPAddress_OnSingleVM() + { + // SETUP: Single VM layout + this.mockFixture.Parameters = new Dictionary(); + var executor = new TestableMongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT: Initialize API clients for single VM + executor.InitializeApiClients(); + + // ASSERT: ServerApiClient should be created + IApiClient serverClient = executor.GetServerApiClient(); + Assert.IsNotNull(serverClient, "API client should be created"); + } + + [Test] + public void MongoDBExecutor_InitializeApiClients_AllowsMultipleInvocations() + { + // SETUP: Single VM layout + this.mockFixture.Parameters = new Dictionary(); + var executor = new TestableMongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT: Call InitializeApiClients multiple times + executor.InitializeApiClients(); + executor.InitializeApiClients(); + executor.InitializeApiClients(); + + // ASSERT: Multiple calls should not throw exceptions + IApiClient client = executor.GetServerApiClient(); + Assert.IsNotNull(client, "API client should be set after multiple initializations"); + } + + [Test] + public void MongoDBExecutor_InitializeApiClients_ServerExecutor_CreatesLoopbackClient() + { + // SETUP: Test with MongoDBServerExecutor (derived class) + this.mockFixture.Parameters = new Dictionary(); + var serverExecutor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT: Initialize API clients for server + serverExecutor.InitializeApiClients(); + + // ASSERT: ServerApiClient should be created + IApiClient serverClient = serverExecutor.GetServerApiClient(); + Assert.IsNotNull(serverClient, "Server executor should have ServerApiClient initialized"); + } + + [Test] + public void MongoDBExecutor_InitializeApiClients_WithMultiVMLayout_ConfiguresServerApiClient() + { + // SETUP: Multi-role layout with valid server IP + this.mockFixture.SetupLayout( + new ClientInstance("ClientAgent", "1.2.3.4", ClientRole.Client), + new ClientInstance("ServerAgent", "1.2.3.5", ClientRole.Server)); + + this.mockFixture.Parameters = new Dictionary(); + var executor = new TestableMongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + executor.InitializeApiClients(); + + // ASSERT: ServerApiClient should be created for the server instance IP + IApiClient serverClient = executor.GetServerApiClient(); + string serverIpAddress = executor.GetServerIpAddress(); + + Assert.IsNotNull(serverClient, "ServerApiClient should be initialized for multi-VM layout"); + Assert.IsNotNull(serverIpAddress, "ServerIpAddress should be set"); + Assert.AreEqual("1.2.3.5", serverIpAddress); + } + + [Test] + public void MongoDBExecutor_InitializeApiClients_MaintainsApiClientReference() + { + // SETUP: Single VM layout + this.mockFixture.Parameters = new Dictionary(); + var executor = new TestableMongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT: Initialize API clients + executor.InitializeApiClients(); + IApiClient firstClient = executor.GetServerApiClient(); + + // Initialize again + executor.InitializeApiClients(); + IApiClient secondClient = executor.GetServerApiClient(); + + // ASSERT: Both calls should return a valid client + Assert.IsNotNull(firstClient, "First API client should be created"); + Assert.IsNotNull(secondClient, "Second API client should be created"); + } + + [Test] + public void MongoDBExecutor_ProtectedPort_AccessibleViaTestClass() + { + // SETUP: Create executor with custom parameters + this.mockFixture.Parameters = new Dictionary + { + { "Port", 27018 } + }; + var executor = new TestableMongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT: Access protected Port property + int port = executor.Port; + + // ASSERT: Protected properties should be accessible through test class + Assert.AreEqual(27018, port, "Port property should be accessible"); + } + + [Test] + public void MongoDBExecutor_InitializeApiClients_WithLoopback_ConfiguresCorrectly() + { + // SETUP: Ensure we're testing single VM configuration + this.mockFixture.Parameters = new Dictionary(); + var executor = new TestableMongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT: Initialize API clients which should use loopback + executor.InitializeApiClients(); + + // ASSERT: Verify configuration + IApiClient client = executor.GetServerApiClient(); + Assert.IsNotNull(client, "Client should be initialized"); + + // Verify that the client was created (the mock manager returns a valid client) + Assert.IsTrue(client != null, "API client should be properly configured for loopback communication"); + } + + [Test] + public void MongoDBExecutor_InitializeApiClients_SingleVM_CallsGetOrCreateApiClientWithLoopback() + { + // SETUP: Single VM layout and explicit setup for loopback overload. + this.mockFixture.Parameters = new Dictionary(); + while (this.mockFixture.Dependencies.Any(d => d.ServiceType == typeof(EnvironmentLayout))) + { + var layoutDescriptor = this.mockFixture.Dependencies.First(d => d.ServiceType == typeof(EnvironmentLayout)); + this.mockFixture.Dependencies.Remove(layoutDescriptor); + } + + this.mockFixture.ApiClientManager + .Setup(mgr => mgr.GetOrCreateApiClient(IPAddress.Loopback.ToString(), IPAddress.Loopback, null)) + .Returns(this.mockFixture.ApiClient.Object); + + var executor = new TestableMongoDBClientExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + executor.InitializeApiClients(); + + // ASSERT + this.mockFixture.ApiClientManager.Verify( + mgr => mgr.GetOrCreateApiClient(IPAddress.Loopback.ToString(), IPAddress.Loopback, null), + Times.Once); + Assert.AreSame(this.mockFixture.ApiClient.Object, executor.GetServerApiClient()); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBMetricsParserTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBMetricsParserTests.cs new file mode 100644 index 0000000000..d4082cc5fc --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBMetricsParserTests.cs @@ -0,0 +1,237 @@ +namespace VirtualClient.Actions.UnitTests.MongoDB +{ + using System; + using System.Collections.Generic; + using System.Linq; + using NUnit.Framework; + using VirtualClient; + using VirtualClient.Actions; + using VirtualClient.Contracts; + + [TestFixture] + [Category("Unit")] + public class MongoDBMetricsParserTests + { + private string exampleOutputPath; + + [OneTimeSetUp] + public void OneTimeSetup() + { + // Build path relative to test assembly root; examples copied to output per csproj settings. + string baseDir = TestContext.CurrentContext.TestDirectory; + exampleOutputPath = System.IO.Path.Combine(baseDir, "Examples", "MongoDB", "YCSBMongoDBOutputExample.txt"); + } + + [Test] + public void MongoDBMetricsParser_Constructor_SetsScenarioCorrectly() + { + // Arrange & Act + var parser = new MongoDBMetricsParser("TestScenario", "dummy text"); + + // Assert - We can't directly test private field, but constructor should not throw + Assert.IsNotNull(parser); + } + + [Test] + public void Parse_WithValidYCSBOutput_ParsesAllMetricsCorrectly() + { + // Arrange + Assert.IsTrue(System.IO.File.Exists(exampleOutputPath), $"Example output file not found at {exampleOutputPath}"); + string rawText = System.IO.File.ReadAllText(exampleOutputPath); + var parser = new MongoDBMetricsParser("MongoDBScenario", rawText); + + // Act + IList metrics = parser.Parse(); + + // Assert + Assert.IsNotNull(metrics); + Assert.AreEqual(34, metrics.Count, "Expected exactly 34 metrics from the example output"); + + // Test specific metrics with exact values + AssertContainsMetric(metrics, "OVERALL-RunTime", 177015, "ms"); + AssertContainsMetric(metrics, "OVERALL-Throughput", 28246.19382538203, "ops/sec"); + AssertContainsMetric(metrics, "READ-AverageLatency", 202.70430571259894, "us"); + AssertContainsMetric(metrics, "READ-95thPercentileLatency", 231, "us"); + AssertContainsMetric(metrics, "UPDATE-AverageLatency", 354.05011122992516, "us"); + AssertContainsMetric(metrics, "UPDATE-99thPercentileLatency", 294, "us"); + AssertContainsMetric(metrics, "TOTAL_GCs-Count", 330, ""); + AssertContainsMetric(metrics, "READ-Operations", 2498425, ""); + AssertContainsMetric(metrics, "UPDATE-Return-OK-Count", 2501575, ""); + } + + [Test] + public void Parse_WithMinimalValidOutput_ParsesBasicMetrics() + { + // Arrange + string minimalOutput = @" + [OVERALL], RunTime(ms), 1000 + [OVERALL], Throughput(ops/sec), 100.5 + [READ], Operations, 50 + [READ], AverageLatency(us), 200.0 + [UPDATE], Operations, 50 + [UPDATE], AverageLatency(us), 300.0"; + + var parser = new MongoDBMetricsParser("TestScenario", minimalOutput); + + // Act + IList metrics = parser.Parse(); + + // Assert + Assert.IsNotNull(metrics); + Assert.AreEqual(6, metrics.Count); + + AssertContainsMetric(metrics, "OVERALL-RunTime", 1000, "ms"); + AssertContainsMetric(metrics, "OVERALL-Throughput", 100.5, "ops/sec"); + AssertContainsMetric(metrics, "READ-Operations", 50, ""); + AssertContainsMetric(metrics, "READ-AverageLatency", 200.0, "us"); + AssertContainsMetric(metrics, "UPDATE-Operations", 50, ""); + AssertContainsMetric(metrics, "UPDATE-AverageLatency", 300.0, "us"); + } + + [Test] + public void Parse_SetsCorrectMetricRelativity_ForDifferentMetricTypes() + { + // Arrange + string outputWithDifferentTypes = @" + [OVERALL], Throughput(ops/sec), 1000 + [READ], AverageLatency(us), 200 + [UPDATE], 95thPercentileLatency(us), 300 + [CLEANUP], 99thPercentileLatency(us), 400"; + + var parser = new MongoDBMetricsParser("TestScenario", outputWithDifferentTypes); + + // Act + IList metrics = parser.Parse(); + + // Assert + AssertContainsMetric(metrics, "OVERALL-Throughput", 1000, "ops/sec", MetricRelativity.HigherIsBetter); + AssertContainsMetric(metrics, "READ-AverageLatency", 200, "us", MetricRelativity.LowerIsBetter); + AssertContainsMetric(metrics, "UPDATE-95thPercentileLatency", 300, "us", MetricRelativity.LowerIsBetter); + AssertContainsMetric(metrics, "CLEANUP-99thPercentileLatency", 400, "us", MetricRelativity.LowerIsBetter); + } + + [Test] + public void Parse_HandlesMetricsWithReturnValues() + { + // Arrange + string outputWithReturns = @" + [OVERALL], Throughput(ops/sec), 1000 + [READ], Return=OK, 500 + [READ], Return=ERROR, 5 + [UPDATE], Return=NOT_FOUND, 10 + [UPDATE], Return=OK, 490"; + + var parser = new MongoDBMetricsParser("TestScenario", outputWithReturns); + + // Act + IList metrics = parser.Parse(); + + // Assert + Assert.AreEqual(5, metrics.Count); + AssertContainsMetric(metrics, "OVERALL-Throughput", 1000, "ops/sec"); + AssertContainsMetric(metrics, "READ-Return-OK-Count", 500, ""); + AssertContainsMetric(metrics, "READ-Return-ERROR-Count", 5, ""); + AssertContainsMetric(metrics, "UPDATE-Return-NOT_FOUND-Count", 10, ""); + AssertContainsMetric(metrics, "UPDATE-Return-OK-Count", 490, ""); + } + + [Test] + public void Parse_ThrowsWorkloadException_WhenOverallSectionMissing() + { + // Arrange + string outputWithoutOverall = @" + [READ], Operations, 100 + [UPDATE], Operations, 100"; + + var parser = new MongoDBMetricsParser("TestScenario", outputWithoutOverall); + + // Act & Assert + var exception = Assert.Throws(() => parser.Parse()); + Assert.AreEqual(ErrorReason.WorkloadResultsParsingFailed, exception.Reason); + StringAssert.Contains("Benchmarking metrics are not present", exception.Message); + } + + [Test] + public void Parse_ThrowsWorkloadException_WhenRawTextIsEmpty() + { + // Arrange + var parser = new MongoDBMetricsParser("TestScenario", ""); + + // Act & Assert + var exception = Assert.Throws(() => parser.Parse()); + Assert.AreEqual(ErrorReason.WorkloadResultsParsingFailed, exception.Reason); + } + + [Test] + public void Parse_ThrowsWorkloadException_WhenRawTextIsNull() + { + // Arrange + var parser = new MongoDBMetricsParser("TestScenario", null); + + // Act & Assert + Assert.Throws(() => parser.Parse()); + } + + [Test] + public void Parse_HandlesMalformedLines_Gracefully() + { + // Arrange - Some lines have wrong format, but parser should still work with valid ones + string outputWithMalformed = @" + // Valid overall section + [OVERALL], RunTime(ms), 1000 + [OVERALL], Throughput(ops/sec), 500 + Malformed line without proper structure + ,,, + // Valid metrics + [READ], AverageLatency(us), 200 + Bad line + [UPDATE], AverageLatency(us), 300"; + + var parser = new MongoDBMetricsParser("TestScenario", outputWithMalformed); + + // Act + IList metrics = parser.Parse(); + + // Assert - Should parse the valid metrics and ignore malformed ones + Assert.IsNotNull(metrics); + Assert.GreaterOrEqual(metrics.Count, 3); // At least the valid ones + + AssertContainsMetric(metrics, "OVERALL-RunTime", 1000, "ms"); + AssertContainsMetric(metrics, "OVERALL-Throughput", 500, "ops/sec"); + AssertContainsMetric(metrics, "READ-AverageLatency", 200, "us"); + AssertContainsMetric(metrics, "UPDATE-AverageLatency", 300, "us"); + } + + [Test] + public void Parse_HandlesMetricsWithUnitsContainingParentheses() + { + // Arrange + string outputWithComplexUnits = @" + [OVERALL], RunTime(ms), 1000 + [TOTAL_GC_TIME_%_G1_Young_Generation], Time(%), 0.5 + [TOTAL_GC_TIME_G1_Concurrent_GC], Time(ms), 150"; + + var parser = new MongoDBMetricsParser("TestScenario", outputWithComplexUnits); + + // Act + IList metrics = parser.Parse(); + + // Assert + AssertContainsMetric(metrics, "OVERALL-RunTime", 1000, "ms"); + AssertContainsMetric(metrics, "TOTAL_GC_TIME_%_G1_Young_Generation-Time", 0.5, "%"); + AssertContainsMetric(metrics, "TOTAL_GC_TIME_G1_Concurrent_GC-Time", 150, "ms"); + } + + private static void AssertContainsMetric(IList metrics, string name, double value, string unit, MetricRelativity? expectedRelativity = null) + { + var metric = metrics.FirstOrDefault(m => m.Name == name); + Assert.IsNotNull(metric, $"Metric '{name}' not found in parsed metrics"); + Assert.AreEqual(value, metric.Value, 0.0001, $"Metric '{name}' value mismatch"); + Assert.AreEqual(unit, metric.Unit, $"Metric '{name}' unit mismatch"); + if (expectedRelativity.HasValue) + { + Assert.AreEqual(expectedRelativity.Value, metric.Relativity, $"Metric '{name}' relativity mismatch"); + } + } + } +} diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBServerExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBServerExecutorTests.cs new file mode 100644 index 0000000000..e188862b90 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/MongoDB/MongoDBServerExecutorTests.cs @@ -0,0 +1,510 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient.Actions.UnitTests.MongoDB +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Reflection; + using System.Runtime.InteropServices; + using System.Threading; + using System.Threading.Tasks; + using Moq; + using Newtonsoft.Json.Linq; + using NUnit.Framework; + using Polly; + using VirtualClient; + using VirtualClient.Actions; + using VirtualClient.Common; + using VirtualClient.Common.Telemetry; + using VirtualClient.Contracts; + using Microsoft.Extensions.DependencyInjection; + + [TestFixture] + [Category("Unit")] + public class MongoDBServerExecutorTests + { + private MockFixture mockFixture; + + // Testable derived class to expose protected members using Pattern 1 + private class TestableMongoDBServerExecutor : MongoDBServerExecutor + { + public TestableMongoDBServerExecutor(IServiceCollection dependencies, IDictionary parameters) + : base(dependencies, parameters) + { + } + + // Expose protected methods + public new void InitializeApiClients() => base.InitializeApiClients(); + + public new Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken) + => base.InitializeAsync(telemetryContext, cancellationToken); + + public new Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken) + => base.ExecuteAsync(telemetryContext, cancellationToken); + + // Expose protected properties via getter methods + public IApiClient GetServerApiClient() => this.ServerApiClient; + public string GetServerIpAddress() => this.ServerIpAddress; + public CancellationTokenSource GetServerCancellationSource() => this.ServerCancellationSource; + public int GetPort() => this.Port; + } + + [SetUp] + public void Setup() + { + this.mockFixture = new MockFixture(); + this.mockFixture.Setup(PlatformID.Unix, Architecture.X64); + + // Setup default parameters + this.mockFixture.Parameters = new Dictionary + { + { "Scenario", "MongoDB-Server" }, + { "DiskFilter", string.Empty } + }; + + // Setup process manager mock + this.mockFixture.ProcessManager.OnCreateProcess = (exe, args, workingDir) => + { + this.mockFixture.Process.StandardOutput.Clear(); + this.mockFixture.Process.StandardOutput.Append("{ \"ok\" : 1 }"); + this.mockFixture.Process.ExitCode = 0; + return this.mockFixture.Process; + }; + } + + [Test] + public void MongoDBServerExecutor_DiskFilter_ReturnsDefaultEmptyString() + { + // SETUP: Parameters without DiskFilter + this.mockFixture.Parameters = new Dictionary + { + { "Scenario", "MongoDB-Server" } + }; + var executor = new MongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + string diskFilter = executor.DiskFilter; + + // ASSERT + Assert.AreEqual(string.Empty, diskFilter); + } + + [Test] + public void MongoDBServerExecutor_DiskFilter_ReturnsParameterValue() + { + // SETUP: Parameters with DiskFilter + this.mockFixture.Parameters["DiskFilter"] = "BiggestSize"; + var executor = new MongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + string diskFilter = executor.DiskFilter; + + // ASSERT + Assert.AreEqual("BiggestSize", diskFilter); + } + + [Test] + public void MongoDBServerExecutor_DiskFilter_WithDifferentValues_ReturnsCorrectly() + { + // SETUP + var executor1 = new MongoDBServerExecutor(this.mockFixture.Dependencies, + new Dictionary { { "DiskFilter", "SmallestSize" } }); + var executor2 = new MongoDBServerExecutor(this.mockFixture.Dependencies, + new Dictionary { { "DiskFilter", "BiggestSize" } }); + + // ACT + string filter1 = executor1.DiskFilter; + string filter2 = executor2.DiskFilter; + + // ASSERT + Assert.AreEqual("SmallestSize", filter1); + Assert.AreEqual("BiggestSize", filter2); + } + + [Test] + public void MongoDBServerExecutor_Constructor_InitializesProperties() + { + // SETUP + var executor = new MongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT - verify constructor properly initializes the executor + Assert.IsNotNull(executor, "Executor should be created"); + } + + [Test] + public void MongoDBServerExecutor_Dispose_CompletesSuccessfully() + { + // SETUP: Create executor + var executor = new MongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT: Dispose + executor.Dispose(); + + // ASSERT: Should not throw exception + Assert.Pass("Dispose completed successfully"); + } + + [Test] + public void MongoDBServerExecutor_DisposeTwice_DoesNotThrow() + { + // SETUP: Create executor + var executor = new MongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT: Dispose twice + executor.Dispose(); + executor.Dispose(); + + // ASSERT: Should not throw exception + Assert.Pass("Double dispose handled correctly"); + } + + [Test] + public void MongoDBServerExecutor_Port_ReturnsDefaultValue() + { + // SETUP + this.mockFixture.Parameters = new Dictionary { { "Scenario", "MongoDB-Server" } }; + var executor = new MongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + int port = executor.Port; + + // ASSERT + Assert.AreEqual(27017, port); + } + + #region Protected Member Tests Using Pattern 1 + + [Test] + public void MongoDBServerExecutor_InitializeApiClients_CreatesServerApiClient() + { + // SETUP + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + executor.InitializeApiClients(); + + // ASSERT + Assert.IsNotNull(executor.GetServerApiClient(), "ServerApiClient should be initialized"); + } + + [Test] + public void MongoDBServerExecutor_InitializeApiClients_UsesLoopbackIPAddress_SingleVM() + { + // SETUP + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + executor.InitializeApiClients(); + + // ASSERT + string serverIp = executor.GetServerIpAddress(); + Assert.IsNotNull(serverIp, "ServerIpAddress should be set"); + } + + [Test] + public void MongoDBServerExecutor_InitializeApiClients_MultipleInvocations_DoesNotThrow() + { + // SETUP + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT & ASSERT + Assert.DoesNotThrow(() => executor.InitializeApiClients()); + Assert.DoesNotThrow(() => executor.InitializeApiClients(), "Multiple calls should be safe"); + } + + [Test] + public void MongoDBServerExecutor_ProtectedPort_AccessibleViaTestClass() + { + // SETUP + this.mockFixture.Parameters["Port"] = 27019; + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + int port = executor.GetPort(); + + // ASSERT + Assert.AreEqual(27019, port); + } + + [Test] + public void MongoDBServerExecutor_ServerCancellationSource_InitiallyNull() + { + // SETUP + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + var cancellationSource = executor.GetServerCancellationSource(); + + // ASSERT + Assert.IsNull(cancellationSource, "ServerCancellationSource should be null before initialization"); + } + + [Test] + public void MongoDBServerExecutor_InitializeApiClients_PreservesApiClientReference() + { + // SETUP + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + executor.InitializeApiClients(); + var apiClient1 = executor.GetServerApiClient(); + var apiClient2 = executor.GetServerApiClient(); + + // ASSERT + Assert.AreSame(apiClient1, apiClient2, "ServerApiClient reference should remain consistent"); + } + + + [Test] + public void MongoDBServerExecutor_ServerApiClient_NullBeforeInitialization() + { + // SETUP + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + var apiClient = executor.GetServerApiClient(); + + // ASSERT + Assert.IsNull(apiClient, "ServerApiClient should be null before InitializeApiClients is called"); + } + + [Test] + public void MongoDBServerExecutor_DiskFilter_WithCustomFilter_ConfiguresCorrectly() + { + // SETUP + this.mockFixture.Parameters["DiskFilter"] = "BiggestSize"; + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + string diskFilter = executor.DiskFilter; + + // ASSERT + Assert.AreEqual("BiggestSize", diskFilter); + } + + [Test] + public void MongoDBServerExecutor_InitializeAsync_CallsBaseInitialization() + { + // SETUP + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var context = new EventContext(Guid.NewGuid()); + var cancellationToken = CancellationToken.None; + + // Setup mock for process execution + this.mockFixture.ProcessManager.OnCreateProcess = (exe, args, workingDir) => + { + this.mockFixture.Process.StandardOutput.Clear(); + this.mockFixture.Process.StandardOutput.Append("{ \"ok\" : 1 }"); + this.mockFixture.Process.ExitCode = 0; + return this.mockFixture.Process; + }; + + // ACT & ASSERT - Should not throw (InitializeAsync attempts to start MongoDB which will fail in test, but we verify it doesn't throw on base initialization) + Assert.DoesNotThrowAsync(async () => await executor.InitializeAsync(context, cancellationToken)); + } + + [Test] + public async Task MongoDBServerExecutor_InitializeAsync_WithDiskFilter_CallsInitializeDiskPathAndConfigureDisk() + { + // SETUP: Set DiskFilter to trigger disk initialization path + this.mockFixture.Parameters["DiskFilter"] = "BiggestSize"; + this.mockFixture.Parameters["DiskDevicePath"] = "/dev/nvme0n1"; // Pre-set to avoid KeyNotFound + + var volumes = new List(); + var disk = new Disk(index: 0, devicePath: "/dev/nvme0n1", volumes: volumes, properties: null); + this.mockFixture.DiskManager.Setup(dm => dm.GetDisksAsync(It.IsAny())) + .ReturnsAsync(new List { disk }); + + this.mockFixture.ProcessManager.OnCreateProcess = (exe, args, workingDir) => + { + this.mockFixture.Process.StandardOutput.Clear(); + this.mockFixture.Process.StandardOutput.Append("{ \"ok\" : 1 }"); + this.mockFixture.Process.ExitCode = 0; + return this.mockFixture.Process; + }; + + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var context = EventContext.Persisted(); + + // ACT: Initialize executor - should trigger disk initialization workflow + await executor.InitializeAsync(context, CancellationToken.None); + + // ASSERT: Verify the DiskFilter caused disk initialization code path to execute + // InitializeDiskPathAsync should have been called (it retrieves disks and filters them) + Assert.AreEqual("BiggestSize", this.mockFixture.Parameters["DiskFilter"], + "DiskFilter should remain set after initialization"); + + // InitializeApiClients should have been called + Assert.IsNotNull(executor.GetServerApiClient(), + "ServerApiClient should be initialized"); + } + + [Test] + public async Task MongoDBServerExecutor_InitializeAsync_CallsConfigureBindAddressAndStartServer() + { + // SETUP: Create executor without DiskFilter to focus on server startup workflow + this.mockFixture.Parameters["DiskFilter"] = string.Empty; // No disk configuration + + // Both OnCreateProcess and CreateElevatedProcess will be called - both return same mocked process + this.mockFixture.ProcessManager.OnCreateProcess = (exe, args, workingDir) => + { + this.mockFixture.Process.StandardOutput.Clear(); + this.mockFixture.Process.StandardError.Clear(); + this.mockFixture.Process.StandardOutput.Append("{\"ok\" : 1}"); + this.mockFixture.Process.ExitCode = 0; + return this.mockFixture.Process; + }; + + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var context = EventContext.Persisted(); + + // ACT: Initialize executor - should call ConfigureMongoDBBindAddressAsync → StartMongoDBServerAsync + await executor.InitializeAsync(context, CancellationToken.None); + + // ASSERT: Verify ServerApiClient was initialized + Assert.IsNotNull(executor.GetServerApiClient(), + "InitializeApiClients should have been called, ServerApiClient should be initialized"); + } + + [Test] + public async Task MongoDBServerExecutor_ExecuteAsync_CompletesWhenCancelled() + { + // SETUP + this.mockFixture.Parameters["DiskFilter"] = string.Empty; + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var context = EventContext.Persisted(); + + // Initialize API clients first + executor.InitializeApiClients(); + + // Create a cancellation token that will cancel after a short delay + using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100))) + { + // ACT & ASSERT: Execute should complete without throwing when cancelled + await executor.ExecuteAsync(context, cts.Token); + + // If we reach here, the test passed - ExecuteAsync handled cancellation properly + Assert.Pass("ExecuteAsync completed successfully when cancelled"); + } + } + + [Test] + public void MongoDBServerExecutor_Constructor_InitializesServerRetryPolicy() + { + // SETUP & ACT + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ASSERT - Verify constructor completes successfully and object is created + Assert.IsNotNull(executor, "Executor should be initialized with retry policy"); + } + + [Test] + public void MongoDBServerExecutor_DiskFilter_EmptyByDefault() + { + // SETUP - No DiskFilter parameter + this.mockFixture.Parameters.Remove("DiskFilter"); + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + string diskFilter = executor.DiskFilter; + + // ASSERT + Assert.AreEqual(string.Empty, diskFilter, "DiskFilter should be empty by default"); + } + + [Test] + public void MongoDBServerExecutor_InitializeApiClients_SetsServerIpAddress() + { + // SETUP + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + executor.InitializeApiClients(); + string serverIp = executor.GetServerIpAddress(); + + // ASSERT + Assert.IsNotNull(serverIp, "ServerIpAddress should be set after InitializeApiClients"); + Assert.IsNotEmpty(serverIp, "ServerIpAddress should not be empty"); + } + + [Test] + public void MongoDBServerExecutor_WithNullDiskFilter_HandlesCorrectly() + { + // SETUP - Set DiskFilter to null + this.mockFixture.Parameters["DiskFilter"] = null; + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + + // ACT + string diskFilter = executor.DiskFilter; + + // ASSERT + Assert.AreEqual(string.Empty, diskFilter, "Null DiskFilter should be treated as empty string"); + } + + [Test] + public void MongoDBServerExecutor_InitializeAsync_ThrowsWhenNoDisksAvailableForFilter() + { + // SETUP: Set DiskFilter and mock DiskManager to return no disks + this.mockFixture.Parameters["DiskFilter"] = "BiggestSize"; + this.mockFixture.DiskManager.Setup(dm => dm.GetDisksAsync(It.IsAny())) + .ReturnsAsync(new List()); // No disks returned + + this.mockFixture.ProcessManager.OnCreateProcess = (exe, args, workingDir) => + { + this.mockFixture.Process.StandardOutput.Clear(); + this.mockFixture.Process.StandardOutput.Append("{ \"ok\" : 1 }"); + this.mockFixture.Process.ExitCode = 0; + return this.mockFixture.Process; + }; + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var context = EventContext.Persisted(); + + var ex = Assert.ThrowsAsync(async () => + await executor.InitializeAsync(context, CancellationToken.None)); + + StringAssert.Contains("No disks are available on the system to match the filter criteria", ex.Message); + } + + [Test] + public void MongoDBServerExecutor_InitializeAsync_ThrowsWhenNoDiskMatchesFilter() + { + // SETUP: Set a DiskFilter but return disks that don't match + this.mockFixture.Parameters["DiskFilter"] = "DiskPath:/dev/sdb"; + this.mockFixture.Parameters["DiskDevicePath"] = "/dev/nvme0n1"; + + // Mock DiskManager to return disks that don't match the filter (e.g., HDD disks) + var volumes = new List(); + var hddDisk = new Disk( + index: 0, + devicePath: "/dev/sda", + volumes: volumes, + properties: new Dictionary { { "DiskType", "HDD" } }); + + this.mockFixture.DiskManager.Setup(dm => dm.GetDisksAsync(It.IsAny())) + .ReturnsAsync(new List { hddDisk }); + + this.mockFixture.ProcessManager.OnCreateProcess = (exe, args, workingDir) => + { + this.mockFixture.Process.StandardOutput.Clear(); + this.mockFixture.Process.StandardOutput.Append("{ \"ok\" : 1 }"); + this.mockFixture.Process.ExitCode = 0; + return this.mockFixture.Process; + }; + + var executor = new TestableMongoDBServerExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters); + var context = EventContext.Persisted(); + + // ACT & ASSERT: Should throw SchemaException with "No disks matched the filter criteria" + var ex = Assert.ThrowsAsync(async () => + await executor.InitializeAsync(context, CancellationToken.None)); + + StringAssert.Contains("No disks matched the filter criteria", ex.Message); + } + + #endregion + } +} + diff --git a/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBClientExecutor.cs b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBClientExecutor.cs new file mode 100644 index 0000000000..6792906ad4 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBClientExecutor.cs @@ -0,0 +1,584 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient.Actions.MongoDB +{ + using System; + using System.Collections.Generic; + using System.IO.Abstractions; + using System.Linq; + using System.Runtime.InteropServices; + using System.Text.RegularExpressions; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Polly; + using VirtualClient; + using VirtualClient.Common; + using VirtualClient.Common.Extensions; + using VirtualClient.Common.Platform; + using VirtualClient.Common.Telemetry; + using VirtualClient.Contracts; + using VirtualClient.Contracts.Metadata; + + /// + /// MongoDB Client Executor - Handles YCSB workload execution against MongoDB server. + /// + [SupportedPlatforms("linux-arm64,linux-x64")] + public class MongoDBClientExecutor : MongoDBExecutor + { + private IFileSystem fileSystem; + private ISystemManagement systemManagement; + private IPackageManager packageManager; + + /// + /// Initializes a new instance of the class. + /// + /// Provides all of the required dependencies to the Virtual Client component. + /// An enumeration of key-value pairs that can control the execution of the component. + public MongoDBClientExecutor(IServiceCollection dependencies, IDictionary parameters = null) + : base(dependencies, parameters) + { + this.packageManager = dependencies.GetService(); + this.fileSystem = dependencies.GetService(); + this.systemManagement = dependencies.GetService(); + + this.ClientFlowRetryPolicy = Policy.Handle(exc => !(exc is OperationCanceledException)) + .WaitAndRetryAsync(3, retries => TimeSpan.FromSeconds(retries * 2)); + + this.ClientRetryPolicy = Policy.Handle(exc => !(exc is OperationCanceledException)) + .WaitAndRetryAsync(3, retries => TimeSpan.FromSeconds(retries)); + + this.PollingTimeout = TimeSpan.FromMinutes(40); + } + + /// + /// Defines the name of the YCSB package. + /// + public string YCSBPackageName + { + get + { + return this.Parameters.GetValue(nameof(this.YCSBPackageName), "ycsb"); + } + } + + /// + /// Gets the run command specified for the MongoDB YCSB workload. + /// + public string RunCommand + { + get + { + return this.Parameters.GetValue(nameof(MongoDBClientExecutor.RunCommand), string.Empty); + } + } + + /// + /// Gets the load command specified for loading the MongoDB YCSB database. + /// + public string LoadCommand + { + get + { + return this.Parameters.GetValue(nameof(MongoDBClientExecutor.LoadCommand), string.Empty); + } + } + + /// + /// Java Development Kit package name. + /// + public string JdkPackageName + { + get + { + return this.Parameters.GetValue(nameof(MongoDBClientExecutor.JdkPackageName), "javadevelopmentkit"); + } + } + + /// + /// Version of workload from YCSB that will be run. Ex. workloada, workload..., workloadf. + /// + public string WorkloadName + { + get + { + return this.Parameters.GetValue(nameof(MongoDBClientExecutor.WorkloadName)); + } + } + + /// + /// The file path for YCSB workloads. + /// + protected string YcsbPackagePath { get; set; } + + /// + /// The path to the YCSB executable script (platform-specific). + /// + protected string YcsbExecutablePath { get; set; } + + /// + /// The setenv.sh path for YCSB workloads. + /// + protected string YcsbSetEnvPath { get; set; } + + /// + /// Export string for JAVA_HOME. + /// + protected string JavaExportString { get; set; } + + /// + /// The folder name for YCSB. + /// + protected string YCSBFolderName { get; set; } = "ycsb-0.17.0"; + + /// + /// The file path for JDK. + /// + protected string JDKPackagePath { get; set; } + + /// + /// The retry policy to apply to the client-side execution workflow. + /// + protected IAsyncPolicy ClientFlowRetryPolicy { get; set; } + + /// + /// The retry policy to apply to each workload instance. + /// + protected IAsyncPolicy ClientRetryPolicy { get; set; } + + /// + /// The timespan at which the client will poll the server for responses before timing out. + /// + protected TimeSpan PollingTimeout { get; set; } + + /// + /// Initializes the environment and dependencies for running YCSB workload. + /// + protected override async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + await base.InitializeAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + + this.InitializeApiClients(); + + // Checks to make sure packages were installed correctly and sets the paths for the packages. + await this.InitializePackageLocationAsync(cancellationToken).ConfigureAwait(false); + + // Sets up YCSB dependencies and environment. + await this.SetYCSBDependenciesAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + + // Wait for server to be online + await this.WaitForServerOnlineAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + } + + /// + /// Executes the MongoDB YCSB workload. + /// + protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + try + { + // If loading database, log size after load + if (this.Scenario?.StartsWith("loaddatabase", StringComparison.OrdinalIgnoreCase) == true) + { + await this.LoadDatabaseAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + await this.LogDatabaseStatsAsync("MongoDB-StatsAfterLoad", telemetryContext, cancellationToken) + .ConfigureAwait(false); + } + + // Drop database if scenario is 'dropdatabase' and exit + else if (this.Scenario?.StartsWith("dropdatabase", StringComparison.OrdinalIgnoreCase) == true) + { + bool dbExists = await this.CheckDatabaseExistsAsync(telemetryContext, cancellationToken) + .ConfigureAwait(false); + + if (dbExists) + { + await this.LogDatabaseStatsAsync("MongoDB-StatsBeforeDrop", telemetryContext, cancellationToken) + .ConfigureAwait(false); + await this.DropDatabaseAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + await this.LogDatabaseStatsAsync("MongoDB-StatsAfterDrop", telemetryContext, cancellationToken) + .ConfigureAwait(false); + } + else + { + this.Logger.LogMessage( + $"{nameof(MongoDBClientExecutor)}.DatabaseDoesNotExistBeforeDrop", + LogLevel.Information, + telemetryContext.Clone().AddContext("database", "ycsb")); + } + } + else + { + // makes the YCSB executable file + await this.systemManagement.MakeFileExecutableAsync(this.YcsbExecutablePath, this.Platform, cancellationToken) + .ConfigureAwait(false); + + // Executes the run portion of the workload + DateTime runStartTime = DateTime.UtcNow; + + // Replace connection string placeholder with actual server IP + string runCommand = this.RunCommand.Replace("{ServerIP}", this.ServerIpAddress); + runCommand = runCommand.Replace("{Port}", this.Port.ToString()); + + var runOutput = await this.ExecuteCommandAsync( + $"{this.YcsbExecutablePath}", + runCommand, + this.YcsbPackagePath, + telemetryContext, + cancellationToken).ConfigureAwait(false); + + DateTime runFinishTime = DateTime.UtcNow; + + // Formats and sends out metrics from runOutput (workload output) and other telemetry parameters + this.CaptureMetrics(runOutput, runCommand, runStartTime, runFinishTime, telemetryContext, cancellationToken); + } + } + catch (OperationCanceledException ex) + { + telemetryContext.AddError(ex); + this.Logger.LogTraceMessage($"{nameof(MongoDBClientExecutor)}.Exception", telemetryContext); + } + } + + /// + /// Waits for the server to be online. + /// + private async Task WaitForServerOnlineAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + if (!cancellationToken.IsCancellationRequested) + { + this.Logger.LogMessage( + $"{nameof(MongoDBClientExecutor)}.WaitingForServer", + LogLevel.Information, + telemetryContext.Clone().AddContext("serverIpAddress", this.ServerIpAddress)); + + await this.ServerApiClient.PollForServerOnlineAsync(this.PollingTimeout, cancellationToken) + .ConfigureAwait(false); + + this.Logger.LogMessage( + $"{nameof(MongoDBClientExecutor)}.ServerOnline", + LogLevel.Information, + telemetryContext.Clone().AddContext("serverIpAddress", this.ServerIpAddress)); + } + } + + /// + /// Sets up YCSB dependencies and environment. + /// + private async Task SetYCSBDependenciesAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + if (!cancellationToken.IsCancellationRequested) + { + try + { + // The path ending in ycsb.sh is specific to the linux platform + this.YcsbExecutablePath = this.PlatformSpecifics.Combine(this.YcsbPackagePath, this.YCSBFolderName, "bin", "ycsb.sh"); + + // Make the file executable + await this.systemManagement.MakeFileExecutableAsync(this.YcsbExecutablePath, this.Platform, cancellationToken) + .ConfigureAwait(false); + + this.JavaExportString = $"export JAVA_HOME={this.JDKPackagePath}"; + + // Create script setenv.sh in YCSB_HOME/bin to set the variables + this.YcsbSetEnvPath = this.PlatformSpecifics.Combine(this.YcsbPackagePath, this.YCSBFolderName, "bin", "setenv.sh"); + await this.ExecuteCommandAsync( + command: "bash", + commandArguments: $"-c \"echo {this.JavaExportString} > {this.YcsbSetEnvPath}\"", + workingDirectory: this.YcsbPackagePath, + telemetryContext: telemetryContext, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + EventContext relatedContext = telemetryContext.Clone().AddError(ex); + this.Logger.LogMessage($"{nameof(MongoDBClientExecutor)}.SetYCSBDependenciesFailed", LogLevel.Error, relatedContext); + throw; + } + } + } + + /// + /// Loads data into the MongoDB database using YCSB. + /// + private async Task LoadDatabaseAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + if (!cancellationToken.IsCancellationRequested) + { + try + { + // Sets JAVA_HOME environment variable and makes the YCSB executable file + this.SetEnvironmentVariable(EnvironmentVariable.JAVA_HOME, this.JDKPackagePath, EnvironmentVariableTarget.Process); + await this.systemManagement.MakeFileExecutableAsync(this.YcsbExecutablePath, this.Platform, cancellationToken) + .ConfigureAwait(false); + + // Replace connection string placeholder with actual server IP + string loadCommand = this.LoadCommand.Replace("{ServerIP}", this.ServerIpAddress); + loadCommand = loadCommand.Replace("{Port}", this.Port.ToString()); + + EventContext relatedContext = EventContext.Persisted() + .AddContext("scenario", this.Scenario) + .AddContext("database", "ycsb") + .AddContext("loadCommand", loadCommand) + .AddContext("serverIpAddress", this.ServerIpAddress); + + this.Logger.LogMessage($"{nameof(MongoDBClientExecutor)}.LoadingDatabase", LogLevel.Information, relatedContext); + + // Execute the load command + DateTime loadStartTime = DateTime.UtcNow; + + string loadOutput = await this.ExecuteCommandAsync( + this.YcsbExecutablePath, + loadCommand, + this.YcsbPackagePath, + telemetryContext, + cancellationToken).ConfigureAwait(false); + + DateTime loadFinishTime = DateTime.UtcNow; + + // Capture metrics for load-database scenario the same way as run workloads. + this.CaptureMetrics(loadOutput, loadCommand, loadStartTime, loadFinishTime, telemetryContext, cancellationToken); + + this.Logger.LogMessage($"{nameof(MongoDBClientExecutor)}.DatabaseLoadedSuccessfully", LogLevel.Information, relatedContext); + } + catch (Exception ex) + { + EventContext relatedContext = telemetryContext.Clone().AddError(ex); + this.Logger.LogMessage($"{nameof(MongoDBClientExecutor)}.LoadDatabaseFailed", LogLevel.Error, relatedContext); + throw; + } + } + } + + /// + /// Drops the MongoDB database if specified in the scenario. + /// + private async Task DropDatabaseAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + if (!cancellationToken.IsCancellationRequested) + { + try + { + string dropDatabaseCommand = $"mongosh mongodb://{this.ServerIpAddress}:{this.Port}/ycsb --eval 'db.dropDatabase()'"; + + EventContext relatedContext = EventContext.Persisted() + .AddContext("command", dropDatabaseCommand) + .AddContext("scenario", this.Scenario) + .AddContext("database", "ycsb") + .AddContext("serverIpAddress", this.ServerIpAddress); + + this.Logger.LogMessage($"{nameof(MongoDBClientExecutor)}.DroppingDatabase", LogLevel.Information, relatedContext); + + using (IProcessProxy process = this.systemManagement.ProcessManager.CreateElevatedProcess( + this.Platform, "mongosh", $"mongodb://{this.ServerIpAddress}:{this.Port}/ycsb --eval 'db.dropDatabase()'")) + { + this.CleanupTasks.Add(() => process.SafeKill()); + this.LogProcessTrace(process); + + await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + + if (!cancellationToken.IsCancellationRequested) + { + await this.LogProcessDetailsAsync(process, telemetryContext, "MongoDBClient-DropDatabase", logToFile: true) + .ConfigureAwait(false); + + if (process.ExitCode != 0) + { + this.Logger.LogMessage( + $"{nameof(MongoDBClientExecutor)}.DropDatabaseWarning", + LogLevel.Warning, + relatedContext.Clone().AddContext("exitCode", process.ExitCode)); + } + else + { + this.Logger.LogMessage( + $"{nameof(MongoDBClientExecutor)}.DatabaseDroppedSuccessfully", + LogLevel.Information, + relatedContext); + } + } + } + } + catch (Exception ex) + { + EventContext relatedContext = telemetryContext.Clone().AddError(ex); + this.Logger.LogMessage($"{nameof(MongoDBClientExecutor)}.DropDatabaseFailed", LogLevel.Warning, relatedContext); + } + } + } + + /// + /// Checks if the specified database exists. + /// + private async Task CheckDatabaseExistsAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + using (IProcessProxy process = this.systemManagement.ProcessManager.CreateElevatedProcess( + this.Platform, "mongosh", $"mongodb://{this.ServerIpAddress}:{this.Port} --eval \"show dbs\"")) + { + await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + string output = process.StandardOutput.ToString(); + bool exists = output.Contains("ycsb", StringComparison.OrdinalIgnoreCase); + + this.Logger.LogMessage( + $"{nameof(MongoDBClientExecutor)}.CheckDatabaseExists", + LogLevel.Information, + telemetryContext.Clone() + .AddContext("database", "ycsb") + .AddContext("exists", exists) + .AddContext("serverIpAddress", this.ServerIpAddress)); + + return exists; + } + } + + /// + /// Logs the database statistics. + /// + private async Task LogDatabaseStatsAsync(string logTag, EventContext telemetryContext, CancellationToken cancellationToken) + { + using (IProcessProxy process = this.systemManagement.ProcessManager.CreateElevatedProcess( + this.Platform, "mongosh", $"mongodb://{this.ServerIpAddress}:{this.Port}/ycsb --eval 'db.stats()'")) + { + await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + await this.LogProcessDetailsAsync(process, telemetryContext, logTag, logToFile: true) + .ConfigureAwait(false); + } + } + + /// + /// Wrapper for the MongoDB workload executor. + /// + private async Task ExecuteCommandAsync( + string command, + string commandArguments, + string workingDirectory, + EventContext telemetryContext, + CancellationToken cancellationToken) + { + EventContext relatedContext = EventContext.Persisted() + .AddContext(nameof(command), command) + .AddContext(nameof(commandArguments), commandArguments); + + using (IProcessProxy process = this.systemManagement.ProcessManager.CreateElevatedProcess( + this.Platform, command, commandArguments, workingDirectory)) + { + this.CleanupTasks.Add(() => process.SafeKill()); + this.LogProcessTrace(process); + + await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + + if (!cancellationToken.IsCancellationRequested) + { + await this.LogProcessDetailsAsync(process, telemetryContext, "MongoDBClient", logToFile: true) + .ConfigureAwait(false); + process.ThrowIfWorkloadFailed(); + } + + return process.StandardOutput.ToString(); + } + } + + /// + /// Captures Metrics + /// + private void CaptureMetrics( + string results, + string commandArguments, + DateTime startTime, + DateTime endtime, + EventContext telemetryContext, + CancellationToken cancellationToken) + { + if (!cancellationToken.IsCancellationRequested) + { + try + { + this.MetadataContract.AddForScenario( + "MongoDBClient", + commandArguments, + toolVersion: null); + + this.MetadataContract.Apply(telemetryContext); + + results.ThrowIfNullOrWhiteSpace(nameof(results)); + this.Logger.LogMessage( + $"{nameof(MongoDBClientExecutor)}.CaptureMetrics", + telemetryContext.Clone().AddContext("results", results)); + + MongoDBMetricsParser resultsParser = new MongoDBMetricsParser(this.Scenario, results); + IList metrics = resultsParser.Parse(); + + string metricScenarioName = this.NormalizeMetricScenarioName(this.MetricScenario ?? this.Scenario); + + this.Logger.LogMetrics( + toolName: "MongoDBClient", + scenarioName: metricScenarioName, + scenarioStartTime: startTime, + scenarioEndTime: endtime, + metrics: metrics, + metricCategorization: null, + scenarioArguments: commandArguments, + this.Tags, + telemetryContext); + } + catch (SchemaException exc) + { + EventContext relatedContext = telemetryContext.Clone().AddError(exc); + this.Logger.LogMessage( + $"{nameof(MongoDBClientExecutor)}.WorkloadOutputParsingFailed", + LogLevel.Warning, + relatedContext); + } + } + } + + private string NormalizeMetricScenarioName(string scenarioName) + { + if (string.IsNullOrWhiteSpace(scenarioName)) + { + return scenarioName; + } + + return Regex.Replace( + scenarioName, + @"^workload([a-z])(?=_|$)", + match => $"workload_{char.ToUpperInvariant(match.Groups[1].Value[0])}", + RegexOptions.CultureInvariant); + } + + /// + /// Checks to make sure packages were installed correctly and sets the paths for the packages. + /// + private async Task InitializePackageLocationAsync(CancellationToken cancellationToken) + { + if (!cancellationToken.IsCancellationRequested) + { + DependencyPath javaPackage = await this.GetPackageAsync(this.JdkPackageName, cancellationToken) + .ConfigureAwait(false); + + if (javaPackage == null) + { + throw new DependencyException( + $"The expected package '{this.JdkPackageName}' does not exist on the system or is not registered.", + ErrorReason.WorkloadDependencyMissing); + } + + this.JDKPackagePath = javaPackage.Path; + + DependencyPath ycsbPackage = await this.packageManager.GetPackageAsync(this.YCSBPackageName, CancellationToken.None) + .ConfigureAwait(false); + + if (ycsbPackage == null) + { + throw new DependencyException( + $"The expected package '{this.YCSBPackageName}' does not exist on the system or is not registered.", + ErrorReason.WorkloadDependencyMissing); + } + + this.YcsbPackagePath = ycsbPackage.Path; + } + } + } +} diff --git a/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBExecutor.cs b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBExecutor.cs new file mode 100644 index 0000000000..504bb8a0c7 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBExecutor.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient.Actions +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using VirtualClient; + using VirtualClient.Common; + using VirtualClient.Common.Extensions; + using VirtualClient.Common.Telemetry; + using VirtualClient.Contracts; + + /// + /// MongoDB workload base executor. + /// + [SupportedPlatforms("linux-arm64,linux-x64")] + public abstract class MongoDBExecutor : VirtualClientComponent + { + /// + /// Initializes a new instance of the class. + /// + /// Provides required dependencies to the component. + /// Parameters defined in the profile or supplied on the command line. + protected MongoDBExecutor(IServiceCollection dependencies, IDictionary parameters = null) + : base(dependencies, parameters) + { + this.ApiClientManager = this.Dependencies.GetService(); + } + + /// + /// Port on which MongoDB server runs. + /// + public int Port + { + get + { + return this.Parameters.GetValue(nameof(this.Port), 27017); + } + } + + /// + /// Provides the ability to create API clients for interacting with local as well as remote instances + /// of the Virtual Client API service. + /// + protected IApiClientManager ApiClientManager { get; } + + /// + /// Client used to communicate with the hosted instance of the + /// Virtual Client API at server side. + /// + protected IApiClient ServerApiClient { get; set; } + + /// + /// Server IpAddress on which MongoDB Server runs. + /// + protected string ServerIpAddress { get; set; } + + /// + /// Cancellation Token Source for Server. + /// + protected CancellationTokenSource ServerCancellationSource { get; set; } + + /// + /// Initializes the environment and dependencies for running the MongoDB workload. + /// + /// The telemetry context. + /// The cancellation token. + /// A task that represents the asynchronous operation. + protected override async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + await this.EvaluateParametersAsync(cancellationToken).ConfigureAwait(false); + + if (this.IsMultiRoleLayout()) + { + ClientInstance clientInstance = this.GetLayoutClientInstance(); + string layoutIPAddress = clientInstance.IPAddress; + + this.ThrowIfLayoutClientIPAddressNotFound(layoutIPAddress); + this.ThrowIfRoleNotSupported(clientInstance.Role); + } + } + + /// + /// Initializes API clients for communication between client and server. + /// + protected void InitializeApiClients() + { + bool isSingleVM = !this.IsMultiRoleLayout(); + + if (isSingleVM) + { + this.ServerIpAddress = IPAddress.Loopback.ToString(); + this.ServerApiClient = this.ApiClientManager.GetOrCreateApiClient(IPAddress.Loopback.ToString(), IPAddress.Loopback); + } + else + { + var serverInstances = this.GetLayoutClientInstances(ClientRole.Server); + if (!serverInstances.Any()) + { + throw new InvalidOperationException("No server instance found. Please check the layout configuration."); + } + + ClientInstance serverInstance = serverInstances.First(); + if (IPAddress.TryParse(serverInstance.IPAddress, out IPAddress serverIPAddress)) + { + this.ServerIpAddress = serverIPAddress.ToString(); + this.ServerApiClient = this.ApiClientManager.GetOrCreateApiClient(serverIPAddress.ToString(), serverIPAddress); + } + else + { + throw new InvalidOperationException($"Invalid IP address format: {serverInstance.IPAddress}"); + } + + this.RegisterToSendExitNotifications($"{this.TypeName}.ExitNotification", this.ServerApiClient); + } + } + } +} diff --git a/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBMetricsParser.cs b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBMetricsParser.cs new file mode 100644 index 0000000000..51799571a3 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBMetricsParser.cs @@ -0,0 +1,346 @@ +namespace VirtualClient.Actions +{ + using System; + using System.Collections.Generic; + using System.Text.RegularExpressions; + using VirtualClient; + using VirtualClient.Contracts; + + /// + /// Parser for MongoDB benchmark output. + /// + public class MongoDBMetricsParser : MetricsParser + { + /// + /// Sectionize by one or more empty lines. + /// + private static readonly Regex MongoDBSectionDelimiter = new Regex(@"(\n)(\s)*(\n)", RegexOptions.ExplicitCapture); + + /// + /// Label for current running Scenario. + /// + private string scenario; + + /// + /// Initializes a new instance of the class. + /// + /// Raw text which is output of the MongoDB workload + /// Scenario name + public MongoDBMetricsParser(string scenario, string rawText) + : base(rawText) + { + this.scenario = scenario; + } + + /// + /// Logic to parse and read metrics + /// + public override IList Parse() + { + try + { + // Validate input before processing + if (string.IsNullOrWhiteSpace(this.RawText)) + { + throw new WorkloadException( + "The raw text provided for parsing is null or empty.", + ErrorReason.WorkloadResultsParsingFailed); + } + + this.Preprocess(); + Dictionary> metricsMap = GetMetricsMap(this.Sections["Metrics"]); + + // Validate that we got some metrics + if (metricsMap == null || metricsMap.Count == 0) + { + throw new WorkloadException( + "No valid metrics could be parsed from the workload output.", + ErrorReason.WorkloadResultsParsingFailed); + } + + List metrics = new List(); + + foreach (var entry in metricsMap) + { + var metricRelativity = MetricRelativity.Undefined; + if (entry.Key.Contains("ERROR") || entry.Key.Contains("FAILED") || entry.Key.Contains("NOT_FOUND") || entry.Key.Contains("TIMEOUT")) + { + metricRelativity = MetricRelativity.LowerIsBetter; + } + else if (entry.Key.Contains("Throughput")) + { + metricRelativity = MetricRelativity.HigherIsBetter; + } + else if (entry.Key.Contains("Count")) + { + metricRelativity = MetricRelativity.HigherIsBetter; + } + else if (entry.Key.Contains("Latency")) + { + metricRelativity = MetricRelativity.LowerIsBetter; + } + else if (entry.Key.Contains("Operations")) + { + metricRelativity = MetricRelativity.HigherIsBetter; + } + else if (entry.Key.Contains("Time")) + { + metricRelativity = MetricRelativity.LowerIsBetter; + } + + metrics.Add(new Metric(name: entry.Key, value: entry.Value.Item2, unit: entry.Value.Item1, relativity: metricRelativity)); + } + + return metrics; + } + catch (WorkloadException) + { + // Re-throw WorkloadException as-is + throw; + } + catch (Exception exc) + { + // Catch any other parsing errors (FormatException, ArgumentException, etc.) + throw new WorkloadException( + "Failed to parse MongoDB workload output. The output format may be invalid or corrupted.", + exc, + ErrorReason.WorkloadResultsParsingFailed); + } + } + + /// + /// Logic for preprocessing raw metrics + /// + protected override void Preprocess() + { + RegexOptions options = RegexOptions.None; + var regex = new Regex("[ ]{2,}", options); + this.PreprocessedText = regex.Replace(this.RawText, " "); + string pattern = @"(?=\[OVERALL\])"; // Pattern to find the position before the word "[OVERALL]" + string newSection = $"{Environment.NewLine}Metrics{Environment.NewLine}"; + + Regex rgx = new Regex(pattern); + this.PreprocessedText = rgx.Replace(this.PreprocessedText, newSection, 1); + this.Sections = TextParsingExtensions.Sectionize(this.PreprocessedText, MongoDBSectionDelimiter); + if (!this.Sections.ContainsKey("Metrics") || string.IsNullOrWhiteSpace(this.Sections["Metrics"])) + { + throw new WorkloadException( + $"Benchmarking metrics are not present", ErrorReason.WorkloadResultsParsingFailed); + } + + // Holds all possible output formats of ycsb (potentially change if upgrading ycsb version/ouput format changes) + this.Sections["Metrics"] = this.Sections["Metrics"] + // OVERALL + .Replace("[OVERALL], RunTime(ms)", "OVERALL-RunTime(ms)") + .Replace("[OVERALL], Throughput(ops/sec)", "OVERALL-Throughput(ops/sec)") + // JVM GC / TOTAL GC metrics (common YCSB + GC exporters) + .Replace("[TOTAL_GCS_G1_Young_Generation], Count", "TOTAL_GCS_G1_Young_Generation-Count") + .Replace("[TOTAL_GC_TIME_G1_Young_Generation], Time(ms)", "TOTAL_GC_TIME_G1_Young_Generation-Time(ms)") + .Replace("[TOTAL_GC_TIME_%_G1_Young_Generation], Time(%)", "TOTAL_GC_TIME_%_G1_Young_Generation-Time(%)") + .Replace("[TOTAL_GCS_G1_Concurrent_GC], Count", "TOTAL_GCS_G1_Concurrent_GC-Count") + .Replace("[TOTAL_GC_TIME_G1_Concurrent_GC], Time(ms)", "TOTAL_GC_TIME_G1_Concurrent_GC-Time(ms)") + .Replace("[TOTAL_GC_TIME_%_G1_Concurrent_GC], Time(%)", "TOTAL_GC_TIME_%_G1_Concurrent_GC-Time(%)") + .Replace("[TOTAL_GCS_G1_Old_Generation], Count", "TOTAL_GCS_G1_Old_Generation-Count") + .Replace("[TOTAL_GC_TIME_G1_Old_Generation], Time(ms)", "TOTAL_GC_TIME_G1_Old_Generation-Time(ms)") + .Replace("[TOTAL_GC_TIME_%_G1_Old_Generation], Time(%)", "TOTAL_GC_TIME_%_G1_Old_Generation-Time(%)") + .Replace("[TOTAL_GCs], Count", "TOTAL_GCs-Count") + .Replace("[TOTAL_GC_TIME], Time(ms)", "TOTAL_GC_TIME-Time(ms)") + .Replace("[TOTAL_GC_TIME_%], Time(%)", "TOTAL_GC_TIME_%-Time(%)") + // LOAD / INIT / CLEANUP + .Replace("[LOAD], Operations", "LOAD-Operations") + .Replace("[LOAD], AverageLatency(us)", "LOAD-AverageLatency(us)") + .Replace("[LOAD], MinLatency(us)", "LOAD-MinLatency(us)") + .Replace("[LOAD], MaxLatency(us)", "LOAD-MaxLatency(us)") + .Replace("[LOAD], 95thPercentileLatency(us)", "LOAD-95thPercentileLatency(us)") + .Replace("[LOAD], 99thPercentileLatency(us)", "LOAD-99thPercentileLatency(us)") + .Replace("[CLEANUP], Operations", "CLEANUP-Operations") + .Replace("[CLEANUP], AverageLatency(us)", "CLEANUP-AverageLatency(us)") + .Replace("[CLEANUP], MinLatency(us)", "CLEANUP-MinLatency(us)") + .Replace("[CLEANUP], MaxLatency(us)", "CLEANUP-MaxLatency(us)") + .Replace("[CLEANUP], 95thPercentileLatency(us)", "CLEANUP-95thPercentileLatency(us)") + .Replace("[CLEANUP], 99thPercentileLatency(us)", "CLEANUP-99thPercentileLatency(us)") + // READ + .Replace("[READ], Operations", "READ-Operations") + .Replace("[READ], AverageLatency(us)", "READ-AverageLatency(us)") + .Replace("[READ], MinLatency(us)", "READ-MinLatency(us)") + .Replace("[READ], MaxLatency(us)", "READ-MaxLatency(us)") + .Replace("[READ], 95thPercentileLatency(us)", "READ-95thPercentileLatency(us)") + .Replace("[READ], 99thPercentileLatency(us)", "READ-99thPercentileLatency(us)") + .Replace("[READ], Return=OK", "READ-Return-OK-Count") + .Replace("[READ], Return=ERROR", "READ-Return-ERROR-Count") + .Replace("[READ], Return=NOT_FOUND", "READ-Return-NOT_FOUND-Count") + .Replace("[READ], Return=FAILED", "READ-Return-FAILED-Count") + .Replace("[READ], Return=TIMEOUT", "READ-Return-TIMEOUT-Count") + // READ-FAILED (some clients emit this block) + .Replace("[READ-FAILED], Operations", "READ-FAILED-Operations") + .Replace("[READ-FAILED], AverageLatency(us)", "READ-FAILED-AverageLatency(us)") + .Replace("[READ-FAILED], MinLatency(us)", "READ-FAILED-MinLatency(us)") + .Replace("[READ-FAILED], MaxLatency(us)", "READ-FAILED-MaxLatency(us)") + .Replace("[READ-FAILED], 95thPercentileLatency(us)", "READ-FAILED-95thPercentileLatency(us)") + .Replace("[READ-FAILED], 99thPercentileLatency(us)", "READ-FAILED-99thPercentileLatency(us)") + // UPDATE + .Replace("[UPDATE], Operations", "UPDATE-Operations") + .Replace("[UPDATE], AverageLatency(us)", "UPDATE-AverageLatency(us)") + .Replace("[UPDATE], MinLatency(us)", "UPDATE-MinLatency(us)") + .Replace("[UPDATE], MaxLatency(us)", "UPDATE-MaxLatency(us)") + .Replace("[UPDATE], 95thPercentileLatency(us)", "UPDATE-95thPercentileLatency(us)") + .Replace("[UPDATE], 99thPercentileLatency(us)", "UPDATE-99thPercentileLatency(us)") + .Replace("[UPDATE], Return=OK", "UPDATE-Return-OK-Count") + .Replace("[UPDATE], Return=ERROR", "UPDATE-Return-ERROR-Count") + .Replace("[UPDATE], Return=NOT_FOUND", "UPDATE-Return-NOT_FOUND-Count") + .Replace("[UPDATE], Return=FAILED", "UPDATE-Return-FAILED-Count") + .Replace("[UPDATE], Return=TIMEOUT", "UPDATE-Return-TIMEOUT-Count") + // UPDATE-FAILED + .Replace("[UPDATE-FAILED], Operations", "UPDATE-FAILED-Operations") + .Replace("[UPDATE-FAILED], AverageLatency(us)", "UPDATE-FAILED-AverageLatency(us)") + .Replace("[UPDATE-FAILED], MinLatency(us)", "UPDATE-FAILED-MinLatency(us)") + .Replace("[UPDATE-FAILED], MaxLatency(us)", "UPDATE-FAILED-MaxLatency(us)") + .Replace("[UPDATE-FAILED], 95thPercentileLatency(us)", "UPDATE-FAILED-95thPercentileLatency(us)") + .Replace("[UPDATE-FAILED], 99thPercentileLatency(us)", "UPDATE-FAILED-99thPercentileLatency(us)") + // INSERT + .Replace("[INSERT], Operations", "INSERT-Operations") + .Replace("[INSERT], AverageLatency(us)", "INSERT-AverageLatency(us)") + .Replace("[INSERT], MinLatency(us)", "INSERT-MinLatency(us)") + .Replace("[INSERT], MaxLatency(us)", "INSERT-MaxLatency(us)") + .Replace("[INSERT], 95thPercentileLatency(us)", "INSERT-95thPercentileLatency(us)") + .Replace("[INSERT], 99thPercentileLatency(us)", "INSERT-99thPercentileLatency(us)") + .Replace("[INSERT], Return=OK", "INSERT-Return-OK-Count") + .Replace("[INSERT], Return=ERROR", "INSERT-Return-ERROR-Count") + .Replace("[INSERT], Return=FAILED", "INSERT-Return-FAILED-Count") + .Replace("[INSERT], Return=TIMEOUT", "INSERT-Return-TIMEOUT-Count") + // INSERT-FAILED + .Replace("[INSERT-FAILED], Operations", "INSERT-FAILED-Operations") + .Replace("[INSERT-FAILED], AverageLatency(us)", "INSERT-FAILED-AverageLatency(us)") + .Replace("[INSERT-FAILED], MinLatency(us)", "INSERT-FAILED-MinLatency(us)") + .Replace("[INSERT-FAILED], MaxLatency(us)", "INSERT-FAILED-MaxLatency(us)") + .Replace("[INSERT-FAILED], 95thPercentileLatency(us)", "INSERT-FAILED-95thPercentileLatency(us)") + .Replace("[INSERT-FAILED], 99thPercentileLatency(us)", "INSERT-FAILED-99thPercentileLatency(us)") + // DELETE + .Replace("[DELETE], Operations", "DELETE-Operations") + .Replace("[DELETE], AverageLatency(us)", "DELETE-AverageLatency(us)") + .Replace("[DELETE], MinLatency(us)", "DELETE-MinLatency(us)") + .Replace("[DELETE], MaxLatency(us)", "DELETE-MaxLatency(us)") + .Replace("[DELETE], 95thPercentileLatency(us)", "DELETE-95thPercentileLatency(us)") + .Replace("[DELETE], 99thPercentileLatency(us)", "DELETE-99thPercentileLatency(us)") + .Replace("[DELETE], Return=OK", "DELETE-Return-OK-Count") + .Replace("[DELETE], Return=ERROR", "DELETE-Return-ERROR-Count") + .Replace("[DELETE], Return=NOT_FOUND", "DELETE-Return-NOT_FOUND-Count") + .Replace("[DELETE], Return=FAILED", "DELETE-Return-FAILED-Count") + // DELETE-FAILED + .Replace("[DELETE-FAILED], Operations", "DELETE-FAILED-Operations") + .Replace("[DELETE-FAILED], AverageLatency(us)", "DELETE-FAILED-AverageLatency(us)") + .Replace("[DELETE-FAILED], MinLatency(us)", "DELETE-FAILED-MinLatency(us)") + .Replace("[DELETE-FAILED], MaxLatency(us)", "DELETE-FAILED-MaxLatency(us)") + .Replace("[DELETE-FAILED], 95thPercentileLatency(us)", "DELETE-FAILED-95thPercentileLatency(us)") + .Replace("[DELETE-FAILED], 99thPercentileLatency(us)", "DELETE-FAILED-99thPercentileLatency(us)") + // SCAN + .Replace("[SCAN], Operations", "SCAN-Operations") + .Replace("[SCAN], AverageLatency(us)", "SCAN-AverageLatency(us)") + .Replace("[SCAN], MinLatency(us)", "SCAN-MinLatency(us)") + .Replace("[SCAN], MaxLatency(us)", "SCAN-MaxLatency(us)") + .Replace("[SCAN], 95thPercentileLatency(us)", "SCAN-95thPercentileLatency(us)") + .Replace("[SCAN], 99thPercentileLatency(us)", "SCAN-99thPercentileLatency(us)") + .Replace("[SCAN], Return=OK", "SCAN-Return-OK-Count") + .Replace("[SCAN], Return=ERROR", "SCAN-Return-ERROR-Count") + // SCAN-FAILED + .Replace("[SCAN-FAILED], Operations", "SCAN-FAILED-Operations") + .Replace("[SCAN-FAILED], AverageLatency(us)", "SCAN-FAILED-AverageLatency(us)") + .Replace("[SCAN-FAILED], MinLatency(us)", "SCAN-FAILED-MinLatency(us)") + .Replace("[SCAN-FAILED], MaxLatency(us)", "SCAN-FAILED-MaxLatency(us)") + .Replace("[SCAN-FAILED], 95thPercentileLatency(us)", "SCAN-FAILED-95thPercentileLatency(us)") + .Replace("[SCAN-FAILED], 99thPercentileLatency(us)", "SCAN-FAILED-99thPercentileLatency(us)") + // READ-MODIFY-WRITE (RMW) + .Replace("[READ-MODIFY-WRITE], Operations", "READ-MODIFY-WRITE-Operations") + .Replace("[READ-MODIFY-WRITE], AverageLatency(us)", "READ-MODIFY-WRITE-AverageLatency(us)") + .Replace("[READ-MODIFY-WRITE], MinLatency(us)", "READ-MODIFY-WRITE-MinLatency(us)") + .Replace("[READ-MODIFY-WRITE], MaxLatency(us)", "READ-MODIFY-WRITE-MaxLatency(us)") + .Replace("[READ-MODIFY-WRITE], 95thPercentileLatency(us)", "READ-MODIFY-WRITE-95thPercentileLatency(us)") + .Replace("[READ-MODIFY-WRITE], 99thPercentileLatency(us)", "READ-MODIFY-WRITE-99thPercentileLatency(us)") + .Replace("[READ-MODIFY-WRITE], Return=OK", "READ-MODIFY-WRITE-Return-OK-Count") + .Replace("[READ-MODIFY-WRITE], Return=ERROR", "READ-MODIFY-WRITE-Return-ERROR-Count") + // RMW-FAILED + .Replace("[READ-MODIFY-WRITE-FAILED], Operations", "READ-MODIFY-WRITE-FAILED-Operations") + .Replace("[READ-MODIFY-WRITE-FAILED], AverageLatency(us)", "READ-MODIFY-WRITE-FAILED-AverageLatency(us)") + .Replace("[READ-MODIFY-WRITE-FAILED], MinLatency(us)", "READ-MODIFY-WRITE-FAILED-MinLatency(us)") + .Replace("[READ-MODIFY-WRITE-FAILED], MaxLatency(us)", "READ-MODIFY-WRITE-FAILED-MaxLatency(us)") + .Replace("[READ-MODIFY-WRITE-FAILED], 95thPercentileLatency(us)", "READ-MODIFY-WRITE-FAILED-95thPercentileLatency(us)") + .Replace("[READ-MODIFY-WRITE-FAILED], 99thPercentileLatency(us)", "READ-MODIFY-WRITE-FAILED-99thPercentileLatency(us)") + // RETURN CODES GENERAL (some clients format these without operation prefix) + .Replace("[Return=OK], Operations", "Return-OK-Count") + .Replace("[Return=ERROR], Operations", "Return-ERROR-Count") + .Replace("[Return=NOT_FOUND], Operations", "Return-NOT_FOUND-Count") + .Replace("[Return=FAILED], Operations", "Return-FAILED-Count") + .Replace("[Return=TIMEOUT], Operations", "Return-TIMEOUT-Count"); + } + + /// + /// Parses the metrics from string format to (string (name), Tuple (data)) format + /// + /// Preprocessed metrics + /// + private static Dictionary> GetMetricsMap(string metrics) + { + char seperator = '\n'; + string[] results = metrics.Split(seperator, StringSplitOptions.RemoveEmptyEntries); + Dictionary> metricMap = new Dictionary>(); + + foreach (string item in results) + { + try + { + string[] metricData = item.Split(',', StringSplitOptions.RemoveEmptyEntries); + if (metricData.Length > 0) + { + string[] metricNameList = metricData[0].Split('(', StringSplitOptions.RemoveEmptyEntries); + + if (metricNameList.Length > 0) + { + string metricName = metricNameList[0].Trim(); + + // Skip empty metric names + if (string.IsNullOrWhiteSpace(metricName)) + { + continue; + } + + double metricValue = 0.0; + string metricUnit = string.Empty; + + if (metricData.Length > 1 && !string.IsNullOrWhiteSpace(metricData[1])) + { + // Try to parse the value, skip if invalid + if (!double.TryParse(metricData[1].Trim(), out metricValue)) + { + continue; + } + } + + if (metricNameList.Length > 1 && !string.IsNullOrWhiteSpace(metricNameList[1])) + { + string[] unitParts = metricNameList[1].Split(')', StringSplitOptions.RemoveEmptyEntries); + if (unitParts.Length > 0) + { + metricUnit = unitParts[0].Trim(); + } + } + + Tuple metricTuple = new Tuple(metricUnit, metricValue); + + if (!metricMap.ContainsKey(metricName)) + { + metricMap.Add(metricName, metricTuple); + } + } + } + } + catch (Exception) + { + // Skip invalid lines and continue parsing + continue; + } + } + + return metricMap; + } + } +} diff --git a/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBServerExecutor.cs b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBServerExecutor.cs new file mode 100644 index 0000000000..2d52e0e828 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBServerExecutor.cs @@ -0,0 +1,431 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient.Actions +{ + using System; + using System.Collections.Generic; + using System.IO.Abstractions; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Polly; + using VirtualClient; + using VirtualClient.Common; + using VirtualClient.Common.Extensions; + using VirtualClient.Common.Telemetry; + using VirtualClient.Contracts; + + /// + /// MongoDB Server Executor - Handles MongoDB server installation and configuration. + /// + [SupportedPlatforms("linux-arm64,linux-x64")] + public class MongoDBServerExecutor : MongoDBExecutor + { + private IFileSystem fileSystem; + private ISystemManagement systemManagement; + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// Provides all of the required dependencies to the Virtual Client component. + /// An enumeration of key-value pairs that can control the execution of the component. + public MongoDBServerExecutor(IServiceCollection dependencies, IDictionary parameters = null) + : base(dependencies, parameters) + { + this.fileSystem = dependencies.GetService(); + this.systemManagement = dependencies.GetService(); + + this.ServerRetryPolicy = Policy.Handle(exc => !(exc is OperationCanceledException)) + .WaitAndRetryAsync(10, (retries) => TimeSpan.FromSeconds(retries)); + } + + /// + /// Disk filter string to filter disks for MongoDB data path. + /// + public string DiskFilter + { + get + { + return this.Parameters.GetValue(nameof(MongoDBServerExecutor.DiskFilter), string.Empty); + } + } + + /// + /// A retry policy to apply to the server when starting to handle transient issues. + /// + protected IAsyncPolicy ServerRetryPolicy { get; set; } + + /// + /// Initializes the environment for running MongoDB server. + /// + protected override async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + await base.InitializeAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + + this.InitializeApiClients(); + + // Initialize disk if DiskFilter is specified + if (!string.IsNullOrWhiteSpace(this.DiskFilter)) + { + await this.InitializeDiskPathAsync(cancellationToken).ConfigureAwait(false); + await this.ConfigureDiskForMongoDBAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + } + + // Ensure MongoDB is configured to listen on all interfaces + await this.ConfigureMongoDBBindAddressAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + + // Start MongoDB server + await this.StartMongoDBServerAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + } + + /// + /// Executes the MongoDB server workload. + /// + protected override Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + return this.Logger.LogMessageAsync($"{this.TypeName}.ExecuteServer", telemetryContext, async () => + { + try + { + this.SetServerOnline(false); + + await this.ServerApiClient.PollForHeartbeatAsync(TimeSpan.FromMinutes(5), cancellationToken) + .ConfigureAwait(false); + + // Server is now online and ready to accept connections + this.SetServerOnline(true); + + // Keep the server running until cancelled + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Expected when cancellation is requested + } + }); + } + + /// + /// Disposes of resources used by the executor. + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing && !this.disposed) + { + this.disposed = true; + } + } + + /// + /// Configures MongoDB to bind to all network interfaces. + /// + private async Task ConfigureMongoDBBindAddressAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + try + { + string configFile = "/etc/mongod.conf"; + string setBindIpCmd = $@"sudo sed -i 's/bindIp:.*/bindIp: 0.0.0.0/g' {configFile}"; + + await this.ExecuteMongoDBCommandAsync( + "bash", + $"-c \"{setBindIpCmd}\"", + "ConfigureBindAddress", + telemetryContext, + cancellationToken).ConfigureAwait(false); + + this.Logger.LogMessage( + $"{nameof(MongoDBServerExecutor)}.BindAddressConfigured", + LogLevel.Information, + EventContext.Persisted().AddContext("bindIp", "0.0.0.0")); + } + catch (Exception ex) + { + EventContext relatedContext = telemetryContext.Clone().AddError(ex); + this.Logger.LogMessage($"{nameof(MongoDBServerExecutor)}.ConfigureBindAddressFailed", LogLevel.Warning, relatedContext); + } + } + + /// + /// Starts the MongoDB server. + /// + private async Task StartMongoDBServerAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + try + { + // Restart MongoDB to apply all configurations + await this.ExecuteMongoDBServiceCommandAsync("restart", telemetryContext, cancellationToken).ConfigureAwait(false); + + // Wait a bit for MongoDB to fully start + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false); + + // Verify MongoDB is running + await this.VerifyMongoDBIsRunningAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + + this.Logger.LogMessage( + $"{nameof(MongoDBServerExecutor)}.MongoDBServerStarted", + LogLevel.Information, + EventContext.Persisted()); + } + catch (Exception ex) + { + EventContext relatedContext = telemetryContext.Clone().AddError(ex); + this.Logger.LogMessage($"{nameof(MongoDBServerExecutor)}.StartMongoDBServerFailed", LogLevel.Error, relatedContext); + throw; + } + } + + /// + /// Verifies that MongoDB is running. + /// + private async Task VerifyMongoDBIsRunningAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + using (IProcessProxy process = this.systemManagement.ProcessManager.CreateElevatedProcess( + this.Platform, "mongosh", "--eval \"db.runCommand({ping: 1})\"")) + { + await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + + if (process.ExitCode != 0) + { + string output = process.StandardOutput.ToString(); + string error = process.StandardError.ToString(); + this.Logger.LogMessage( + $"{nameof(MongoDBServerExecutor)}.MongoDBVerificationDebug", + LogLevel.Warning, + telemetryContext.Clone() + .AddContext("exitCode", process.ExitCode) + .AddContext("output", output) + .AddContext("error", error)); + + throw new WorkloadException( + $"MongoDB server verification failed with exit code {process.ExitCode}. Output: {output}. Error: {error}", + ErrorReason.WorkloadFailed); + } + + this.Logger.LogMessage( + $"{nameof(MongoDBServerExecutor)}.MongoDBVerified", + LogLevel.Information, + telemetryContext); + } + } + + /// + /// Configures the disk for MongoDB data storage. + /// + private async Task ConfigureDiskForMongoDBAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested || string.IsNullOrWhiteSpace(this.DiskFilter)) + { + return; + } + + try + { + string diskDevicePath = this.Parameters["DiskDevicePath"].ToString(); + string mongoDataPath = "/mnt/mongodb-data"; + + // Stop MongoDB service before mounting + await this.ExecuteMongoDBServiceCommandAsync("stop", telemetryContext, cancellationToken).ConfigureAwait(false); + + // Create filesystem on disk + await this.ExecuteMongoDBCommandAsync( + "bash", + $"-c \"sudo mkfs.ext4 -F {diskDevicePath}\"", + "CreateFilesystem", + telemetryContext, + cancellationToken).ConfigureAwait(false); + + // Create mount point + await this.ExecuteMongoDBCommandAsync( + "bash", + $"-c \"sudo mkdir -p {mongoDataPath}\"", + "CreateMountPoint", + telemetryContext, + cancellationToken).ConfigureAwait(false); + + // Mount the disk + await this.ExecuteMongoDBCommandAsync( + "bash", + $"-c \"sudo mount -t ext4 {diskDevicePath} {mongoDataPath}\"", + "MountDisk", + telemetryContext, + cancellationToken).ConfigureAwait(false); + + // Set permissions + await this.ExecuteMongoDBCommandAsync( + "bash", + $"-c \"sudo chown -R mongodb:mongodb {mongoDataPath}\"", + "SetPermissions", + telemetryContext, + cancellationToken).ConfigureAwait(false); + + // Update mongod.conf to use the new dbPath + string configFile = "/etc/mongod.conf"; + string setDbPathCmd = $@"sudo sed -i 's|^\s*dbPath:.*| dbPath: {mongoDataPath}|g' {configFile}"; + await this.ExecuteMongoDBCommandAsync( + "bash", + $"-c \"{setDbPathCmd}\"", + "UpdateMongoConf", + telemetryContext, + cancellationToken).ConfigureAwait(false); + + this.Logger.LogMessage( + $"{nameof(MongoDBServerExecutor)}.DiskConfigurationComplete", + LogLevel.Information, + EventContext.Persisted().AddContext("diskDevicePath", diskDevicePath).AddContext("mongoDataPath", mongoDataPath)); + } + catch (Exception ex) + { + EventContext relatedContext = telemetryContext.Clone().AddError(ex); + this.Logger.LogMessage($"{nameof(MongoDBServerExecutor)}.DiskConfigurationFailed", LogLevel.Warning, relatedContext); + // Continue - disk configuration is not critical if already configured + } + } + + /// + /// Executes a MongoDB-related command. + /// + private async Task ExecuteMongoDBCommandAsync( + string command, + string commandArguments, + string scenario, + EventContext telemetryContext, + CancellationToken cancellationToken) + { + try + { + using (IProcessProxy process = this.systemManagement.ProcessManager.CreateElevatedProcess( + this.Platform, command, commandArguments)) + { + this.CleanupTasks.Add(() => process.SafeKill()); + this.LogProcessTrace(process); + + await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + + if (!cancellationToken.IsCancellationRequested) + { + await this.LogProcessDetailsAsync(process, telemetryContext, $"MongoDBServer-{scenario}", logToFile: true) + .ConfigureAwait(false); + + if (process.ExitCode != 0) + { + this.Logger.LogMessage( + $"{nameof(MongoDBServerExecutor)}.{scenario}Warning", + LogLevel.Warning, + telemetryContext.Clone() + .AddContext("command", command) + .AddContext("commandArguments", commandArguments) + .AddContext("exitCode", process.ExitCode)); + } + } + } + } + catch (Exception ex) + { + this.Logger.LogMessage( + $"{nameof(MongoDBServerExecutor)}.{scenario}Failed", + LogLevel.Warning, + telemetryContext.Clone().AddError(ex).AddContext("command", command)); + } + } + + /// + /// Executes MongoDB service command. + /// + private async Task ExecuteMongoDBServiceCommandAsync(string action, EventContext telemetryContext, CancellationToken cancellationToken) + { + try + { + using (IProcessProxy process = this.systemManagement.ProcessManager.CreateElevatedProcess( + this.Platform, "sudo", $"systemctl {action} mongod")) + { + this.CleanupTasks.Add(() => process.SafeKill()); + this.LogProcessTrace(process); + + await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + + if (!cancellationToken.IsCancellationRequested) + { + await this.LogProcessDetailsAsync(process, telemetryContext, $"MongoDBServer-Service-{action}", logToFile: true) + .ConfigureAwait(false); + + if (process.ExitCode != 0) + { + this.Logger.LogMessage( + $"{nameof(MongoDBServerExecutor)}.ServiceCommandWarning", + LogLevel.Warning, + telemetryContext.Clone() + .AddContext("action", action) + .AddContext("exitCode", process.ExitCode)); + } + } + } + } + catch (Exception ex) + { + this.Logger.LogMessage( + $"{nameof(MongoDBServerExecutor)}.MongoServiceCommandFailed", + LogLevel.Warning, + telemetryContext.Clone().AddError(ex).AddContext("action", action)); + } + } + + /// + /// Initializes the disk path for MongoDB based on DiskFilter parameter. + /// + private async Task InitializeDiskPathAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested || string.IsNullOrWhiteSpace(this.DiskFilter)) + { + return; + } + + IEnumerable disks = await this.systemManagement.DiskManager.GetDisksAsync(cancellationToken) + .ConfigureAwait(false); + + if (disks == null || !disks.Any()) + { + throw new DependencyException( + $"No disks are available on the system to match the filter criteria '{this.DiskFilter}'.", + ErrorReason.DiskInformationNotAvailable); + } + + IEnumerable disksToTest = DiskFilters.FilterDisks(disks, this.DiskFilter, this.Platform); + + if (disksToTest == null || !disksToTest.Any()) + { + throw new DependencyException( + $"No disks matched the filter criteria '{this.DiskFilter}'.", + ErrorReason.DiskInformationNotAvailable); + } + + Disk selectedDisk = disksToTest.First(); + + // Store the device path for use in configuration + this.Parameters["DiskDevicePath"] = selectedDisk.DevicePath; + + EventContext telemetryContext = EventContext.Persisted() + .AddContext("diskFilter", this.DiskFilter) + .AddContext("selectedDisk", selectedDisk.DevicePath) + .AddContext("totalDisks", disks.Count()) + .AddContext("filteredDisks", disksToTest.Count()); + + this.Logger.LogMessage($"{nameof(MongoDBServerExecutor)}.DiskSelected", LogLevel.Information, telemetryContext); + } + } +} diff --git a/website/docs/workloads/mongodb/YCSB.md b/website/docs/workloads/mongodb/YCSB.md new file mode 100644 index 0000000000..8bde6a1312 --- /dev/null +++ b/website/docs/workloads/mongodb/YCSB.md @@ -0,0 +1,150 @@ +# YCSB +An extensible workload generator + +Yahoo Cloud Serving Benchmark (YCSB) project is to develop a framework and common set of workloads for evaluating the performance of different "key-value" and "cloud" serving stores. + +--- +## Loading YCSB: + +``` +$ ./bin/ycsb load (database name) -P workloads/workloada -p recordcount=10000000 +``` + +.dat files can be created to specify recordcount property. + +`large.dat: recordcount = 100000000` + +``` +$ ./bin/ycsb load (database name) -P workloads/workloada -P large.dat +``` + +*When using this strategy to update recordcount during the load phase it must also be used during the run/transaction phase* +### To see output while loading data: +If dealing with a big data load it can be helpful to make sure everything is going well with status updates. + +-s will require the Client to produce status report on stderr. + +The character > will send load data to a file (aka. load.dat below) +``` +$ ./bin/ycsb load (database name) -P workloads/workloada -P large.dat -s > load.dat +``` + +### Command Line Options (Load): +`-P` Load Property files +*Running Multiple Clients in Parallel*: +`-p insertstart` The index of the record to start at +`-p insertcount` The number of records to insert +## Executing YCSB: +Executing a workload/running transaction phase: + +``` +$./bin/ycsb run (database name) -P workloads/workloada -P large.dat -s -threads 10 -target 100 -p operationcount=50000000 -p measurementtype=timeseries -p timeseries.granularity=2000 > transactions.dat +``` + +### Command Line Options (Tx): +`-threads` number of client threads (default: 1) +`-target` throttle ops (default: un-throttled) +      (used to generate latency vs throughput curves) +`-p` Set parameters +`-s ... > transacations.day` output on stderr & transactions.dat +`-p operationcount=10000000` Amount of ops to run +`-p maxexecutiontime=300` Run length regardless of operation count in. (default=off) (Seconds) +`-p measurementtype=timeseries` sets latency reporting to timeseries (default: histogram) +`-p timeseries.granularity=2000` sets latency report rate (default: 1000) + +## Running Multiple Clients in Parallel: + +### Loading database from multiple clients: + +Loading the database from multiple clients is done by partitioning the workload records. + +Normally YCSB just loads all the records, but Command Line Options (Load) allow the ability to cut up the records in a workload. + +Example: Loading 10 million records/ 2 clients + +First Client: +`-p insertstart=0` +`-p insertcount=5000000` + +Second Client: +`-p insertstart=5000000` +`-p insertcount=5000000` + +### Executing from multiple clients: +Run transaction phase of the workload from multiple servers. Start up multiple client servers, each running the same workload targeted at the same database server. + +``` +DEPRICATED - Just connect using below format *Excecuting remotely* +1. Get connection string from host server +2. Boot client, install YCSB +3. Connect to server using connection string and Mongosh +``` + +***Executing remotely:*** +Add: + `-p mongodb.url="mongodb://{serverIP}:27017/{dbname}?w=0"` + +Ex. Synchronous +``` +$./bin/ycsb run mongodb -P workloads/workloada -P large.dat -s -threads 10 -p mongodb.url="mongodb://10.7.0.18:27017/ycsb?w=0" > transactions.dat +``` + +--- +### Data Collection: + +This tool spits out the following type of telemetry: + +*Under tags**: + +OVERALL: +* Total Execution time (ms) +* Average throughput across all threads (ops/s) +* Garbage Collection data +UPDATE/CLEANUP/READ: +* Total operations (num operations) +* Average latency (ms) +* Max latency (ms) +* 95th percentile latency (ms) +* 99th percentile latency (ms) +* Return code counts (num codes) +* Histogram/Time series (optional) of operation times + +--- +### Mongo DB Specific Config Options: + +`mongodb.batchsize` - Submits inserts in batches (improving throughput). Good for insert heavy workloads. Default is 1. + +--- + +## Resources: + +### YCSB Properties: +https://github.com/brianfrankcooper/YCSB/wiki/Core-Properties#core-workload-package-properties +https://github.com/brianfrankcooper/YCSB/wiki/Core-Workloads#running-the-workloads + +### Sharding (For Implementation of Client/Server): +https://www.mongodb.com/resources/products/capabilities/database-sharding-explained +https://www.youtube.com/watch?v=aBaD0qHK1as&list=PLIRAZAlr4cfY1gugVw2enf6uVXyJaWwwv +https://github.com/neerajg5/mongodb-tutorial/blob/main/mongodb-sharding-ubuntu-git.txt +https://www.mongodb.com/docs/manual/sharding/#shard-keys +https://github.com/brianfrankcooper/YCSB/wiki/Running-a-Workload-in-Parallel +https://github.com/brianfrankcooper/YCSB/blob/master/mongodb/README.md +https://www.digitalocean.com/community/tutorials/how-to-configure-remote-access-for-mongodb-on-ubuntu-20-04 + +### Helpful MongoDB commands: +```bash + +# For stop/start/restart/status MongoDB Server, command depends on if your system uses service or systemctl +sudo service mongod {command} +sudo systemctl {command} mongod + +# Often time when the server won't start after restarting the VM it is on this is a good fix +sudo chown -R mongodb:mongodb /var/lib/mongodb +sudo chown mongodb:mongodb /tmp/mongodb-27017.sock + +# Great Command for checking attached clients (while in thge mongoshell): +db.currentOp(true).inprog.reduce((accumulator, connection) => { ipaddress = connection.client ? connection.client.split(":")[0] : "Internal"; accumulator[ipaddress] = (accumulator[ipaddress] || 0) + 1; accumulator["TOTAL_CONNECTION_COUNT"]++; return accumulator; }, { TOTAL_CONNECTION_COUNT: 0 }) + +# Drop YCSB database +mongosh --host localhost --eval "use ycsb" --eval "db.dropDatabase()" +``` \ No newline at end of file diff --git a/website/docs/workloads/mongodb/mongodb-profiles.md b/website/docs/workloads/mongodb/mongodb-profiles.md new file mode 100644 index 0000000000..a8d400da67 --- /dev/null +++ b/website/docs/workloads/mongodb/mongodb-profiles.md @@ -0,0 +1,118 @@ +# MongoDB Profile Components +--- + +## Dependencies +* **JDKPackageDependencyInstallation** + Jdk package installation which is necessary for the intallation of YCSB. (Client) + +* **DependencyPackageInstallation** + Installation of the YCSB package zip file. (Client) + +* **LinuxPackageInstallation** + Installation of necessary Linux packages. (Server/Client) + +### TBA (To Be Added): +* **FormatDisks** and **MountDisks** + Format any unformatted disks on the server, then mount any unmounted disks. (Server) + +* **MongoDBServerInstallation** + Installation of MongoDB server. (Server) + +* **ApiServer** + Starts the API server for Client-Server workloads. + +## Actions +* **MongoDBRunYCSB** + Runs a given workload on the MongoDB server. + +## PERF-MONGODB-TYPE.json +Runs multiple workload variations using YCSB's built in workloads to test the bandwitch of CPU, Memory, and Disk I/O. +Loads a single type of dataset at a time, then runs various workloads against it. + +* **Supperted Platform/Architectures** + * linux-x64 (Ubuntu) + + +* **Supports Disconnected Scenarios** + * No. Internet connection required. + +* **Dependencies** + The dependencies defined in the 'Dependencies' section of the profile itself are required in order to run the workload operations effectively. + * Internet connection. + + Additional information on components that exist within the 'Dependencies' section of the profile can be found in the following locations: + * [Installing Dependencies](https://microsoft.github.io/VirtualClient/docs/category/dependencies/) + +* **Profile Parameters** +The following parameters can be optionally supplied on the command line. See the 'Usage Scenarios/Examples' above for examples on how to supply parameters to Virtual Client profiles. + +| Parameter | Purpose |Default | +|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------| +| ThreadCount | Optional. Number of threads to use during workload execution. | calculate(LogicalCoreCount/2) | +| WorkloadName | Optional. **Name of workload to initialy load**; options listed [here](https://github.com/brianfrankcooper/YCSB/wiki/Core-Workloads#running-the-workloads). | workloada | +| Duration | Optional. Timespan duration of each action in the workload. | 00:05:00 | + +--- +***Warning*** + workloade and workloadd insert records into the database. This will cause the dataset to grow in size over time. + This can lead to a server failure if MongoDB runs out of disk space. + +--- + +* **Profile Runtimes** + See the 'Metadata' section of the profile for estimated runtimes. These timings represent the length of time required to run a single round of profile actions. These timings can be used to determine minimum required runtimes for the Virtual Client in order to get results. These are often estimates based on the + number of system cores. + +* **Usage Examples** + The following section provides a few basic examples of how to use the workload profile. + + ``` bash + # When running on a single system (environment layout not required) + ./VirtualClient --profile=PERF-MONGODB-TYPE.json --packageStore="{BlobConnectionString} + ``` + +## PERF-MONGODB-LOAD.json +Runs a single workload variation against different record sizes to test the bandwitch of CPU, Memory, and Disk I/O. +Loads a single type of dataset at a time, then runs a single workload variation with increasing record sizes against it. + +--- +***Warning*** + This workload can cause the dataset to grow in size over time if given record sizes are larger than those already in the database. + This can lead to a server failure if MongoDBruns out of disk space. + +--- + +* **Supperted Platform/Architectures** + * linux-x64 (Ubuntu) + + +* **Supports Disconnected Scenarios** + * No. Internet connection required. + +* **Dependencies** + The dependencies defined in the 'Dependencies' section of the profile itself are required in order to run the workload operations effectively. + * Internet connection. + + Additional information on components that exist within the 'Dependencies' section of the profile can be found in the following locations: + * [Installing Dependencies](https://microsoft.github.io/VirtualClient/docs/category/dependencies/) + +* **Profile Parameters** +The following parameters can be optionally supplied on the command line. See the 'Usage Scenarios/Examples' above for examples on how to supply parameters to Virtual Client profiles. + +| Parameter | Purpose |Default | +|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------| +| ThreadCount | Optional. Number of threads to use during workload execution. | calculate(LogicalCoreCount/2) | +| WorkloadName | Optional. **Name of workload to run**; options listed [here](https://github.com/brianfrankcooper/YCSB/wiki/Core-Workloads#running-the-workloads).| workloada | +| Duration | Optional. Timespan duration of each action in the workload. | 00:05:00 | + +* **Profile Runtimes** + See the 'Metadata' section of the profile for estimated runtimes. These timings represent the length of time required to run a single round of profile actions. These timings can be used to determine minimum required runtimes for the Virtual Client in order to get results. These are often estimates based on the + number of system cores. + +* **Usage Examples** + The following section provides a few basic examples of how to use the workload profile. + + ``` bash + # When running on a single system (environment layout not required) + ./VirtualClient --profile=PERF-MONGODB-LOAD.json --packageStore="{BlobConnectionString} + ``` \ No newline at end of file diff --git a/website/docs/workloads/mongodb/testing_scripts.md b/website/docs/workloads/mongodb/testing_scripts.md new file mode 100644 index 0000000000..be4343c0ed --- /dev/null +++ b/website/docs/workloads/mongodb/testing_scripts.md @@ -0,0 +1,201 @@ +# Testing Installation Scripts +These scripts are used for testing. Made for quickly installing mongo or YCSB onto a VM. + +## Installing MongoDB on x64 Ubuntu System + +```bash +#!/bin/bash + +# Script to Install MongoDB locally onto linux machine, assuming fresh machine +# Run from root + +echo "Grabbing GPG key" +# Get MongoDB public GPG key +curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor +echo "Done grabbing GPG key" + +# Run the command "cat /etc/lsb-release" and capture the output +output=$(cat /etc/lsb-release) + +# Extract the value of DISTRIB_CODENAME +codename=$(echo "$output" | grep -oP 'DISTRIB_CODENAME=\K[^ ]+') + +echo $codename + +case $codename in + "jammy"|"focal"|"bionic"|"noble") + echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu ${codename}/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list + ;; + *) + echo "DISTRIB_CODENAME is unsupported [Supported: jammy, focal, bionic, noble]" + ;; +esac + +echo "sudo apt-get update" +#Reload local package database +sudo apt-get update + +echo "Starting Mongo install" +#Install latest Mongo +sudo apt-get install -y mongodb-org=7.0.12 mongodb-org-database=7.0.12 mongodb-org-server=7.0.12 mongodb-mongosh=1.10.6 mongodb-org-mongos=7.0.12 mongodb-org-tools=7.0.12 + +echo "Done installing Mongo!" + +# Locking Mongo packages at current version +echo "mongodb-org-database hold" | sudo dpkg --set-selections +echo "mongodb-org-server hold" | sudo dpkg --set-selections +echo "mongodb-mongosh hold" | sudo dpkg --set-selections +echo "mongodb-org-mongos hold" | sudo dpkg --set-selections +echo "mongodb-org-tools hold" | sudo dpkg --set-selections + +# Fix for server being down +sudo chown -R mongodb:mongodb /var/lib/mongodb +sudo chown mongodb:mongodb /tmp/mongodb-27017.sock + +# Server Restart for good measure +sudo systemctl restart mongod || sudo service mongod restart + +echo "end." +``` + +## Installing YCSB (Version 0.17.0) on Linux system + +``` +# !/bin/bash + +# Script to install and run YCSB locally on a Linux machine. +# +MAVEN_VERSION="3.9.8" +YCSB_VERSION="0.17.0" + +echo "Installing java Runtime Environment" +sudo apt install -y default-jre +echo "java Runtime Environment: Done" +echo "Installing java Development Kit" +sudo apt install -y default-jdk +echo "java Development Kit: Done" + +echo "Installing YCSB ..." +curl -O --location https://github.com/brianfrankcooper/YCSB/releases/download/${YCSB_VERSION}/ycsb-${YCSB_VERSION}.tar.gz +tar xfvz ycsb-${YCSB_VERSION}.tar.gz +rm ycsb-${YCSB_VERSION}.tar.gz +cd ycsb-${YCSB_VERSION} + +echo "YCSB install complete! :)" +echo "Done." +``` + +## Configure MongoDB Server +``` +# !/bin/bash + +# Add VM's private ip to bindIp list in /etc/mongod.conf + +# Grab hostname +ip=$(hostname -I) + +# Navigate to etc +cd +cd /etc + +# Add hostname to mongod.conf +sudo sed -i "/bindIp:/ s/$/, $ip/" mongod.conf + +# Gets rid of OS limit connection +ulimit -Sn +``` + +## Test Workload +``` +# !/bin/bash + +# Navigate to YCSB package +cd YCSB-production/ycsb-mongodb + +### r50w50small +# Load MongoDB database with data for r50w50small +./bin/ycsb.sh load mongodb -P workloads/workloada -p recordcount=1000000 -p maxexecutiontime=300 -p fieldcount=10 -p fieldlength=10 -p readallfields=true -threads 4 -s > small_test_load_data.txt + +echo "r50w50small Loaded!" + +# Run workload +./bin/ycsb.sh run mongodb -P workloads/workloada -p recordcount=1000000 -p maxexecutiontime=300 -p fieldcount=10 -p fieldlength=10 -p readallfields=true -p operationcount=5000000 -threads 4 -s > small_workload_run_data.txt + +echo "r50w50small Ran!" +### + +mongosh --host localhost --eval "use ycsb" --eval "db.dropDatabase()" +echo "Small Database Dropped!" + +### r50w50medium +# Load MongoDB database with data for r50w50medium +./bin/ycsb.sh load mongodb -P workloads/workloada -p recordcount=1000000 -p maxexecutiontime=300 -p fieldcount=50 -p fieldlength=50 -p readallfields=true -threads 4 -s > medium_test_load_data.txt + +echo "r50w50medium Loaded!" + +# Run workload +./bin/ycsb.sh run mongodb -P workloads/workloada -p recordcount=1000000 -p maxexecutiontime=300 -p fieldcount=50 -p fieldlength=50 -p readallfields=true -threads 4 -p operationcount=5000000 -s > medium_workload_run_data.txt + +echo "r50w50medium Ran!" +### + +mongosh --host localhost --eval "use ycsb" --eval "db.dropDatabase()" +echo "Medium Database Dropped!" + +### r50w50large +# Load MongoDB database with data for r50w50medium +./bin/ycsb.sh load mongodb -P workloads/workloada -p recordcount=1000000 -p maxexecutiontime=300 -p fieldcount=100 -p fieldlength=100 -p readallfields=true -threads 4 -s > large_test_load_data.txt + +echo "r50w50large Loaded!" + +# Run workload +./bin/ycsb.sh run mongodb -P workloads/workloada -p recordcount=1000000 -p maxexecutiontime=300 -p fieldcount=100 -p fieldlength=100 -p readallfields=true -s -p operationcount=5000000 -threads 4 > large_workload_run_data.txt + +echo "r50w50large Ran!" +### + +mongosh --host localhost --eval "use ycsb" --eval "db.dropDatabase()" +echo "Large Database Dropped!" + +### GAUNTLET (Running benchmark recommended by YCSB authors) + +# Load MongoDB database A +./bin/ycsb.sh load mongodb -P workloads/workloada -p recordcount=1000000 -p maxexecutiontime=300 -p readallfields=true -threads 4 -s > a_test_load_data.txt + +echo "Database A Loaded!" + +# Run workload A +./bin/ycsb.sh run mongodb -P workloads/workloada -p recordcount=1000000 -p maxexecutiontime=300 -p readallfields=true -s -p operationcount=5000000 -threads 4 > a_workload_run_data.txt + +echo "Workload A Ran!" + +# Run workload B +./bin/ycsb.sh run mongodb -P workloads/workloadb -p recordcount=1000000 -p maxexecutiontime=300 -p readallfields=true -s -p operationcount=5000000 -threads 4 > b_workload_run_data.txt + +echo "Workload B Ran!" + +# Run workload C +./bin/ycsb.sh run mongodb -P workloads/workloadc -p recordcount=1000000 -p maxexecutiontime=300 -p readallfields=true -s -p operationcount=5000000 -threads 4 > c_workload_run_data.txt + +echo "Workload C Ran!" + +# Run workload D +./bin/ycsb.sh run mongodb -P workloads/workloadd -p recordcount=1000000 -p maxexecutiontime=300 -p readallfields=true -s -p operationcount=5000000 -threads 4 > d_workload_run_data.txt + +echo "Workload D Ran!" + +mongosh --host localhost --eval "use ycsb" --eval "db.dropDatabase()" +echo "Database A Dropped!" + +# Load MongoDB database with data +./bin/ycsb.sh load mongodb -P workloads/workloade -p recordcount=1000000 -p maxexecutiontime=300 -s > test_loade_data.txt + +echo "Database E Loaded!" + +./bin/ycsb.sh run mongodb -P workloads/workloade -p recordcount=1000000 -p maxexecutiontime=300 readallfields=true -s -threads 4 -p operationcount=5000000 > workloade_run_data.txt + +echo "Workload E Ran!" +### + +echo "Workload Done!" +``` \ No newline at end of file From a667a8b8c04cd2adb9b9226faa5abd686f25dd23 Mon Sep 17 00:00:00 2001 From: saibulusu Date: Mon, 1 Jun 2026 23:49:00 -0700 Subject: [PATCH 2/4] copied over mongodb profiles --- .../PERF-MONGODB-FULLPASS-CLIENTSERVER.json | 383 ++++++++++++++++++ .../PERF-MONGODB-SINGLE-CLIENTSERVER.json | 252 ++++++++++++ 2 files changed, 635 insertions(+) create mode 100644 src/VirtualClient/VirtualClient.Main/profiles/PERF-MONGODB-FULLPASS-CLIENTSERVER.json create mode 100644 src/VirtualClient/VirtualClient.Main/profiles/PERF-MONGODB-SINGLE-CLIENTSERVER.json diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-MONGODB-FULLPASS-CLIENTSERVER.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-MONGODB-FULLPASS-CLIENTSERVER.json new file mode 100644 index 0000000000..fe2c808b02 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-MONGODB-FULLPASS-CLIENTSERVER.json @@ -0,0 +1,383 @@ +{ + "Description": "MongoDB YCSB client-server workload (2-VM)", + "Metadata": { + "RecommendedMinimumExecutionTime": "00:15:00", + "SupportedPlatforms": "linux-x64, linux-arm64", + "SupportedOperatingSystems": "Ubuntu" + }, + "Parameters": { + "Port": 27017, + "Database": "mongodb", + "ThreadCount": "{calculate({LogicalCoreCount}/2)}", + "WorkloadName": "workloada", + "Duration": "00:05:00", + "FieldCount": "128", + "FieldLength": "128", + "OperationCount": "5000000", + "RecordCount": "2500000", + "YCSBPackageName": "ycsb", + "JdkPackageName": "javadevelopmentkit", + "DiskFilter": "BiggestSize", + "dbSize": "medium", + "CommentOnDbSize": "dbSize is approx 40-50 GB" + }, + "ParametersOn": [ + { + "Condition": "{calculate(\"{dbSize}\" == \"small\")}", + "RecordCount": "500000", + "FieldCount": "128", + "FieldLength": "128", + "Duration": "00:05:00", + "CommentOnDbSize": "dbSize is approx 8-10 GB" + }, + { + "Condition": "{calculate(\"{dbSize}\" == \"medium\")}", + "RecordCount": "2500000", + "FieldCount": "128", + "FieldLength": "128", + "Duration": "00:30:00", + "CommentOnDbSize": "dbSize is approx 40-50 GB" + }, + { + "Condition": "{calculate(\"{dbSize}\" == \"large\")}", + "RecordCount": "20000000", + "FieldCount": "128", + "FieldLength": "128", + "Duration": "00:45:00", + "CommentOnDbSize": "dbSize is approx 320-400 GB" + }, + { + "Condition": "{calculate(\"{dbSize}\" == \"xlarge\")}", + "RecordCount": "55000000", + "FieldCount": "128", + "FieldLength": "128", + "Duration": "01:00:00", + "CommentOnDbSize": "dbSize is approx 900 GB - 1 TB" + } + ], + "Actions": [ + { + "Type": "MongoDBServerExecutor", + "Parameters": { + "Scenario": "serversetup", + "Role": "Server", + "Port": "$.Parameters.Port", + "Database": "$.Parameters.Database", + "DiskFilter": "$.Parameters.DiskFilter" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "loaddatabase_initial", + "Role": "Client", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "LoadCommand": "load {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p recordcount={RecordCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -threads {ThreadCount} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/workloada", + "Duration": "$.Parameters.Duration", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "ThreadCount": "$.Parameters.ThreadCount", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "workloada", + "Role": "Client", + "MetricScenario": "{Scenario}_read50_write50_operationcnt{OperationCount}_fieldcnt{FieldCount}_fieldlength{FieldLength}_th{ThreadCount}", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/{Scenario}", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "OperationCount": "$.Parameters.OperationCount", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "workloadb", + "Role": "Client", + "MetricScenario": "{Scenario}_read95_write05_operationcnt{OperationCount}_fieldcnt{FieldCount}_fieldlength{FieldLength}_th{ThreadCount}", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/{Scenario}", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "OperationCount": "$.Parameters.OperationCount", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "workloadc", + "Role": "Client", + "MetricScenario": "{Scenario}_read100_operationcnt{OperationCount}_fieldcnt{FieldCount}_fieldlength{FieldLength}_th{ThreadCount}", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/{Scenario}", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "OperationCount": "$.Parameters.OperationCount", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "workloadf", + "Role": "Client", + "MetricScenario": "{Scenario}_read50_rmw50_operationcnt{OperationCount}_fieldcnt{FieldCount}_fieldlength{FieldLength}_th{ThreadCount}", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/{Scenario}", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "OperationCount": "$.Parameters.OperationCount", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "workloadd", + "Role": "Client", + "MetricScenario": "{Scenario}_read95_insert05_operationcnt{OperationCount}_fieldcnt{FieldCount}_fieldlength{FieldLength}_th{ThreadCount}", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/{Scenario}", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "OperationCount": "$.Parameters.OperationCount", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "dropdatabase_after_d_before_e", + "Role": "Client", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "loaddatabase_after_d_before_e", + "Role": "Client", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "LoadCommand": "load {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p recordcount={RecordCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -threads {ThreadCount} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/workloada", + "Duration": "$.Parameters.Duration", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "ThreadCount": "$.Parameters.ThreadCount", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "workloade", + "Role": "Client", + "MetricScenario": "{Scenario}_scan95_insert05_operationcnt{OperationCount}_fieldcnt{FieldCount}_fieldlength{FieldLength}_th{ThreadCount}", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/{Scenario}", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "OperationCount": "$.Parameters.OperationCount", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "dropdatabase_atlast", + "Role": "Client", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName" + } + } + ], + "Dependencies": [ + { + "Type": "JDKPackageDependencyInstallation", + "Parameters": { + "Scenario": "InstallJDKPackage", + "BlobContainer": "packages", + "BlobName": "microsoft-jdk-21.0.1.zip", + "PackageName": "javadevelopmentkit", + "Extract": true, + "Role": "Client" + } + }, + { + "Type": "LinuxPackageInstallation", + "Parameters": { + "Scenario": "InstallMongoPrereqs", + "Packages-Apt": "gnupg, curl", + "Packages-Dnf": "gnupg, curl", + "Packages-Yum": "gnupg, curl", + "Packages-Zypper": "gpg2, curl" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoGPGKey", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc -o server-8.0.asc" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoGPGDearmor", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg server-8.0.asc" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoAddRepository", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo sh -c \"echo 'deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse' > /etc/apt/sources.list.d/mongodb-org-8.0.list\"" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoUpdatePackageList", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo apt-get update" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoInstallServer", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo apt-get install -y mongodb-org", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoInstallClient", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo apt-get install -y mongodb-mongosh", + "Role": "Client" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoDaemon", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl daemon-reload", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoStartService", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl start mongod", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoEnableService", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl enable mongod", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoServerStatus", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl status mongod", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoStopService", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl stop mongod", + "Role": "Server" + } + }, + { + "Type": "DependencyPackageInstallation", + "Parameters": { + "Scenario": "InstallYCSBPackage", + "BlobContainer": "packages", + "BlobName": "ycsb-0.17.0.zip", + "PackageName": "ycsb", + "Extract": true, + "Role": "Client" + } + }, + { + "Type": "ApiServer", + "Parameters": { + "Scenario": "StartAPIServer" + } + } + ] +} \ No newline at end of file diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-MONGODB-SINGLE-CLIENTSERVER.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-MONGODB-SINGLE-CLIENTSERVER.json new file mode 100644 index 0000000000..8ebf51c5c9 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-MONGODB-SINGLE-CLIENTSERVER.json @@ -0,0 +1,252 @@ +{ + "Description": "MongoDB Single Workload Client-Server Profile", + "Metadata": { + "RecommendedMinimumExecutionTime": "00:10:00", + "SupportedPlatforms": "linux-x64,linux-arm64", + "SupportedOperatingSystems": "Ubuntu" + }, + "Parameters": { + "ThreadCount": "{calculate({LogicalCoreCount}/2)}", + "WorkloadName": "workloada", + "Duration": "00:05:00", + "Database": "mongodb", + "Port": "27017", + "FieldCount": "128", + "FieldLength": "128", + "OperationCount": "5000000", + "RecordCount": "500000", + "YCSBPackageName": "ycsb", + "JdkPackageName": "javadevelopmentkit", + "DiskFilter": "BiggestSize", + "dbSize": "small", + "CommentOnDbSize": "dbSize is approx 8-10 GB" + }, + "ParametersOn": [ + { + "Condition": "{calculate(\"{dbSize}\" == \"small\")}", + "RecordCount": "500000", + "FieldCount": "128", + "FieldLength": "128", + "CommentOnDbSize": "dbSize is approx 8-10 GB" + }, + { + "Condition": "{calculate(\"{dbSize}\" == \"medium\")}", + "RecordCount": "2500000", + "FieldCount": "128", + "FieldLength": "128", + "Duration": "00:30:00", + "CommentOnDbSize": "dbSize is approx 40-50 GB" + }, + { + "Condition": "{calculate(\"{dbSize}\" == \"large\")}", + "RecordCount": "20000000", + "FieldCount": "128", + "FieldLength": "128", + "Duration": "00:45:00", + "CommentOnDbSize": "dbSize is approx 320-400 GB" + }, + { + "Condition": "{calculate(\"{dbSize}\" == \"xlarge\")}", + "RecordCount": "55000000", + "FieldCount": "128", + "FieldLength": "128", + "Duration": "01:00:00", + "CommentOnDbSize": "dbSize is approx 900 GB - 1 TB" + } + ], + "Actions": [ + { + "Type": "MongoDBServerExecutor", + "Parameters": { + "Scenario": "MongoDBServerSetup", + "Role": "Server", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "DiskFilter": "$.Parameters.DiskFilter" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "loaddatabase", + "Role": "Client", + "LoadCommand": "load {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p recordcount={RecordCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -threads {ThreadCount} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/{WorkloadName}", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "Duration": "$.Parameters.Duration", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "ThreadCount": "$.Parameters.ThreadCount", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "{WorkloadName}_fieldcount{FieldCount}_fieldlength{FieldLength}_OpCount{OperationCount}", + "Role": "Client", + "Database": "$.Parameters.Database", + "RunCommand": "run {Database} -s -p maxexecutiontime={Duration.TotalSeconds} -p operationcount={OperationCount} -p recordcount={RecordCount} -threads {ThreadCount} -p fieldcount={FieldCount} -p fieldlength={FieldLength} -p mongodb.url=mongodb://{ServerIP}:{Port}/ycsb -P {PackagePath:ycsb}/ycsb-0.17.0/workloads/{WorkloadName}", + "Port": "$.Parameters.Port", + "Duration": "$.Parameters.Duration", + "ThreadCount": "$.Parameters.ThreadCount", + "OperationCount": "$.Parameters.OperationCount", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName", + "RecordCount": "$.Parameters.RecordCount", + "FieldCount": "$.Parameters.FieldCount", + "FieldLength": "$.Parameters.FieldLength", + "WorkloadName": "$.Parameters.WorkloadName" + } + }, + { + "Type": "MongoDBClientExecutor", + "Parameters": { + "Scenario": "dropdatabase", + "Role": "Client", + "Database": "$.Parameters.Database", + "Port": "$.Parameters.Port", + "YCSBPackageName": "$.Parameters.YCSBPackageName", + "JdkPackageName": "$.Parameters.JdkPackageName" + } + } + ], + "Dependencies": [ + { + "Type": "JDKPackageDependencyInstallation", + "Parameters": { + "Scenario": "InstallJDKPackage", + "BlobContainer": "packages", + "BlobName": "microsoft-jdk-21.0.1.zip", + "PackageName": "javadevelopmentkit", + "Extract": true, + "Role": "Client" + } + }, + { + "Type": "LinuxPackageInstallation", + "Parameters": { + "Scenario": "InstallMongoPrereqs", + "Packages-Apt": "gnupg, curl", + "Packages-Dnf": "gnupg, curl", + "Packages-Yum": "gnupg, curl", + "Packages-Zypper": "gpg2, curl" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoGPGKey", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc -o server-8.0.asc" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoGPGDearmor", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg server-8.0.asc" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoAddRepository", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo sh -c \"echo 'deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse' > /etc/apt/sources.list.d/mongodb-org-8.0.list\"" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoUpdatePackageList", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo apt-get update" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoInstallServer", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo apt-get install -y mongodb-org", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoInstallClient", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo apt-get install -y mongodb-mongosh", + "Role": "Client" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoDaemon", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl daemon-reload", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoStartService", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl start mongod", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoEnableService", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl enable mongod", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoServerStatus", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl status mongod", + "Role": "Server" + } + }, + { + "Type": "ExecuteCommand", + "Parameters": { + "Scenario": "MongoStopService", + "SupportedPlatforms": "linux-x64,linux-arm64", + "Command": "sudo systemctl stop mongod", + "Role": "Server" + } + }, + { + "Type": "DependencyPackageInstallation", + "Parameters": { + "Scenario": "InstallYCSBPackage", + "BlobContainer": "packages", + "BlobName": "ycsb-0.17.0.zip", + "PackageName": "ycsb", + "Extract": true, + "Role": "Client" + } + }, + { + "Type": "ApiServer", + "Parameters": { + "Scenario": "StartAPIServer" + } + } + ] +} From f395a6a312837d0ce69191ed8c2da160b773ee58 Mon Sep 17 00:00:00 2001 From: saibulusu Date: Tue, 2 Jun 2026 16:50:57 -0700 Subject: [PATCH 3/4] mongodb.md documentation --- .../website/docs/workloads/mongodb/mongodb.md | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/VirtualClient/website/docs/workloads/mongodb/mongodb.md diff --git a/src/VirtualClient/website/docs/workloads/mongodb/mongodb.md b/src/VirtualClient/website/docs/workloads/mongodb/mongodb.md new file mode 100644 index 0000000000..c5cee4e62f --- /dev/null +++ b/src/VirtualClient/website/docs/workloads/mongodb/mongodb.md @@ -0,0 +1,217 @@ +# MongoDB +MongoDB is a document-oriented NoSQL database used for high volume data storage. Instead of using tables and rows as in traditional relational databases, MongoDB makes use of collections and documents. Documents consist of key-value pairs which are the basic unit of data in MongoDB. + +* [Official MongoDB Documentation](https://www.mongodb.com/docs/) +* [MongoDB GitHub Repo](https://github.com/mongodb/mongo) + +The widely used tool for benchmarking performance of a MongoDB server is Yahoo Cloud Serving Benchmark (YCSB): +* [YCSB Documentation](https://github.com/brianfrankcooper/YCSB/wiki) +* [YCSB MongoDB Binding](https://github.com/brianfrankcooper/YCSB/blob/master/mongodb/README.md) + +## What is Being Measured? +The YCSB (Yahoo Cloud Serving Benchmark) toolset is used to generate various workload patterns against MongoDB instances. YCSB performs operations such as INSERT, READ, UPDATE, and SCAN against the MongoDB server and provides throughput and latency percentile distributions. + +YCSB includes six core workload types (workloada through workloadf), each representing different use case scenarios: +- **Workload A (Update Heavy)**: 50% reads, 50% updates - Simulates a session store with recent data updates +- **Workload B (Read Mostly)**: 95% reads, 5% updates - Typical photo tagging application +- **Workload C (Read Only)**: 100% reads - User profile cache where profiles are constructed elsewhere +- **Workload D (Read Latest)**: 95% reads, 5% inserts - User status updates with latest data being more popular +- **Workload E (Short Ranges)**: 95% scans, 5% inserts - Threaded conversations where each scan picks up recent posts +- **Workload F (Read-Modify-Write)**: 50% reads, 50% read-modify-write - User database with transactions + +## Workload Metrics +The following metrics are examples of those captured by the Virtual Client when running the YCSB workload against a +MongoDB server. + +### YCSB Workload Metrics +The following table shows the list of metrics that are captured from the execution of the YCSB workload against a MongoDB server. + +| Metric Name | Example Value | Unit | Description | +|--------------|----------------|------|-------------| +| Throughput | 45235.67 | operations/sec | Overall operations processed per second | +| Operations | 5000000 | count | Total number of operations performed | +| RunTime | 110532.0 | milliseconds | Total execution time of the workload | +| INSERT-Operations | 250000 | count | Number of insert operations performed | +| INSERT-AverageLatency | 2.34 | milliseconds | Average latency for insert operations | +| INSERT-MinLatency | 0.89 | milliseconds | Minimum latency for insert operations | +| INSERT-MaxLatency | 156.78 | milliseconds | Maximum latency for insert operations | +| INSERT-95thPercentileLatency | 4.52 | milliseconds | 95th percentile latency for insert operations | +| INSERT-99thPercentileLatency | 8.91 | milliseconds | 99th percentile latency for insert operations | +| READ-Operations | 2375000 | count | Number of read operations performed | +| READ-AverageLatency | 1.87 | milliseconds | Average latency for read operations | +| READ-MinLatency | 0.45 | milliseconds | Minimum latency for read operations | +| READ-MaxLatency | 98.34 | milliseconds | Maximum latency for read operations | +| READ-95thPercentileLatency | 3.21 | milliseconds | 95th percentile latency for read operations | +| READ-99thPercentileLatency | 6.78 | milliseconds | 99th percentile latency for read operations | +| UPDATE-Operations | 2375000 | count | Number of update operations performed | +| UPDATE-AverageLatency | 2.12 | milliseconds | Average latency for update operations | +| UPDATE-MinLatency | 0.67 | milliseconds | Minimum latency for update operations | +| UPDATE-MaxLatency | 134.56 | milliseconds | Maximum latency for update operations | +| UPDATE-95thPercentileLatency | 4.23 | milliseconds | 95th percentile latency for update operations | +| UPDATE-99thPercentileLatency | 7.89 | milliseconds | 99th percentile latency for update operations | +| SCAN-Operations | 125000 | count | Number of scan operations performed | +| SCAN-AverageLatency | 15.67 | milliseconds | Average latency for scan operations | +| SCAN-MinLatency | 5.23 | milliseconds | Minimum latency for scan operations | +| SCAN-MaxLatency | 456.78 | milliseconds | Maximum latency for scan operations | +| SCAN-95thPercentileLatency | 34.56 | milliseconds | 95th percentile latency for scan operations | +| SCAN-99thPercentileLatency | 78.90 | milliseconds | 99th percentile latency for scan operations | + +## Useful MongoDB Server Commands +The following section contains commands that are useful for MongoDB server management, investigations, and debugging. + +``` bash +# Key files and directories associated with MongoDB +# - /etc/mongod.conf +# The main configuration file for the MongoDB server. +# +# - /var/lib/mongodb +# Default data directory where MongoDB stores database files. +# +# - /var/log/mongodb/mongod.log +# Default log file location. + +# Show MongoDB server status (systemd-based systems) +sudo systemctl status mongod + +# Show MongoDB server status (init.d-based systems) +sudo service mongod status + +# Start MongoDB server +sudo systemctl start mongod +# or +sudo service mongod start + +# Stop MongoDB server +sudo systemctl stop mongod +# or +sudo service mongod stop + +# Restart MongoDB server +sudo systemctl restart mongod +# or +sudo service mongod restart + +# Enable MongoDB to start on boot +sudo systemctl enable mongod + +# Fix common ownership issues (when server won't start after VM restart) +sudo chown -R mongodb:mongodb /var/lib/mongodb +sudo chown mongodb:mongodb /tmp/mongodb-27017.sock + +# Enter MongoDB shell (legacy mongo client) +mongosh + +# Connect to MongoDB server on specific host and port +mongosh --host localhost --port 27017 + +# Show all databases +mongosh --eval "show dbs" + +# Drop YCSB database +mongosh --host localhost --eval "use ycsb" --eval "db.dropDatabase()" + +# Show database collections +mongosh --eval "use ycsb" --eval "show collections" + +# Check database size +mongosh --eval "use ycsb" --eval "db.stats(1024*1024)" + +# Show current operations +mongosh --eval "db.currentOp()" + +# Check connected clients (useful for debugging client-server scenarios) +mongosh --eval "db.currentOp(true).inprog.reduce((accumulator, connection) => { ipaddress = connection.client ? connection.client.split(':')[0] : 'Internal'; accumulator[ipaddress] = (accumulator[ipaddress] || 0) + 1; accumulator['TOTAL_CONNECTION_COUNT']++; return accumulator; }, { TOTAL_CONNECTION_COUNT: 0 })" + +# Show server status with detailed metrics +mongosh --eval "db.serverStatus()" + +# Check replication status (if using replica sets) +mongosh --eval "rs.status()" + +# Show MongoDB server logs +sudo tail -f /var/log/mongodb/mongod.log + +# Check MongoDB version +mongod --version +``` + +## MongoDB Configuration for Remote Access +When running MongoDB in client-server scenarios, you need to configure the server to accept remote connections: + +``` bash +# Edit MongoDB configuration file +sudo nano /etc/mongod.conf + +# Update the network interfaces section to bind to all interfaces: +# net: +# port: 27017 +# bindIp: 0.0.0.0 + +# Restart MongoDB after configuration changes +sudo systemctl restart mongod + +# Verify MongoDB is listening on the correct port +sudo netstat -plntu | grep mongod +# or +sudo ss -tlnp | grep mongod +``` + +## YCSB Command Examples +The following are common YCSB command patterns used with MongoDB: + +``` bash +# Load data into MongoDB (basic) +./bin/ycsb load mongodb -s -P workloads/workloada -p recordcount=1000000 + +# Load data with custom properties file +./bin/ycsb load mongodb -s -P workloads/workloada -P large.dat + +# Run workload against MongoDB +./bin/ycsb run mongodb -s -P workloads/workloada -threads 16 -target 10000 + +# Run workload against remote MongoDB server +./bin/ycsb run mongodb -s -P workloads/workloada -threads 16 \ + -p mongodb.url="mongodb://10.0.0.5:27017/ycsb?w=0" + +# Run with custom operation count and time series measurements +./bin/ycsb run mongodb -s -P workloads/workloada \ + -threads 16 \ + -p operationcount=5000000 \ + -p measurementtype=timeseries \ + -p timeseries.granularity=2000 + +# Load data from multiple clients (parallel loading) +# Client 1: +./bin/ycsb load mongodb -s -P workloads/workloada \ + -p insertstart=0 -p insertcount=5000000 + +# Client 2: +./bin/ycsb load mongodb -s -P workloads/workloada \ + -p insertstart=5000000 -p insertcount=5000000 +``` + +## Important Notes and Warnings + +### Database Growth +⚠️ **Warning**: Workload E (short ranges) and Workload D (read latest) insert new records into the database. Running these workloads repeatedly will cause the dataset to grow in size over time. This can lead to server failure if MongoDB runs out of disk space. Monitor disk usage and periodically clean the database when running these workloads. + +### Disk Space Requirements +Different database sizes require different amounts of disk space: +- **Small (500K records)**: ~8-10 GB +- **Medium (2.5M records)**: ~40-50 GB +- **Large (20M records)**: ~320-400 GB +- **XLarge (55M records)**: ~880-1100 GB + +### Performance Considerations +- **Thread Count**: Generally set to half the logical core count for balanced performance +- **Target Operations**: Use `-target` parameter to control throughput for latency testing +- **Write Concern**: The `w=0` parameter in the MongoDB URL provides better performance but less durability +- **Batch Size**: For insert-heavy workloads, increase `mongodb.batchsize` for better throughput + +## Additional Resources +For more detailed information on MongoDB workload profiles and testing scenarios, see: +- [MongoDB Profile Components](./mongodb-profiles.md) +- [YCSB Details and Usage](./YCSB.md) +- [MongoDB Testing Scripts](./testing_scripts.md) +- [YCSB Core Workloads Documentation](https://github.com/brianfrankcooper/YCSB/wiki/Core-Workloads) +- [YCSB Core Properties](https://github.com/brianfrankcooper/YCSB/wiki/Core-Properties) From da4b73d2b27f6d6be2c0e8459f7660f107d18a7f Mon Sep 17 00:00:00 2001 From: saibulusu Date: Tue, 2 Jun 2026 16:55:48 -0700 Subject: [PATCH 4/4] small mistake --- .../website => website}/docs/workloads/mongodb/mongodb.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {src/VirtualClient/website => website}/docs/workloads/mongodb/mongodb.md (100%) diff --git a/src/VirtualClient/website/docs/workloads/mongodb/mongodb.md b/website/docs/workloads/mongodb/mongodb.md similarity index 100% rename from src/VirtualClient/website/docs/workloads/mongodb/mongodb.md rename to website/docs/workloads/mongodb/mongodb.md