Refactor inbound connection handling to schedule outbound activity before canceling remaining activities (#7340)
* Refactor inbound connection handling to schedule outbound activity before canceling remaining activities * Add regression test for WaitAny join schedule-before-cancel ordering (#7341) * Initial plan * Add integration test for WaitAny join with blocking branch (schedule-before-cancel regression test) Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
This commit is contained in:
parent
71438596f3
commit
d4b69be44b
|
|
@ -313,11 +313,12 @@ public partial class Flowchart
|
|||
|
||||
if (flowScope.AnyInboundConnectionsFollowed(flowGraph, outboundActivity))
|
||||
{
|
||||
// This is the first inbound connection followed; schedule the outbound activity
|
||||
var scheduleResponse = await ScheduleOutboundActivityAsync(flowchartContext, outboundActivity, completionCallback);
|
||||
// An inbound connection has been followed; cancel remaining inbound activities
|
||||
await CancelRemainingInboundActivitiesAsync(flowchartContext, outboundActivity);
|
||||
|
||||
// This is the first inbound connection followed; schedule the outbound activity
|
||||
return await ScheduleOutboundActivityAsync(flowchartContext, outboundActivity, completionCallback);
|
||||
return scheduleResponse;
|
||||
}
|
||||
|
||||
if (flowScope.AllInboundConnectionsVisited(flowGraph, outboundActivity))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
|
|
@ -341,4 +342,67 @@ public class FlowchartCounterBasedTests
|
|||
Assert.Equal("Connected", _output.Lines.ElementAt(0));
|
||||
Assert.DoesNotContain("Unconnected", _output.Lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for the ordering fix in <see cref="Flowchart.MaybeScheduleWaitAnyActivityAsync"/>:
|
||||
/// the outbound activity must be scheduled <em>before</em> remaining inbound branches are canceled.
|
||||
/// Previously, canceling first could trigger <c>CompleteIfNoPendingWorkAsync</c> while the outbound
|
||||
/// activity was not yet in the scheduler, causing the flowchart to finish prematurely without running
|
||||
/// the activity downstream of the join.
|
||||
/// </summary>
|
||||
[Fact(DisplayName = "WaitAny join schedules outbound before canceling blocked branch, preventing premature completion")]
|
||||
public async Task WaitAnyJoin_SchedulesOutboundBeforeCancelingBlockedBranch()
|
||||
{
|
||||
// Arrange
|
||||
// Flowchart structure:
|
||||
// Start
|
||||
// ├─► Branch1 (fast) ─► Join (WaitAny) ─► AfterJoin
|
||||
// └─► Branch2 (blocking, creates a bookmark) ─►┘
|
||||
//
|
||||
// Because the scheduler is LIFO, Branch2 executes first and suspends with a bookmark.
|
||||
// Branch1 then completes and triggers the WaitAny join.
|
||||
// The join must schedule AfterJoin before canceling Branch2; otherwise
|
||||
// CompleteIfNoPendingWorkAsync fires while AfterJoin is not yet queued,
|
||||
// completing the flowchart prematurely without executing AfterJoin.
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new BlockingActivity { Id = "BlockingBranch" };
|
||||
var join = new FlowJoin { Mode = new(FlowJoinMode.WaitAny) };
|
||||
var afterJoin = new WriteLine("AfterJoin");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, branch1, branch2, join, afterJoin },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, branch1),
|
||||
CreateConnection(start, branch2),
|
||||
CreateConnection(branch1, join),
|
||||
CreateConnection(branch2, join),
|
||||
CreateConnection(join, afterJoin)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
|
||||
|
||||
// Assert: the outbound path of the join must have executed
|
||||
Assert.Contains("AfterJoin", _output.Lines);
|
||||
|
||||
// Assert: the workflow must have finished, not suspended waiting for the canceled bookmark
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
|
||||
// Assert: the blocked branch's bookmark was cleared when it was canceled
|
||||
Assert.Empty(result.WorkflowState.Bookmarks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A minimal activity that suspends by creating a bookmark, simulating a blocking activity
|
||||
/// such as a Delay or Timer that would normally be resumed by an external stimulus.
|
||||
/// </summary>
|
||||
private sealed class BlockingActivity : Activity
|
||||
{
|
||||
protected override void Execute(ActivityExecutionContext context) => context.CreateBookmark();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue