Merge 3.5.0
This commit is contained in:
commit
bc3543fcb6
2
Elsa.sln
2
Elsa.sln
|
|
@ -222,6 +222,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "adr", "adr", "{0A04B1FD-06C
|
|||
doc\adr\0004-token-centric-flowchart-execution-model.md = doc\adr\0004-token-centric-flowchart-execution-model.md
|
||||
doc\adr\graph.dot = doc\adr\graph.dot
|
||||
doc\adr\toc.md = doc\adr\toc.md
|
||||
doc\adr\0005-activity-execution-snapshots.md = doc\adr\0004-activity-execution-snapshots.md
|
||||
doc\adr\0006-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}"
|
||||
|
|
|
|||
23
doc/adr/0005-tenant-deleted-event.md
Normal file
23
doc/adr/0005-tenant-deleted-event.md
Normal 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.
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
"Name": "Tenant 1",
|
||||
"Configuration": {
|
||||
"Http": {
|
||||
"Prefix": "/tenant-1",
|
||||
"Prefix": "tenant-1",
|
||||
"Host": "localhost:5001"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
using Elsa.Mediator.Abstractions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
|
||||
namespace Elsa.Mediator.Channels;
|
||||
|
||||
/// <inheritdoc cref="Elsa.Mediator.Contracts.INotificationsChannel" />
|
||||
public class NotificationsChannel : ChannelBase<INotification>, INotificationsChannel
|
||||
public class NotificationsChannel : ChannelBase<NotificationContext>, INotificationsChannel
|
||||
{
|
||||
}
|
||||
|
|
@ -14,11 +14,10 @@ public class DefaultStrategy : ICommandStrategy
|
|||
{
|
||||
var commandContext = context.CommandContext;
|
||||
var command = commandContext.Command;
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var commandType = command.GetType();
|
||||
var handleMethod = commandType.GetCommandHandlerMethod();
|
||||
var handler = context.Handler;
|
||||
|
||||
return await handler.InvokeAsync<TResult>(handleMethod, command, cancellationToken);
|
||||
return await handler.InvokeAsync<TResult>(handleMethod, commandContext);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Mediator.Contexts;
|
||||
|
|
@ -6,9 +7,9 @@ namespace Elsa.Mediator.Contexts;
|
|||
/// <summary>
|
||||
/// Represents a context for publishing events.
|
||||
/// </summary>
|
||||
/// <param name="Notification">The notification to publish.</param>
|
||||
/// <param name="NotificationContext">The notification to publish.</param>
|
||||
/// <param name="Handlers">The handlers to publish the notification to.</param>
|
||||
/// <param name="Logger">The logger.</param>
|
||||
/// <param name="ServiceProvider">The service provider to resolve services from.</param>
|
||||
/// <param name="CancellationToken">The cancellation token.</param>
|
||||
public record NotificationStrategyContext(INotification Notification, INotificationHandler[] Handlers, ILogger Logger, IServiceProvider ServiceProvider, CancellationToken CancellationToken = default);
|
||||
public record NotificationStrategyContext(NotificationContext NotificationContext, INotificationHandler[] Handlers, ILogger Logger, IServiceProvider ServiceProvider, CancellationToken CancellationToken = default);
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
|
|
@ -8,12 +9,12 @@ namespace Elsa.Mediator.Contracts;
|
|||
public interface INotificationsChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the writer for the notifications queue.
|
||||
/// Gets the writer for the notification queue.
|
||||
/// </summary>
|
||||
ChannelWriter<INotification> Writer { get; }
|
||||
ChannelWriter<NotificationContext> Writer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reader for the notifications queue.
|
||||
/// Gets the reader for the notification queue.
|
||||
/// </summary>
|
||||
ChannelReader<INotification> Reader { get; }
|
||||
ChannelReader<NotificationContext> Reader { get; }
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Elsa.Mediator.Models;
|
||||
|
||||
namespace Elsa.Mediator.Extensions;
|
||||
|
|
@ -40,10 +42,12 @@ public static class HandlerExtensions
|
|||
/// </summary>
|
||||
/// <param name="handler">The handler to invoke.</param>
|
||||
/// <param name="handleMethod">The handle method.</param>
|
||||
/// <param name="notification">The notification to handle.</param>
|
||||
/// <param name="notificationContext">The notification to handle.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static Task InvokeAsync(this INotificationHandler handler, MethodBase handleMethod, INotification notification, CancellationToken cancellationToken)
|
||||
public static Task InvokeAsync(this INotificationHandler handler, MethodBase handleMethod, NotificationContext notificationContext)
|
||||
{
|
||||
var notification = notificationContext.Notification;
|
||||
var cancellationToken = notificationContext.CancellationToken;
|
||||
return (Task)handleMethod.Invoke(handler, [notification, cancellationToken])!;
|
||||
}
|
||||
|
||||
|
|
@ -52,10 +56,11 @@ public static class HandlerExtensions
|
|||
/// </summary>
|
||||
/// <param name="handler">The handler to invoke.</param>
|
||||
/// <param name="handleMethod">The handle method.</param>
|
||||
/// <param name="command">The command to handle.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static Task<TResult> InvokeAsync<TResult>(this ICommandHandler handler, MethodBase handleMethod, ICommand command, CancellationToken cancellationToken)
|
||||
/// <param name="commandContext">The command to handle.</param>
|
||||
public static Task<TResult> InvokeAsync<TResult>(this ICommandHandler handler, MethodBase handleMethod, CommandContext commandContext)
|
||||
{
|
||||
var command = commandContext.Command;
|
||||
var cancellationToken = commandContext.CancellationToken;
|
||||
var task = (Task<TResult>)handleMethod.Invoke(handler, [command, cancellationToken])!;
|
||||
return task;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,48 +24,66 @@ public class BackgroundCommandSenderHostedService : BackgroundService
|
|||
public BackgroundCommandSenderHostedService(IOptions<MediatorOptions> options, ICommandsChannel commandsChannel, IServiceScopeFactory scopeFactory, ILogger<BackgroundCommandSenderHostedService> logger)
|
||||
{
|
||||
_workerCount = options.Value.CommandWorkerCount;
|
||||
_commandsChannel = commandsChannel;
|
||||
_commandsChannel = commandsChannel; // The shared input channel for all commands
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_outputs = new(_workerCount);
|
||||
_outputs = new(_workerCount); // Prepare a list to hold worker-specific channels
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var index = 0;
|
||||
var index = 0; // Used for round-robin distribution of work
|
||||
|
||||
// Set up worker channels and start background tasks for each worker
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
{
|
||||
var output = Channel.CreateUnbounded<CommandContext>();
|
||||
_outputs.Add(output);
|
||||
// Start a background task that processes commands from this worker's channel
|
||||
_ = ReadOutputAsync(output, cancellationToken);
|
||||
}
|
||||
|
||||
// Main dispatcher loop: read from the input channel and distribute to worker channels
|
||||
await foreach (var commandContext in _commandsChannel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var output = _outputs[index];
|
||||
await output.Writer.WriteAsync(commandContext, cancellationToken);
|
||||
// Round-robin distribution - move to next worker
|
||||
index = (index + 1) % _workerCount;
|
||||
}
|
||||
|
||||
foreach (var output in _outputs)
|
||||
// If the input channel is completed, complete all worker channels
|
||||
foreach (var output in _outputs)
|
||||
output.Writer.Complete();
|
||||
}
|
||||
|
||||
private async Task ReadOutputAsync(Channel<CommandContext> output, CancellationToken cancellationToken)
|
||||
{
|
||||
// Worker task: process commands from the worker's channel
|
||||
await foreach (var commandContext in output.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a fresh scope for each command to ensure proper service lifetime
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
|
||||
await commandSender.SendAsync(commandContext.Command, CommandStrategy.Default, commandContext.Headers, cancellationToken);
|
||||
// Link the service cancellation token with the command's token to ensure proper cancellation
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken,
|
||||
commandContext.CancellationToken);
|
||||
|
||||
// Process the command using the command sender service with the linked token
|
||||
await commandSender.SendAsync(
|
||||
commandContext.Command,
|
||||
CommandStrategy.Default,
|
||||
commandContext.Headers,
|
||||
linkedTokenSource.Token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Log errors but continue processing other commands
|
||||
_logger.LogError(e, "An unhandled exception occurred while processing the queue");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Elsa.Mediator.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
|
@ -16,7 +17,7 @@ public class BackgroundEventPublisherHostedService : BackgroundService
|
|||
private readonly int _workerCount;
|
||||
private readonly INotificationsChannel _notificationsChannel;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly List<Channel<INotification>> _outputs;
|
||||
private readonly List<Channel<NotificationContext>> _outputs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -26,46 +27,62 @@ public class BackgroundEventPublisherHostedService : BackgroundService
|
|||
_notificationsChannel = notificationsChannel;
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_outputs = new List<Channel<INotification>>(_workerCount);
|
||||
_outputs = new(_workerCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Index to round-robin distribute notifications across worker channels
|
||||
var index = 0;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var notificationSender = scope.ServiceProvider.GetRequiredService<INotificationSender>();
|
||||
|
||||
// Create multiple output channels and start worker tasks for parallel processing
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
{
|
||||
var output = Channel.CreateUnbounded<INotification>();
|
||||
var output = Channel.CreateUnbounded<NotificationContext>();
|
||||
_outputs.Add(output);
|
||||
// Start a background task to process notifications from this output channel
|
||||
_ = ReadOutputAsync(output, notificationSender, cancellationToken);
|
||||
}
|
||||
|
||||
var channelReader = _notificationsChannel.Reader;
|
||||
|
||||
// Continuously read notifications from the input channel and distribute them to worker channels
|
||||
// using round-robin distribution for load balancing
|
||||
await foreach (var notification in channelReader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var output = _outputs[index];
|
||||
await output.Writer.WriteAsync(notification, cancellationToken);
|
||||
// Move to the next worker in a circular fashion
|
||||
index = (index + 1) % _workerCount;
|
||||
}
|
||||
|
||||
// When the input channel is completed, complete all output channels
|
||||
foreach (var output in _outputs)
|
||||
{
|
||||
output.Writer.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReadOutputAsync(Channel<INotification> output, INotificationSender notificationSender, CancellationToken cancellationToken)
|
||||
/// <summary>
|
||||
/// Processes notifications from an output channel asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="output">The channel to read notifications from</param>
|
||||
/// <param name="notificationSender">The service used to send notifications</param>
|
||||
/// <param name="cancellationToken">Cancellation token from the hosted service</param>
|
||||
private async Task ReadOutputAsync(Channel<NotificationContext> output, INotificationSender notificationSender, CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var notification in output.Reader.ReadAllAsync(cancellationToken))
|
||||
await foreach (var notificationContext in output.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
await notificationSender.SendAsync(notification, NotificationStrategy.Sequential, cancellationToken);
|
||||
var notification = notificationContext.Notification;
|
||||
// Link the cancellation tokens so that cancellation can happen from either source
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, notificationContext.CancellationToken);
|
||||
await notificationSender.SendAsync(notification, NotificationStrategy.Sequential, linkedTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class JobRunnerHostedService : BackgroundService
|
|||
|
||||
/// <inheritdoc />
|
||||
public JobRunnerHostedService(IOptions<MediatorOptions> options, IJobsChannel jobsChannel, ILogger<JobRunnerHostedService> logger)
|
||||
{
|
||||
{
|
||||
_workerCount = options.Value.JobWorkerCount;
|
||||
_jobsChannel = jobsChannel;
|
||||
_logger = logger;
|
||||
|
|
@ -26,21 +26,31 @@ public class JobRunnerHostedService : BackgroundService
|
|||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var workers = new Task[_workerCount];
|
||||
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
// Create an array of worker tasks to process jobs in parallel
|
||||
var workers = new Task[_workerCount];
|
||||
|
||||
// Start multiple workers (tasks) to process jobs concurrently
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
workers[i] = ProcessJobsAsync(stoppingToken);
|
||||
|
||||
// Wait for all worker tasks to complete (typically when the application is shutting down)
|
||||
await Task.WhenAll(workers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continuously processes jobs from the job channel until cancellation is requested.
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken">Cancellation token from the hosted service</param>
|
||||
private async Task ProcessJobsAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Process all jobs from the channel until it's completed or cancellation is requested
|
||||
await foreach (var jobItem in _jobsChannel.Reader.ReadAllAsync(stoppingToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
await jobItem.Action(jobItem.CancellationTokenSource.Token);
|
||||
// Link the cancellation tokens so that cancellation can happen from either source
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, jobItem.CancellationTokenSource.Token);
|
||||
await jobItem.Action(linkedTokenSource.Token);
|
||||
_logger.LogInformation("Worker {CurrentTaskId} processed job {JobId}", Task.CurrentId, jobItem.JobId);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
|
@ -53,6 +63,7 @@ public class JobRunnerHostedService : BackgroundService
|
|||
}
|
||||
finally
|
||||
{
|
||||
// Notify that the job has completed (whether successfully or not)
|
||||
jobItem.OnJobCompleted(jobItem.JobId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public class NotificationHandlerInvokerMiddleware(
|
|||
var serviceProvider = context.ServiceProvider;
|
||||
var notificationHandlers = serviceProvider.GetServices<INotificationHandler>();
|
||||
var handlers = notificationHandlers.Where(x => handlerType.IsInstanceOfType(x)).DistinctBy(x => x.GetType()).ToArray();
|
||||
var strategyContext = new NotificationStrategyContext(notification, handlers, logger, serviceProvider, context.CancellationToken);
|
||||
var strategyContext = new NotificationStrategyContext(context, handlers, logger, serviceProvider, context.CancellationToken);
|
||||
|
||||
await context.NotificationStrategy.PublishAsync(strategyContext);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,6 @@ public class BackgroundProcessingStrategy : IEventPublishingStrategy
|
|||
{
|
||||
var notificationsChannel = context.ServiceProvider.GetRequiredService<INotificationsChannel>();
|
||||
|
||||
await notificationsChannel.Writer.WriteAsync(context.Notification, context.CancellationToken);
|
||||
await notificationsChannel.Writer.WriteAsync(context.NotificationContext, context.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,11 +12,11 @@ public class ParallelProcessingStrategy : IEventPublishingStrategy
|
|||
/// <inheritdoc />
|
||||
public async Task PublishAsync(NotificationStrategyContext context)
|
||||
{
|
||||
var notification = context.Notification;
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var notificationContext = context.NotificationContext;
|
||||
var notification = notificationContext.Notification;
|
||||
var notificationType = notification.GetType();
|
||||
var handleMethod = notificationType.GetNotificationHandlerMethod();
|
||||
var tasks = context.Handlers.Select(handler => handler.InvokeAsync(handleMethod, notification, cancellationToken)).ToList();
|
||||
var tasks = context.Handlers.Select(handler => handler.InvokeAsync(handleMethod, context.NotificationContext)).ToList();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ public class SequentialProcessingStrategy : IEventPublishingStrategy
|
|||
/// <inheritdoc />
|
||||
public async Task PublishAsync(NotificationStrategyContext context)
|
||||
{
|
||||
var notification = context.Notification;
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var notificationContext = context.NotificationContext;
|
||||
var notification = notificationContext.Notification;
|
||||
var notificationType = notification.GetType();
|
||||
var handleMethod = notificationType.GetNotificationHandlerMethod();
|
||||
|
||||
foreach (var handler in context.Handlers)
|
||||
await handler.InvokeAsync(handleMethod, notification, cancellationToken);
|
||||
await handler.InvokeAsync(handleMethod, context.NotificationContext);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Common.Multitenancy;
|
||||
|
||||
public interface ITenantDeletedEvent
|
||||
{
|
||||
Task TenantDeletedAsync(TenantDeletedEventArgs args);
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
namespace Elsa.Common.Multitenancy;
|
||||
|
||||
public record TenantDeletedEventArgs(Tenant Tenant, TenantScope TenantScope, CancellationToken CancellationToken) : TenantEventArgs(Tenant, TenantScope, CancellationToken);
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -63,9 +63,6 @@ public partial class ActivityExecutionContext
|
|||
|
||||
// Update the completed at timestamp.
|
||||
CompletedAt = WorkflowExecutionContext.SystemClock.UtcNow;
|
||||
|
||||
var mediator = GetRequiredService<INotificationSender>();
|
||||
await mediator.SendAsync(new Notifications.ActivityCompleted(this), CancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Reflection;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.CommitStates;
|
||||
using Elsa.Workflows.Pipelines.ActivityExecution;
|
||||
|
|
@ -59,10 +60,14 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I
|
|||
if (ShouldCommit(context, ActivityLifetimeEvent.ActivityExecuting))
|
||||
await context.WorkflowExecutionContext.CommitAsync();
|
||||
|
||||
var previousActivityStatus = context.Status;
|
||||
context.TransitionTo(ActivityStatus.Running);
|
||||
|
||||
// Execute activity.
|
||||
await ExecuteActivityAsync(context);
|
||||
|
||||
var currentActivityStatus = context.Status;
|
||||
var activityDidComplete = previousActivityStatus != ActivityStatus.Completed && currentActivityStatus == ActivityStatus.Completed;
|
||||
|
||||
// Reset execute delegate.
|
||||
workflowExecutionContext.ExecuteDelegate = null;
|
||||
|
|
@ -81,6 +86,13 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I
|
|||
|
||||
// Invoke next middleware.
|
||||
await next(context);
|
||||
|
||||
// If the activity completed, send a notification.
|
||||
if (activityDidComplete)
|
||||
{
|
||||
var mediator = context.GetRequiredService<INotificationSender>();
|
||||
await mediator.SendAsync(new Notifications.ActivityCompleted(context), context.CancellationToken);
|
||||
}
|
||||
|
||||
// Conditionally commit the workflow state.
|
||||
if (ShouldCommit(context, ActivityLifetimeEvent.ActivityExecuted))
|
||||
|
|
|
|||
Loading…
Reference in a new issue