elsa-workflows / elsa-workflows/elsa-core

"If" activity does not execute "Done" outcome inside a "Fork"

Open
#6,649 0 comments 0 reactions 0 assignees View on GitHub
elsa 2 triaged
Dominant language
C#
Stars
7.9k
Forks
1.5k
Avg merge
15h 22m
Merged PRs (30d)
114

Description

## Description
Hello, I found a problem in ELSA v2.13 (I see no differences in 2.15, anyway) in certain specific workflow configurations, e.g., when an `If` activity is placed in the non-last branch of a `Fork` activity, the `Done` outcome of the `If` is never executed.

Actually, the issue addresses all scoped control flow activities when placed inside a `Fork`.

## Steps to Reproduce

Create a simple workflow with a Fork with two outcomes. On each outcome place an If, and some activity on all possbile outcomes. The "Done" outcome of the first If will not be executed. (example screenshot and JSON in the following).

3. **Attachments**:

This is the workflow that reproduces the issue:

[if_fork_bug_workflowexample.json](https://github.com/user-attachments/files/20225831/if_fork_bug_workflowexample.json)

4. **Reproduction Rate**: 100%

5. **Video/Screenshots**:

This is the screenshot of the example workflow

![Image](https://github.com/user-attachments/assets/b855ee80-496b-4617-bde0-cea16093999a)

## Expected Behavior
All If branches should be executed.

## Actual Behavior
The first/leftmost If in the example does not execute the "Done" branch/outcome.

## Environment
- **Elsa Package Version**: 2.13
- **Operating System**: Tested in Windows 11 and Dockerized Ubuntu Linux

## Troubleshooting Attempts

I tracked down the origin of the issues in the `Elsa.Handlers.RescheduleBranchingActivitiesAndContainers` class:
it tries to collect scoped control flow activities to schedule by looking only at the antecedents of the last executed activity, which in the presence of a Fork may not have all unfinished control flow activities in its antecedents.

This is fixed version of the class I am testing, and seems to work correctly:

```
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Events;
using Elsa.Models;
using Elsa.Services.Models;
using MediatR;

namespace Elsa.Handlers
{
///
/// Reschedules the current parent activity in scope.
///
public class RescheduleBranchingActivitiesAndContainers : INotificationHandler
{
public Task Handle(WorkflowExecutionBurstCompleted notification, CancellationToken cancellationToken)
{
var activityExecutionContext = notification.ActivityExecutionContext;
var workflowExecutionContext = activityExecutionContext.WorkflowExecutionContext;
var workflowInstance = workflowExecutionContext.WorkflowInstance;

if (workflowExecutionContext.Status == WorkflowStatus.Finished || !workflowExecutionContext.WorkflowInstance.Scopes.Any())
return Task.CompletedTask;

List availableScopes = workflowExecutionContext.WorkflowInstance.Scopes;

// Check to see if a suspension / completion has been instructed.
if (workflowExecutionContext.HasScheduledActivities || workflowExecutionContext.Status != WorkflowStatus.Running)
{
availableScopes = availableScopes
.Where(s => !this.IsAnyBlockingActivityInScope(workflowExecutionContext, s.ActivityId))
.ToList();
}

var inboundPaths = availableScopes
.Select(s => s.ActivityId)
.Distinct()
.ToDictionary(activityId => activityId, activityId => workflowExecutionContext.GetInboundActivityPath(activityId).ToHashSet());

List independentScopes = availableScopes
.Where(s => !inboundPaths.Where(kv => kv.Key != s.ActivityId).Any(kv => kv.Value.Contains(s.ActivityId)))
.ToList();

foreach (var scope in Enumerable.Reverse(independentScopes))
{
// Re-schedule the current scope activity.
workflowInstance.Scopes.Remove(scope);
workflowInstance.ActivityData.GetItem(scope.ActivityId)!.SetState("Unwinding", true);
// prevent rescheduling if faulted, if this is not done workflow will loop forever
// executing faulting activity over and over......
if (workflowInstance.WorkflowStatus != WorkflowStatus.Faulted)
workflowExecutionContext.ScheduleActivity(scope.ActivityId);
}

return Task.CompletedTask;
}

private bool IsAnyBlockingActivityInScope(WorkflowExecutionContext workflowExecutionContext, string scopeActivityId)
{
// Get all blocking activity IDs.
var blockingActivityIds = workflowExecutionContext.WorkflowInstance.BlockingActivities.Select(x => x.ActivityId).ToHashSet();

// For each blocking activity, check if it is within the current scope (taking the first one).
foreach (var blockingActivityId in blockingActivityIds)
{
var inboundActivityIds = new[] { blockingActivityId }.Concat(workflowExecutionContext.GetInboundActivityPath(blockingActivityId)).ToHashSet();
if (inboundActivityIds.Contains(scopeActivityId))
return true;
}

return false;
}
}
}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.