Skip to content

Commit 22fe633

Browse files
Copilotnchapagain001Nirjan Chapagain
authored
Emit explicit telemetry when workload execution is canceled due to experiment timeout (#697)
* Emit explicit telemetry when workload is canceled due to experiment timeout Add warning-level telemetry events in two key locations: 1. ProfileExecutor.ExperimentTimeoutReached - logged when the experiment timeout is reached, before canceling in-progress workloads 2. {TypeName}.ExecutionCancelled - logged per-component when an OperationCanceledException is caught due to cancellation token Both events include a structured 'terminationReason' field set to 'ExperimentTimeout' and a descriptive message to help users and dashboards distinguish timeout-induced cancellations from real failures. Agent-Logs-Url: https://github.com/microsoft/VirtualClient/sessions/ed26e102-6dfe-4c37-ab5f-c52008932555 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nchapagain001 <165215502+nchapagain001@users.noreply.github.com> Co-authored-by: Nirjan Chapagain <nchapagain@example.com>
1 parent d949dba commit 22fe633

4 files changed

Lines changed: 98 additions & 0 deletions

File tree

src/VirtualClient/VirtualClient.Contracts.UnitTests/VirtualClientComponentTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,53 @@ public async Task VirtualClientComponentLogsExpectedMetricsOnFailedExecutions_Sc
668668
&& context.Properties["toolName"].ToString() == component.TypeName);
669669
}
670670

671+
[Test]
672+
public async Task VirtualClientComponentLogsExplicitTelemetryWhenExecutionIsCancelled()
673+
{
674+
// Scenario:
675+
// The cancellation token is cancelled while the component is executing.
676+
// The component should emit explicit telemetry indicating the execution was cancelled.
677+
using (CancellationTokenSource cts = new CancellationTokenSource())
678+
{
679+
TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);
680+
component.OnExecute = (EventContext telemetryContext, CancellationToken cancellationToken) =>
681+
{
682+
cts.Cancel();
683+
cancellationToken.ThrowIfCancellationRequested();
684+
};
685+
686+
await component.ExecuteAsync(cts.Token);
687+
688+
var cancelMessages = this.mockFixture.Logger.MessagesLogged($"{component.TypeName}.ExecutionCancelled");
689+
Assert.IsNotEmpty(cancelMessages);
690+
Assert.AreEqual(1, cancelMessages.Count());
691+
692+
EventContext context = cancelMessages.First().Item3 as EventContext;
693+
Assert.IsNotNull(context);
694+
Assert.IsTrue(context.Properties.ContainsKey("executionCancelled"));
695+
Assert.AreEqual(true, context.Properties["executionCancelled"]);
696+
}
697+
}
698+
699+
[Test]
700+
public async Task VirtualClientComponentDoesNotLogCancellationTelemetryWhenTokenIsNotCancelled()
701+
{
702+
// Scenario:
703+
// The component throws OperationCanceledException for reasons other than
704+
// cancellation token being cancelled (e.g. Task.Delay cancellation).
705+
// No cancellation telemetry should be emitted.
706+
TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);
707+
component.OnExecute = (EventContext telemetryContext, CancellationToken cancellationToken) =>
708+
{
709+
throw new OperationCanceledException();
710+
};
711+
712+
await component.ExecuteAsync(CancellationToken.None);
713+
714+
var cancelMessages = this.mockFixture.Logger.MessagesLogged($"{component.TypeName}.ExecutionCancelled");
715+
Assert.IsEmpty(cancelMessages);
716+
}
717+
671718
[Test]
672719
public void VirtualClientComponentIsSupportedRespectsSupportedPlatformAttribute()
673720
{

src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,13 @@ await this.Logger.LogMessageAsync($"{this.TypeName}.Execute", telemetryContext,
780780
catch (OperationCanceledException)
781781
{
782782
// Expected for cases where a cancellation token is cancelled.
783+
if (cancellationToken.IsCancellationRequested)
784+
{
785+
EventContext cancelContext = telemetryContext.Clone()
786+
.AddContext("executionCancelled", true);
787+
788+
this.Logger.LogMessage($"{this.TypeName}.ExecutionCancelled", LogLevel.Warning, cancelContext);
789+
}
783790
}
784791
catch (Exception)
785792
{

src/VirtualClient/VirtualClient.Core.UnitTests/ProfileExecutorTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,40 @@ public async Task ProfileExecutorExitsImmediatelyOnAnyErrorWheneverTheFailFastOp
861861
}
862862
}
863863

864+
[Test]
865+
public async Task ProfileExecutorLogsExplicitTelemetryWhenExperimentTimeoutIsReached()
866+
{
867+
// Scenario:
868+
// An explicit timeout is provided and the profile executor reaches that timeout.
869+
// The executor should emit explicit telemetry indicating the experiment timeout was reached.
870+
using (TestProfileExecutor executor = new TestProfileExecutor(this.mockProfile, this.mockFixture.Dependencies, logger: this.mockFixture.Logger))
871+
{
872+
executor.ExecuteActions = true;
873+
executor.ExecuteDependencies = false;
874+
875+
ProfileTiming explicitTimeout = new ProfileTiming(TimeSpan.FromMicroseconds(50));
876+
Task executionTask = executor.ExecuteAsync(explicitTimeout, CancellationToken.None);
877+
878+
while (!executionTask.IsCompleted)
879+
{
880+
await Task.Delay(10).ConfigureAwait(false);
881+
}
882+
883+
executionTask.ThrowIfErrored();
884+
885+
Assert.IsTrue(explicitTimeout.IsTimedOut);
886+
887+
var timeoutMessages = this.mockFixture.Logger.MessagesLogged("ProfileExecutor.ExperimentTimeoutReached");
888+
Assert.IsNotEmpty(timeoutMessages);
889+
Assert.AreEqual(1, timeoutMessages.Count());
890+
891+
EventContext context = timeoutMessages.First().Item3 as EventContext;
892+
Assert.IsNotNull(context);
893+
Assert.IsTrue(context.Properties.ContainsKey("timeout"));
894+
Assert.IsTrue(context.Properties.ContainsKey("timeoutTimestamp"));
895+
}
896+
}
897+
864898
private class TestProfileExecutor : ProfileExecutor
865899
{
866900
public TestProfileExecutor(ExecutionProfile profile, IServiceCollection dependencies, ComponentSettings settings = null, IEnumerable<string> scenarios = null, ILogger logger = null)

src/VirtualClient/VirtualClient.Core/ProfileExecutor.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,16 @@ public async Task ExecuteAsync(ProfileTiming timing, CancellationToken cancellat
268268
}
269269

270270
// If we timeout or a reboot is requested, we will request all background processes to cancel/exit.
271+
if (timing.IsTimedOut)
272+
{
273+
EventContext timeoutContext = EventContext.Persisted()
274+
.AddContext("timeout", timing.Duration)
275+
.AddContext("timeoutTimestamp", timing.Timeout);
276+
277+
this.Logger.LogMessage($"{nameof(ProfileExecutor)}.ExperimentTimeoutReached", LogLevel.Warning, timeoutContext);
278+
ConsoleLogger.Default.LogWarning("Profile: Experiment timeout reached. Canceling in-progress workloads.");
279+
}
280+
271281
await tokenSource.CancelAsync();
272282

273283
// We allow the user to supply an instruction on the command line to force the application

0 commit comments

Comments
 (0)