Add support for implicit looping constructs

Fixes the issue where a workflow would not complete, even though all flow chart activities executed
This commit is contained in:
Sipke Schoorstra 2023-04-06 22:37:53 +02:00
parent 53cb1cc63c
commit 5331476081
10 changed files with 213 additions and 5 deletions

View file

@ -5,6 +5,7 @@ using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Activities.Flowchart.Models;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Core.Activities.Flowchart.Activities;
@ -13,12 +14,25 @@ namespace Elsa.Workflows.Core.Activities.Flowchart.Activities;
/// </summary>
[FlowNode("True", "False")]
[Activity("Elsa", "Flow", "Evaluate a Boolean condition to determine which path to execute next.")]
[PublicAPI]
public class FlowDecision : Activity
{
/// <inheritdoc />
public FlowDecision([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
public FlowDecision(Func<ExpressionExecutionContext, bool> condition, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line)
{
Condition = new(condition);
}
/// <inheritdoc />
public FlowDecision(Func<ExpressionExecutionContext, ValueTask<bool>> condition, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line)
{
Condition = new(condition);
}
/// <summary>
/// The condition to evaluate.

View file

@ -38,7 +38,7 @@ public class Flowchart : Container
{
var triggerActivityId = context.WorkflowExecutionContext.TriggerActivityId;
var triggerActivity = triggerActivityId != null ? Activities.FirstOrDefault(x => x.Id == triggerActivityId) : default;
var startActivity = triggerActivity ?? Start;
var startActivity = triggerActivity ?? Start ?? Activities.FirstOrDefault();
if (startActivity == null!)
{
@ -101,7 +101,7 @@ public class Flowchart : Container
var executionCount = scope.GetExecutionCount(activity);
var haveInboundActivitiesExecuted = inboundActivities.All(x => scope.GetExecutionCount(x) > executionCount);
if (haveInboundActivitiesExecuted)
if (haveInboundActivitiesExecuted)
await flowchartActivityExecutionContext.ScheduleActivityAsync(activity);
}
else
@ -114,7 +114,7 @@ public class Flowchart : Container
if (!children.Any())
{
// If there are no more pending activities, mark this activity as completed.
var hasPendingChildren = scope.HasPendingActivities();
var hasPendingChildren = !HaveAllLeafsExecuted(scope);
if (!hasPendingChildren)
await flowchartActivityExecutionContext.CompleteActivityAsync();
@ -123,4 +123,11 @@ public class Flowchart : Container
flowchartActivityExecutionContext.SetProperty(ScopeProperty, scope);
context.StopPropagation();
}
private bool HaveAllLeafsExecuted(FlowScope scope)
{
var leafs = Activities.Where(x => Connections.All(y => y.Source != x)).ToList();
var leafsExecuted = leafs.All(x => scope.GetExecutionCount(x) > 0);
return leafsExecuted;
}
}

View file

@ -72,4 +72,6 @@ internal class FlowScope
}
public long GetExecutionCount(IActivity activity) => Activities.ContainsKey(activity.Id) ? Activities[activity.Id].ExecutionCount : 0;
public void Clear() => Activities.Clear();
}

View file

@ -105,4 +105,10 @@ public static class WorkflowExecutionContextExtensions
workflowExecutionContext.Scheduler.Schedule(workItem);
workflowExecutionContext.AddCompletionCallback(owner, activityNode, completionCallback);
}
/// <summary>
/// Returns true if all activities have completed.
/// </summary>
/// <returns>True if all activities have completed, otherwise false.</returns>
public static bool AllActivitiesCompleted(this WorkflowExecutionContext workflowExecutionContext) => !workflowExecutionContext.ActivityExecutionContexts.Any();
}

View file

@ -1,3 +1,4 @@
using Elsa.Extensions;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Pipelines.WorkflowExecution;
@ -46,8 +47,8 @@ public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware
// Invoke next middleware.
await Next(context);
// If there are no bookmarks and all activities are completed, complete the workflow.
// If all activities are completed, complete the workflow.
if (context.Status == WorkflowStatus.Running)
context.TransitionTo(!context.Bookmarks.Any() ? WorkflowSubStatus.Finished : WorkflowSubStatus.Suspended);
context.TransitionTo(context.AllActivitiesCompleted() ? WorkflowSubStatus.Finished : WorkflowSubStatus.Suspended);
}
}

View file

@ -246,6 +246,12 @@ public class ActivityExecutionContext : IExecutionContext
Properties[key] = value;
return value;
}
/// <summary>
/// Removes a property associated with the current activity context.
/// </summary>
/// <param name="key">The property key.</param>
public void RemoveProperty(string key) => Properties.Remove(key);
public T GetRequiredService<T>() where T : notnull => WorkflowExecutionContext.GetRequiredService<T>();
public object GetRequiredService(Type serviceType) => WorkflowExecutionContext.GetRequiredService(serviceType);

