Fix flowchart not completing when using explicit WaitAny joins

This commit is contained in:
Sipke Schoorstra 2023-09-08 21:15:40 +02:00
parent 0d05acb98d
commit 018b3a3672
8 changed files with 314 additions and 11 deletions

View file

@ -7,6 +7,7 @@ using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Requests;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Testing.Shared;
@ -14,6 +15,7 @@ namespace Elsa.Testing.Shared;
/// <summary>
/// Provides extension methods for <see cref="IServiceProvider"/>.
/// </summary>
[PublicAPI]
public static class ServiceProviderExtensions
{
/// <summary>

View file

@ -20,14 +20,14 @@ public class FlowJoin : Activity, IJoinNode
public FlowJoin([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The join mode determines whether this activity should continue as soon as one inbound path comes in (Wait Any), or once all inbound paths have executed (Wait All).
/// </summary>
[Input(
Description = "The join mode determines whether this activity should continue as soon as one inbound path comes in (Wait Any), or once all inbound paths have executed (Wait All).",
DefaultValue = FlowJoinMode.WaitAny
)]
)]
public Input<FlowJoinMode> Mode { get; set; } = new(FlowJoinMode.WaitAny);
/// <inheritdoc />
@ -50,14 +50,12 @@ public class FlowJoin : Activity, IJoinNode
await context.CompleteActivityAsync();
break;
case FlowJoinMode.WaitAny:
// Only complete if we haven't already executed.
// If this activity was already executed, complete it with the AlreadyCompleted result. This will prevent the Flowchart activity from scheduling its siblings again.
var alreadyExecuted = inboundActivities.Max(x => flowScope.GetExecutionCount(x)) == executionCount;
var result = alreadyExecuted ? new AlreadyCompleted() : default;
if (!alreadyExecuted)
{
await context.CompleteActivityAsync();
await ClearBookmarksAsync(flowchart, context);
}
await context.CompleteActivityAsync(result);
await ClearBookmarksAsync(flowchart, context);
break;
}
}
@ -71,7 +69,7 @@ public class FlowJoin : Activity, IJoinNode
var inboundActivityExecutionContexts = workflowExecutionContext.ActiveActivityExecutionContexts.Where(x => inboundActivities.Contains(x.Activity)).ToList();
// Cancel each inbound activity.
foreach (var activityExecutionContext in inboundActivityExecutionContexts)
foreach (var activityExecutionContext in inboundActivityExecutionContexts)
await activityExecutionContext.CancelActivityAsync();
}
}

View file

@ -57,13 +57,15 @@ public class Flowchart : Container
var childContext = context.ChildContext;
var completedActivity = childContext.Activity;
var result = context.Result;
var alreadyCompleted = result is AlreadyCompleted;
// If specific outcomes were provided by the completed activity, use them to find the connection to the next activity.
Func<Connection, bool> outboundConnectionsQuery = result is Outcomes outcomes
? connection => connection.Source.Activity == completedActivity && outcomes.Names.Contains(connection.Source.Port)
: connection => connection.Source.Activity == completedActivity;
var outboundConnections = Connections.Where(outboundConnectionsQuery).ToList();
// Only query the outbound connections if the completed activity wasn't already completed.
var outboundConnections = alreadyCompleted ? new List<Connection>() : Connections.Where(outboundConnectionsQuery).ToList();
var children = outboundConnections.Select(x => x.Target.Activity).ToList();
var scope = targetContext.GetProperty(ScopeProperty, () => new FlowScope());

View file

@ -0,0 +1,6 @@
namespace Elsa.Workflows.Core.Activities.Flowchart.Models;
/// <summary>
/// Represents an activity completion result that indicates that the activity has already been completed. See <see cref="Activities.Flowchart"/> and <see cref="Activities.FlowJoin"/>.
/// </summary>
public record AlreadyCompleted;

View file

@ -1,3 +1,7 @@
namespace Elsa.Workflows.Core.Activities.Flowchart.Models;
/// <summary>
/// Represents a list of outcomes that can be send when completing an activity. This information is used by <see cref="Activities.Flowchart"/>.
/// </summary>
/// <param name="Names">A list of outcome names.</param>
public record Outcomes(params string[] Names);

View file

@ -96,6 +96,11 @@
<None Update="Scenarios\ParentChildInputs\Workflows\parent2.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\ExplicitJoins\Workflows\flow-join-any.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

View file

