From 32bcf9a7511f9fe48c3c475853bf93feb58da0fc Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Tue, 9 Mar 2021 19:18:31 +0000 Subject: [PATCH] WIP #728 - Add test cases for persistence This also includes a change to the workflow used in the test-case. There are quite complex reasons for this, as explained here: https://github.com/elsa-workflows/elsa-core/issues/728#issuecomment-794319236 The real crux of it is that the workflow must have an activity which suspends it, and that activity that suspends the workflow must not be the starting activity. This is why I added an unused set-variable activity as the starter. This commit shows that the various persistence test cases _mostly_ work OK, with the one exception of ActivityExecuted. --- .../Elsa.Core/Handlers/PersistWorkflow.cs | 15 +++++ src/core/Elsa.Core/Services/WorkflowRunner.cs | 11 ++++ ...thElsaSampleWorkflowAndMongoDbAttribute.cs | 2 +- ...tBuilderWithElsaSampleWorkflowAttribute.cs | 6 +- .../InMemory/InMemoryStoreIntegrationTests.cs | 48 +++++++++++++-- .../MongoDb/MongoDbIntegrationTests.cs | 6 +- .../Workflows/PersistableWorkflow.cs | 59 +++++++++++++++++++ .../Workflows/SampleWorkflow.cs | 20 ------- 8 files changed, 135 insertions(+), 32 deletions(-) create mode 100644 test/integration/Elsa.Core.IntegrationTests/Workflows/PersistableWorkflow.cs delete mode 100644 test/integration/Elsa.Core.IntegrationTests/Workflows/SampleWorkflow.cs diff --git a/src/core/Elsa.Core/Handlers/PersistWorkflow.cs b/src/core/Elsa.Core/Handlers/PersistWorkflow.cs index 9cfd3ff2a..b2470959f 100644 --- a/src/core/Elsa.Core/Handlers/PersistWorkflow.cs +++ b/src/core/Elsa.Core/Handlers/PersistWorkflow.cs @@ -28,12 +28,16 @@ namespace Elsa.Handlers public async Task Handle(WorkflowSuspended notification, CancellationToken cancellationToken) { + _logger.LogTrace("Received notification {notification}", notification); + if (notification.WorkflowExecutionContext.WorkflowBlueprint.PersistenceBehavior == WorkflowPersistenceBehavior.Suspended) await SaveWorkflowAsync(notification.WorkflowExecutionContext, cancellationToken); } public async Task Handle(WorkflowExecutionBurstStarting notification, CancellationToken cancellationToken) { + _logger.LogTrace("Received notification {notification}", notification); + var behavior = notification.WorkflowExecutionContext.WorkflowBlueprint.PersistenceBehavior; if (behavior == WorkflowPersistenceBehavior.WorkflowBurst || behavior == WorkflowPersistenceBehavior.WorkflowPassCompleted) @@ -42,6 +46,8 @@ namespace Elsa.Handlers public async Task Handle(WorkflowExecutionBurstCompleted notification, CancellationToken cancellationToken) { + _logger.LogTrace("Received notification {notification}", notification); + var behavior = notification.WorkflowExecutionContext.WorkflowBlueprint.PersistenceBehavior; if (behavior == WorkflowPersistenceBehavior.WorkflowBurst) @@ -50,6 +56,8 @@ namespace Elsa.Handlers public async Task Handle(WorkflowExecutionPassCompleted notification, CancellationToken cancellationToken) { + _logger.LogTrace("Received notification {notification}", notification); + var behavior = notification.WorkflowExecutionContext.WorkflowBlueprint.PersistenceBehavior; if (behavior == WorkflowPersistenceBehavior.WorkflowPassCompleted || notification.ActivityExecutionContext.ActivityBlueprint.PersistWorkflow) @@ -58,6 +66,8 @@ namespace Elsa.Handlers public async Task Handle(WorkflowExecuted notification, CancellationToken cancellationToken) { + _logger.LogTrace("Received notification {notification}", notification); + var behavior = notification.WorkflowExecutionContext.WorkflowBlueprint.PersistenceBehavior; if (behavior == WorkflowPersistenceBehavior.WorkflowPassCompleted || behavior == WorkflowPersistenceBehavior.WorkflowBurst) @@ -66,6 +76,8 @@ namespace Elsa.Handlers public async Task Handle(WorkflowExecutionFinished notification, CancellationToken cancellationToken) { + _logger.LogTrace("Received notification {notification}", notification); + var workflowExecutionContext = notification.WorkflowExecutionContext; if (workflowExecutionContext.DeleteCompletedInstances) @@ -84,9 +96,12 @@ namespace Elsa.Handlers private async ValueTask SaveWorkflowAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) { + _logger.LogTrace("Persisting workflow instance {instanceId}", workflowExecutionContext.WorkflowInstance.Id); + workflowExecutionContext.PruneActivityData(); var workflowInstance = workflowExecutionContext.WorkflowInstance; await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); + _logger.LogDebug("Committed workflow {WorkflowInstanceId} to storage", workflowInstance.Id); } } diff --git a/src/core/Elsa.Core/Services/WorkflowRunner.cs b/src/core/Elsa.Core/Services/WorkflowRunner.cs index f2016859d..2499f3d38 100644 --- a/src/core/Elsa.Core/Services/WorkflowRunner.cs +++ b/src/core/Elsa.Core/Services/WorkflowRunner.cs @@ -171,6 +171,8 @@ namespace Elsa.Services using var workflowExecutionScope = _serviceScopeFactory.CreateScope(); var workflowExecutionContext = new WorkflowExecutionContext(workflowExecutionScope.ServiceProvider, workflowBlueprint, workflowInstance, input); + _logger.LogDebug("{methodName} is running workflow instance Id {instanceId}", nameof(RunWorkflowAsync), workflowInstance.Id); + if (!string.IsNullOrWhiteSpace(workflowInstance.ContextId)) { var loadContext = new LoadWorkflowContext(workflowExecutionContext); @@ -204,7 +206,10 @@ namespace Elsa.Services { case WorkflowStatus.Idle: if (!await BeginWorkflow(workflowExecutionContext, activity, input, cancellationToken)) + { + _logger.LogDebug("Workflow {WorkflowInstanceId} cannot begin from an idle state (perhaps it needs a specific input)", workflowInstance.Id); return workflowInstance; + } break; case WorkflowStatus.Running: @@ -213,7 +218,10 @@ namespace Elsa.Services case WorkflowStatus.Suspended: if (!await ResumeWorkflowAsync(workflowExecutionContext, activity!, input, cancellationToken)) + { + _logger.LogDebug("Workflow {WorkflowInstanceId} cannot be resumed from a suspended state (perhaps it needs a specific input)", workflowInstance.Id); return workflowInstance; + } break; default: throw new ArgumentOutOfRangeException(); @@ -231,7 +239,10 @@ namespace Elsa.Services }; if (statusEvent != null) + { + _logger.LogTrace("Publishing a status event of type {eventType} for workflow {WorkflowInstanceId}", statusEvent.GetType().Name, workflowInstance.Id); await _mediator.Publish(statusEvent, cancellationToken); + } await _mediator.Publish(new WorkflowExecutionFinished(workflowExecutionContext), cancellationToken); return workflowExecutionContext.WorkflowInstance; diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs index f395dfc3e..ca0734145 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs @@ -22,7 +22,7 @@ namespace Elsa.Core.IntegrationTests.Autofixture opts.Db = "IntegrationTests"; }); elsa.AddActivity(); - elsa.AddWorkflow(); + elsa.AddWorkflow(); }) ; }, parameter); diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs index 1217dbd94..d3048e265 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs @@ -5,6 +5,7 @@ using Elsa.Activities.UserTask.Activities; using Elsa.Core.IntegrationTests.Workflows; using Elsa.Testing.Shared.AutoFixture.Customizations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Elsa.Core.IntegrationTests.Autofixture { @@ -16,9 +17,8 @@ namespace Elsa.Core.IntegrationTests.Autofixture services .AddElsa(elsa => { elsa.AddActivity(); - elsa.AddWorkflow(); - }) - ; + elsa.AddWorkflow(); + }); }, parameter); } } diff --git a/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs b/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs index ca07003f7..20308ff38 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs @@ -12,21 +12,59 @@ namespace Elsa.Core.IntegrationTests.Persistence.InMemory { public class InMemoryStoreIntegrationTests { - [Theory(DisplayName = "A saved workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] - public async Task ASavedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + [Theory(DisplayName = "A persistable workflow instance with default persistence behaviour should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableWorkflowInstanceWithDefaultPersistanceBehaviourShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) { - hostBuilder.ConfigureServices((ctx, services) => services.AddHostedService()); + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); var host = await hostBuilder.StartAsync(); } - class HostedWorkflowRunner : IHostedService + [Theory(DisplayName = "A persistable-on-suspend workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableOnSuspendWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); + var host = await hostBuilder.StartAsync(); + } + + [Theory(DisplayName = "A persistable-on-activity-executed workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableOnActivityExecutedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); + var host = await hostBuilder.StartAsync(); + } + + [Theory(DisplayName = "A persistable-on-workflow-burst workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableOnWorkflowBurstWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); + var host = await hostBuilder.StartAsync(); + } + + [Theory(DisplayName = "A persistable-on-workflow-pass-completed workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableOnWorkflowPassCompletedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); + var host = await hostBuilder.StartAsync(); + } + + class HostedWorkflowRunner : IHostedService where TWorkflow : PersistableWorkflow { readonly IWorkflowRunner workflowRunner; readonly IWorkflowInstanceStore instanceStore; public async Task StartAsync(CancellationToken cancellationToken) { - var instance = await workflowRunner.RunWorkflowAsync(); + var instance = await workflowRunner.RunWorkflowAsync(); var retrievedInstance = await instanceStore.FindByIdAsync(instance.Id); // An instance should totally be retrieved from the store diff --git a/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs b/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs index 3aa4af2cf..27fc34065 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs @@ -13,8 +13,8 @@ namespace Elsa.Core.IntegrationTests.Persistence.MongoDb { public class MongoDbIntegrationTests { - [Theory(DisplayName = "A saved workflow instance should be persisted-to and readable-from a MongoDb store after being run"), AutoMoqData] - public async Task ASavedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute] IHostBuilder hostBuilder) + [Theory(DisplayName = "A persistable workflow instance with default persistence behaviour should be persisted-to and readable-from a MongoDb store after being run"), AutoMoqData] + public async Task APersistableWorkflowInstanceWithDefaultPersistanceBehaviourShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute] IHostBuilder hostBuilder) { hostBuilder.ConfigureServices((ctx, services) => services.AddHostedService()); var host = await hostBuilder.StartAsync(); @@ -27,7 +27,7 @@ namespace Elsa.Core.IntegrationTests.Persistence.MongoDb public async Task StartAsync(CancellationToken cancellationToken) { - var instance = await workflowRunner.RunWorkflowAsync(); + var instance = await workflowRunner.RunWorkflowAsync(); var retrievedInstance = await instanceStore.FindByIdAsync(instance.Id); Assert.NotNull(retrievedInstance); diff --git a/test/integration/Elsa.Core.IntegrationTests/Workflows/PersistableWorkflow.cs b/test/integration/Elsa.Core.IntegrationTests/Workflows/PersistableWorkflow.cs new file mode 100644 index 000000000..af7a1b36b --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Workflows/PersistableWorkflow.cs @@ -0,0 +1,59 @@ +using Elsa.Activities.UserTask.Activities; +using Elsa.Builders; +using Elsa.Activities.ControlFlow; +using Elsa.Activities.Primitives; +using Elsa.Models; + +namespace Elsa.Core.IntegrationTests.Workflows +{ + public class PersistableWorkflow : IWorkflow + { + public static readonly object Result = new object(); + + public virtual void Build(IWorkflowBuilder builder) + { + builder + .StartWith(a => a.Set(x => x.VariableName, "Unused").Set(x => x.Value, "Unused")) + .Then(setup: s => { + s.Set(x => x.Actions, c => new [] { "Foo", "Bar" }); + }) + .Finish(x => x.WithOutput(Result)); + } + + public class OnSuspend : PersistableWorkflow + { + public override void Build(IWorkflowBuilder builder) + { + builder.WithPersistenceBehavior(WorkflowPersistenceBehavior.Suspended); + base.Build(builder); + } + } + + public class OnActivityExecuted : PersistableWorkflow + { + public override void Build(IWorkflowBuilder builder) + { + builder.WithPersistenceBehavior(WorkflowPersistenceBehavior.ActivityExecuted); + base.Build(builder); + } + } + + public class OnWorkflowBurst : PersistableWorkflow + { + public override void Build(IWorkflowBuilder builder) + { + builder.WithPersistenceBehavior(WorkflowPersistenceBehavior.WorkflowBurst); + base.Build(builder); + } + } + + public class OnWorkflowPassCompleted : PersistableWorkflow + { + public override void Build(IWorkflowBuilder builder) + { + builder.WithPersistenceBehavior(WorkflowPersistenceBehavior.WorkflowPassCompleted); + base.Build(builder); + } + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Workflows/SampleWorkflow.cs b/test/integration/Elsa.Core.IntegrationTests/Workflows/SampleWorkflow.cs deleted file mode 100644 index 0ee6bd155..000000000 --- a/test/integration/Elsa.Core.IntegrationTests/Workflows/SampleWorkflow.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Elsa.Activities.UserTask.Activities; -using Elsa.Builders; -using Elsa.Activities.ControlFlow; - -namespace Elsa.Core.IntegrationTests.Workflows -{ - public class SampleWorkflow : IWorkflow - { - public static readonly object Result = new object(); - - public void Build(IWorkflowBuilder builder) - { - builder - .StartWith(setup: s => { - s.Set(x => x.Actions, c => new [] { "Foo", "Bar" }); - }) - .Finish(x => x.WithOutput(Result)); - } - } -} \ No newline at end of file