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
This commit is contained in:
Sipke Schoorstra 2025-08-06 08:43:39 +02:00 committed by GitHub
parent fc5612687d
commit 133e8dfc63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 91 additions and 30 deletions

View file

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

View file

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

View file

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

View file

@ -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)
* [4. Activity Execution Snapshots](0004-activity-execution-snapshots.md)
* [5. Tenant Deleted Event](0005-tenant-deleted-event.md)

View file

@ -0,0 +1,6 @@
namespace Elsa.Common.Multitenancy;
public interface ITenantDeletedEvent
{
Task TenantDeletedAsync(TenantDeletedEventArgs args);
}

View file

@ -0,0 +1,3 @@
namespace Elsa.Common.Multitenancy;
public record TenantDeletedEventArgs(Tenant Tenant, TenantScope TenantScope, CancellationToken CancellationToken) : TenantEventArgs(Tenant, TenantScope, CancellationToken);

View file

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

View file

@ -2,34 +2,54 @@ using Microsoft.Extensions.Logging;
namespace Elsa.Common.Multitenancy;
public class TenantEventsManager(IEnumerable<ITenantActivatedEvent> tenantActivatedEvents, IEnumerable<ITenantDeactivatedEvent> tenantDeactivatedEvents, ILogger<TenantEventsManager> logger)
public class TenantEventsManager(
IEnumerable<ITenantActivatedEvent> tenantActivatedEvents,
IEnumerable<ITenantDeactivatedEvent> tenantDeactivatedEvents,
IEnumerable<ITenantDeletedEvent> tenantDeletedEvents,
ILogger<TenantEventsManager> 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<THandler, TArgs>(
IEnumerable<THandler> handlers,
Func<THandler, TArgs, Task> 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);
}
}
}

View file

@ -39,7 +39,7 @@ public class SchedulingFeature : FeatureBase
Services
.AddSingleton<UpdateTenantSchedules>()
.AddSingleton<ITenantActivatedEvent>(sp => sp.GetRequiredService<UpdateTenantSchedules>())
.AddSingleton<ITenantDeactivatedEvent>(sp => sp.GetRequiredService<UpdateTenantSchedules>())
.AddSingleton<ITenantDeletedEvent>(sp => sp.GetRequiredService<UpdateTenantSchedules>())
.AddSingleton<IScheduler, LocalScheduler>()
.AddSingleton<CronosCronParser>()
.AddSingleton(CronParser)

View file

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