Merge 3.5.0

This commit is contained in:
Sipke Schoorstra 2025-08-06 20:40:27 +02:00
commit bc3543fcb6
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
25 changed files with 196 additions and 72 deletions

View file

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

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

@ -29,7 +29,7 @@
"Name": "Tenant 1",
"Configuration": {
"Http": {
"Prefix": "/tenant-1",
"Prefix": "tenant-1",
"Host": "localhost:5001"
},
"ConnectionStrings": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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;

View file

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

View file

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