using System.Reflection;
using Elsa.Common.Helpers;
using Elsa.Common.RecurringTasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Elsa.Common.Multitenancy.EventHandlers;
///
/// Manages the lifecycle of startup, background, and recurring tasks for tenants.
/// Executes tasks in the proper sequence: startup tasks first, then background tasks, then recurring tasks.
///
public class TenantTaskManager(RecurringTaskScheduleManager scheduleManager, ILogger logger) : ITenantActivatedEvent, ITenantDeactivatedEvent
{
private readonly ICollection _runningBackgroundTasks = new List();
private readonly ICollection _scheduledTimers = new List();
private CancellationTokenSource _cancellationTokenSource = null!;
public async Task TenantActivatedAsync(TenantActivatedEventArgs args)
{
var cancellationToken = args.CancellationToken;
var tenantScope = args.TenantScope;
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService();
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Step 1: Run startup tasks (with dependency ordering)
await RunStartupTasksAsync(tenantScope, taskExecutor, cancellationToken);
// Step 2: Run background tasks
await RunBackgroundTasksAsync(tenantScope, taskExecutor, cancellationToken);
// Step 3: Start recurring tasks
await StartRecurringTasksAsync(tenantScope, taskExecutor, cancellationToken);
}
public async Task TenantDeactivatedAsync(TenantDeactivatedEventArgs args)
{
var tenantScope = args.TenantScope;
// Cancel all running tasks
_cancellationTokenSource.Cancel();
// Wait for background tasks to complete (with cancellation they should finish quickly)
if (_runningBackgroundTasks.Any())
{
try
{
await Task.WhenAll(_runningBackgroundTasks);
}
catch (OperationCanceledException)
{
// Expected when tasks are cancelled
}
_runningBackgroundTasks.Clear();
}
// Stop all recurring task timers
foreach (var timer in _scheduledTimers)
await timer.DisposeAsync();
_scheduledTimers.Clear();
// Stop recurring tasks
var recurringTasks = tenantScope.ServiceProvider.GetServices();
foreach (var task in recurringTasks)
await task.StopAsync(args.CancellationToken);
}
private async Task RunStartupTasksAsync(ITenantScope tenantScope, ITaskExecutor taskExecutor, CancellationToken cancellationToken)
{
var startupTasks = tenantScope.ServiceProvider.GetServices()
.OrderBy(x => x.GetType().GetCustomAttribute()?.Order ?? 0f)
.ToList();
// First apply OrderAttribute to determine a base order, then perform topological sorting.
// The topological sort is the final ordering step to ensure dependency constraints are respected.
var sortedTasks = TopologicalTaskSorter.Sort(startupTasks).ToList();
foreach (var task in sortedTasks)
await taskExecutor.ExecuteTaskAsync(task, cancellationToken);
}
private Task RunBackgroundTasksAsync(ITenantScope tenantScope, ITaskExecutor taskExecutor, CancellationToken cancellationToken)
{
var backgroundTasks = tenantScope.ServiceProvider.GetServices();
var backgroundTaskStarter = tenantScope.ServiceProvider.GetRequiredService();
foreach (var backgroundTask in backgroundTasks)
{
var task = backgroundTaskStarter
.StartAsync(backgroundTask, _cancellationTokenSource.Token)
.ContinueWith(t => taskExecutor.ExecuteTaskAsync(backgroundTask, _cancellationTokenSource.Token),
cancellationToken,
TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default)
.Unwrap();
if (!task.IsCompleted)
_runningBackgroundTasks.Add(task);
}
return Task.CompletedTask;
}
private async Task StartRecurringTasksAsync(ITenantScope tenantScope, ITaskExecutor taskExecutor, CancellationToken cancellationToken)
{
var recurringTasks = tenantScope.ServiceProvider.GetServices().ToList();
foreach (var task in recurringTasks)
{
var schedule = scheduleManager.GetScheduleFor(task.GetType());
var timer = schedule.CreateTimer(async () =>
{
try
{
await taskExecutor.ExecuteTaskAsync(task, _cancellationTokenSource.Token);
}
catch (OperationCanceledException e)
{
logger.LogInformation(e, "Recurring task {TaskType} was cancelled", task.GetType().Name);
}
});
_scheduledTimers.Add(timer);
await task.StartAsync(cancellationToken);
}
}
}