From 471bff728aca8d073212d2e4c2e8a0637b090027 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:17:56 +0000 Subject: [PATCH 1/3] Initial plan From d41fe65cd148a80c0e6da05bdd1b632481ea2caa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:38:16 +0000 Subject: [PATCH 2/3] Add memory persistence purger for terminated workflows Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> --- .../ServiceCollectionExtensions.cs | 5 +- .../MemoryPersistenceProvider.cs | 20 +++++- .../MemoryPersistenceProviderFixture.cs | 61 +++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/WorkflowCore/ServiceCollectionExtensions.cs b/src/WorkflowCore/ServiceCollectionExtensions.cs index f38f44a5b..7f1d5185b 100644 --- a/src/WorkflowCore/ServiceCollectionExtensions.cs +++ b/src/WorkflowCore/ServiceCollectionExtensions.cs @@ -20,6 +20,10 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A var options = new WorkflowOptions(services); setupAction?.Invoke(options); services.AddSingleton(); + if (!services.Any(x => x.ServiceType == typeof(IWorkflowPurger))) + { + services.AddSingleton(sp => (IWorkflowPurger)sp.GetService()); + } services.AddTransient(options.PersistenceFactory); services.AddTransient(options.PersistenceFactory); services.AddTransient(options.PersistenceFactory); @@ -120,4 +124,3 @@ public static IServiceCollection AddWorkflowMiddleware( : services.AddTransient(factory); } } - diff --git a/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs b/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs index cb69c8791..d605a7a43 100644 --- a/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs +++ b/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs @@ -17,7 +17,7 @@ public interface ISingletonMemoryProvider : IPersistenceProvider /// /// In-memory implementation of IPersistenceProvider for demo and testing purposes /// - public class MemoryPersistenceProvider : ISingletonMemoryProvider + public class MemoryPersistenceProvider : ISingletonMemoryProvider, IWorkflowPurger { private readonly List _instances = new List(); private readonly List _subscriptions = new List(); @@ -286,6 +286,24 @@ public Task ProcessCommands(DateTimeOffset asOf, Func ac { throw new NotImplementedException(); } + + public Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan, CancellationToken cancellationToken = default) + { + var olderThanUtc = olderThan.ToUniversalTime(); + if (status != WorkflowStatus.Complete && status != WorkflowStatus.Terminated) + { + return Task.CompletedTask; + } + + lock (_instances) + { + _instances.RemoveAll(x => x.Status == status + && x.CompleteTime.HasValue + && x.CompleteTime.Value < olderThanUtc); + } + + return Task.CompletedTask; + } } #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously diff --git a/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs b/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs index 1bfa2b3f7..f6eb9b7f1 100644 --- a/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs @@ -1,6 +1,9 @@ using System; +using FluentAssertions; using WorkflowCore.Interface; +using WorkflowCore.Models; using WorkflowCore.Services; +using Xunit; namespace WorkflowCore.UnitTests.Services { @@ -9,5 +12,63 @@ public class MemoryPersistenceProviderFixture : BasePersistenceFixture private readonly IPersistenceProvider _subject = new MemoryPersistenceProvider(); protected override IPersistenceProvider Subject => _subject; + + private IWorkflowPurger Purger => (IWorkflowPurger)_subject; + + [Fact] + public void PurgeWorkflows_should_remove_terminated_instances() + { + var terminatedWorkflow = new WorkflowInstance + { + Status = WorkflowStatus.Terminated, + CreateTime = DateTime.UtcNow.AddDays(-2), + CompleteTime = DateTime.UtcNow.AddDays(-2), + WorkflowDefinitionId = "terminated" + }; + + var activeWorkflow = new WorkflowInstance + { + Status = WorkflowStatus.Runnable, + CreateTime = DateTime.UtcNow.AddDays(-2), + NextExecution = 0, + WorkflowDefinitionId = "active" + }; + + var terminatedId = _subject.CreateNewWorkflow(terminatedWorkflow).Result; + var activeId = _subject.CreateNewWorkflow(activeWorkflow).Result; + + Purger.PurgeWorkflows(WorkflowStatus.Terminated, DateTime.UtcNow.AddDays(-1)).Wait(); + + var remaining = _subject.GetWorkflowInstances(new[] { terminatedId, activeId }).Result; + remaining.Should().ContainSingle(x => x.Id == activeId); + } + + [Fact] + public void PurgeWorkflows_should_remove_completed_instances() + { + var completedWorkflow = new WorkflowInstance + { + Status = WorkflowStatus.Complete, + CreateTime = DateTime.UtcNow.AddDays(-3), + CompleteTime = DateTime.UtcNow.AddDays(-3), + WorkflowDefinitionId = "completed" + }; + + var activeWorkflow = new WorkflowInstance + { + Status = WorkflowStatus.Runnable, + CreateTime = DateTime.UtcNow.AddDays(-1), + NextExecution = 0, + WorkflowDefinitionId = "active" + }; + + var completedId = _subject.CreateNewWorkflow(completedWorkflow).Result; + var activeId = _subject.CreateNewWorkflow(activeWorkflow).Result; + + Purger.PurgeWorkflows(WorkflowStatus.Complete, DateTime.UtcNow.AddDays(-2)).Wait(); + + var remaining = _subject.GetWorkflowInstances(new[] { completedId, activeId }).Result; + remaining.Should().ContainSingle(x => x.Id == activeId); + } } } From 8b7fe06676e9d716c0d558df2ad9fea2c1bb478f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:42:45 +0000 Subject: [PATCH 3/3] Address purge tests and validation Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> --- .../MemoryPersistenceProvider.cs | 8 ++++++- .../MemoryPersistenceProviderFixture.cs | 21 ++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs b/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs index d605a7a43..2f7139446 100644 --- a/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs +++ b/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs @@ -287,12 +287,18 @@ public Task ProcessCommands(DateTimeOffset asOf, Func ac throw new NotImplementedException(); } + /// + /// Purges only completed or terminated workflows; other status values are rejected. + /// + /// + /// Workflows without a value are not eligible for purge. + /// public Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan, CancellationToken cancellationToken = default) { var olderThanUtc = olderThan.ToUniversalTime(); if (status != WorkflowStatus.Complete && status != WorkflowStatus.Terminated) { - return Task.CompletedTask; + throw new ArgumentOutOfRangeException(nameof(status), status, "Only complete or terminated workflows can be purged."); } lock (_instances) diff --git a/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs b/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs index f6eb9b7f1..e329c93df 100644 --- a/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using FluentAssertions; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -16,7 +17,7 @@ public class MemoryPersistenceProviderFixture : BasePersistenceFixture private IWorkflowPurger Purger => (IWorkflowPurger)_subject; [Fact] - public void PurgeWorkflows_should_remove_terminated_instances() + public async Task PurgeWorkflows_should_remove_terminated_instances() { var terminatedWorkflow = new WorkflowInstance { @@ -34,17 +35,17 @@ public void PurgeWorkflows_should_remove_terminated_instances() WorkflowDefinitionId = "active" }; - var terminatedId = _subject.CreateNewWorkflow(terminatedWorkflow).Result; - var activeId = _subject.CreateNewWorkflow(activeWorkflow).Result; + var terminatedId = await _subject.CreateNewWorkflow(terminatedWorkflow); + var activeId = await _subject.CreateNewWorkflow(activeWorkflow); - Purger.PurgeWorkflows(WorkflowStatus.Terminated, DateTime.UtcNow.AddDays(-1)).Wait(); + await Purger.PurgeWorkflows(WorkflowStatus.Terminated, DateTime.UtcNow.AddDays(-1)); - var remaining = _subject.GetWorkflowInstances(new[] { terminatedId, activeId }).Result; + var remaining = await _subject.GetWorkflowInstances(new[] { terminatedId, activeId }); remaining.Should().ContainSingle(x => x.Id == activeId); } [Fact] - public void PurgeWorkflows_should_remove_completed_instances() + public async Task PurgeWorkflows_should_remove_completed_instances() { var completedWorkflow = new WorkflowInstance { @@ -62,12 +63,12 @@ public void PurgeWorkflows_should_remove_completed_instances() WorkflowDefinitionId = "active" }; - var completedId = _subject.CreateNewWorkflow(completedWorkflow).Result; - var activeId = _subject.CreateNewWorkflow(activeWorkflow).Result; + var completedId = await _subject.CreateNewWorkflow(completedWorkflow); + var activeId = await _subject.CreateNewWorkflow(activeWorkflow); - Purger.PurgeWorkflows(WorkflowStatus.Complete, DateTime.UtcNow.AddDays(-2)).Wait(); + await Purger.PurgeWorkflows(WorkflowStatus.Complete, DateTime.UtcNow.AddDays(-2)); - var remaining = _subject.GetWorkflowInstances(new[] { completedId, activeId }).Result; + var remaining = await _subject.GetWorkflowInstances(new[] { completedId, activeId }); remaining.Should().ContainSingle(x => x.Id == activeId); } }