View file

@ -0,0 +1,38 @@
using System.Linq;
using System.Threading.Tasks;
using Elsa.IntegrationTests.Scenarios.ImplicitJoins.Workflows;
using Elsa.Testing.Shared;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.IntegrationTests.Scenarios.ImplicitJoins;
public class BraidedWorkflowTests
{
private readonly IWorkflowRunner _workflowRunner;
private readonly CapturingTextWriter _capturingTextWriter = new();
public BraidedWorkflowTests(ITestOutputHelper testOutputHelper)
{
var services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
_workflowRunner = services.GetRequiredService<IWorkflowRunner>();
}
[Fact(DisplayName = "Braided workflows are executed correctly")]
public async Task Test1()
{
await _workflowRunner.RunAsync<BraidedWorkflow>();
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "WriteLine1", "WriteLine2", "WriteLine3", "WriteLine4", "WriteLine5", "WriteLine6", "WriteLine7" }, lines);
}
[Fact(DisplayName = "Braided workflows complete the workflow")]
public async Task Test2()
{
var result = await _workflowRunner.RunAsync<BraidedWorkflow>();
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
}
}

View file

@ -0,0 +1,38 @@
using System.Linq;
using System.Threading.Tasks;
using Elsa.IntegrationTests.Scenarios.ImplicitJoins.Workflows;
using Elsa.Testing.Shared;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.IntegrationTests.Scenarios.ImplicitJoins;
public class ImplicitWorkflowTests
{
private readonly IWorkflowRunner _workflowRunner;
private readonly CapturingTextWriter _capturingTextWriter = new();
public ImplicitWorkflowTests(ITestOutputHelper testOutputHelper)
{
var services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
_workflowRunner = services.GetRequiredService<IWorkflowRunner>();
}
[Fact(DisplayName = "Implicit loop workflows are executed correctly")]
public async Task Test1()
{
await _workflowRunner.RunAsync<ImplicitLoopWorkflow>();
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Start", "Retry", "End" }, lines);
}
[Fact(DisplayName = "Implicit loop workflows complete the workflow")]
public async Task Test2()
{
var result = await _workflowRunner.RunAsync<ImplicitLoopWorkflow>();
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
}
}

View file

@ -0,0 +1,53 @@
using Elsa.Workflows.Core.Abstractions;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Activities.Flowchart.Activities;
using Elsa.Workflows.Core.Activities.Flowchart.Models;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.IntegrationTests.Scenarios.ImplicitJoins.Workflows;
public class BraidedWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var writeLine1 = new WriteLine("WriteLine1");
var writeLine2 = new WriteLine("WriteLine2");
var writeLine3 = new WriteLine("WriteLine3");
var writeLine4 = new WriteLine("WriteLine4");
var writeLine5 = new WriteLine("WriteLine5");
var writeLine6 = new WriteLine("WriteLine6");
var writeLine7 = new WriteLine("WriteLine7");
workflow.Root = new Flowchart
{
Start = writeLine1,
Activities =
{
writeLine1,
writeLine2,
writeLine3,
writeLine4,
writeLine5,
writeLine6,
writeLine7,
},
Connections =
{
new Connection(writeLine1, writeLine2),
new Connection(writeLine1, writeLine3),
new Connection(writeLine1, writeLine4),
new Connection(writeLine2, writeLine5),
new Connection(writeLine3, writeLine5),
new Connection(writeLine3, writeLine6),
new Connection(writeLine4, writeLine6),
new Connection(writeLine5, writeLine7),
new Connection(writeLine6, writeLine7),
}
};
}
}

View file

@ -0,0 +1,43 @@
using Elsa.Workflows.Core.Abstractions;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Activities.Flowchart.Activities;
using Elsa.Workflows.Core.Activities.Flowchart.Models;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.IntegrationTests.Scenarios.ImplicitJoins.Workflows;
public class ImplicitLoopWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
var counterVariable = builder.WithVariable(0);
var start = new WriteLine("Start");
var incrementCounter = new SetVariable<int>(counterVariable, context => counterVariable.Get(context) + 1);
var counterGreaterThanOne = new FlowDecision(context => counterVariable.Get(context) > 1);
var retry = new WriteLine("Retry");
var end = new WriteLine("End");
builder.Root = new Flowchart
{
Start = start,
Activities =
{
start,
incrementCounter,
counterGreaterThanOne,
retry,
end
},
Connections =
{
new Connection(start, incrementCounter),
new Connection(incrementCounter, counterGreaterThanOne),
new Connection(counterGreaterThanOne, retry, SourcePort: "False"),
new Connection(counterGreaterThanOne, end, SourcePort: "True"),
new Connection(retry, incrementCounter),
}
};
}
}