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 <sipkeschoorstra@outlook.com>
This commit is contained in:
Jan-Willem de Bruyn 2026-05-18 21:19:23 +02:00 committed by GitHub
parent 4eeb26feca
commit 02cd8085c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 97 additions and 1 deletions

View file

@ -63,7 +63,8 @@ public class RestartInterruptedWorkflowsTask(
return new()
{
IsExecuting = true,
BeforeLastUpdated = cutoffTimestamp
BeforeLastUpdated = cutoffTimestamp,
WorkflowStatus = WorkflowStatus.Running,
};
}
}

View file

@ -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;
/// <summary>
/// Integration tests for the <see cref="RestartInterruptedWorkflowsTask" />
/// </summary>
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<IWorkflowInstanceStore>();
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<RestartInterruptedWorkflowsTask>(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<ISystemClock>();
var options = serviceProvider.GetRequiredService<IOptions<RuntimeOptions>>().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);
}
}
/// <summary>Captures restart calls without actually invoking the workflow runtime — keeps the integration test focused.</summary>
private sealed class RecordingRestarter : IWorkflowRestarter
{
public List<string> RestartedIds { get; } = [];
public Task RestartWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default)
{
RestartedIds.Add(workflowInstanceId);
return Task.CompletedTask;
}
}
}