Implement cleanup service (#1580)

This commit is contained in:
Sipke Schoorstra 2021-09-27 13:55:24 +02:00 committed by GitHub
parent 95424f39fb
commit f15f14b240
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 201 additions and 2 deletions

View file

@ -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}

View file

@ -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;
}
}

View file

@ -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;
}
}

View 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>

View file

@ -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;
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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;
}
}

View file

@ -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" />

View file

@ -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

View file

@ -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",