diff --git a/src/OrchardCore.Cms.Web/appsettings.json b/src/OrchardCore.Cms.Web/appsettings.json index 841c0055de6..a4e2b9e1074 100644 --- a/src/OrchardCore.Cms.Web/appsettings.json +++ b/src/OrchardCore.Cms.Web/appsettings.json @@ -312,6 +312,11 @@ //}, //"OrchardCore_Tenants": { // "TenantRemovalAllowed": true // Whether tenant removal is allowed, false by default. + //}, + //"OrchardCore_Workflows": { + // "Trimming": { + // "BatchSize": 1000 + // } //} } } diff --git a/src/OrchardCore.Modules/OrchardCore.AuditTrail/Services/AuditTrailManager.cs b/src/OrchardCore.Modules/OrchardCore.AuditTrail/Services/AuditTrailManager.cs index c0b15fa5ffe..ceb30ac1eb6 100644 --- a/src/OrchardCore.Modules/OrchardCore.AuditTrail/Services/AuditTrailManager.cs +++ b/src/OrchardCore.Modules/OrchardCore.AuditTrail/Services/AuditTrailManager.cs @@ -116,7 +116,7 @@ public Task GetEventAsync(string eventId) => public async Task TrimEventsAsync(TimeSpan retentionPeriod) { - var dateThreshold = _clock.UtcNow.AddDays(1) - retentionPeriod; + var dateThreshold = _clock.UtcNow - retentionPeriod; var events = await _session.Query(collection: AuditTrailEvent.Collection) .Where(index => index.CreatedUtc <= dateThreshold) diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Startup.cs index e7b9d772071..766e3574bf0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Startup.cs @@ -5,6 +5,7 @@ using OrchardCore.Data.Migration; using OrchardCore.Deployment; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Environment.Shell.Configuration; using OrchardCore.Liquid; using OrchardCore.Modules; using OrchardCore.Navigation; @@ -29,6 +30,13 @@ namespace OrchardCore.Workflows; public sealed class Startup : StartupBase { + private readonly IShellConfiguration _shellConfiguration; + + public Startup(IShellConfiguration shellConfiguration) + { + _shellConfiguration = shellConfiguration; + } + public override void ConfigureServices(IServiceCollection services) { services.Configure(o => @@ -78,6 +86,8 @@ public override void ConfigureServices(IServiceCollection services) services.AddRecipeExecutionStep(); services.AddTransient, ResourceManagementOptionsConfiguration>(); + + services.AddTrimmingServices(_shellConfiguration); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/AdminMenu.cs similarity index 80% rename from src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/AdminMenu.cs rename to src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/AdminMenu.cs index 2eaaf1ca6fc..76dd3c8cde1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/AdminMenu.cs @@ -2,16 +2,16 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; using OrchardCore.Navigation; -using OrchardCore.Workflows.WorkflowPruning.Drivers; +using OrchardCore.Workflows.Trimming.Drivers; -namespace OrchardCore.Workflows.WorkflowPruning; +namespace OrchardCore.Workflows.Trimming; public sealed class AdminMenu : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() { { "area", "OrchardCore.Settings" }, - { "groupId", WorkflowPruningDisplayDriver.GroupId }, + { "groupId", WorkflowTrimmingDisplayDriver.GroupId }, }; internal readonly IStringLocalizer S; @@ -30,7 +30,7 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) builder.Add(S["Configuration"], configuration => configuration .Add(S["Settings"], settings => settings - .Add(S["Workflow Pruning"], S["Workflow Pruning"], pruning => pruning + .Add(S["Workflow Trimming"], S["Workflow Trimming"], trimming => trimming .Action("Index", "Admin", _routeValues) .Permission(Permissions.ManageWorkflowSettings) .LocalNav() diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Drivers/WorkflowTrimmingDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Drivers/WorkflowTrimmingDisplayDriver.cs new file mode 100644 index 00000000000..3cb976150c5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Drivers/WorkflowTrimmingDisplayDriver.cs @@ -0,0 +1,71 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Documents; +using OrchardCore.Settings; +using OrchardCore.Workflows.Trimming.Models; +using OrchardCore.Workflows.Trimming.ViewModels; + +namespace OrchardCore.Workflows.Trimming.Drivers; + +public sealed class WorkflowTrimmingDisplayDriver : SiteDisplayDriver +{ + public const string GroupId = "WorkflowTrimmingSettings"; + + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + private readonly IDocumentManager _workflowTrimmingStateDocumentManager; + + public WorkflowTrimmingDisplayDriver( + IAuthorizationService authorizationService, + IHttpContextAccessor httpContextAccessor, + IDocumentManager workflowTrimmingStateDocumentManager) + { + _authorizationService = authorizationService; + _httpContextAccessor = httpContextAccessor; + _workflowTrimmingStateDocumentManager = workflowTrimmingStateDocumentManager; + } + + protected override string SettingsGroupId + => GroupId; + + public override IDisplayResult Edit(ISite site, WorkflowTrimmingSettings settings, BuildEditorContext context) + { + return Initialize("WorkflowTrimming_Fields_Edit", async model => + { + model.RetentionDays = settings.RetentionDays; + model.LastRunUtc = (await _workflowTrimmingStateDocumentManager.GetOrCreateImmutableAsync()).LastRunUtc; + model.Disabled = settings.Disabled; + + foreach (var status in settings.Statuses ?? []) + { + model.Statuses.Single(statusItem => statusItem.Status == status).IsSelected = true; + } + }).Location("Content:5") + .OnGroup(GroupId); + } + + public override async Task UpdateAsync(ISite site, WorkflowTrimmingSettings settings, UpdateEditorContext context) + { + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageWorkflowSettings)) + { + return null; + } + + var viewModel = new WorkflowTrimmingViewModel(); + await context.Updater.TryUpdateModelAsync(viewModel, Prefix); + + settings.RetentionDays = viewModel.RetentionDays; + settings.Disabled = viewModel.Disabled; + settings.Statuses = viewModel.Statuses + .Where(statusItem => statusItem.IsSelected) + .Select(statusItem => statusItem.Status) + .ToArray(); + + return await EditAsync(site, settings, context); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Extensions/ServiceCollectionExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000000..f927e318a79 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using OrchardCore.BackgroundTasks; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Environment.Shell.Configuration; +using OrchardCore.Navigation; +using OrchardCore.Settings; +using OrchardCore.Workflows.Trimming; +using OrchardCore.Workflows.Trimming.Drivers; +using OrchardCore.Workflows.Trimming.Services; + +namespace Microsoft.Extensions.DependencyInjection; + +internal static class ServiceCollectionExtensions +{ + public static IServiceCollection AddTrimmingServices(this IServiceCollection services, IShellConfiguration shellConfiguration) + { + services.AddScoped(); + services.AddSingleton(); + services.AddScoped, WorkflowTrimmingDisplayDriver>(); + services.AddScoped(); + services.Configure(shellConfiguration.GetSection("OrchardCore_Workflows:Trimming")); + + return services; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Models/WorkflowPruningSettings.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Models/WorkflowTrimmingSettings.cs similarity index 56% rename from src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Models/WorkflowPruningSettings.cs rename to src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Models/WorkflowTrimmingSettings.cs index b7dc16e5be4..156b38dea96 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Models/WorkflowPruningSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Models/WorkflowTrimmingSettings.cs @@ -1,15 +1,12 @@ -using System; using OrchardCore.Entities; using OrchardCore.Workflows.Models; -namespace OrchardCore.Workflows.WorkflowPruning.Models; +namespace OrchardCore.Workflows.Trimming.Models; -public class WorkflowPruningSettings : Entity +public class WorkflowTrimmingSettings : Entity { public int RetentionDays { get; set; } = 90; - public DateTime? LastRunUtc { get; set; } - public bool Disabled { get; set; } public WorkflowStatus[] Statuses { get; set; } diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Models/WorkflowTrimmingState.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Models/WorkflowTrimmingState.cs new file mode 100644 index 00000000000..7b2ef07708c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Models/WorkflowTrimmingState.cs @@ -0,0 +1,9 @@ +using System; +using OrchardCore.Data.Documents; + +namespace OrchardCore.Workflows.Trimming.Models; + +public class WorkflowTrimmingState : Document +{ + public DateTime? LastRunUtc { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Services/WorkflowTrimmingBackgroundTask.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Services/WorkflowTrimmingBackgroundTask.cs new file mode 100644 index 00000000000..e294d25c667 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Services/WorkflowTrimmingBackgroundTask.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.BackgroundTasks; +using OrchardCore.Documents; +using OrchardCore.Modules; +using OrchardCore.Settings; +using OrchardCore.Workflows.Trimming.Models; + +namespace OrchardCore.Workflows.Trimming.Services; + +[BackgroundTask( + Schedule = "0 0 * * *", + Title = "Workflow Trimming Background Task", + Description = "Regularly deletes old workflow instances.", + LockTimeout = 3_000, + LockExpiration = 30_000 +)] +public class WorkflowTrimmingBackgroundTask : IBackgroundTask +{ + public async Task DoWorkAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var siteService = serviceProvider.GetRequiredService(); + + var workflowTrimmingSettings = await siteService.GetSettingsAsync(); + if (workflowTrimmingSettings.Disabled) + { + return; + } + + var logger = serviceProvider.GetRequiredService>(); + + try + { + var clock = serviceProvider.GetRequiredService(); + var workflowTrimmingManager = serviceProvider.GetRequiredService(); + var batchSize = serviceProvider.GetRequiredService>().Value.BatchSize; + + logger.LogDebug("Starting trimming Workflow instances."); + + var trimmedCount = await workflowTrimmingManager.TrimWorkflowInstancesAsync( + TimeSpan.FromDays(workflowTrimmingSettings.RetentionDays), + batchSize + ); + + logger.LogDebug("Trimmed {TrimmedCount} workflow instances.", trimmedCount); + + var workflowTrimmingSateDocumentManager = serviceProvider.GetRequiredService>(); + var workflowTrimmingState = await workflowTrimmingSateDocumentManager.GetOrCreateMutableAsync(); + workflowTrimmingState.LastRunUtc = clock.UtcNow; + await workflowTrimmingSateDocumentManager.UpdateAsync(workflowTrimmingState); + } + catch (Exception ex) when (!ex.IsFatal()) + { + logger.LogError(ex, "Error while trimming workflow instances."); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Services/WorkflowPruningManager.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Services/WorkflowTrimmingService.cs similarity index 72% rename from src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Services/WorkflowPruningManager.cs rename to src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Services/WorkflowTrimmingService.cs index b53fb3da2d6..db04554d50e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Services/WorkflowPruningManager.cs +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/Services/WorkflowTrimmingService.cs @@ -5,29 +5,32 @@ using OrchardCore.Settings; using OrchardCore.Workflows.Indexes; using OrchardCore.Workflows.Models; -using OrchardCore.Workflows.WorkflowPruning.Models; +using OrchardCore.Workflows.Trimming.Models; using YesSql; using YesSql.Services; -namespace OrchardCore.Workflows.WorkflowPruning.Services; +namespace OrchardCore.Workflows.Trimming.Services; -public class WorkflowPruningManager : IWorkflowPruningManager +public class WorkflowTrimmingService : IWorkflowTrimmingService { private readonly ISiteService _siteService; private readonly ISession _session; private readonly IClock _clock; - public WorkflowPruningManager(ISiteService siteService, ISession session, IClock clock) + public WorkflowTrimmingService( + ISiteService siteService, + ISession session, + IClock clock) { _siteService = siteService; _session = session; _clock = clock; } - public async Task PruneWorkflowInstancesAsync(TimeSpan retentionPeriod) + public async Task TrimWorkflowInstancesAsync(TimeSpan retentionPeriod, int batchSize) { - var dateThreshold = _clock.UtcNow.AddDays(1) - retentionPeriod; - var settings = await _siteService.GetSettingsAsync(); + var dateThreshold = _clock.UtcNow - retentionPeriod; + var settings = await _siteService.GetSettingsAsync(); settings.Statuses ??= [ @@ -49,6 +52,8 @@ public async Task PruneWorkflowInstancesAsync(TimeSpan retentionPeriod) var statuses = settings.Statuses.Select(x => (int)x).ToArray(); var workflowInstances = await _session .Query(x => x.WorkflowStatus.IsIn(statuses) && x.CreatedUtc <= dateThreshold) + .OrderBy(x => x.Id) + .Take(batchSize) .ListAsync(); var total = 0; diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/ViewModels/WorkflowStatusItem.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/ViewModels/WorkflowStatusItem.cs new file mode 100644 index 00000000000..0efd464fd18 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/ViewModels/WorkflowStatusItem.cs @@ -0,0 +1,11 @@ +using OrchardCore.Workflows.Models; + +namespace OrchardCore.Workflows.Trimming.ViewModels; + +public class WorkflowStatusItem +{ + public WorkflowStatus Status { get; set; } + + public bool IsSelected { get; set; } +} + diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/ViewModels/WorkflowTrimmingViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/ViewModels/WorkflowTrimmingViewModel.cs new file mode 100644 index 00000000000..22e61777c46 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Trimming/ViewModels/WorkflowTrimmingViewModel.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using OrchardCore.Workflows.Models; + +namespace OrchardCore.Workflows.Trimming.ViewModels; + +public class WorkflowTrimmingViewModel +{ + public DateTime? LastRunUtc { get; set; } + + public bool Disabled { get; set; } + + public int RetentionDays { get; set; } + + public WorkflowStatusItem[] Statuses { get; set; } + + public WorkflowTrimmingViewModel() + { + Statuses = Enum.GetValues(typeof(WorkflowStatus)) + .Cast() + .Select(status => new WorkflowStatusItem + { + Status = status, + IsSelected = false + }) + .ToArray(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/WorkflowPruning.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/WorkflowPruning.Fields.Edit.cshtml deleted file mode 100644 index 79e3de3ee6b..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/WorkflowPruning.Fields.Edit.cshtml +++ /dev/null @@ -1,42 +0,0 @@ -@using OrchardCore.Workflows.WorkflowPruning.Services -@using OrchardCore.Workflows.WorkflowPruning.ViewModels -@model WorkflowPruningViewModel -@{ - var status = WorkflowStatusBuilder.Build(Model.Statuses); -} - -
-
- - - @T["Whether the task is disabled."] -
- -
- - - @T["The number of days a workflow instance is retained."] -
- -
- -
- @(Model.LastRunUtc.HasValue ? await DisplayAsync(await New.DateTime(Utc: - Model.LastRunUtc.Value, Format: "g")) : T["Never"]) -
- @T["The last time the background pruning task was run."] -
- -
- @for (var i = 0; i < status.Length; i++) - { -
-
- - -
-
- } -
-
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/WorkflowTrimming.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/WorkflowTrimming.Fields.Edit.cshtml new file mode 100644 index 00000000000..6da49f1b70b --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/WorkflowTrimming.Fields.Edit.cshtml @@ -0,0 +1,113 @@ +@using OrchardCore.Workflows.Models +@using OrchardCore.Workflows.Trimming.Services +@using OrchardCore.Workflows.Trimming.ViewModels +@model WorkflowTrimmingViewModel + +
+
+ + + @T["Whether the task is disabled."] +
+
+ +
+ + + @T["The number of days a workflow instance is retained for."] +
+ +
+ +
+ @(Model.LastRunUtc.HasValue + ? await DisplayAsync(await New.DateTime(Utc: Model.LastRunUtc.Value, Format: "g")) + : T["Never"]) +
+ @T["The last time the background trimming task was run."] +
+ +@* We use a hard-coded list of statuses here instead of iterating over the enum so the explicit T-strings make + PO file extraction and thus localization easier (and so the default English displayed string can be different than + the enum value's name). *@ +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Drivers/WorkflowPruningDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Drivers/WorkflowPruningDisplayDriver.cs deleted file mode 100644 index 71fc7cc0da5..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Drivers/WorkflowPruningDisplayDriver.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using OrchardCore.DisplayManagement.Entities; -using OrchardCore.DisplayManagement.Handlers; -using OrchardCore.DisplayManagement.Views; -using OrchardCore.Settings; -using OrchardCore.Workflows.Models; -using OrchardCore.Workflows.WorkflowPruning.Models; -using OrchardCore.Workflows.WorkflowPruning.ViewModels; - -namespace OrchardCore.Workflows.WorkflowPruning.Drivers; - -public sealed class WorkflowPruningDisplayDriver : SiteDisplayDriver -{ - public const string GroupId = "WorkflowPruningSettings"; - - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IAuthorizationService _authorizationService; - - public WorkflowPruningDisplayDriver( - IAuthorizationService authorizationService, - IHttpContextAccessor httpContextAccessor - ) - { - _authorizationService = authorizationService; - _httpContextAccessor = httpContextAccessor; - } - - protected override string SettingsGroupId - => GroupId; - - public override IDisplayResult Edit(ISite site, WorkflowPruningSettings settings, BuildEditorContext context) - { - return Initialize("WorkflowPruning_Fields_Edit", model => - { - model.RetentionDays = settings.RetentionDays; - model.LastRunUtc = settings.LastRunUtc; - model.Disabled = settings.Disabled; - model.Statuses = - settings.Statuses ?? new WorkflowStatus[] - { - WorkflowStatus.Idle, - WorkflowStatus.Starting, - WorkflowStatus.Resuming, - WorkflowStatus.Executing, - WorkflowStatus.Halted, - WorkflowStatus.Finished, - WorkflowStatus.Faulted, - WorkflowStatus.Aborted - }; - }).Location("Content:5") - .OnGroup(GroupId); - } - - public override async Task UpdateAsync(ISite site, WorkflowPruningSettings settings, UpdateEditorContext context) - { - if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageWorkflowSettings)) - { - return null; - } - - var viewModel = new WorkflowPruningViewModel(); - await context.Updater.TryUpdateModelAsync(viewModel, Prefix); - - settings.RetentionDays = viewModel.RetentionDays; - settings.Disabled = viewModel.Disabled; - settings.Statuses = viewModel.Statuses; - - return await EditAsync(site, settings, context); - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Services/WorkflowPruningBackgroundTask.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Services/WorkflowPruningBackgroundTask.cs deleted file mode 100644 index 443176610f1..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Services/WorkflowPruningBackgroundTask.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using OrchardCore.BackgroundTasks; -using OrchardCore.Entities; -using OrchardCore.Modules; -using OrchardCore.Settings; -using OrchardCore.Workflows.WorkflowPruning.Models; - -namespace OrchardCore.Workflows.WorkflowPruning.Services; - -[BackgroundTask( - Schedule = "0 0 * * *", - Title = "Workflow Pruning Background Task", - Description = "Regularly prunes old workflow instances.", - LockTimeout = 3_000, - LockExpiration = 30_000 -)] -public class WorkflowPruningBackgroundTask : IBackgroundTask -{ - public async Task DoWorkAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) - { - var siteService = serviceProvider.GetRequiredService(); - - var workflowPruningSettings = await siteService.GetSettingsAsync(); - if (workflowPruningSettings.Disabled) - { - return; - } - - var logger = serviceProvider.GetRequiredService>(); - - try - { - var clock = serviceProvider.GetRequiredService(); - var workflowCleanUpManager = serviceProvider.GetRequiredService(); - - logger.LogDebug("Starting pruning Workflow instances."); - var prunedQty = await workflowCleanUpManager.PruneWorkflowInstancesAsync( - TimeSpan.FromDays(workflowPruningSettings.RetentionDays) - ); - logger.LogDebug("Pruned {PrunedCount} workflow instances.", prunedQty); - - workflowPruningSettings.LastRunUtc = clock.UtcNow; - - var siteSettings = await siteService.LoadSiteSettingsAsync(); - siteSettings.Alter(settings => - { - settings.LastRunUtc = clock.UtcNow; - }); - - await siteService.UpdateSiteSettingsAsync(siteSettings); - } - catch (Exception ex) when (!ex.IsFatal()) - { - logger.LogError(ex, "Error while pruning workflow instances."); - } - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Services/WorkflowStatusBuilder.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Services/WorkflowStatusBuilder.cs deleted file mode 100644 index f0d630c7ab5..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Services/WorkflowStatusBuilder.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Linq; -using OrchardCore.Workflows.Models; - -namespace OrchardCore.Workflows.WorkflowPruning.Services; - -internal sealed class WorkflowStatusBuilder -{ - public bool IsSelected { get; set; } - - public string Value { get; set; } - - public static WorkflowStatusBuilder[] Build(WorkflowStatus[] selectedStatuses) => - Enum.GetValues() - .Cast() - .Select(x => new WorkflowStatusBuilder - { - IsSelected = selectedStatuses?.Contains(x) ?? false, - Value = x.ToString() - }) - .OrderBy(x => x.Value) - .ToArray(); -} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Startup.cs deleted file mode 100644 index 7f9a994b23f..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/Startup.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using OrchardCore.BackgroundTasks; -using OrchardCore.DisplayManagement.Handlers; -using OrchardCore.Modules; -using OrchardCore.Navigation; -using OrchardCore.Security.Permissions; -using OrchardCore.Settings; -using OrchardCore.Workflows.WorkflowPruning.Drivers; -using OrchardCore.Workflows.WorkflowPruning.Services; - -namespace OrchardCore.Workflows.WorkflowPruning; - -[Feature("OrchardCore.Workflows")] -public sealed class Startup : StartupBase -{ - public override void ConfigureServices(IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); - services.AddScoped, WorkflowPruningDisplayDriver>(); - services.AddScoped(); - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/ViewModels/WorkflowPruningViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/ViewModels/WorkflowPruningViewModel.cs deleted file mode 100644 index 9beefb69f92..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/WorkflowPruning/ViewModels/WorkflowPruningViewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using OrchardCore.Workflows.Models; - -namespace OrchardCore.Workflows.WorkflowPruning.ViewModels; - -public class WorkflowPruningViewModel -{ - public DateTime? LastRunUtc { get; set; } - - public bool Disabled { get; set; } - - public int RetentionDays { get; set; } - - public WorkflowStatus[] Statuses { get; set; } -} diff --git a/src/OrchardCore/OrchardCore.Workflows.Abstractions/Trimming/Services/IWorkflowTrimmingService.cs b/src/OrchardCore/OrchardCore.Workflows.Abstractions/Trimming/Services/IWorkflowTrimmingService.cs new file mode 100644 index 00000000000..4ece2e299bc --- /dev/null +++ b/src/OrchardCore/OrchardCore.Workflows.Abstractions/Trimming/Services/IWorkflowTrimmingService.cs @@ -0,0 +1,9 @@ +using System; +using System.Threading.Tasks; + +namespace OrchardCore.Workflows.Trimming.Services; + +public interface IWorkflowTrimmingService +{ + Task TrimWorkflowInstancesAsync(TimeSpan retentionPeriod, int batchSize); +} diff --git a/src/OrchardCore/OrchardCore.Workflows.Abstractions/Trimming/WorkflowTrimmingOptions.cs b/src/OrchardCore/OrchardCore.Workflows.Abstractions/Trimming/WorkflowTrimmingOptions.cs new file mode 100644 index 00000000000..c3131f934a1 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Workflows.Abstractions/Trimming/WorkflowTrimmingOptions.cs @@ -0,0 +1,10 @@ +namespace OrchardCore.Workflows.Trimming; + +public class WorkflowTrimmingOptions +{ + /// + /// The number of workflow instances to delete in a single run. Lower this if workflow trimming is timing out, or + /// increase it if you have a large number of workflow instances to delete and you don't experience any timeouts. + /// + public int BatchSize { get; set; } = 5000; +} diff --git a/src/OrchardCore/OrchardCore.Workflows.Abstractions/WorkflowPruning/Services/IWorkflowPruningManager.cs b/src/OrchardCore/OrchardCore.Workflows.Abstractions/WorkflowPruning/Services/IWorkflowPruningManager.cs deleted file mode 100644 index 61d34e66646..00000000000 --- a/src/OrchardCore/OrchardCore.Workflows.Abstractions/WorkflowPruning/Services/IWorkflowPruningManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace OrchardCore.Workflows.WorkflowPruning.Services; - -public interface IWorkflowPruningManager -{ - Task PruneWorkflowInstancesAsync(TimeSpan retentionPeriod); -} diff --git a/src/docs/reference/modules/BackgroundTasks/README.md b/src/docs/reference/modules/BackgroundTasks/README.md index a92d1a1ec0c..d3de286af96 100644 --- a/src/docs/reference/modules/BackgroundTasks/README.md +++ b/src/docs/reference/modules/BackgroundTasks/README.md @@ -2,6 +2,12 @@ This module provides tools to manage background tasks. This includes an admin UI to show which background tasks are registered with the ability to enable and disable them. +After enabling the feature, you'll be able to manage background tasks under Configuration → Tasks → Background Tasks. This includes the following: + +- Enable or disable a task. +- Change the schedule of the task, i.e. how frequently it runs, with [cron expressions](https://en.wikipedia.org/wiki/Cron#Cron_expression). +- Whether to build the tenant routing pipeline for the task, allowing it to generate correct routes. + ## Video diff --git a/src/docs/reference/modules/Workflows/README.md b/src/docs/reference/modules/Workflows/README.md index db23117c315..415ab84566c 100644 --- a/src/docs/reference/modules/Workflows/README.md +++ b/src/docs/reference/modules/Workflows/README.md @@ -436,6 +436,25 @@ Continuing with the `NotifyTask` example, we now need to create the following Ra - `NotifyTask.Fields.Thumbnail.cshtml` - `NotifyTask.Fields.Edit.cshtml` +## Trimming + +Old workflow instances can be automatically deleted with the Trimming feature. This is enabled by default and you can configure it (including disabling it) in Configuration → Settings → Workflows Trimming. Without trimming, workflow instances remain in the database indefinitely. + +By default, the trimming background task runs once a day and removes at most 5000 workflow instances. You can change the frequency of the background task via [the `OrchardCore.BackgroundTasks` configuration](../BackgroundTasks/README.md), and the batch size via the `OrchardCore_Workflows` configuration from e.g. an `appsettings` file: + +```json +"OrchardCore_Workflows": { + "Trimming": { + "BatchSize": 1000 + } +} +``` + +See [Configuration](../../core/Configuration/README.md) for more information on such configuration. + +!!! tip + If you enable the trimming feature on a site that has tens or even hundreds of thousands of workflow instances already, the initial trimming operation may take weeks to complete. You can expedite this by lowering the background task's frequency, even to once a minute temporarily with the `* * * * *` cron expression. + ## Videos diff --git a/src/docs/releases/2.0.0.md b/src/docs/releases/2.0.0.md index 2c0cbe606d3..86a243bf77f 100644 --- a/src/docs/releases/2.0.0.md +++ b/src/docs/releases/2.0.0.md @@ -416,7 +416,7 @@ Now, to handle errors, we have introduced a new property named `Errors` in the ` Additionally, if an error occurs, a new custom exception, RecipeExecutionException, is thrown. For more info visit the [related pull-request](https://github.com/OrchardCMS/OrchardCore/pull/16148/). -### Workflow Module +### Workflows Module The method `Task TriggerEventAsync(string name, IDictionary input = null, string correlationId = null, bool isExclusive = false, bool isAlwaysCorrelated = false)` was changed to return `Task>` instead. @@ -735,3 +735,7 @@ Many type commonly used by modules can be `sealed`, which improves runtime perfo !!! note Do not seal classes that are used to create shapes like view-models. Sealing these classes can break your code at runtime, as these classes need to be unsealed to allow for proxy creation. + +### Workflow Trimming + +The Workflows module now has a `Trimming` feature to automatically clean up old workflow instances. See [the corresponding documentation](../reference/modules/Workflows/README.md#trimming) for details.