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