From 2b3b58f1e8940bc03f65aa164490ca411a733869 Mon Sep 17 00:00:00 2001 From: Rakeshwar Reddy Kambaiahgari Date: Mon, 13 Apr 2026 15:13:11 -0700 Subject: [PATCH 1/3] wrk fix for cpu affinity and condition evaluation for ParametersOn --- .../Wrk/WrkExecutorTest.cs | 48 +++++++++++++++- .../ExampleWorkloadWithAffinityExecutor.cs | 12 ++-- .../VirtualClient.Actions/Wrk/WrkExecutor.cs | 15 +++-- .../VirtualClient.Actions/Wrk/runwrk.sh | 2 +- .../LinuxProcessAffinityConfigurationTests.cs | 57 +++++++++++++++++-- .../LinuxProcessAffinityConfiguration.cs | 25 +++++++- .../ExecutionProfileExtensions.cs | 19 +++++-- 7 files changed, 150 insertions(+), 28 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/Wrk/WrkExecutorTest.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/Wrk/WrkExecutorTest.cs index 7d272e474a..0deed0c6fa 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/Wrk/WrkExecutorTest.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/Wrk/WrkExecutorTest.cs @@ -164,7 +164,7 @@ public async Task WrkClientExecutorRunsWorkloadWithCorrectArguments() } else { - Assert.AreEqual(arguments, $"bash {executor.Combine(directory, "runwrk.sh")} \"{results}\""); + Assert.AreEqual(arguments, $"bash {executor.Combine(directory, "runwrk.sh")} {results}"); string examplesDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Examples", "Wrk"); string outputPath = Path.Combine(examplesDirectory, @"wrkStandardExample1.txt"); this.memoryProcess.StandardOutput = new ConcurrentBuffer(new StringBuilder(File.ReadAllText(outputPath))); @@ -612,6 +612,52 @@ public async Task GetWrkVersion_ReturnsNull_WhenVersionCannotBeParsed() this.mockFixture.Tracking.AssertCommandsExecuted(true, "sudo bash .* --version"); } + [Test] + public async Task WrkClientExecutorRunsWorkloadWithAffinityUsingCorrectQuoting() + { + string commandArgumentInput = @"--latency --threads 8 --connections 256 --duration 30s --timeout 10s http://1.2.3.4:9876/json --header ""Accept: application/json"""; + ClientInstance serverInstance = new ClientInstance(name: nameof(ClientRole.Server), ipAddress: "1.2.3.4", role: ClientRole.Server); + ClientInstance clientInstance = new ClientInstance(name: nameof(ClientRole.Client), ipAddress: "5.6.7.8", role: ClientRole.Client); + + string directory = @"/some/random/dir/name/"; + this.mockFixture.Setup(PlatformID.Unix, Architecture.X64, nameof(State)); + this.mockFixture.Layout = new EnvironmentLayout(new List() { serverInstance, clientInstance }); + this.mockFixture.Parameters = new Dictionary() + { + { "CommandArguments", commandArgumentInput }, + { "Scenario", "affinity_test" }, + { "ToolName", "wrk" }, + { "PackageName", "wrk" }, + { "BindToCores", true }, + { "CoreAffinity", "8-15" }, + { "TargetService", "server" } + }; + + TestWrkExecutor executor = new TestWrkExecutor(this.mockFixture); + executor.PackageDirectory = directory; + + this.mockFixture.FileSystem + .Setup(x => x.File.Exists(It.IsAny())) + .Returns(true); + + string examplesDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Examples", "Wrk"); + string wrkOutput = File.ReadAllText(Path.Combine(examplesDirectory, @"wrkStandardExample1.txt")); + + this.mockFixture + .TrackProcesses() + .SetupProcessOutput("--version", "wrk 4.2.0 [epoll] Copyright (C) 2012 Will Glozer") + .SetupProcessOutput("numactl", wrkOutput); + + string result = executor.GetCommandLineArguments(); + await executor.ExecuteWorkloadAsync(result, workingDir: directory).ConfigureAwait(false); + + // The affinity path uses GetAffinityProcessInfo which sets numactl as the + // executable, then CreateElevatedProcess wraps it with sudo to ensure + // ulimit and process elevation work correctly (matching non-affinity path). + string scriptPath = Regex.Escape(executor.Combine(directory, WrkExecutor.WrkRunShell)); + this.mockFixture.Tracking.AssertCommandsExecuted(true, + $@"sudo numactl -C 8-15 bash {scriptPath} {Regex.Escape(commandArgumentInput)}"); + } public void SetUpWorkloadOutput() { diff --git a/src/VirtualClient/VirtualClient.Actions/Examples/ExampleWorkloadWithAffinityExecutor.cs b/src/VirtualClient/VirtualClient.Actions/Examples/ExampleWorkloadWithAffinityExecutor.cs index 068ef59680..cc0aa72023 100644 --- a/src/VirtualClient/VirtualClient.Actions/Examples/ExampleWorkloadWithAffinityExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/Examples/ExampleWorkloadWithAffinityExecutor.cs @@ -240,15 +240,17 @@ private Task ExecuteWorkloadAsync(string commandArguments, EventContext } else { - // Linux: Wrap command with numactl for CPU binding + // Linux: Wrap command with numactl for CPU binding. + // Pass null for command and include the executable path in the arguments + // so that GetCommandWithAffinity returns "numactl -C {cores} {exe} {args}" + // wrapped in double quotes for bash -c. + string fullCommandLine = $"{this.WorkloadExecutablePath} {commandArguments}"; LinuxProcessAffinityConfiguration linuxConfig = (LinuxProcessAffinityConfiguration)affinityConfig; - string fullCommand = linuxConfig.GetCommandWithAffinity( - this.WorkloadExecutablePath, - commandArguments); + string wrappedCommand = linuxConfig.GetCommandWithAffinity(null, fullCommandLine); workloadProcess = this.processManager.CreateProcess( "/bin/bash", - $"-c \"{fullCommand}\"", + $"-c {wrappedCommand}", this.WorkloadPackage.Path); } } diff --git a/src/VirtualClient/VirtualClient.Actions/Wrk/WrkExecutor.cs b/src/VirtualClient/VirtualClient.Actions/Wrk/WrkExecutor.cs index 246130ca7d..4a369e99ba 100644 --- a/src/VirtualClient/VirtualClient.Actions/Wrk/WrkExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/Wrk/WrkExecutor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. namespace VirtualClient.Actions @@ -427,18 +427,17 @@ protected async Task ExecuteWorkloadAsync(string commandArguments, string workin relatedContext.AddContext("affinityMask", affinityConfig.ToString()); - string fullCommandLine = $"{command} \"{commandArguments}\""; LinuxProcessAffinityConfiguration linuxConfig = (LinuxProcessAffinityConfiguration)affinityConfig; - string wrappedCommand = linuxConfig.GetCommandWithAffinity(null, fullCommandLine); - process = this.SystemManagement.ProcessManager.CreateProcess( - "/bin/bash", - $"-c {wrappedCommand}", - workingDir); + // Use direct numactl invocation to avoid bash -c shell wrapping. + // The wrk arguments contain embedded double quotes (e.g., --header "Accept: ..."), + // which break the nested quoting in bash -c "numactl ... \"args\"" patterns. + var (executable, numaArguments) = linuxConfig.GetAffinityProcessInfo(command, commandArguments); + process = this.SystemManagement.ProcessManager.CreateElevatedProcess(this.Platform, executable, numaArguments, workingDir); } else { - process = await this.ExecuteCommandAsync(command, commandArguments: $"\"{commandArguments}\"", workingDir, telemetryContext, cancellationToken, runElevated: true); + process = await this.ExecuteCommandAsync(command, commandArguments: $"{commandArguments}", workingDir, telemetryContext, cancellationToken, runElevated: true); } using (process) diff --git a/src/VirtualClient/VirtualClient.Actions/Wrk/runwrk.sh b/src/VirtualClient/VirtualClient.Actions/Wrk/runwrk.sh index d7c59a5c11..14ad14f9e5 100644 --- a/src/VirtualClient/VirtualClient.Actions/Wrk/runwrk.sh +++ b/src/VirtualClient/VirtualClient.Actions/Wrk/runwrk.sh @@ -1,2 +1,2 @@ ulimit -n 65535 -./wrk $1 \ No newline at end of file +./wrk "$@" \ No newline at end of file diff --git a/src/VirtualClient/VirtualClient.Common.UnitTests/ProcessAffinity/LinuxProcessAffinityConfigurationTests.cs b/src/VirtualClient/VirtualClient.Common.UnitTests/ProcessAffinity/LinuxProcessAffinityConfigurationTests.cs index 3e55e59eb5..06fe5dcfe9 100644 --- a/src/VirtualClient/VirtualClient.Common.UnitTests/ProcessAffinity/LinuxProcessAffinityConfigurationTests.cs +++ b/src/VirtualClient/VirtualClient.Common.UnitTests/ProcessAffinity/LinuxProcessAffinityConfigurationTests.cs @@ -83,7 +83,7 @@ public void LinuxProcessAffinityConfigurationGeneratesCorrectNumactlCommandForSi LinuxProcessAffinityConfiguration config = new LinuxProcessAffinityConfiguration(new[] { 0 }); string command = config.GetCommandWithAffinity(null, "myworkload --arg1 --arg2"); - + Assert.AreEqual("\"numactl -C 0 myworkload --arg1 --arg2\"", command); } @@ -93,7 +93,7 @@ public void LinuxProcessAffinityConfigurationGeneratesCorrectNumactlCommandForMu LinuxProcessAffinityConfiguration config = new LinuxProcessAffinityConfiguration(new[] { 0, 1, 2 }); string command = config.GetCommandWithAffinity(null, "myworkload --arg1 --arg2"); - + Assert.AreEqual("\"numactl -C 0-2 myworkload --arg1 --arg2\"", command); } @@ -103,7 +103,7 @@ public void LinuxProcessAffinityConfigurationGeneratesCorrectNumactlCommandWithE LinuxProcessAffinityConfiguration config = new LinuxProcessAffinityConfiguration(new[] { 1, 3, 5 }); string command = config.GetCommandWithAffinity(null, "myworkload"); - + Assert.AreEqual("\"numactl -C 1,3,5 myworkload\"", command); } @@ -163,11 +163,58 @@ public void LinuxProcessAffinityConfigurationHandlesUnsortedCores() { // Cores should be sorted before optimization LinuxProcessAffinityConfiguration config = new LinuxProcessAffinityConfiguration(new[] { 5, 0, 2, 1, 3 }); - + string command = config.GetCommandWithAffinity("test", null); - + // Should sort and optimize: 0-3,5 Assert.IsTrue(command.Contains("-C 0-3,5")); } + + [Test] + public void GetAffinityProcessInfoReturnsNumactlAsExecutable() + { + LinuxProcessAffinityConfiguration config = new LinuxProcessAffinityConfiguration(new[] { 0 }); + + var (executable, arguments) = config.GetAffinityProcessInfo("mycommand"); + + Assert.AreEqual("numactl", executable); + } + + [Test] + public void GetAffinityProcessInfoReturnsCorrectArgumentsWithCommand() + { + LinuxProcessAffinityConfiguration config = new LinuxProcessAffinityConfiguration(new[] { 8, 9, 10, 11, 12, 13, 14, 15 }); + + var (executable, arguments) = config.GetAffinityProcessInfo("bash /path/to/script.sh", "--latency --threads 64"); + + Assert.AreEqual("numactl", executable); + Assert.AreEqual("-C 8-15 bash /path/to/script.sh --latency --threads 64", arguments); + } + + [Test] + public void GetAffinityProcessInfoReturnsCorrectArgumentsWithoutArguments() + { + LinuxProcessAffinityConfiguration config = new LinuxProcessAffinityConfiguration(new[] { 0, 1, 2 }); + + var (executable, arguments) = config.GetAffinityProcessInfo("bash /path/to/script.sh"); + + Assert.AreEqual("numactl", executable); + Assert.AreEqual("-C 0-2 bash /path/to/script.sh", arguments); + } + + [Test] + public void GetAffinityProcessInfoHandlesArgumentsWithEmbeddedDoubleQuotes() + { + LinuxProcessAffinityConfiguration config = new LinuxProcessAffinityConfiguration(new[] { 0, 1 }); + + var (executable, arguments) = config.GetAffinityProcessInfo( + "bash /path/runwrk.sh", + "--latency --header \"Accept: application/json\""); + + Assert.AreEqual("numactl", executable); + Assert.AreEqual( + "-C 0,1 bash /path/runwrk.sh --latency --header \"Accept: application/json\"", + arguments); + } } } diff --git a/src/VirtualClient/VirtualClient.Common/ProcessAffinity/LinuxProcessAffinityConfiguration.cs b/src/VirtualClient/VirtualClient.Common/ProcessAffinity/LinuxProcessAffinityConfiguration.cs index 52e8817e91..354248ac8c 100644 --- a/src/VirtualClient/VirtualClient.Common/ProcessAffinity/LinuxProcessAffinityConfiguration.cs +++ b/src/VirtualClient/VirtualClient.Common/ProcessAffinity/LinuxProcessAffinityConfiguration.cs @@ -36,13 +36,34 @@ public string NumactlCoreSpec /// /// Wraps a command with numactl to apply CPU affinity. /// Returns the full bash command string ready for execution. + /// The returned string is wrapped in double quotes so that it can be passed + /// as a single argument to "bash -c" (e.g., bash -c "numactl -C 0,1 command args"). /// /// The command to wrap. /// Optional arguments for the command. - /// The complete command string with numactl wrapper (e.g., "bash -c \"numactl -C 0,1 redis-server --port 6379\""). + /// The complete command string with numactl wrapper (e.g., "\"numactl -C 0,1 redis-server --port 6379\""). public string GetCommandWithAffinity(string command, string arguments = null) { - return string.IsNullOrEmpty(command) ? $"\"numactl -C {this.NumactlCoreSpec} {arguments}\"" : $"{command} \"numactl -C {this.NumactlCoreSpec} {arguments}\""; + return string.IsNullOrEmpty(command) + ? $"\"numactl -C {this.NumactlCoreSpec} {arguments}\"" + : $"{command} \"numactl -C {this.NumactlCoreSpec} {arguments}\""; + } + + /// + /// Gets the numactl executable name and arguments for direct process invocation + /// without a bash -c shell wrapper. This approach avoids double-quote escaping + /// issues when arguments contain embedded quotes (e.g., HTTP headers). + /// + /// The command to run under numactl (e.g., "bash /path/script.sh"). + /// Optional arguments for the command. + /// A tuple of (Executable, Arguments) for use with ProcessManager.CreateProcess. + public (string Executable, string Arguments) GetAffinityProcessInfo(string command, string arguments = null) + { + string numaArgs = string.IsNullOrWhiteSpace(arguments) + ? $"-C {this.NumactlCoreSpec} {command}" + : $"-C {this.NumactlCoreSpec} {command} {arguments}"; + + return ("numactl", numaArgs); } /// diff --git a/src/VirtualClient/VirtualClient.Contracts/ExecutionProfileExtensions.cs b/src/VirtualClient/VirtualClient.Contracts/ExecutionProfileExtensions.cs index 906db61c33..eba406cb2c 100644 --- a/src/VirtualClient/VirtualClient.Contracts/ExecutionProfileExtensions.cs +++ b/src/VirtualClient/VirtualClient.Contracts/ExecutionProfileExtensions.cs @@ -55,13 +55,16 @@ public static async Task EvaluateConditionalParametersAsync(this ExecutionProfil $"Invalid '{nameof(profile.ParametersOn)}' configuration. A '{conditionKey}' must be defined in each '{nameof(profile.ParametersOn)}' section."); } - // Parameters in ParametersOn sections take priority over the profile's default parameters. - var conditionalParameters = new Dictionary(profileParameters, StringComparer.OrdinalIgnoreCase); - conditionalParameters.AddRange(profileConditionalParameters, true); + // Evaluate the condition using only the original profile parameters so that + // override values from the ParametersOn section do not affect the condition result. + var conditionContext = new Dictionary(profileParameters, StringComparer.OrdinalIgnoreCase) + { + [conditionKey] = condition + }; - await evaluator.EvaluateAsync(dependencies, conditionalParameters); + await evaluator.EvaluateAsync(dependencies, conditionContext); - if (!bool.TryParse(conditionalParameters[conditionKey].ToString(), out bool conditionMatches)) + if (!bool.TryParse(conditionContext[conditionKey].ToString(), out bool conditionMatches)) { throw new SchemaException( $"Invalid '{nameof(profile.ParametersOn)}' configuration. A '{conditionKey}' must always evaluate to true or false."); @@ -69,7 +72,11 @@ public static async Task EvaluateConditionalParametersAsync(this ExecutionProfil if (conditionMatches) { - profile.Parameters.AddRange(conditionalParameters.Where(p => p.Key != conditionKey), true); + // Merge override parameters and re-evaluate for expression resolution. + var resolvedParameters = new Dictionary(profileParameters, StringComparer.OrdinalIgnoreCase); + resolvedParameters.AddRange(profileConditionalParameters.Where(p => p.Key != conditionKey), true); + await evaluator.EvaluateAsync(dependencies, resolvedParameters); + profile.Parameters.AddRange(resolvedParameters.Where(p => p.Key != conditionKey), true); break; } } From b7bacfef8487a02490785af21af7026760b3ebeb Mon Sep 17 00:00:00 2001 From: Rakeshwar Reddy Kambaiahgari Date: Mon, 13 Apr 2026 15:43:07 -0700 Subject: [PATCH 2/3] up version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ec187c4425..d19cf84f9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.16 +3.0.17 From 9f9945e76cc843b298be5dab2f19351c7bf59e74 Mon Sep 17 00:00:00 2001 From: Rakeshwar Reddy Kambaiahgari Date: Mon, 13 Apr 2026 15:59:38 -0700 Subject: [PATCH 3/3] wrk2 fix --- .../VirtualClient.Actions.UnitTests/Wrk/Wrk2ExecutorTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/Wrk/Wrk2ExecutorTest.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/Wrk/Wrk2ExecutorTest.cs index ce9e42d36a..ec53b6172d 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/Wrk/Wrk2ExecutorTest.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/Wrk/Wrk2ExecutorTest.cs @@ -469,7 +469,7 @@ public async Task WrkClientExecutorReturnsCorrectArguments() } else { - Assert.AreEqual(arguments, $"bash {executor.Combine(directory, "runwrk.sh")} \"{results}\""); + Assert.AreEqual(arguments, $"bash {executor.Combine(directory, "runwrk.sh")} {results}"); string examplesDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Examples", "Wrk"); string outputPath = Path.Combine(examplesDirectory, @"wrkStandardExample1.txt"); this.memoryProcess.StandardOutput = new ConcurrentBuffer(new StringBuilder(File.ReadAllText(outputPath)));