From 5dcf6f382a06ab724d652ad71f1024dd298b770c Mon Sep 17 00:00:00 2001 From: Raymond den Haan Date: Fri, 17 May 2024 10:34:43 +0200 Subject: [PATCH] Fix workflow deletion and updates handling --- .../Elsa.ServerAndStudio.Web/Program.cs | 4 +- .../WorkflowDefinitionEventsConsumer.cs | 4 +- .../Handlers/RefreshActivityRegistry.cs | 6 +- .../Services/ActivityRegistryPopulator.cs | 2 +- .../WorkflowDefinitionEventHandlers.cs | 15 ++++ .../Contracts/IWorkflowDefinitionEvents.cs | 8 ++ .../WorkflowDefinitionDeletedEventArgs.cs | 6 ++ .../Helpers/Fixtures/WorkflowServer.cs | 6 +- .../Services/WorkflowDefinitionEvents.cs | 7 ++ .../WorkflowActivities/DeleteWorkflowTests.cs | 86 +++++++++++++++++++ .../WorkflowActivities/SaveWorkflowTests.cs | 61 +++++++++++++ .../Workflows/DeleteWorkflow.cs | 25 ++++++ 12 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 test/component/Elsa.Workflows.ComponentTests/Helpers/Consumers/WorkflowDefinitionEventHandlers.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Helpers/Contracts/IWorkflowDefinitionEvents.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Helpers/EventArgs/WorkflowDefinitionDeletedEventArgs.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Helpers/Services/WorkflowDefinitionEvents.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/SaveWorkflowTests.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow.cs diff --git a/src/bundles/Elsa.ServerAndStudio.Web/Program.cs b/src/bundles/Elsa.ServerAndStudio.Web/Program.cs index c3e092e56..8e3252f92 100644 --- a/src/bundles/Elsa.ServerAndStudio.Web/Program.cs +++ b/src/bundles/Elsa.ServerAndStudio.Web/Program.cs @@ -1,4 +1,3 @@ -using Elsa.Common.DistributedLocks.Noop; using Elsa.EntityFrameworkCore.Extensions; using Elsa.EntityFrameworkCore.Modules.Management; using Elsa.EntityFrameworkCore.Modules.Runtime; @@ -7,6 +6,7 @@ using Elsa.Extensions; using Elsa.ServerAndStudio.Web.Extensions; using Elsa.MassTransit.Extensions; using Elsa.ServerAndStudio.Web.Enums; +using Medallion.Threading.FileSystem; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Proto.Persistence.Sqlite; @@ -73,7 +73,7 @@ services }); } - runtime.DistributedLockProvider = _ => new NoopDistributedSynchronizationProvider(); + runtime.DistributedLockProvider = _ => new FileDistributedSynchronizationProvider(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "locks"))); runtime.WorkflowInboxCleanupOptions = options => configuration.GetSection("Runtime:WorkflowInboxCleanup").Bind(options); runtime.WorkflowDispatcherOptions = options => configuration.GetSection("Runtime:WorkflowDispatcher").Bind(options); }) diff --git a/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs b/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs index 0d1b52a75..3b5faab42 100644 --- a/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs +++ b/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs @@ -41,7 +41,7 @@ public class WorkflowDefinitionEventsConsumer(IActivityRegistryPopulator activit /// public Task Consume(ConsumeContext context) { - activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), context.Message.Id); + activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), context.Message.Id); return Task.CompletedTask; } @@ -79,7 +79,7 @@ public class WorkflowDefinitionEventsConsumer(IActivityRegistryPopulator activit if (usableAsActivity) return activityRegistryPopulator.AddToRegistry(typeof(WorkflowDefinitionActivityProvider), id); - activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), id); + activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), id); return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs b/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs index 471377c34..434fc2672 100644 --- a/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs +++ b/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs @@ -30,7 +30,7 @@ public class RefreshActivityRegistry(IActivityRegistryPopulator activityRegistry /// public Task HandleAsync(WorkflowDefinitionRetracted notification, CancellationToken cancellationToken) { - activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), notification.WorkflowDefinition.Id, cancellationToken); + activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), notification.WorkflowDefinition.Id, cancellationToken); return Task.CompletedTask; } @@ -61,7 +61,7 @@ public class RefreshActivityRegistry(IActivityRegistryPopulator activityRegistry /// public Task HandleAsync(WorkflowDefinitionVersionDeleted notification, CancellationToken cancellationToken) { - activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), notification.WorkflowDefinition.Id, cancellationToken); + activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), notification.WorkflowDefinition.Id, cancellationToken); return Task.CompletedTask; } @@ -81,7 +81,7 @@ public class RefreshActivityRegistry(IActivityRegistryPopulator activityRegistry if (usableAsActivity.GetValueOrDefault()) return activityRegistryPopulator.AddToRegistry(typeof(WorkflowDefinitionActivityProvider), id); - activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), id); + activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), id); return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs b/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs index a7836f5ac..c69c3bf99 100644 --- a/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs +++ b/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs @@ -37,7 +37,7 @@ public class ActivityRegistryPopulator(IEnumerable providers, var descriptorsToRemove = providerDescriptors .Where(d => d.CustomProperties.TryGetValue("WorkflowDefinitionId", out var val) && - val.ToString() == workflowDefinitionId); + val.ToString() == workflowDefinitionId).ToList(); foreach (ActivityDescriptor activityDescriptor in descriptorsToRemove) { diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Consumers/WorkflowDefinitionEventHandlers.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Consumers/WorkflowDefinitionEventHandlers.cs new file mode 100644 index 000000000..790606986 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Consumers/WorkflowDefinitionEventHandlers.cs @@ -0,0 +1,15 @@ +using Elsa.MassTransit.Messages; +using Hangfire.Annotations; +using MassTransit; + +namespace Elsa.Workflows.ComponentTests.Consumers; + +[UsedImplicitly] +public class WorkflowDefinitionEventHandlers(IWorkflowDefinitionEvents workflowDefinitionEvents) : IConsumer +{ + public Task Consume(ConsumeContext context) + { + workflowDefinitionEvents.OnWorkflowDefinitionDeleted(new WorkflowDefinitionDeletedEventArgs(context.Message.Id)); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Contracts/IWorkflowDefinitionEvents.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Contracts/IWorkflowDefinitionEvents.cs new file mode 100644 index 000000000..dc98ee3e4 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Contracts/IWorkflowDefinitionEvents.cs @@ -0,0 +1,8 @@ +namespace Elsa.Workflows.ComponentTests; + +public interface IWorkflowDefinitionEvents +{ + event EventHandler WorkflowDefinitionDeleted; + + void OnWorkflowDefinitionDeleted(WorkflowDefinitionDeletedEventArgs args); +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/EventArgs/WorkflowDefinitionDeletedEventArgs.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/EventArgs/WorkflowDefinitionDeletedEventArgs.cs new file mode 100644 index 000000000..93324767c --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/EventArgs/WorkflowDefinitionDeletedEventArgs.cs @@ -0,0 +1,6 @@ +namespace Elsa.Workflows.ComponentTests; + +public class WorkflowDefinitionDeletedEventArgs(string definitionId) : EventArgs +{ + public string DefinitionId { get; } = definitionId; +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs index df2afc214..75e5301b9 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs @@ -1,9 +1,11 @@ using System.Net.Http.Headers; +using System.Reflection; using Elsa.EntityFrameworkCore.Extensions; using Elsa.EntityFrameworkCore.Modules.Management; using Elsa.Extensions; using Elsa.Identity.Providers; using Elsa.MassTransit.Extensions; +using Elsa.Workflows.ComponentTests.Consumers; using Elsa.Workflows.ComponentTests.Services; using FluentStorage; using Hangfire.Annotations; @@ -52,7 +54,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl elsa.UseDefaultAuthentication(defaultAuthentication => defaultAuthentication.UseAdminApiKey()); elsa.UseFluentStorageProvider(sp => { - var assemblyLocation = System.Reflection.Assembly.GetExecutingAssembly().Location; + var assemblyLocation = Assembly.GetExecutingAssembly().Location; var assemblyDirectory = Path.GetDirectoryName(assemblyLocation)!; var workflowsDirectorySegments = new[] { @@ -64,6 +66,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl elsa.UseMassTransit(massTransit => { massTransit.UseRabbitMq(rabbitMqConnectionString); + massTransit.AddConsumer("elsa-test-workflow-definition-updates", true); }); elsa.UseWorkflowManagement(management => { @@ -82,6 +85,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddNotificationHandlersFrom(); }); } diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/WorkflowDefinitionEvents.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/WorkflowDefinitionEvents.cs new file mode 100644 index 000000000..514c1a19c --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/WorkflowDefinitionEvents.cs @@ -0,0 +1,7 @@ +namespace Elsa.Workflows.ComponentTests.Services; + +public class WorkflowDefinitionEvents : IWorkflowDefinitionEvents +{ + public event EventHandler? WorkflowDefinitionDeleted; + public void OnWorkflowDefinitionDeleted(WorkflowDefinitionDeletedEventArgs args) => WorkflowDefinitionDeleted?.Invoke(this, args); +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs new file mode 100644 index 000000000..63df0cb74 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs @@ -0,0 +1,86 @@ +using Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities.Workflows; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity; +using Elsa.Workflows.Management.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities; + +public class DeleteWorkflowTests : AppComponentTest +{ + private readonly ISignalManager _signalManager; + private readonly IWorkflowDefinitionEvents _workflowDefinitionEvents; + private readonly IServiceScope _scope1; + private readonly IServiceScope _scope2; + private readonly IServiceScope _scope3; + private static readonly object WorkflowDeletedSignal = new(); + + public DeleteWorkflowTests(App app) : base(app) + { + _scope1 = app.Cluster.Pod1.Services.CreateScope(); + _scope2 = app.Cluster.Pod2.Services.CreateScope(); + _scope3 = app.Cluster.Pod3.Services.CreateScope(); + _signalManager = Scope.ServiceProvider.GetRequiredService(); + _workflowDefinitionEvents = Scope.ServiceProvider.GetRequiredService(); + _workflowDefinitionEvents.WorkflowDefinitionDeleted += OnWorkflowDefinionDeleted; + } + + [Fact] + public async Task DeleteWorkflow() + { + EnsureWorkflowInRegistry(_scope1); + + var workflowDefinitionManager = _scope1.ServiceProvider.GetRequiredService(); + await workflowDefinitionManager.DeleteByDefinitionIdAsync(Workflows.DeleteWorkflow.DefinitionId); + + WorkflowTypeDeletedFromRegistry(_scope1); + } + + [Fact] + public async Task DeleteWorkflow_Clustered() + { + EnsureWorkflowInRegistry(_scope1); + EnsureWorkflowInRegistry(_scope2); + EnsureWorkflowInRegistry(_scope3); + + var workflowDefinitionManager = _scope3.ServiceProvider.GetRequiredService(); + await workflowDefinitionManager.DeleteByDefinitionIdAsync(Workflows.DeleteWorkflow.DefinitionId); + + WorkflowTypeDeletedFromRegistry(_scope1); + + await _signalManager.WaitAsync(WorkflowDeletedSignal); + WorkflowTypeDeletedFromRegistry(_scope2); + WorkflowTypeDeletedFromRegistry(_scope3); + } + + private void EnsureWorkflowInRegistry(IServiceScope scope) + { + var activityRegistry = scope.ServiceProvider.GetRequiredService(); + var descriptor = activityRegistry.Find(Workflows.DeleteWorkflow.Type); + if (descriptor is null) + activityRegistry.Add(typeof(WorkflowDefinitionActivityProvider), descriptor); + } + + private void WorkflowTypeDeletedFromRegistry(IServiceScope scope) + { + var activityRegistry = scope.ServiceProvider.GetRequiredService(); + var descriptor = activityRegistry.Find(Workflows.DeleteWorkflow.Type); + + Assert.Null(descriptor); + } + + private void OnWorkflowDefinionDeleted(object? sender, WorkflowDefinitionDeletedEventArgs args) + { + if (args.DefinitionId == Workflows.DeleteWorkflow.DefinitionId) + { + _signalManager.Trigger(WorkflowDeletedSignal, args); + } + } + + protected override void OnDispose() + { + _scope1.Dispose(); + _scope2.Dispose(); + _scope3.Dispose(); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/SaveWorkflowTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/SaveWorkflowTests.cs new file mode 100644 index 000000000..c5d869069 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/SaveWorkflowTests.cs @@ -0,0 +1,61 @@ +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity; +using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities; + +public class SaveWorkflowTests(App app) : AppComponentTest(app) +{ + private readonly IServiceScope _scope = app.Cluster.Pod1.Services.CreateScope(); + + [Theory] + [InlineData("Save1", true, true, true, true)] + [InlineData("Save2", true, false, true, false)] + [InlineData("Save3", false, true, false, false)] + [InlineData("Save4", false, false, false, false)] + private async Task SaveWorkflow(string name, bool usableAsActivity, bool publish, bool expectedInRegistry, bool isBrowsable) + { + var activityRegistry = _scope.ServiceProvider.GetRequiredService(); + + var descriptor = activityRegistry.Find(name); + if (descriptor is not null) + activityRegistry.Remove(typeof(WorkflowDefinitionActivityProvider), descriptor); + + var importer = _scope.ServiceProvider.GetRequiredService(); + var request = new SaveWorkflowDefinitionRequest + { + Model = new WorkflowDefinitionModel + { + Name = name, + DefinitionId = name, + Options = new WorkflowOptions + { + UsableAsActivity = usableAsActivity, + AutoUpdateConsumingWorkflows = true + } + }, + Publish = publish + }; + await importer.ImportAsync(request); + + descriptor = activityRegistry.Find(name); + + if (expectedInRegistry) + { + Assert.NotNull(descriptor); + Assert.Equal(isBrowsable, descriptor.IsBrowsable); + } + else + { + Assert.Null(descriptor); + } + } + + protected override void OnDispose() + { + _scope.Dispose(); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow.cs new file mode 100644 index 000000000..1e3ba478e --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow.cs @@ -0,0 +1,25 @@ +using Elsa.Scheduling.Activities; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; + +namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities.Workflows; + +public class DeleteWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + public static readonly string Type = nameof(DeleteWorkflow); + protected override void Build(IWorkflowBuilder builder) + { + builder.Name = Type; + builder.WithDefinitionId(DefinitionId); + builder.WorkflowOptions.UsableAsActivity = true; + builder.Root = new Sequence + { + Activities = + { + new Delay(TimeSpan.FromMilliseconds(250)), + new WriteLine("This workflow will be deleted!") + } + }; + } +} \ No newline at end of file