Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.0.16
3.0.17
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down Expand Up @@ -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<ClientInstance>() { serverInstance, clientInstance });
this.mockFixture.Parameters = new Dictionary<string, IConvertible>()
{
{ "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<string>()))
.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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,17 @@ private Task<string> 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);
}
}
Expand Down
15 changes: 7 additions & 8 deletions src/VirtualClient/VirtualClient.Actions/Wrk/WrkExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace VirtualClient.Actions
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/VirtualClient/VirtualClient.Actions/Wrk/runwrk.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ulimit -n 65535
./wrk $1
./wrk "$@"
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,34 @@ public string NumactlCoreSpec
/// <summary>
/// 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").
/// </summary>
/// <param name="command">The command to wrap.</param>
/// <param name="arguments">Optional arguments for the command.</param>
/// <returns>The complete command string with numactl wrapper (e.g., "bash -c \"numactl -C 0,1 redis-server --port 6379\"").</returns>
/// <returns>The complete command string with numactl wrapper (e.g., "\"numactl -C 0,1 redis-server --port 6379\"").</returns>
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}\"";
}

/// <summary>
/// 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).
/// </summary>
/// <param name="command">The command to run under numactl (e.g., "bash /path/script.sh").</param>
/// <param name="arguments">Optional arguments for the command.</param>
/// <returns>A tuple of (Executable, Arguments) for use with ProcessManager.CreateProcess.</returns>
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);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,28 @@ 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<string, IConvertible>(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<string, IConvertible>(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.");
}

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<string, IConvertible>(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;
}
}
Expand Down
Loading