From f1d7343530c7fa8bc6113d201f6d0312b8658155 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:37:23 +0000 Subject: [PATCH 1/4] Initial plan From f43d46022e15e3982e4b764b9fb5c52f2e69f54d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:48:59 +0000 Subject: [PATCH 2/4] Add test for StopAsync waiting for running steps Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> --- .../Scenarios/StopAsyncScenario.cs | 93 +++++++++++++++++++ .../WorkflowCore.IntegrationTests.csproj | 2 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 test/WorkflowCore.IntegrationTests/Scenarios/StopAsyncScenario.cs diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/StopAsyncScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/StopAsyncScenario.cs new file mode 100644 index 000000000..e7c6e8b1b --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/StopAsyncScenario.cs @@ -0,0 +1,93 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class StopAsyncWorkflow : IWorkflow + { + internal static DateTime? StepStartTime = null; + internal static DateTime? StepEndTime = null; + + public string Id => "StopAsyncWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(); + } + } + + internal class LongRunningStep : StepBodyAsync + { + public override async Task RunAsync(IStepExecutionContext context) + { + StopAsyncWorkflow.StepStartTime = DateTime.Now; + await Task.Delay(5000); // 5 second delay + StopAsyncWorkflow.StepEndTime = DateTime.Now; + return ExecutionResult.Next(); + } + } + + public class StopAsyncScenario : IDisposable + { + protected IWorkflowHost Host; + protected IPersistenceProvider PersistenceProvider; + + public StopAsyncScenario() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddLogging(); + services.AddWorkflow(options => options.UsePollInterval(TimeSpan.FromSeconds(3))); + + var serviceProvider = services.BuildServiceProvider(); + + PersistenceProvider = serviceProvider.GetService(); + Host = serviceProvider.GetService(); + Host.RegisterWorkflow(); + Host.Start(); + } + + [Fact] + public async Task StopAsync_should_wait_for_running_steps_to_complete() + { + // Arrange + StopAsyncWorkflow.StepStartTime = null; + StopAsyncWorkflow.StepEndTime = null; + + // Start a workflow with a long-running step + var workflowId = await Host.StartWorkflow("StopAsyncWorkflow", null); + + // Wait for the step to start executing + await Task.Delay(500); + var stepStartedTime = DateTime.Now; + + // Act - Call StopAsync which should wait for the step to complete + var stopwatch = Stopwatch.StartNew(); + await Host.StopAsync(default); + stopwatch.Stop(); + + // Assert + // The step should have started + StopAsyncWorkflow.StepStartTime.Should().NotBeNull("the step should have started"); + + // The step should have completed + StopAsyncWorkflow.StepEndTime.Should().NotBeNull("the step should have completed before StopAsync returned"); + + // StopAsync should have taken at least 4 seconds (5 seconds delay minus the 500ms we waited) + stopwatch.ElapsedMilliseconds.Should().BeGreaterThan(4000, + "StopAsync should wait for the running step to complete"); + } + + public void Dispose() + { + // Dispose is intentionally empty to avoid double-stop + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj index c92182936..2a404558c 100644 --- a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj +++ b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj @@ -7,7 +7,7 @@ false false false - net6.0 + net6.0;net8.0 From 0509e6f5cc0103e21f5b6a93d94cbadaf6d799f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:53:49 +0000 Subject: [PATCH 3/4] Update test to verify async step behavior more thoroughly Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> --- .../Scenarios/StopAsyncScenario.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/StopAsyncScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/StopAsyncScenario.cs index e7c6e8b1b..7339f736e 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/StopAsyncScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/StopAsyncScenario.cs @@ -65,8 +65,15 @@ public async Task StopAsync_should_wait_for_running_steps_to_complete() var workflowId = await Host.StartWorkflow("StopAsyncWorkflow", null); // Wait for the step to start executing - await Task.Delay(500); - var stepStartedTime = DateTime.Now; + var waitCount = 0; + while (StopAsyncWorkflow.StepStartTime == null && waitCount < 50) + { + await Task.Delay(100); + waitCount++; + } + + StopAsyncWorkflow.StepStartTime.Should().NotBeNull("the step should have started before stopping"); + StopAsyncWorkflow.StepEndTime.Should().BeNull("the step should still be running"); // Act - Call StopAsync which should wait for the step to complete var stopwatch = Stopwatch.StartNew(); @@ -74,14 +81,11 @@ public async Task StopAsync_should_wait_for_running_steps_to_complete() stopwatch.Stop(); // Assert - // The step should have started - StopAsyncWorkflow.StepStartTime.Should().NotBeNull("the step should have started"); - // The step should have completed StopAsyncWorkflow.StepEndTime.Should().NotBeNull("the step should have completed before StopAsync returned"); - // StopAsync should have taken at least 4 seconds (5 seconds delay minus the 500ms we waited) - stopwatch.ElapsedMilliseconds.Should().BeGreaterThan(4000, + // StopAsync should have taken at least 3 seconds (the remaining delay time) + stopwatch.ElapsedMilliseconds.Should().BeGreaterOrEqualTo(3000, "StopAsync should wait for the running step to complete"); } From 0b73e5717ed71a0a444df3ac20ffbe85e4a8372f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:57:53 +0000 Subject: [PATCH 4/4] Fix StopAsync to properly await all running async tasks Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> --- .../Services/BackgroundTasks/QueueConsumer.cs | 26 +++++++++++++++++++ .../WorkflowCore.IntegrationTests.csproj | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs b/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs index 75a7583ad..8295ea983 100644 --- a/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs +++ b/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs @@ -24,6 +24,8 @@ internal abstract class QueueConsumer : IBackgroundTask protected Task DispatchTask; private CancellationTokenSource _cancellationTokenSource; private Dictionary _activeTasks; + private List _runningTasks; + private readonly object _runningTasksLock = new object(); private ConcurrentHashSet _secondPasses; protected QueueConsumer(IQueueProvider queueProvider, ILoggerFactory loggerFactory, WorkflowOptions options) @@ -33,6 +35,7 @@ protected QueueConsumer(IQueueProvider queueProvider, ILoggerFactory loggerFacto Logger = loggerFactory.CreateLogger(GetType()); _activeTasks = new Dictionary(); + _runningTasks = new List(); _secondPasses = new ConcurrentHashSet(); } @@ -115,6 +118,10 @@ private async Task Execute() _activeTasks.Add(item, waitHandle); } var task = ExecuteItem(item, waitHandle, activity); + lock (_runningTasksLock) + { + _runningTasks.Add(task); + } } catch (OperationCanceledException) { @@ -138,6 +145,25 @@ private async Task Execute() foreach (var handle in toComplete) handle.WaitOne(); + + // Also await all running tasks to ensure proper async completion + Task[] tasksToAwait; + lock (_runningTasksLock) + { + tasksToAwait = _runningTasks.ToArray(); + } + + if (tasksToAwait.Length > 0) + { + try + { + await Task.WhenAll(tasksToAwait); + } + catch + { + // Individual task exceptions are already logged in ExecuteItem + } + } } private async Task ExecuteItem(string itemId, EventWaitHandle waitHandle, Activity activity) diff --git a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj index 2a404558c..c92182936 100644 --- a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj +++ b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj @@ -7,7 +7,7 @@ false false false - net6.0;net8.0 + net6.0