Skip to content

Commit

Permalink
Add cancellation token support (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyredondo committed Oct 3, 2023
1 parent ab01fc1 commit 3705bab
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 48 deletions.
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>0.1.0</Version>
<Version>0.1.1</Version>
<Authors>Tony Redondo, Grégory Léocadie</Authors>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
125 changes: 88 additions & 37 deletions src/TimeItSharp.Common/ScenarioProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public void CleanScenario(Scenario scenario)
{
}

public async Task<ScenarioResult> ProcessScenarioAsync(int index, Scenario scenario)
public async Task<ScenarioResult?> ProcessScenarioAsync(int index, Scenario scenario, CancellationToken cancellationToken)
{
_callbacksTriggers.ScenarioStart(scenario);
Stopwatch? watch = null;
Expand Down Expand Up @@ -190,12 +190,22 @@ public async Task<ScenarioResult> ProcessScenarioAsync(int index, Scenario scena

AnsiConsole.Markup(" [gold3_1]Warming up[/]");
watch = Stopwatch.StartNew();
await RunScenarioAsync(_configuration.WarmUpCount, index, scenario, false).ConfigureAwait(false);
await RunScenarioAsync(_configuration.WarmUpCount, index, scenario, false, cancellationToken: cancellationToken).ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
{
return null;
}

AnsiConsole.MarkupLine(" Duration: {0}s", watch.Elapsed.TotalSeconds);
AnsiConsole.Markup(" [green3]Run[/]");
var start = DateTime.UtcNow;
watch = Stopwatch.StartNew();
var dataPoints = await RunScenarioAsync(_configuration.Count, index, scenario, true).ConfigureAwait(false);
watch.Restart();
var dataPoints = await RunScenarioAsync(_configuration.Count, index, scenario, true, cancellationToken: cancellationToken).ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
{
return null;
}

watch.Stop();
AnsiConsole.MarkupLine(" Duration: {0}s", watch.Elapsed.TotalSeconds);
AnsiConsole.WriteLine();
Expand Down Expand Up @@ -301,13 +311,19 @@ public async Task<ScenarioResult> ProcessScenarioAsync(int index, Scenario scena
};
}

private async Task<List<DataPoint>> RunScenarioAsync(int count, int index, Scenario scenario, bool checkShouldContinue)
private async Task<List<DataPoint>> RunScenarioAsync(int count, int index, Scenario scenario, bool checkShouldContinue, CancellationToken cancellationToken)
{
var dataPoints = new List<DataPoint>();
AnsiConsole.Markup(" ");
for (var i = 0; i < count; i++)
{
var currentRun = await RunCommandAsync(index, scenario).ConfigureAwait(false);
var currentRun = await RunCommandAsync(index, scenario, cancellationToken).ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
{
AnsiConsole.Markup("[red]cancelled[/]");
break;
}

dataPoints.Add(currentRun);
AnsiConsole.Markup(currentRun.Status == Status.Failed ? "[red]x[/]" : "[green].[/]");

Expand All @@ -322,7 +338,7 @@ private async Task<List<DataPoint>> RunScenarioAsync(int count, int index, Scena
return dataPoints;
}

private async Task<DataPoint> RunCommandAsync(int index, Scenario scenario)
private async Task<DataPoint> RunCommandAsync(int index, Scenario scenario, CancellationToken cancellationToken)
{
// Prepare variables
var cmdString = scenario.ProcessName ?? string.Empty;
Expand Down Expand Up @@ -374,24 +390,43 @@ private async Task<DataPoint> RunCommandAsync(int index, Scenario scenario)
_callbacksTriggers.ExecutionStart(dataPoint, ref cmd);
if (cmdTimeout <= 0)
{
var cmdResult = await cmd.ExecuteBufferedAsync().ConfigureAwait(false);
dataPoint.End = DateTime.UtcNow;
dataPoint.Duration = cmdResult.RunTime;
dataPoint.Start = dataPoint.End - dataPoint.Duration;
ExecuteAssertions(index, scenario.Name, dataPoint, cmdResult);
try
{
var cmdResult = await cmd.ExecuteBufferedAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
dataPoint.End = DateTime.UtcNow;
dataPoint.Duration = cmdResult.RunTime;
dataPoint.Start = dataPoint.End - dataPoint.Duration;
ExecuteAssertions(index, scenario.Name, dataPoint, cmdResult);
}
catch (TaskCanceledException)
{
dataPoint.End = DateTime.UtcNow;
dataPoint.Duration = dataPoint.End - dataPoint.Start;
dataPoint.Error = "Execution cancelled.";
return dataPoint;
}
catch (OperationCanceledException)
{
dataPoint.End = DateTime.UtcNow;
dataPoint.Duration = dataPoint.End - dataPoint.Start;
dataPoint.Error = "Execution cancelled.";
return dataPoint;
}
}
else
{
CancellationTokenSource? timeoutCts = null;
var cmdCts = new CancellationTokenSource();
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cmdCts.Token);
dataPoint.Start = DateTime.UtcNow;
var cmdTask = cmd.ExecuteBufferedAsync(cmdCts.Token);
var cmdTask = cmd.ExecuteBufferedAsync(linkedCts.Token);

if (!string.IsNullOrEmpty(timeoutCmdString))
{
timeoutCts = new CancellationTokenSource();
_ = RunCommandTimeoutAsync(TimeSpan.FromSeconds(cmdTimeout), timeoutCmdString, timeoutCmdArguments,
workingDirectory, cmdTask.ProcessId, () => cmdCts.Cancel(), timeoutCts.Token);
workingDirectory, cmdTask.ProcessId, () => cmdCts.Cancel(), timeoutCts.Token, cancellationToken);
}
else
{
Expand All @@ -411,12 +446,24 @@ private async Task<DataPoint> RunCommandAsync(int index, Scenario scenario)
{
dataPoint.End = DateTime.UtcNow;
dataPoint.Duration = dataPoint.End - dataPoint.Start;
if (cancellationToken.IsCancellationRequested)
{
dataPoint.Error = "Execution cancelled.";
return dataPoint;
}

dataPoint.Error = "Process timeout.";
}
catch (OperationCanceledException)
{
dataPoint.End = DateTime.UtcNow;
dataPoint.Duration = dataPoint.End - dataPoint.Start;
if (cancellationToken.IsCancellationRequested)
{
dataPoint.Error = "Execution cancelled.";
return dataPoint;
}

dataPoint.Error = "Process timeout.";
}
}
Expand Down Expand Up @@ -576,37 +623,41 @@ private void ExecuteAssertions(int scenarioId, string scenarioName, DataPoint da
}

private async Task RunCommandTimeoutAsync(TimeSpan timeout, string timeoutCmd, string timeoutArgument,
string workingDirectory, int targetPid, Action? targetCancellation, CancellationToken cancellationToken)
string workingDirectory, int targetPid, Action? targetCancellation, CancellationToken timeoutCancellationToken, CancellationToken applicationCancellationToken)
{
try
{
await Task.Delay(timeout, cancellationToken).ConfigureAwait(false);
if (!cancellationToken.IsCancellationRequested)
using var linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationToken, applicationCancellationToken);
await Task.Delay(timeout, linkedCts.Token).ConfigureAwait(false);
if (linkedCts.Token.IsCancellationRequested)
{
var targetPidString = targetPid.ToString();
var templateVariables = _templateVariables.Clone();
templateVariables.Add("PID", targetPidString);
return;
}

timeoutCmd = templateVariables.Expand(timeoutCmd);
timeoutCmd = timeoutCmd.Replace("%pid%", targetPidString);
var targetPidString = targetPid.ToString();
var templateVariables = _templateVariables.Clone();
templateVariables.Add("PID", targetPidString);

timeoutArgument = templateVariables.Expand(timeoutArgument);
timeoutArgument = timeoutArgument.Replace("%pid%", targetPidString);
timeoutCmd = templateVariables.Expand(timeoutCmd);
timeoutCmd = timeoutCmd.Replace("%pid%", targetPidString);

var cmd = Cli.Wrap(timeoutCmd)
.WithWorkingDirectory(workingDirectory)
.WithValidation(CommandResultValidation.None);
if (!string.IsNullOrEmpty(timeoutArgument))
{
cmd = cmd.WithArguments(timeoutArgument);
}
timeoutArgument = templateVariables.Expand(timeoutArgument);
timeoutArgument = timeoutArgument.Replace("%pid%", targetPidString);

var cmdResult = await cmd.ExecuteBufferedAsync().ConfigureAwait(false);
if (cmdResult.ExitCode != 0)
{
AnsiConsole.MarkupLine($"[red]{cmdResult.StandardError}[/]");
AnsiConsole.MarkupLine(cmdResult.StandardOutput);
}
var cmd = Cli.Wrap(timeoutCmd)
.WithWorkingDirectory(workingDirectory)
.WithValidation(CommandResultValidation.None);
if (!string.IsNullOrEmpty(timeoutArgument))
{
cmd = cmd.WithArguments(timeoutArgument);
}

var cmdResult = await cmd.ExecuteBufferedAsync(applicationCancellationToken).ConfigureAwait(false);
if (cmdResult.ExitCode != 0)
{
AnsiConsole.MarkupLine($"[red]{cmdResult.StandardError}[/]");
AnsiConsole.MarkupLine(cmdResult.StandardOutput);
}
}
catch (TaskCanceledException)
Expand Down
28 changes: 20 additions & 8 deletions src/TimeItSharp.Common/TimeItEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,37 @@ public static class TimeItEngine
/// </summary>
/// <param name="configurationFile">Configuration file to be executed</param>
/// <param name="templateVariables">Template variables instance</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Exit code of the TimeIt engine</returns>
public static Task<int> RunAsync(string configurationFile, TemplateVariables? templateVariables = null)
public static Task<int> RunAsync(string configurationFile, TemplateVariables? templateVariables = null, CancellationToken? cancellationToken = null)
{
// Load configuration
var config = Config.LoadConfiguration(configurationFile);
return RunAsync(config, templateVariables);
return RunAsync(config, templateVariables, cancellationToken);
}

/// <summary>
/// Runs TimeIt
/// </summary>
/// <param name="configBuilder">Configuration builder instance to be executed</param>
/// <param name="templateVariables">Template variables instance</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Exit code of the TimeIt engine</returns>
public static Task<int> RunAsync(ConfigBuilder configBuilder, TemplateVariables? templateVariables = null)
public static Task<int> RunAsync(ConfigBuilder configBuilder, TemplateVariables? templateVariables = null, CancellationToken? cancellationToken = null)
{
return RunAsync(configBuilder.Build(), templateVariables);
return RunAsync(configBuilder.Build(), templateVariables, cancellationToken);
}

/// <summary>
/// Runs TimeIt
/// </summary>
/// <param name="config">Configuration instance to be executed</param>
/// <param name="templateVariables">Template variables instance</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Exit code of the TimeIt engine</returns>
public static async Task<int> RunAsync(Config config, TemplateVariables? templateVariables = null)
public static async Task<int> RunAsync(Config config, TemplateVariables? templateVariables = null, CancellationToken? cancellationToken = null)
{
cancellationToken ??= CancellationToken.None;
templateVariables ??= new TemplateVariables();

// Prepare configuration
Expand Down Expand Up @@ -96,13 +100,21 @@ public static async Task<int> RunAsync(Config config, TemplateVariables? templat
processor.PrepareScenario(scenario);

// Process scenario
var result = await processor.ProcessScenarioAsync(i, scenario).ConfigureAwait(false);
if (result.Status != Status.Passed)
var result = await processor.ProcessScenarioAsync(i, scenario, cancellationToken: cancellationToken.Value).ConfigureAwait(false);
if (cancellationToken.Value.IsCancellationRequested)
{
return 1;
}

if (result is null || result.Status != Status.Passed)
{
scenarioWithErrors++;
}

scenariosResults.Add(result);
if (result is not null)
{
scenariosResults.Add(result);
}
}

// Export data
Expand Down
4 changes: 2 additions & 2 deletions test/TimeItSharp.FluentConfiguration.Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@
.WithName("CallTarget & Inlining")
.WithEnvironmentVariable("DD_TRACE_CALLTARGET_ENABLED", "true")
.WithEnvironmentVariable("DD_CLR_ENABLE_INLINING", "true"));
await TimeItEngine.RunAsync(config);

Environment.ExitCode = await TimeItEngine.RunAsync(config);

0 comments on commit 3705bab

Please sign in to comment.