@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Elsa.Testing.Shared;
using Elsa.Workflows.Core;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.IntegrationTests.Scenarios.ExplicitJoins;
public class ExplicitJoinWaitAnyTests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
public ExplicitJoinWaitAnyTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
}
[Fact(DisplayName = "Workflows with explicit joins using WaitAny wait for any of the specified activities to complete and complete the workflow.")]
public async Task Test1()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Import workflow.
var fileName = "Scenarios/ExplicitJoins/Workflows/flow-join-any.json";
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync(fileName);
// Execute.
var workflowState = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);
// Assert expected output.
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Start", "End" }, lines);
// Assert expected workflow status.
Assert.Equal(WorkflowStatus.Finished, workflowState.Status);
}
}

View file

@ -0,0 +1,245 @@
{
"id": "2484d1b14e2348f7a916ecef042536e6",
"definitionId": "7204b870946b4968aa786a2c91431242",
"name": "Flow Join Any",
"createdAt": "2023-09-08T19:07:08.435872+00:00",
"version": 1,
"toolVersion": "3.0.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isLatest": true,
"isPublished": false,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "F44h770hvkSaOWI_jjbM_A",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"NotFoundConnectionsKey": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"mode": {
"typeName": "Elsa.Workflows.Core.Activities.Flowchart.Models.FlowJoinMode, Elsa.Workflows.Core",
"expression": {
"type": "Literal",
"value": "WaitAny"
},
"memoryReference": {
"id": "ImdIbHXtHkiLlrExFlW51A:input-0"
}
},
"id": "ImdIbHXtHkiLlrExFlW51A",
"name": "FlowJoin1",
"type": "Elsa.FlowJoin",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -180,
"y": -320
},
"size": {
"width": 136.859375,
"height": 50
}
}
}
},
{
"mode": {
"typeName": "Elsa.Workflows.Core.Activities.Flowchart.Models.FlowJoinMode, Elsa.Workflows.Core",
"expression": {
"type": "Literal",
"value": "WaitAny"
},
"memoryReference": {
"id": "cxnS1whoCESvwwPyBnno_Q:input-0"
}
},
"id": "cxnS1whoCESvwwPyBnno_Q",
"name": "FlowJoin2",
"type": "Elsa.FlowJoin",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -180,
"y": -80
},
"size": {
"width": 136.859375,
"height": 50
}
}
}
},
{
"mode": {
"typeName": "Elsa.Workflows.Core.Activities.Flowchart.Models.FlowJoinMode, Elsa.Workflows.Core",
"expression": {
"type": "Literal",
"value": "WaitAny"
},
"memoryReference": {
"id": "H0apwXYdoESn2TJ5T_3xAg:input-0"
}
},
"id": "H0apwXYdoESn2TJ5T_3xAg",
"name": "FlowJoin3",
"type": "Elsa.FlowJoin",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 40,
"y": -194
},
"size": {
"width": 136.859375,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Start"
},
"memoryReference": {
"id": "Kn9d-EVn2EiaYJqHd8VcmQ:input-0"
}
},
"id": "Kn9d-EVn2EiaYJqHd8VcmQ",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -393.5,
"y": -194
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "End"
},
"memoryReference": {
"id": "haHoM3A5EU6aYu_8dCYaYg:input-0"
}
},
"id": "haHoM3A5EU6aYu_8dCYaYg",
"name": "WriteLine2",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 310.5,
"y": -194
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": [
{
"source": {
"activity": "Kn9d-EVn2EiaYJqHd8VcmQ",
"port": "Done"
},
"target": {
"activity": "ImdIbHXtHkiLlrExFlW51A",
"port": "In"
}
},
{
"source": {
"activity": "Kn9d-EVn2EiaYJqHd8VcmQ",
"port": "Done"
},
"target": {
"activity": "cxnS1whoCESvwwPyBnno_Q",
"port": "In"
}
},
{
"source": {
"activity": "ImdIbHXtHkiLlrExFlW51A",
"port": "Done"
},
"target": {
"activity": "H0apwXYdoESn2TJ5T_3xAg",
"port": "In"
}
},
{
"source": {
"activity": "cxnS1whoCESvwwPyBnno_Q",
"port": "Done"
},
"target": {
"activity": "H0apwXYdoESn2TJ5T_3xAg",
"port": "In"
}
},
{
"source": {
"activity": "H0apwXYdoESn2TJ5T_3xAg",
"port": "Done"
},
"target": {
"activity": "haHoM3A5EU6aYu_8dCYaYg",
"port": "In"
}
}
]
}
}