diff --git a/Elsa.sln b/Elsa.sln
index b4e9ed417..12318883a 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -364,6 +364,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.InfiniteLoopDe
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Forks", "src\samples\console\Elsa.Samples.Forks\Elsa.Samples.Forks.csproj", "{AB67BA19-5BC2-4C6D-A994-61D51A6A6FD5}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "retention", "retention", "{235ABC3F-A075-4682-B6D7-837098BA6B00}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Retention", "src\modules\Elsa.Retention\Elsa.Retention.csproj", "{6DD8EB07-95CD-4F5A-9C0C-FD7F317EBC5A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -862,6 +866,10 @@ Global
{AB67BA19-5BC2-4C6D-A994-61D51A6A6FD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB67BA19-5BC2-4C6D-A994-61D51A6A6FD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB67BA19-5BC2-4C6D-A994-61D51A6A6FD5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6DD8EB07-95CD-4F5A-9C0C-FD7F317EBC5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6DD8EB07-95CD-4F5A-9C0C-FD7F317EBC5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6DD8EB07-95CD-4F5A-9C0C-FD7F317EBC5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6DD8EB07-95CD-4F5A-9C0C-FD7F317EBC5A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1024,6 +1032,8 @@ Global
{8EC3CF51-EBC1-4B45-881E-4B1B8190D579} = {E42743A0-FBDD-4150-9D53-6000496D9B87}
{A2572DBF-AF11-4A02-AF99-B343871F2D85} = {22E75696-6FE9-436A-9097-EE21C603F818}
{AB67BA19-5BC2-4C6D-A994-61D51A6A6FD5} = {FC9F520F-BA51-4AD2-BFEE-EF787798E734}
+ {235ABC3F-A075-4682-B6D7-837098BA6B00} = {69BB424A-AAA3-415C-9651-9F4349CA3AC5}
+ {6DD8EB07-95CD-4F5A-9C0C-FD7F317EBC5A} = {235ABC3F-A075-4682-B6D7-837098BA6B00}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158}
diff --git a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj
index b0e469d72..c2e2823b8 100644
--- a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj
+++ b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj
@@ -4,7 +4,7 @@
- netstandard2.1
+ netcoreapp3.1;net5.0
Elsa is a set of workflow libraries and tools that enable lean and mean workflowing capabilities in any .NET Core application.
This package provides the following Console activities:
@@ -16,6 +16,13 @@
elsa, workflows, http
+
+
+
+
+
+
+
@@ -23,7 +30,6 @@
-
diff --git a/src/core/Elsa.Core/Persistence/Specifications/WorkflowInstances/WorkflowCreatedBeforeSpecification.cs b/src/core/Elsa.Core/Persistence/Specifications/WorkflowInstances/WorkflowCreatedBeforeSpecification.cs
new file mode 100644
index 000000000..b9aee4027
--- /dev/null
+++ b/src/core/Elsa.Core/Persistence/Specifications/WorkflowInstances/WorkflowCreatedBeforeSpecification.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Linq.Expressions;
+using Elsa.Models;
+using NodaTime;
+
+namespace Elsa.Persistence.Specifications.WorkflowInstances
+{
+ public class WorkflowCreatedBeforeSpecification : Specification
+ {
+ public WorkflowCreatedBeforeSpecification(Instant instant) => Instant = instant;
+ public Instant Instant { get; }
+
+ public override Expression> ToExpression() => x => x.CreatedAt <= Instant;
+ }
+}
\ No newline at end of file
diff --git a/src/core/Elsa.Core/Persistence/Specifications/WorkflowInstances/WorkflowStatusSpecification.cs b/src/core/Elsa.Core/Persistence/Specifications/WorkflowInstances/WorkflowStatusSpecification.cs
index ce4321241..5ad00dba4 100644
--- a/src/core/Elsa.Core/Persistence/Specifications/WorkflowInstances/WorkflowStatusSpecification.cs
+++ b/src/core/Elsa.Core/Persistence/Specifications/WorkflowInstances/WorkflowStatusSpecification.cs
@@ -6,8 +6,8 @@ namespace Elsa.Persistence.Specifications.WorkflowInstances
{
public class WorkflowStatusSpecification : Specification
{
- public WorkflowStatus WorkflowStatus { get; set; }
public WorkflowStatusSpecification(WorkflowStatus workflowStatus) => WorkflowStatus = workflowStatus;
+ public WorkflowStatus WorkflowStatus { get; set; }
public override Expression> ToExpression() => x => x.WorkflowStatus == WorkflowStatus;
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Retention/Elsa.Retention.csproj b/src/modules/Elsa.Retention/Elsa.Retention.csproj
new file mode 100644
index 000000000..17a65f405
--- /dev/null
+++ b/src/modules/Elsa.Retention/Elsa.Retention.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.1
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Elsa.Retention/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.Retention/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..68c774632
--- /dev/null
+++ b/src/modules/Elsa.Retention/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,21 @@
+using System;
+using Elsa.Retention.HostedServices;
+using Elsa.Retention.Jobs;
+using Elsa.Retention.Options;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Retention.Extensions
+{
+ public static class ServiceCollectionExtensions
+ {
+ public static IServiceCollection AddRetentionServices(this IServiceCollection services, Action configureOptions)
+ {
+ services
+ .Configure(configureOptions)
+ .AddScoped()
+ .AddHostedService();
+
+ return services;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Retention/HostedServices/CleanupService.cs b/src/modules/Elsa.Retention/HostedServices/CleanupService.cs
new file mode 100644
index 000000000..08482213a
--- /dev/null
+++ b/src/modules/Elsa.Retention/HostedServices/CleanupService.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Elsa.Retention.Jobs;
+using Elsa.Retention.Options;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+
+namespace Elsa.Retention.HostedServices
+{
+ ///
+ /// Periodically wipes workflow instances and their execution logs.
+ ///
+ public class CleanupService : IHostedService, IAsyncDisposable
+ {
+ private readonly IServiceScopeFactory _serviceScopeFactory;
+ private readonly CleanupOptions _options;
+ private readonly Timer _timer;
+
+ public CleanupService(IOptions options, IServiceScopeFactory serviceScopeFactory)
+ {
+ _serviceScopeFactory = serviceScopeFactory;
+ _options = options.Value;
+ _timer = new Timer(ExecuteAsync, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _timer.Change(_options.SweepInterval.ToTimeSpan(), Timeout.InfiniteTimeSpan);
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+ return Task.CompletedTask;
+ }
+
+ public async ValueTask DisposeAsync() => await _timer.DisposeAsync();
+
+ private async void ExecuteAsync(object state)
+ {
+ using var scope = _serviceScopeFactory.CreateScope();
+ var job = scope.ServiceProvider.GetRequiredService();
+ await job.ExecuteAsync();
+
+ _timer.Change(_options.SweepInterval.ToTimeSpan(), Timeout.InfiniteTimeSpan);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Retention/Jobs/CleanupJob.cs b/src/modules/Elsa.Retention/Jobs/CleanupJob.cs
new file mode 100644
index 000000000..f097dbdd8
--- /dev/null
+++ b/src/modules/Elsa.Retention/Jobs/CleanupJob.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Elsa.Models;
+using Elsa.Persistence;
+using Elsa.Persistence.Specifications;
+using Elsa.Persistence.Specifications.WorkflowInstances;
+using Elsa.Retention.Options;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using NodaTime;
+
+namespace Elsa.Retention.Jobs
+{
+ public class CleanupJob
+ {
+ private readonly IWorkflowInstanceStore _workflowInstanceStore;
+ private readonly IClock _clock;
+ private readonly CleanupOptions _options;
+ private readonly ILogger _logger;
+
+ public CleanupJob(IWorkflowInstanceStore workflowInstanceStore, IClock clock, IOptions options, ILogger logger)
+ {
+ _workflowInstanceStore = workflowInstanceStore;
+ _clock = clock;
+ _options = options.Value;
+ _logger = logger;
+ }
+
+ public async Task ExecuteAsync(CancellationToken cancellationToken = default)
+ {
+ var threshold = _clock.GetCurrentInstant().Minus(_options.TimeToLive);
+ var specification = new WorkflowCreatedBeforeSpecification(threshold);
+ var take = _options.PageSize;
+ IList workflowInstanceIds;
+
+ do
+ {
+ workflowInstanceIds = (await _workflowInstanceStore.FindManyAsync(specification, new OrderBy(x => x.CreatedAt, SortDirection.Descending), new Paging(0, take), cancellationToken: cancellationToken))
+ .Select(x => x.Id).ToList();
+ _logger.LogInformation("Deleting {WorkflowInstanceCount} workflow instances", workflowInstanceIds.Count);
+
+ if (workflowInstanceIds.Any())
+ await DeleteManyAsync(workflowInstanceIds, cancellationToken);
+ } while (workflowInstanceIds.Any());
+ }
+
+ private async Task DeleteManyAsync(IEnumerable workflowInstanceIds, CancellationToken cancellationToken)
+ {
+ var specification = new WorkflowInstanceIdsSpecification(workflowInstanceIds);
+ await _workflowInstanceStore.DeleteManyAsync(specification, cancellationToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Retention/Options/CleanupOptions.cs b/src/modules/Elsa.Retention/Options/CleanupOptions.cs
new file mode 100644
index 000000000..261c0bf51
--- /dev/null
+++ b/src/modules/Elsa.Retention/Options/CleanupOptions.cs
@@ -0,0 +1,22 @@
+using NodaTime;
+
+namespace Elsa.Retention.Options
+{
+ public class CleanupOptions
+ {
+ ///
+ /// Controls how often the database is checked for workflow instances and execution log records to remove.
+ ///
+ public Duration SweepInterval { get; set; } = Duration.FromHours(4);
+
+ ///
+ /// The maximum age a workflow instance is allowed to exist before being removed.
+ ///
+ public Duration TimeToLive { get; set; }
+
+ ///
+ /// The maximum number of workflow instances to delete at the same time.
+ ///
+ public int PageSize { get; set; } = 100;
+ }
+}
\ No newline at end of file
diff --git a/src/samples/server/Elsa.Samples.Server.Host/Elsa.Samples.Server.Host.csproj b/src/samples/server/Elsa.Samples.Server.Host/Elsa.Samples.Server.Host.csproj
index 3aca7d573..cc375458c 100644
--- a/src/samples/server/Elsa.Samples.Server.Host/Elsa.Samples.Server.Host.csproj
+++ b/src/samples/server/Elsa.Samples.Server.Host/Elsa.Samples.Server.Host.csproj
@@ -36,6 +36,7 @@
+
diff --git a/src/samples/server/Elsa.Samples.Server.Host/Startup.cs b/src/samples/server/Elsa.Samples.Server.Host/Startup.cs
index 97d2b128f..7f558f0e0 100644
--- a/src/samples/server/Elsa.Samples.Server.Host/Startup.cs
+++ b/src/samples/server/Elsa.Samples.Server.Host/Startup.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Elsa.Retention.Extensions;
using Elsa.Server.Hangfire.Extensions;
using Hangfire;
using Microsoft.AspNetCore.Builder;
@@ -78,7 +79,8 @@ namespace Elsa.Samples.Server.Host
.AddWorkflowsFrom()
.AddFeatures(startups, Configuration)
.ConfigureWorkflowChannels(options => elsaSection.GetSection("WorkflowChannels").Bind(options))
- );
+ )
+ .AddRetentionServices(options => elsaSection.GetSection("Retention").Bind(options));
// Elsa API endpoints.
services
diff --git a/src/samples/server/Elsa.Samples.Server.Host/appsettings.json b/src/samples/server/Elsa.Samples.Server.Host/appsettings.json
index 1242e2d91..f49981c47 100644
--- a/src/samples/server/Elsa.Samples.Server.Host/appsettings.json
+++ b/src/samples/server/Elsa.Samples.Server.Host/appsettings.json
@@ -62,6 +62,11 @@
"Port": "2525",
"DefaultSender": "noreply@acme.com"
},
+ "Retention": {
+ "SweepInterval": "0:00:00:10",
+ "TimeToLive": "0:00:10:00",
+ "PageSize": "10"
+ },
"Conductor": {
"CommandsHookUrl": "https://localhost:16001/elsa-hook/commands",
"TasksHookUrl": "https://localhost:16001/elsa-hook/tasks",