From 133e8dfc63fb81aa06f6a5c9d9491f6047bd3ddb Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 6 Aug 2025 08:43:39 +0200 Subject: [PATCH] Introduce `TenantDeleted` event to handle tenant cleanup (#6843) * Introduce `TenantDeleted` event to handle tenant cleanup Added a `TenantDeleted` event to differentiate between tenant deactivation and deletion. Updated event handlers and services to support unregistering resources only during tenant deletion, ensuring clearer separation of responsibilities. Included ADR documentation for the new event. * Remove unnecessary blank line in tenant deactivation logic --- Elsa.sln | 1 + doc/adr/0005-tenant-deleted-event.md | 23 +++++++++ doc/adr/graph.dot | 18 ++++--- doc/adr/toc.md | 3 +- .../Contracts/ITenantDeletedEvent.cs | 6 +++ .../EventArgs/TenantDeletedEventArgs.cs | 3 ++ .../Implementations/DefaultTenantService.cs | 11 ++-- .../Implementations/TenantEventsManager.cs | 50 +++++++++++++------ .../Features/SchedulingFeature.cs | 2 +- .../Handlers/UpdateTenantSchedules.cs | 4 +- 10 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 doc/adr/0005-tenant-deleted-event.md create mode 100644 src/modules/Elsa.Common/Multitenancy/Contracts/ITenantDeletedEvent.cs create mode 100644 src/modules/Elsa.Common/Multitenancy/EventArgs/TenantDeletedEventArgs.cs diff --git a/Elsa.sln b/Elsa.sln index a5cad563a..29feedd62 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -392,6 +392,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "adr", "adr", "{0A04B1FD-06C doc\adr\graph.dot = doc\adr\graph.dot doc\adr\0003-direct-bookmark-management-in-workflowexecutioncontext.md = doc\adr\0003-direct-bookmark-management-in-workflowexecutioncontext.md doc\adr\0004-activity-execution-snapshots.md = doc\adr\0004-activity-execution-snapshots.md + doc\adr\0005-tenant-deleted-event.md = doc\adr\0005-tenant-deleted-event.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bounty", "bounty", "{9B80A705-2E31-4012-964A-83963DCDB384}" diff --git a/doc/adr/0005-tenant-deleted-event.md b/doc/adr/0005-tenant-deleted-event.md new file mode 100644 index 000000000..d0d2fa9f1 --- /dev/null +++ b/doc/adr/0005-tenant-deleted-event.md @@ -0,0 +1,23 @@ +# 5. Tenant Deleted Event + +Date: 2025-08-05 + +## Status + +Accepted + +## Context + +As outlined in issue [#6661](https://github.com/elsa-workflows/elsa-core/issues/6661), there is a need to differentiate between **Tenant Deactivating** and **Tenant Deleting** events. + +Currently, the `TenantDeactivated` event is used to unregister timer-based triggers. However, this causes the triggers to be unregistered when the application host shuts down, which is not the desired behavior. Instead, we want the triggers to remain registered until the tenant is explicitly deleted. + +## Decision + +To address this, we will introduce a new event called `TenantDeleted`. This event will be raised when a tenant is deleted and will be responsible for unregistering timer-based triggers. This ensures that the triggers remain active until the tenant is explicitly deleted. + +## Consequences + +- Timer-based triggers will no longer be unregistered during tenant deactivation. Instead, they will remain active until the tenant is deleted. +- The `TenantDeactivated` event will continue to be used for deactivating tenants without affecting the registration of timer-based triggers. +- The new `TenantDeleted` event will specifically handle the cleanup of resources associated with a tenant when it is deleted, ensuring a clear separation of responsibilities. diff --git a/doc/adr/graph.dot b/doc/adr/graph.dot index 61e1e6f7c..4818df2a2 100644 --- a/doc/adr/graph.dot +++ b/doc/adr/graph.dot @@ -1,12 +1,14 @@ digraph { -node [shape = plaintext]; +node [shape=plaintext]; subgraph { -_1 [label = "1. Record architecture decisions"; URL = "0001-record-architecture-decisions.html"]; -_2 [label = "2. Fault Propagation from Child to Parent Activities"; URL ="0002-fault-propagation-from-child-to-parent-activities.html"]; -_1 -> _2 [style= "dotted", weight = 1]; -_3 [label = "3. Direct Bookmark Management in WorkflowExecutionContext"; URL = "0003-direct-bookmark-management-in-workflowexecutioncontext.html"]; -_2 -> _3 [style = "dotted", weight = 1]; -_4 [label ="4. Activity Execution Snapshots"; URL = "0004-activity-execution-snapshots.html"]; -_3 -> _4 [style = "dotted", weight = 1]; +_1 [label="1. Record architecture decisions"; URL="0001-record-architecture-decisions.html"]; +_2 [label="2. Fault Propagation from Child to Parent Activities"; URL="0002-fault-propagation-from-child-to-parent-activities.html"]; +_1 -> _2 [style="dotted", weight=1]; +_3 [label="3. Direct Bookmark Management in WorkflowExecutionContext"; URL="0003-direct-bookmark-management-in-workflowexecutioncontext.html"]; +_2 -> _3 [style="dotted", weight=1]; +_4 [label="4. Activity Execution Snapshots"; URL="0004-activity-execution-snapshots.html"]; +_3 -> _4 [style="dotted", weight=1]; +_5 [label="5. Tenant Deleted Event"; URL="0005-tenant-deleted-event.html"]; +_4 -> _5 [style="dotted", weight=1]; } } \ No newline at end of file diff --git a/doc/adr/toc.md b/doc/adr/toc.md index 7eecf8af6..26b15683d 100644 --- a/doc/adr/toc.md +++ b/doc/adr/toc.md @@ -3,4 +3,5 @@ * [1. Record architecture decisions](0001-record-architecture-decisions.md) * [2. Fault Propagation from Child to Parent Activities](0002-fault-propagation-from-child-to-parent-activities.md) * [3. Direct Bookmark Management in WorkflowExecutionContext](0003-direct-bookmark-management-in-workflowexecutioncontext.md) -* [4. Activity Execution Snapshots](0004-activity-execution-snapshots.md) \ No newline at end of file +* [4. Activity Execution Snapshots](0004-activity-execution-snapshots.md) +* [5. Tenant Deleted Event](0005-tenant-deleted-event.md) \ No newline at end of file diff --git a/src/modules/Elsa.Common/Multitenancy/Contracts/ITenantDeletedEvent.cs b/src/modules/Elsa.Common/Multitenancy/Contracts/ITenantDeletedEvent.cs new file mode 100644 index 000000000..f651cdf70 --- /dev/null +++ b/src/modules/Elsa.Common/Multitenancy/Contracts/ITenantDeletedEvent.cs @@ -0,0 +1,6 @@ +namespace Elsa.Common.Multitenancy; + +public interface ITenantDeletedEvent +{ + Task TenantDeletedAsync(TenantDeletedEventArgs args); +} \ No newline at end of file diff --git a/src/modules/Elsa.Common/Multitenancy/EventArgs/TenantDeletedEventArgs.cs b/src/modules/Elsa.Common/Multitenancy/EventArgs/TenantDeletedEventArgs.cs new file mode 100644 index 000000000..07f6f1136 --- /dev/null +++ b/src/modules/Elsa.Common/Multitenancy/EventArgs/TenantDeletedEventArgs.cs @@ -0,0 +1,3 @@ +namespace Elsa.Common.Multitenancy; + +public record TenantDeletedEventArgs(Tenant Tenant, TenantScope TenantScope, CancellationToken CancellationToken) : TenantEventArgs(Tenant, TenantScope, CancellationToken); \ No newline at end of file diff --git a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs index 89298e5ac..6a1f9cb98 100644 --- a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs +++ b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs @@ -64,7 +64,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop var tenants = dictionary.Values.ToArray(); foreach (var tenant in tenants) - await UnregisterTenantAsync(tenant, cancellationToken); + await UnregisterTenantAsync(tenant, false, cancellationToken); } public async Task RefreshAsync(CancellationToken cancellationToken = default) @@ -85,7 +85,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop foreach (var removedTenantId in removedTenantIds) { var removedTenant = currentTenants[removedTenantId]; - await UnregisterTenantAsync(removedTenant, cancellationToken); + await UnregisterTenantAsync(removedTenant, true, cancellationToken); } foreach (var addedTenantId in addedTenantIds) @@ -137,14 +137,19 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop await tenantEvents.TenantActivatedAsync(new(tenant, scope, cancellationToken)); } - private async Task UnregisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default) + private async Task UnregisterTenantAsync(Tenant tenant, bool isDeleted, CancellationToken cancellationToken = default) { if (_tenantScopesDictionary!.Remove(tenant, out var scope)) { _tenantsDictionary!.Remove(tenant.Id.EmptyIfNull(), out _); using (tenantAccessor.PushContext(tenant)) + { await tenantEvents.TenantDeactivatedAsync(new(tenant, scope, cancellationToken)); + + if (isDeleted) + await tenantEvents.TenantDeletedAsync(new(tenant, scope, cancellationToken)); + } } } } \ No newline at end of file diff --git a/src/modules/Elsa.Common/Multitenancy/Implementations/TenantEventsManager.cs b/src/modules/Elsa.Common/Multitenancy/Implementations/TenantEventsManager.cs index 1c8e85a85..c8b97639a 100644 --- a/src/modules/Elsa.Common/Multitenancy/Implementations/TenantEventsManager.cs +++ b/src/modules/Elsa.Common/Multitenancy/Implementations/TenantEventsManager.cs @@ -2,34 +2,54 @@ using Microsoft.Extensions.Logging; namespace Elsa.Common.Multitenancy; -public class TenantEventsManager(IEnumerable tenantActivatedEvents, IEnumerable tenantDeactivatedEvents, ILogger logger) +public class TenantEventsManager( + IEnumerable tenantActivatedEvents, + IEnumerable tenantDeactivatedEvents, + IEnumerable tenantDeletedEvents, + ILogger logger) { public async Task TenantActivatedAsync(TenantActivatedEventArgs args) { - foreach (var tenantActivatedEvent in tenantActivatedEvents) - { - try - { - await tenantActivatedEvent.TenantActivatedAsync(args); - } - catch (Exception e) - { - logger.LogError(e, "Error occurred while processing tenant activated event."); - } - } + await ExecuteEventHandlersAsync( + tenantActivatedEvents, + (handler, eventArgs) => handler.TenantActivatedAsync(eventArgs), + args, + "activated"); } public async Task TenantDeactivatedAsync(TenantDeactivatedEventArgs args) { - foreach (var tenantDeactivatedEvent in tenantDeactivatedEvents) + await ExecuteEventHandlersAsync( + tenantDeactivatedEvents, + (handler, eventArgs) => handler.TenantDeactivatedAsync(eventArgs), + args, + "deactivated"); + } + + public async Task TenantDeletedAsync(TenantDeletedEventArgs args) + { + await ExecuteEventHandlersAsync( + tenantDeletedEvents, + (handler, eventArgs) => handler.TenantDeletedAsync(eventArgs), + args, + "deleted"); + } + + private async Task ExecuteEventHandlersAsync( + IEnumerable handlers, + Func handlerAction, + TArgs args, + string eventType) + { + foreach (var handler in handlers) { try { - await tenantDeactivatedEvent.TenantDeactivatedAsync(args); + await handlerAction(handler, args); } catch (Exception e) { - logger.LogError(e, "Error occurred while processing tenant deactivated event."); + logger.LogError(e, "Error occurred while processing tenant {EventType} event.", eventType); } } } diff --git a/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs b/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs index 7ef3dfdec..33bf2eff4 100644 --- a/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs +++ b/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs @@ -39,7 +39,7 @@ public class SchedulingFeature : FeatureBase Services .AddSingleton() .AddSingleton(sp => sp.GetRequiredService()) - .AddSingleton(sp => sp.GetRequiredService()) + .AddSingleton(sp => sp.GetRequiredService()) .AddSingleton() .AddSingleton() .AddSingleton(CronParser) diff --git a/src/modules/Elsa.Scheduling/Handlers/UpdateTenantSchedules.cs b/src/modules/Elsa.Scheduling/Handlers/UpdateTenantSchedules.cs index bdeecad26..46456cb9b 100644 --- a/src/modules/Elsa.Scheduling/Handlers/UpdateTenantSchedules.cs +++ b/src/modules/Elsa.Scheduling/Handlers/UpdateTenantSchedules.cs @@ -9,7 +9,7 @@ using Timer = Elsa.Scheduling.Activities.Timer; namespace Elsa.Scheduling.Handlers; -public class UpdateTenantSchedules : ITenantActivatedEvent, ITenantDeactivatedEvent +public class UpdateTenantSchedules : ITenantActivatedEvent, ITenantDeletedEvent { private static readonly string[] ActivityTypeNames = [ @@ -31,7 +31,7 @@ public class UpdateTenantSchedules : ITenantActivatedEvent, ITenantDeactivatedEv await bookmarkScheduler.ScheduleAsync(bookmarks, args.CancellationToken); } - public async Task TenantDeactivatedAsync(TenantDeactivatedEventArgs args) + public async Task TenantDeletedAsync(TenantDeletedEventArgs args) { var serviceProvider = args.TenantScope.ServiceProvider; var cancellationToken = args.CancellationToken;