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.
This commit is contained in:
Craig Fowler 2021-03-09 19:18:31 +00:00
parent 2c79a9395c
commit 32bcf9a751
8 changed files with 135 additions and 32 deletions

View file

@ -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);
}
}

View file

@ -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;

View file

@ -22,7 +22,7 @@ namespace Elsa.Core.IntegrationTests.Autofixture
opts.Db = "IntegrationTests";
});
elsa.AddActivity<UserTask>();
elsa.AddWorkflow<SampleWorkflow>();
elsa.AddWorkflow<PersistableWorkflow>();
})
;
}, parameter);

View file

@ -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<UserTask>();
elsa.AddWorkflow<SampleWorkflow>();
})
;
elsa.AddWorkflow<PersistableWorkflow>();
});
}, parameter);
}
}

View file

@ -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<HostedWorkflowRunner>());
hostBuilder.ConfigureServices((ctx, services) => {
services.AddHostedService<HostedWorkflowRunner<PersistableWorkflow>>();
});
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<HostedWorkflowRunner<PersistableWorkflow.OnSuspend>>();
});
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<HostedWorkflowRunner<PersistableWorkflow.OnActivityExecuted>>();
});
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<HostedWorkflowRunner<PersistableWorkflow.OnWorkflowBurst>>();
});
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<HostedWorkflowRunner<PersistableWorkflow.OnWorkflowPassCompleted>>();
});
var host = await hostBuilder.StartAsync();
}
class HostedWorkflowRunner<TWorkflow> : IHostedService where TWorkflow : PersistableWorkflow
{
readonly IWorkflowRunner workflowRunner;
readonly IWorkflowInstanceStore instanceStore;
public async Task StartAsync(CancellationToken cancellationToken)
{
var instance = await workflowRunner.RunWorkflowAsync<SampleWorkflow>();
var instance = await workflowRunner.RunWorkflowAsync<TWorkflow>();
var retrievedInstance = await instanceStore.FindByIdAsync(instance.Id);
// An instance should totally be retrieved from the store

View file

@ -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<HostedWorkflowRunner>());
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<SampleWorkflow>();
var instance = await workflowRunner.RunWorkflowAsync<PersistableWorkflow>();
var retrievedInstance = await instanceStore.FindByIdAsync(instance.Id);
Assert.NotNull(retrievedInstance);

View file

@ -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<SetVariable>(a => a.Set(x => x.VariableName, "Unused").Set(x => x.Value, "Unused"))
.Then<UserTask>(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);
}
}
}
}

View file

@ -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<UserTask>(setup: s => {
s.Set(x => x.Actions, c => new [] { "Foo", "Bar" });
})
.Finish(x => x.WithOutput(Result));
}
}
}