Implement cleanup service (#1580)
This commit is contained in:
parent
95424f39fb
commit
f15f14b240
10
Elsa.sln
10
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}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using Elsa.Models;
|
||||
using NodaTime;
|
||||
|
||||
namespace Elsa.Persistence.Specifications.WorkflowInstances
|
||||
{
|
||||
public class WorkflowCreatedBeforeSpecification : Specification<WorkflowInstance>
|
||||
{
|
||||
public WorkflowCreatedBeforeSpecification(Instant instant) => Instant = instant;
|
||||
public Instant Instant { get; }
|
||||
|
||||
public override Expression<Func<WorkflowInstance, bool>> ToExpression() => x => x.CreatedAt <= Instant;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ namespace Elsa.Persistence.Specifications.WorkflowInstances
|
|||
{
|
||||
public class WorkflowStatusSpecification : Specification<WorkflowInstance>
|
||||
{
|
||||
public WorkflowStatus WorkflowStatus { get; set; }
|
||||
public WorkflowStatusSpecification(WorkflowStatus workflowStatus) => WorkflowStatus = workflowStatus;
|
||||
public WorkflowStatus WorkflowStatus { get; set; }
|
||||
public override Expression<Func<WorkflowInstance, bool>> ToExpression() => x => x.WorkflowStatus == WorkflowStatus;
|
||||
}
|
||||
}
|
||||
17
src/modules/Elsa.Retention/Elsa.Retention.csproj
Normal file
17
src/modules/Elsa.Retention/Elsa.Retention.csproj
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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<CleanupOptions> configureOptions)
|
||||
{
|
||||
services
|
||||
.Configure(configureOptions)
|
||||
.AddScoped<CleanupJob>()
|
||||
.AddHostedService<CleanupService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/modules/Elsa.Retention/HostedServices/CleanupService.cs
Normal file
51
src/modules/Elsa.Retention/HostedServices/CleanupService.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Periodically wipes workflow instances and their execution logs.
|
||||
/// </summary>
|
||||
public class CleanupService : IHostedService, IAsyncDisposable
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly CleanupOptions _options;
|
||||
private readonly Timer _timer;
|
||||
|
||||
public CleanupService(IOptions<CleanupOptions> 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<CleanupJob>();
|
||||
await job.ExecuteAsync();
|
||||
|
||||
_timer.Change(_options.SweepInterval.ToTimeSpan(), Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/modules/Elsa.Retention/Jobs/CleanupJob.cs
Normal file
55
src/modules/Elsa.Retention/Jobs/CleanupJob.cs
Normal file
|
|
@ -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<CleanupOptions> options, ILogger<CleanupJob> 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<string> workflowInstanceIds;
|
||||
|
||||
do
|
||||
{
|
||||
workflowInstanceIds = (await _workflowInstanceStore.FindManyAsync(specification, new OrderBy<WorkflowInstance>(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<string> workflowInstanceIds, CancellationToken cancellationToken)
|
||||
{
|
||||
var specification = new WorkflowInstanceIdsSpecification(workflowInstanceIds);
|
||||
await _workflowInstanceStore.DeleteManyAsync(specification, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/modules/Elsa.Retention/Options/CleanupOptions.cs
Normal file
22
src/modules/Elsa.Retention/Options/CleanupOptions.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using NodaTime;
|
||||
|
||||
namespace Elsa.Retention.Options
|
||||
{
|
||||
public class CleanupOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls how often the database is checked for workflow instances and execution log records to remove.
|
||||
/// </summary>
|
||||
public Duration SweepInterval { get; set; } = Duration.FromHours(4);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum age a workflow instance is allowed to exist before being removed.
|
||||
/// </summary>
|
||||
public Duration TimeToLive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of workflow instances to delete at the same time.
|
||||
/// </summary>
|
||||
public int PageSize { get; set; } = 100;
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
<ProjectReference Include="..\..\..\locking\Elsa.DistributedLocking.AzureBlob\Elsa.DistributedLocking.AzureBlob.csproj" />
|
||||
<ProjectReference Include="..\..\..\locking\Elsa.DistributedLocking.Redis\Elsa.DistributedLocking.Redis.csproj" />
|
||||
<ProjectReference Include="..\..\..\locking\Elsa.DistributedLocking.SqlServer\Elsa.DistributedLocking.SqlServer.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Retention\Elsa.Retention.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\workflowsettings\Elsa.WorkflowSettings.Api\Elsa.WorkflowSettings.Api.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\workflowsettings\Elsa.WorkflowSettings.Persistence.EntityFramework.MySql\Elsa.WorkflowSettings.Persistence.EntityFramework.MySql.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\workflowsettings\Elsa.WorkflowSettings.Persistence.EntityFramework.PostgreSql\Elsa.WorkflowSettings.Persistence.EntityFramework.PostgreSql.csproj" />
|
||||
|
|
|
|||
|
|
@ -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<Startup>()
|
||||
.AddFeatures(startups, Configuration)
|
||||
.ConfigureWorkflowChannels(options => elsaSection.GetSection("WorkflowChannels").Bind(options))
|
||||
);
|
||||
)
|
||||
.AddRetentionServices(options => elsaSection.GetSection("Retention").Bind(options));
|
||||
|
||||
// Elsa API endpoints.
|
||||
services
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue