Improves tenant task management with dependencies (#7174)
* Consolidate tenant task lifecycle logic into `TenantTaskManager` and remove obsolete task event handlers (`RunStartupTasks`, `RunBackgroundTasks`, `StartRecurringTasks`). Introduce `TopologicalTaskSorter` for dependency-based task execution. * Handles multiple tasks of the same type Updates the topological task sorter to handle multiple tasks of the same type. Previously, the sorter assumed a one-to-one mapping between task types and task instances, which caused issues when multiple tasks of the same type were present. Now, it groups tasks by type and adds them to the result in the correct order. * Add unit tests for `TopologicalTaskSorter` Introduce `Elsa.Common.UnitTests` project with comprehensive test coverage for `TopologicalTaskSorter`, including dependency resolution, circular dependency handling, and task ordering scenarios. Update `Elsa.sln` to include the new test project. * Update src/modules/Elsa.Common/Multitenancy/EventHandlers/TenantTaskManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Unwrap background task continuation in `TenantTaskManager` to ensure proper task execution tracking. * Update src/modules/Elsa.Common/Helpers/TopologicalTaskSorter.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor `TryGet` method to prioritize memory register lookup and update `Output` constructor to use `MemoryBlockReference`. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
fa798b0a47
commit
b577279321
7
Elsa.sln
7
Elsa.sln
|
|
@ -325,6 +325,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dsl", "dsl", "{477C2416-312
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Resilience.Core.UnitTests", "test\unit\Elsa.Resilience.Core.UnitTests\Elsa.Resilience.Core.UnitTests.csproj", "{B8006D70-1630-43DB-A043-FA89FAC70F37}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Common.UnitTests", "test\unit\Elsa.Common.UnitTests\Elsa.Common.UnitTests.csproj", "{A3C07D5B-2A30-494E-B9BC-4B1594B31ABC}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -585,6 +587,10 @@ Global
|
|||
{B8006D70-1630-43DB-A043-FA89FAC70F37}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8006D70-1630-43DB-A043-FA89FAC70F37}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8006D70-1630-43DB-A043-FA89FAC70F37}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A3C07D5B-2A30-494E-B9BC-4B1594B31ABC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A3C07D5B-2A30-494E-B9BC-4B1594B31ABC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A3C07D5B-2A30-494E-B9BC-4B1594B31ABC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A3C07D5B-2A30-494E-B9BC-4B1594B31ABC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -687,6 +693,7 @@ Global
|
|||
{477C2416-312D-46AE-BCD6-8FA1FAB43624} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{874F5A44-DB06-47AB-A18C-2D13942E0147} = {477C2416-312D-46AE-BCD6-8FA1FAB43624}
|
||||
{B8006D70-1630-43DB-A043-FA89FAC70F37} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{A3C07D5B-2A30-494E-B9BC-4B1594B31ABC} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
namespace Elsa.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies dependencies for a task implementation. Tasks with dependencies will be executed after their dependencies have completed.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
public class TaskDependencyAttribute(Type dependencyTaskType) : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the task that must complete before this task can execute.
|
||||
/// </summary>
|
||||
public Type DependencyTaskType { get; } = dependencyTaskType;
|
||||
}
|
||||
|
|
@ -39,18 +39,12 @@ public class MultitenancyFeature(IModule module) : FeatureBase(module)
|
|||
.AddSingleton<ITenantAccessor, DefaultTenantAccessor>()
|
||||
.AddSingleton<ITenantFinder, DefaultTenantFinder>()
|
||||
.AddSingleton<ITenantService, DefaultTenantService>()
|
||||
|
||||
// Order is important: Startup task first, then background and recurring tasks.
|
||||
.AddSingleton<ITenantActivatedEvent, RunStartupTasks>()
|
||||
|
||||
.AddSingleton<RunBackgroundTasks>()
|
||||
.AddSingleton<ITenantActivatedEvent>(sp => sp.GetRequiredService<RunBackgroundTasks>())
|
||||
.AddSingleton<ITenantDeactivatedEvent>(sp => sp.GetRequiredService<RunBackgroundTasks>())
|
||||
|
||||
.AddSingleton<StartRecurringTasks>()
|
||||
.AddSingleton<ITenantActivatedEvent>(sp => sp.GetRequiredService<StartRecurringTasks>())
|
||||
.AddSingleton<ITenantDeactivatedEvent>(sp => sp.GetRequiredService<StartRecurringTasks>())
|
||||
|
||||
|
||||
// TenantTaskManager handles all task lifecycle in the correct order
|
||||
.AddSingleton<TenantTaskManager>()
|
||||
.AddSingleton<ITenantActivatedEvent>(sp => sp.GetRequiredService<TenantTaskManager>())
|
||||
.AddSingleton<ITenantDeactivatedEvent>(sp => sp.GetRequiredService<TenantTaskManager>())
|
||||
|
||||
.AddSingleton<RecurringTaskScheduleManager>()
|
||||
.AddSingleton<TenantEventsManager>()
|
||||
.AddScoped<DefaultTenantsProvider>()
|
||||
|
|
|
|||
93
src/modules/Elsa.Common/Helpers/TopologicalTaskSorter.cs
Normal file
93
src/modules/Elsa.Common/Helpers/TopologicalTaskSorter.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace Elsa.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Sorts tasks based on their dependencies using topological ordering.
|
||||
/// </summary>
|
||||
public static class TopologicalTaskSorter
|
||||
{
|
||||
/// <summary>
|
||||
/// Sorts tasks in topological order based on their TaskDependencyAttribute declarations.
|
||||
/// </summary>
|
||||
/// <param name="tasks">The tasks to sort.</param>
|
||||
/// <typeparam name="T">The task type.</typeparam>
|
||||
/// <returns>A list of tasks sorted in dependency order.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a circular dependency is detected.</exception>
|
||||
public static IReadOnlyList<T> Sort<T>(IEnumerable<T> tasks) where T : ITask
|
||||
{
|
||||
var taskList = tasks.ToList();
|
||||
var taskTypes = taskList.Select(t => t.GetType()).ToList();
|
||||
var dependencyGraph = BuildDependencyGraph(taskTypes);
|
||||
var sortedTypes = TopologicalSort(dependencyGraph);
|
||||
|
||||
// Map sorted types back to original task instances, preserving multiples
|
||||
var taskGroups = taskList.GroupBy(t => t.GetType()).ToDictionary(g => g.Key, g => g.ToList());
|
||||
var result = new List<T>();
|
||||
|
||||
foreach (var type in sortedTypes.Where(taskGroups.ContainsKey))
|
||||
{
|
||||
var group = taskGroups[type];
|
||||
result.AddRange(group);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<Type, List<Type>> BuildDependencyGraph(IEnumerable<Type> taskTypes)
|
||||
{
|
||||
var graph = new Dictionary<Type, List<Type>>();
|
||||
|
||||
foreach (var taskType in taskTypes)
|
||||
{
|
||||
if (!graph.ContainsKey(taskType))
|
||||
graph[taskType] = new();
|
||||
|
||||
var dependencies = taskType.GetCustomAttributes<TaskDependencyAttribute>()
|
||||
.Select(attr => attr.DependencyTaskType)
|
||||
.ToList();
|
||||
|
||||
graph[taskType].AddRange(dependencies);
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
private static List<Type> TopologicalSort(Dictionary<Type, List<Type>> graph)
|
||||
{
|
||||
var sorted = new List<Type>();
|
||||
var visited = new HashSet<Type>();
|
||||
var visiting = new HashSet<Type>();
|
||||
|
||||
foreach (var node in graph.Keys)
|
||||
{
|
||||
if (!visited.Contains(node))
|
||||
Visit(node, graph, visited, visiting, sorted);
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static void Visit(Type node, Dictionary<Type, List<Type>> graph, HashSet<Type> visited, HashSet<Type> visiting, List<Type> sorted)
|
||||
{
|
||||
if (visiting.Contains(node))
|
||||
throw new InvalidOperationException($"Circular dependency detected involving task type: {node.Name}");
|
||||
|
||||
if (visited.Contains(node))
|
||||
return;
|
||||
|
||||
visiting.Add(node);
|
||||
|
||||
if (graph.TryGetValue(node, out var dependencies))
|
||||
{
|
||||
foreach (var dependency in dependencies)
|
||||
{
|
||||
Visit(dependency, graph, visited, visiting, sorted);
|
||||
}
|
||||
}
|
||||
|
||||
visiting.Remove(node);
|
||||
visited.Add(node);
|
||||
sorted.Add(node);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Common.Multitenancy;
|
||||
|
||||
public interface ITenantScope
|
||||
{
|
||||
public IServiceScope ServiceScope { get; }
|
||||
IServiceProvider ServiceProvider { get; }
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Common.Multitenancy.EventHandlers;
|
||||
|
||||
public class RunBackgroundTasks : ITenantActivatedEvent, ITenantDeactivatedEvent
|
||||
{
|
||||
private readonly ICollection<Task> _runningTasks = new List<Task>();
|
||||
private CancellationTokenSource _cancellationTokenSource = default!;
|
||||
|
||||
public Task TenantActivatedAsync(TenantActivatedEventArgs args)
|
||||
{
|
||||
var cancellationToken = args.CancellationToken;
|
||||
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
var tenantScope = args.TenantScope;
|
||||
var backgroundTasks = tenantScope.ServiceProvider.GetServices<IBackgroundTask>();
|
||||
var backgroundTaskStarter = tenantScope.ServiceProvider.GetRequiredService<IBackgroundTaskStarter>();
|
||||
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService<ITaskExecutor>();
|
||||
|
||||
foreach (var backgroundTask in backgroundTasks)
|
||||
{
|
||||
var task = backgroundTaskStarter
|
||||
.StartAsync(backgroundTask, _cancellationTokenSource.Token)
|
||||
.ContinueWith(t => taskExecutor.ExecuteTaskAsync(backgroundTask, _cancellationTokenSource.Token), cancellationToken, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.Default);
|
||||
if (!task.IsCompleted) _runningTasks.Add(task);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task TenantDeactivatedAsync(TenantDeactivatedEventArgs args)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
_runningTasks.Clear();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
using System.Reflection;
|
||||
using Elsa.Common.RecurringTasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Common.Multitenancy.EventHandlers;
|
||||
|
||||
public class RunStartupTasks : ITenantActivatedEvent
|
||||
{
|
||||
public async Task TenantActivatedAsync(TenantActivatedEventArgs args)
|
||||
{
|
||||
var cancellationToken = args.CancellationToken;
|
||||
var tenantScope = args.TenantScope;
|
||||
var tasks = tenantScope.ServiceProvider.GetServices<IStartupTask>().OrderBy(x => x.GetType().GetCustomAttribute<OrderAttribute>()?.Order ?? 0f).ToList();
|
||||
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService<ITaskExecutor>();
|
||||
|
||||
foreach (var task in tasks)
|
||||
await taskExecutor.ExecuteTaskAsync(task, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
using Elsa.Common.RecurringTasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Common.Multitenancy.EventHandlers;
|
||||
|
||||
public class StartRecurringTasks(RecurringTaskScheduleManager scheduleManager, ILogger<StartRecurringTasks> logger) : ITenantActivatedEvent, ITenantDeactivatedEvent
|
||||
{
|
||||
private readonly ICollection<ScheduledTimer> _scheduledTimers = new List<ScheduledTimer>();
|
||||
private CancellationTokenSource _cancellationTokenSource = null!;
|
||||
|
||||
public async Task TenantActivatedAsync(TenantActivatedEventArgs args)
|
||||
{
|
||||
var cancellationToken = args.CancellationToken;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
var tenantScope = args.TenantScope;
|
||||
var tasks = tenantScope.ServiceProvider.GetServices<IRecurringTask>().ToList();
|
||||
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService<ITaskExecutor>();
|
||||
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
var schedule = scheduleManager.GetScheduleFor(task.GetType());
|
||||
var timer = schedule.CreateTimer(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await taskExecutor.ExecuteTaskAsync(task, _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
logger.LogInformation(e, "Task {TaskType} was cancelled", task.GetType().Name);
|
||||
}
|
||||
|
||||
});
|
||||
_scheduledTimers.Add(timer);
|
||||
await task.StartAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TenantDeactivatedAsync(TenantDeactivatedEventArgs args)
|
||||
{
|
||||
var tenantScope = args.TenantScope;
|
||||
_cancellationTokenSource.Cancel();
|
||||
foreach (var timer in _scheduledTimers) await timer.DisposeAsync();
|
||||
_scheduledTimers.Clear();
|
||||
var tasks = tenantScope.ServiceProvider.GetServices<IRecurringTask>();
|
||||
foreach (var task in tasks) await task.StopAsync(args.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
using System.Reflection;
|
||||
using Elsa.Common.Helpers;
|
||||
using Elsa.Common.RecurringTasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Common.Multitenancy.EventHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class TenantTaskManager(RecurringTaskScheduleManager scheduleManager, ILogger<TenantTaskManager> logger) : ITenantActivatedEvent, ITenantDeactivatedEvent
|
||||
{
|
||||
private readonly ICollection<Task> _runningBackgroundTasks = new List<Task>();
|
||||
private readonly ICollection<ScheduledTimer> _scheduledTimers = new List<ScheduledTimer>();
|
||||
private CancellationTokenSource _cancellationTokenSource = null!;
|
||||
|
||||
public async Task TenantActivatedAsync(TenantActivatedEventArgs args)
|
||||
{
|
||||
var cancellationToken = args.CancellationToken;
|
||||
var tenantScope = args.TenantScope;
|
||||
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService<ITaskExecutor>();
|
||||
|
||||
_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<IRecurringTask>();
|
||||
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<IStartupTask>()
|
||||
.OrderBy(x => x.GetType().GetCustomAttribute<OrderAttribute>()?.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<IBackgroundTask>();
|
||||
var backgroundTaskStarter = tenantScope.ServiceProvider.GetRequiredService<IBackgroundTaskStarter>();
|
||||
|
||||
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<IRecurringTask>().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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ namespace Elsa.Common.Multitenancy;
|
|||
/// Represents a tenant scope, which sets the current tenant for the duration of the scope.
|
||||
/// After the scope is disposed, the original tenant is restored.
|
||||
/// </summary>
|
||||
public class TenantScope(IServiceScope serviceScope, ITenantAccessor tenantAccessor, Tenant? tenant) : IAsyncDisposable
|
||||
public class TenantScope(IServiceScope serviceScope, ITenantAccessor tenantAccessor, Tenant? tenant) : ITenantScope, IAsyncDisposable
|
||||
{
|
||||
private readonly IDisposable _tenantContext = tenantAccessor.PushContext(tenant);
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class ConfigurationTenantsProvider : ITenantsProvider
|
|||
var tenants = options.Tenants.ToList();
|
||||
|
||||
// Rebind each Tenant's Configuration property manually using array indices
|
||||
for (int i = 0; i < tenants.Count; i++)
|
||||
for (var i = 0; i < tenants.Count; i++)
|
||||
tenants[i].Configuration = _configuration.GetSection($"Multitenancy:Tenants:{i}:Configuration");
|
||||
|
||||
_tenants = tenants;
|
||||
|
|
|
|||
|
|
@ -701,13 +701,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
/// <returns>True if the memory block exists, false otherwise.</returns>
|
||||
public bool TryGet(MemoryBlockReference blockReference, out object? value)
|
||||
{
|
||||
// Handle Literal references directly - they hold their value and don't need to be in the memory register
|
||||
if (blockReference is Literal literal)
|
||||
{
|
||||
value = literal.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// First, try to get the value from the memory register
|
||||
var memoryBlock = GetMemoryBlock(blockReference);
|
||||
|
||||
if (memoryBlock != null)
|
||||
|
|
@ -716,6 +710,13 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
// Handle Literal references as a fallback - they can hold their value directly
|
||||
if (blockReference is Literal literal)
|
||||
{
|
||||
value = literal.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ namespace Elsa.Workflows.Models;
|
|||
|
||||
public class Output : Argument
|
||||
{
|
||||
public Output() : base(new Literal())
|
||||
public Output() : base(new MemoryBlockReference())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
13
test/unit/Elsa.Common.UnitTests/Elsa.Common.UnitTests.csproj
Normal file
13
test/unit/Elsa.Common.UnitTests/Elsa.Common.UnitTests.csproj
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Include>[Elsa.Common]*</Include>
|
||||
<Threshold>0</Threshold>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Common\Elsa.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
using Elsa.Common.Helpers;
|
||||
|
||||
namespace Elsa.Common.UnitTests.Helpers;
|
||||
|
||||
public class TopologicalTaskSorterTests
|
||||
{
|
||||
// Task with single dependency
|
||||
[TaskDependency(typeof(TaskA))]
|
||||
private class TaskWithDependency : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Task with multiple dependencies
|
||||
[TaskDependency(typeof(TaskA))]
|
||||
[TaskDependency(typeof(TaskB))]
|
||||
private class TaskWithMultipleDependencies : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Chain of dependencies: TaskChainC -> TaskChainB -> TaskChainA
|
||||
private class TaskChainA : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[TaskDependency(typeof(TaskChainA))]
|
||||
private class TaskChainB : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[TaskDependency(typeof(TaskChainB))]
|
||||
private class TaskChainC : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Circular dependency: TaskCircular1 -> TaskCircular2 -> TaskCircular1
|
||||
[TaskDependency(typeof(TaskCircular2))]
|
||||
private class TaskCircular1 : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[TaskDependency(typeof(TaskCircular1))]
|
||||
private class TaskCircular2 : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Self-referencing circular dependency
|
||||
[TaskDependency(typeof(TaskSelfCircular))]
|
||||
private class TaskSelfCircular : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithNoTasks_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var tasks = Array.Empty<ITask>();
|
||||
|
||||
// Act
|
||||
var result = TopologicalTaskSorter.Sort(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithSingleTask_ReturnsSingleTask()
|
||||
{
|
||||
// Arrange
|
||||
var tasks = new ITask[] { new TaskA() };
|
||||
|
||||
// Act
|
||||
var result = TopologicalTaskSorter.Sort(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.IsType<TaskA>(result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithNoDependencies_ReturnsAllTasks()
|
||||
{
|
||||
// Arrange
|
||||
var taskA = new TaskA();
|
||||
var taskB = new TaskB();
|
||||
var taskC = new TaskC();
|
||||
var tasks = new ITask[] { taskA, taskB, taskC };
|
||||
|
||||
// Act
|
||||
var result = TopologicalTaskSorter.Sort(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Contains(taskA, result);
|
||||
Assert.Contains(taskB, result);
|
||||
Assert.Contains(taskC, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithSingleDependency_OrdersCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var taskA = new TaskA();
|
||||
var taskWithDependency = new TaskWithDependency();
|
||||
var tasks = new ITask[] { taskWithDependency, taskA };
|
||||
|
||||
// Act
|
||||
var result = TopologicalTaskSorter.Sort(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.IsType<TaskA>(result[0]);
|
||||
Assert.IsType<TaskWithDependency>(result[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithMultipleDependencies_OrdersCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var taskA = new TaskA();
|
||||
var taskB = new TaskB();
|
||||
var taskWithMultipleDeps = new TaskWithMultipleDependencies();
|
||||
var tasks = new ITask[] { taskWithMultipleDeps, taskB, taskA };
|
||||
|
||||
// Act
|
||||
var result = TopologicalTaskSorter.Sort(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
var resultList = result.ToList();
|
||||
var dependentIndex = resultList.IndexOf(taskWithMultipleDeps);
|
||||
var taskAIndex = resultList.IndexOf(taskA);
|
||||
var taskBIndex = resultList.IndexOf(taskB);
|
||||
|
||||
// Both dependencies should come before the dependent task
|
||||
Assert.True(taskAIndex < dependentIndex);
|
||||
Assert.True(taskBIndex < dependentIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithChainedDependencies_OrdersCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var taskChainA = new TaskChainA();
|
||||
var taskChainB = new TaskChainB();
|
||||
var taskChainC = new TaskChainC();
|
||||
var tasks = new ITask[] { taskChainC, taskChainA, taskChainB };
|
||||
|
||||
// Act
|
||||
var result = TopologicalTaskSorter.Sort(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.IsType<TaskChainA>(result[0]);
|
||||
Assert.IsType<TaskChainB>(result[1]);
|
||||
Assert.IsType<TaskChainC>(result[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithCircularDependency_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var task1 = new TaskCircular1();
|
||||
var task2 = new TaskCircular2();
|
||||
var tasks = new ITask[] { task1, task2 };
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => TopologicalTaskSorter.Sort(tasks));
|
||||
Assert.Contains("Circular dependency detected", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithSelfCircularDependency_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TaskSelfCircular();
|
||||
var tasks = new ITask[] { task };
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => TopologicalTaskSorter.Sort(tasks));
|
||||
Assert.Contains("Circular dependency detected", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithMultipleInstancesOfSameType_PreservesAllInstances()
|
||||
{
|
||||
// Arrange
|
||||
var taskA1 = new TaskA();
|
||||
var taskA2 = new TaskA();
|
||||
var taskA3 = new TaskA();
|
||||
var tasks = new ITask[] { taskA1, taskA2, taskA3 };
|
||||
|
||||
// Act
|
||||
var result = TopologicalTaskSorter.Sort(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Contains(taskA1, result);
|
||||
Assert.Contains(taskA2, result);
|
||||
Assert.Contains(taskA3, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithMultipleInstancesOfSameTypeWithDependencies_OrdersCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var taskA1 = new TaskA();
|
||||
var taskA2 = new TaskA();
|
||||
var taskWithDep1 = new TaskWithDependency();
|
||||
var taskWithDep2 = new TaskWithDependency();
|
||||
var tasks = new ITask[] { taskWithDep1, taskA1, taskWithDep2, taskA2 };
|
||||
|
||||
// Act
|
||||
var result = TopologicalTaskSorter.Sort(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, result.Count);
|
||||
|
||||
// All TaskA instances should come before TaskWithDependency instances
|
||||
var resultList = result.ToList();
|
||||
var firstTaskAIndex = resultList.IndexOf(taskA1);
|
||||
var secondTaskAIndex = resultList.IndexOf(taskA2);
|
||||
var firstTaskWithDepIndex = resultList.IndexOf(taskWithDep1);
|
||||
var secondTaskWithDepIndex = resultList.IndexOf(taskWithDep2);
|
||||
|
||||
Assert.True(firstTaskAIndex < firstTaskWithDepIndex);
|
||||
Assert.True(firstTaskAIndex < secondTaskWithDepIndex);
|
||||
Assert.True(secondTaskAIndex < firstTaskWithDepIndex);
|
||||
Assert.True(secondTaskAIndex < secondTaskWithDepIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_WithMixedDependenciesAndIndependentTasks_OrdersCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var taskA = new TaskA();
|
||||
var taskB = new TaskB();
|
||||
var taskC = new TaskC();
|
||||
var taskWithDep = new TaskWithDependency();
|
||||
var tasks = new ITask[] { taskC, taskWithDep, taskB, taskA };
|
||||
|
||||
// Act
|
||||
var result = TopologicalTaskSorter.Sort(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, result.Count);
|
||||
|
||||
// TaskA must come before TaskWithDependency
|
||||
var resultList = result.ToList();
|
||||
var taskAIndex = resultList.IndexOf(taskA);
|
||||
var taskWithDepIndex = resultList.IndexOf(taskWithDep);
|
||||
Assert.True(taskAIndex < taskWithDepIndex);
|
||||
|
||||
// TaskB and TaskC can be anywhere (no dependencies)
|
||||
Assert.Contains(taskB, result);
|
||||
Assert.Contains(taskC, result);
|
||||
}
|
||||
|
||||
// Test task classes without dependencies
|
||||
private class TaskA : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private class TaskB : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private class TaskC : ITask
|
||||
{
|
||||
public Task ExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue