From f9891b635a48795f82c0b2d43c97e1b71bfbeb06 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Fri, 14 Nov 2025 10:07:44 -0800 Subject: [PATCH 1/3] Use `CancellationToken` in `AsyncExecution` --- .../AsyncExecution.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs b/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs index c0728eec5..0b244da89 100644 --- a/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs +++ b/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs @@ -19,19 +19,23 @@ public static class AsyncExecution /// The result of the function. /// Thrown when is null. /// Thrown when the execution does not complete within the timeout. - public static async Task ExecuteWithTimeoutAsync(Func> toExecute, TimeSpan timeout, CancellationToken cancellationToken) + public static async Task ExecuteWithTimeoutAsync(Func> toExecute, TimeSpan timeout, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(toExecute); - var work = Task.Run(toExecute); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(timeout); - var completedInTime = await Task.Run(() => work.Wait(timeout)); - if (!completedInTime) + var work = Task.Run(toExecute, cts.Token); + + try + { + return await work; + } + catch (OperationCanceledException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) { throw new TimeoutException($"The execution did not complete in the allotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion"); } - - return await work; } /// @@ -43,13 +47,20 @@ public static async Task ExecuteWithTimeoutAsync(Func> toExecute, /// A representing the asynchronous operation. /// Thrown when is null. /// Thrown when the execution does not complete within the timeout. - public static async Task ExecuteVoidWithTimeoutAsync(Action toExecute, TimeSpan timeout, CancellationToken cancellationToken) + public static async Task ExecuteVoidWithTimeoutAsync(Action toExecute, TimeSpan timeout, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(toExecute); - var work = Task.Run(toExecute, cancellationToken); - var completedInTime = await Task.Run(() => work.Wait(timeout)); - if (!completedInTime) + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(timeout); + + var work = Task.Run(toExecute, cts.Token); + + try + { + await work; + } + catch (OperationCanceledException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) { throw new TimeoutException($"The execution did not complete in the allotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion"); } From a4be1a9a374df313f064c30a66652d9c16d9d523 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Fri, 14 Nov 2025 10:42:43 -0800 Subject: [PATCH 2/3] Review comments --- .../AsyncExecution.cs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs b/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs index 0b244da89..bec556cca 100644 --- a/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs +++ b/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs @@ -23,16 +23,12 @@ public static async Task ExecuteWithTimeoutAsync(Func> toExecute, { ArgumentNullException.ThrowIfNull(toExecute); - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(timeout); - - var work = Task.Run(toExecute, cts.Token); - + var work = toExecute(); try { - return await work; + return await work.WaitAsync(timeout, cancellationToken); } - catch (OperationCanceledException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + catch (TimeoutException) { throw new TimeoutException($"The execution did not complete in the allotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion"); } @@ -47,20 +43,18 @@ public static async Task ExecuteWithTimeoutAsync(Func> toExecute, /// A representing the asynchronous operation. /// Thrown when is null. /// Thrown when the execution does not complete within the timeout. + /// NOTE: If the function to execute does not respect cancellation tokens, it may continue running in the background after a timeout. public static async Task ExecuteVoidWithTimeoutAsync(Action toExecute, TimeSpan timeout, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(toExecute); - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(timeout); - - var work = Task.Run(toExecute, cts.Token); + var work = Task.Run(toExecute, CancellationToken.None); try { - await work; + await work.WaitAsync(timeout, cancellationToken); } - catch (OperationCanceledException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + catch (TimeoutException) { throw new TimeoutException($"The execution did not complete in the allotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion"); } From af86d53f89f7c0ad8becf8a5b51e23261596bb04 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Fri, 14 Nov 2025 13:07:42 -0800 Subject: [PATCH 3/3] retain original TimeoutException --- .../AsyncExecution.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs b/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs index bec556cca..62dfaa254 100644 --- a/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs +++ b/src/Microsoft.ComponentDetection.Common/AsyncExecution.cs @@ -28,9 +28,9 @@ public static async Task ExecuteWithTimeoutAsync(Func> toExecute, { return await work.WaitAsync(timeout, cancellationToken); } - catch (TimeoutException) + catch (TimeoutException ex) { - throw new TimeoutException($"The execution did not complete in the allotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion"); + throw new TimeoutException($"The execution did not complete in the allotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion", ex); } } @@ -43,7 +43,6 @@ public static async Task ExecuteWithTimeoutAsync(Func> toExecute, /// A representing the asynchronous operation. /// Thrown when is null. /// Thrown when the execution does not complete within the timeout. - /// NOTE: If the function to execute does not respect cancellation tokens, it may continue running in the background after a timeout. public static async Task ExecuteVoidWithTimeoutAsync(Action toExecute, TimeSpan timeout, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(toExecute); @@ -54,9 +53,9 @@ public static async Task ExecuteVoidWithTimeoutAsync(Action toExecute, TimeSpan { await work.WaitAsync(timeout, cancellationToken); } - catch (TimeoutException) + catch (TimeoutException ex) { - throw new TimeoutException($"The execution did not complete in the allotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion"); + throw new TimeoutException($"The execution did not complete in the allotted time ({timeout.TotalSeconds} seconds) and has been terminated prior to completion", ex); } } }