Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,12 @@ private async ValueTask OnCounterFlowActivityCanceledAsync(CancelSignal signal,
var flowGraph = flowchartContext.GetFlowGraph();
var flowScope = flowchart.GetFlowScope(flowchartContext);

// A dangling activity is not reachable from the flow graph's start (for example a self-looping parallel
// branch that gets canceled when the flowchart completes through another branch). It has no forward
// outbound connections to propagate, so there is nothing to schedule. See https://github.com/elsa-workflows/elsa-core/issues/7717.
if (flowGraph.IsDanglingActivity(canceledActivity))
return;

// Propagate canceled connections visited count by scheduling with Outcomes.Empty
await MaybeScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, canceledActivity, context.SenderActivityExecutionContext, Outcomes.Empty, OnChildCompletedAsync);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Options;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;

namespace Elsa.Workflows.IntegrationTests.Scenarios.DanglingActivityCancellation;

public class Tests
{
private readonly IWorkflowRunner _workflowRunner;
private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
private readonly IServiceProvider _services;

public Tests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper).Build();
_workflowBuilderFactory = _services.GetRequiredService<IWorkflowBuilderFactory>();
_workflowRunner = _services.GetRequiredService<IWorkflowRunner>();
}

/// <summary>
/// Reproduces https://github.com/elsa-workflows/elsa-core/issues/7717.
/// When the workflow is resumed via a trigger (which sets <see cref="WorkflowExecutionContext.TriggerActivityId"/>)
/// and the blocking branch completes through a terminal node, the parallel self-looping branch is canceled.
/// The canceled activity is "dangling" relative to the trigger-rooted flow graph, which previously faulted the workflow.
/// </summary>
[Fact(DisplayName = "Canceling a dangling parallel branch on trigger-resume does not fault the workflow")]
public async Task CancelingDanglingParallelBranchDoesNotFault()
{
await _services.PopulateRegistriesAsync();

var workflow = await _workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync<ApprovalWithReminderWorkflow>();

// Start the workflow. Both the approval and the (self-looping) reminder branches block.
var result = await _workflowRunner.RunAsync(workflow);
Assert.Equal(WorkflowSubStatus.Suspended, result.WorkflowState.SubStatus);

var approvalBookmark = result.WorkflowState.Bookmarks.First(x => x.ActivityId == "Approval");

// Resume the approval branch as if triggered (this is what e.g. the HTTP endpoint middleware does).
var runOptions = new RunWorkflowOptions
{
BookmarkId = approvalBookmark.Id,
TriggerActivityId = "Approval"
};
var resumeResult = await _workflowRunner.RunAsync(workflow, result.WorkflowState, runOptions);

Assert.Equal(WorkflowStatus.Finished, resumeResult.WorkflowState.Status);
Assert.Equal(WorkflowSubStatus.Finished, resumeResult.WorkflowState.SubStatus);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Runtime.Activities;

namespace Elsa.Workflows.IntegrationTests.Scenarios.DanglingActivityCancellation;

/// <summary>
/// Reproduces https://github.com/elsa-workflows/elsa-core/issues/7717.
/// A blocking approval branch runs in parallel with a self-looping reminder branch.
/// When the approval branch completes through a terminal node, the reminder branch is canceled.
/// </summary>
class ApprovalWithReminderWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var start = new Start
{
Id = "Start"
};
var approval = new Event("Approval")
{
Id = "Approval"
};
var reminder = new Event("Reminder")
{
Id = "Reminder"
};
var end = new End
{
Id = "End"
};

workflow.Root = new Flowchart
{
Activities =
{
start,
approval,
reminder,
end
},
Connections =
{
new(start, approval),
new(approval, end),
new(start, reminder),
new(reminder, reminder) // self-loop (periodic reminder)
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,38 @@ public void SameEdgeDifferentPortDuplicateTest()
flowGraph.ValidateAncestorActivities([c, d, a, b, start], e);
}

// Regression for https://github.com/elsa-workflows/elsa-core/issues/7717.
// start ──> approval ──> end
// └─────> reminder ──┐ (self-loop)
// ↑───────┘
// When the flow graph is rooted at the structural start, the self-looping reminder branch is reachable.
// When it is rooted at the resumed trigger (approval), the reminder branch becomes dangling.
[Fact]
public void SelfLoopingParallelBranchIsDanglingOnlyWhenRootedAtTrigger()
{
var start = new TestActivity("start");
var approval = new TestActivity("approval");
var end = new TestActivity("end");
var reminder = new TestActivity("reminder");

var connections = new List<Connection>
{
new(start, approval),
new(approval, end),
new(start, reminder),
new(reminder, reminder) // self-loop (periodic reminder)
};

// Rooted at the structural start: the reminder branch is reachable, so it is not dangling.
var startRootedGraph = new FlowGraph(connections, start);
startRootedGraph.ValidateDanglingActivity(false, reminder);

// Rooted at the resumed trigger (e.g. an HTTP endpoint): start, and therefore the reminder branch,
// are no longer reachable, so the reminder is considered dangling.
var triggerRootedGraph = new FlowGraph(connections, approval);
triggerRootedGraph.ValidateDanglingActivity(true, reminder);
}

class TestActivity : Activity
{
public TestActivity(string id)
Expand Down