From 02cd8085c6f15e00630bc7569403fb13469b905f Mon Sep 17 00:00:00 2001 From: Jan-Willem de Bruyn <5670267+jwdb@users.noreply.github.com> Date: Mon, 18 May 2026 21:19:23 +0200 Subject: [PATCH] fix: do not resume interrupted workflows that are already finished (#7435) * Fix: do not resume interrupted workflows that are already finished * Avoid fixed timestamps in restart workflow test --------- Co-authored-by: Sipke Schoorstra --- .../Tasks/RestartInterruptedWorkflowsTask.cs | 3 +- .../RestartInterruptedWorkflowsTests.cs | 95 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/GracefulShutdown/RestartInterruptedWorkflowsTests.cs diff --git a/src/modules/Elsa.Workflows.Runtime/Tasks/RestartInterruptedWorkflowsTask.cs b/src/modules/Elsa.Workflows.Runtime/Tasks/RestartInterruptedWorkflowsTask.cs index 288edb611..2941170c0 100644 --- a/src/modules/Elsa.Workflows.Runtime/Tasks/RestartInterruptedWorkflowsTask.cs +++ b/src/modules/Elsa.Workflows.Runtime/Tasks/RestartInterruptedWorkflowsTask.cs @@ -63,7 +63,8 @@ public class RestartInterruptedWorkflowsTask( return new() { IsExecuting = true, - BeforeLastUpdated = cutoffTimestamp + BeforeLastUpdated = cutoffTimestamp, + WorkflowStatus = WorkflowStatus.Running, }; } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/GracefulShutdown/RestartInterruptedWorkflowsTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/GracefulShutdown/RestartInterruptedWorkflowsTests.cs new file mode 100644 index 000000000..8b489c05e --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/GracefulShutdown/RestartInterruptedWorkflowsTests.cs @@ -0,0 +1,95 @@ +using Elsa.Common; +using Elsa.Extensions; +using Elsa.Testing.Shared; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Options; +using Elsa.Workflows.Runtime.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.GracefulShutdown; + +/// +/// Integration tests for the +/// +public class RestartInterruptedWorkflowsTests +{ + private readonly IServiceProvider _services; + + public RestartInterruptedWorkflowsTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .ConfigureElsa(elsa => elsa.UseWorkflowRuntime()) + .Build(); + } + + [Fact(DisplayName = "Task ignores instances NOT Interrupted")] + public async Task Filter() + { + var fakeRestarter = new RecordingRestarter(); + using var scope = _services.CreateScope(); + var instanceStore = scope.ServiceProvider.GetRequiredService(); + var staleTimestamp = GetStaleTimestamp(scope.ServiceProvider); + + await SeedInstancesAsync(instanceStore, 3, WorkflowStatus.Running, WorkflowSubStatus.Executing, isExecuting: true, idPrefix: "stale-", timestamp: staleTimestamp); + await SeedInstancesAsync(instanceStore, 2, WorkflowStatus.Finished, WorkflowSubStatus.Executing, isExecuting: true, idPrefix: "finished-stale-", timestamp: staleTimestamp); + await SeedInstancesAsync(instanceStore, 1, WorkflowStatus.Finished, WorkflowSubStatus.Executing, isExecuting: false, idPrefix: "finished-", timestamp: staleTimestamp); + + var scanner = ActivatorUtilities.CreateInstance(scope.ServiceProvider, fakeRestarter); + await scanner.ExecuteAsync(CancellationToken.None); + + Assert.Equal(3, fakeRestarter.RestartedIds.Count); + Assert.All(fakeRestarter.RestartedIds, id => Assert.StartsWith("stale-", id)); + } + + private static DateTimeOffset GetStaleTimestamp(IServiceProvider serviceProvider) + { + var clock = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>().Value; + return clock.UtcNow - options.InactivityThreshold - TimeSpan.FromMinutes(1); + } + + private static async Task SeedInstancesAsync(IWorkflowInstanceStore store, int count, WorkflowStatus status, WorkflowSubStatus subStatus, bool isExecuting, string idPrefix = "instance-", DateTimeOffset? timestamp = null) + { + var createdAt = timestamp ?? DateTimeOffset.UtcNow; + + for (var i = 0; i < count; i++) + { + await store.SaveAsync(new WorkflowInstance + { + Id = $"{idPrefix}{i}", + DefinitionId = "def-1", + DefinitionVersionId = "ver-1", + Version = 1, + Status = status, + SubStatus = subStatus, + IsExecuting = isExecuting, + CreatedAt = createdAt, + UpdatedAt = createdAt, + WorkflowState = new State.WorkflowState + { + Id = $"{idPrefix}{i}", + DefinitionId = "def-1", + DefinitionVersionId = "ver-1", + Status = status, + SubStatus = subStatus, + }, + }, CancellationToken.None); + } + } + + /// Captures restart calls without actually invoking the workflow runtime — keeps the integration test focused. + private sealed class RecordingRestarter : IWorkflowRestarter + { + public List RestartedIds { get; } = []; + + public Task RestartWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) + { + RestartedIds.Add(workflowInstanceId); + return Task.CompletedTask; + } + } +}