Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Catch the exception and handling the exception in the workflow #5422

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

fanslead
Copy link

@fanslead fanslead commented May 22, 2024

Implemented a catch activity to catch the exception in the flowchart workflow.
When the flowchart has a catch activity, it will catch the exception and can excute the custom exception handling workflow.
image
image


This change is Reviewable

@fanslead
Copy link
Author

@dotnet-policy-service agree

@fanslead
Copy link
Author

@sfmskywalker any suggestion for this pr?

@sfmskywalker
Copy link
Member

Hi @fanslead , thank you for your PR. I will need some time to think about this approach and to explore your PR in detail. Thank you for your patience.

@fanslead
Copy link
Author

fanslead commented Jun 4, 2024

Hi @fanslead , thank you for your PR. I will need some time to think about this approach and to explore your PR in detail. Thank you for your patience.

@sfmskywalker I think this will be a very practical, commonly used and flexible feature. For example, in the event of an abnormality, catch activity can execute custom event or http notifications or compensation measures without any coding.

@fanslead
Copy link
Author

@sfmskywalker Hi, Do you have any thoughts about this PR?

@sfmskywalker
Copy link
Member

Hi @fanslead , I'm afraid I haven't had the chance yet to sit down and look through the PR and think about the proposed feature, but generally I think it makes sense. I can't say when I might be able to circle back to this, but please rest assured that your PR will be reviewed and that you'll get proper feedback. Thank you for your patience.

@fanslead
Copy link
Author

@sfmskywalker any update for this pr?

Copy link
Member

@sfmskywalker sfmskywalker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All in all, I can see that the Catch activity could be interesting for handling exceptions centrally.

However, I think that we could do something nicer that is similar to BPMN's boundary event mechanisms.

Although it definitely doesn't have to be the same, I think that we might do the following:

  1. Introduce a new activity called Try
  2. The Try activity exposes two outcomes:
    1. Done
    2. Catch
  3. The Try activity has one embedded port called Body and is of type IActivity - effectively making this a composite activity.
  4. Similar what you did with Flowchart, the Try activity would listen for exception signals.
  5. When an exception signal is received, it completes with the Catch outcome.
  6. Otherwise, it completes with the Done outcome.

This way, we have a kind of a "boundary event" in the form of the "Catch" outcome on the Try activity, achieving the same centrally managed exception handling but without having to update the Flowchart activity.

What do you think?

@@ -28,6 +28,7 @@ public Flowchart([CallerFilePath] string? source = default, [CallerLineNumber] i
OnSignalReceived<ScheduleActivityOutcomes>(OnScheduleOutcomesAsync);
OnSignalReceived<ScheduleChildActivity>(OnScheduleChildActivityAsync);
OnSignalReceived<CancelSignal>(OnActivityCanceledAsync);
OnSignalCaptured<ExceptionSignal>(OnActivityExceptionAsync);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I had removed the "capturing" phase - do we really need it for this?

}

/// <summary>
/// Catch all the Exception.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Catch all the Exception.
/// A value indicating whether to catch all exceptions from all activities.

/// <summary>
/// Catch all the Exception.
/// </summary>
[Input(Description = "A value that is the tag a global catcher")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[Input(Description = "A value that is the tag a global catcher")]
[Input(Description = "A value indicating whether to catch all exceptions from all activities.")]

Comment on lines 28 to 29
[Input(Description = "The Activities will be catch",
UIHint = InputUIHints.MultiText)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[Input(Description = "The Activities will be catch",
UIHint = InputUIHints.MultiText)]
[Input(
Description = "The IDs of the activities to catch exceptions from.",
UIHint = InputUIHints.MultiText)]

public Input<bool> CatchAll { get; set; } = default!;

/// <summary>
/// Catch the Activities.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Catch the Activities.
/// The IDs of the activities to catch exceptions from.

{
var avtivities = Activities.Where(a => a is Catch).Select(a => a as Catch).ToList();

if (avtivities.Count == 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (avtivities.Count == 0)
if (activities.Count == 0)

Owner = flowchartContext
};

foreach (var catchAvtivity in avtivities)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach (var catchAvtivity in avtivities)
foreach (var catchActivity in activities)

Comment on lines 348 to 379
{
var catchContext = workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Activity.Id == catchAvtivity!.Id);

if (catchContext == null)
{
catchContext = workflowExecutionContext.CreateActivityExecutionContext(catchAvtivity!, options);
workflowExecutionContext.AddActivityExecutionContext(catchContext);

await catchContext.EvaluateInputPropertiesAsync();
}

var scheduleWorkOptions = new ScheduleWorkOptions
{
CompletionCallback = OnChildCompletedAsync,
ExistingActivityExecutionContext = catchContext
};

var catchAll = catchAvtivity!.CatchAll.Get(catchContext);
if (catchAll)
{
var connections = Connections.Descendants(catchAvtivity).ToList();
if (!connections.Any(x => x.Target.Activity == exceptionActivity || x.Source.Activity == exceptionActivity))
await flowchartContext.ScheduleActivityAsync(catchAvtivity, scheduleWorkOptions);
}
else
{
var catchActivities = catchAvtivity.CatchActivities?.Get(catchContext);
if (catchActivities != null && catchActivities.Contains(exceptionActivity.Id))
await flowchartContext.ScheduleActivityAsync(catchAvtivity, scheduleWorkOptions);
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure about this approach. Although I understand that you have to evaluate the properties of the Catch activity in order to see if the faulting activity ID is present in its list and whether the CatchAll is set to true, I would think that, after it is determined that the CatchAll activity should execute, you should simply schedule the catch activity for execution. I.e, I think that if either catchAll is true or the faulting activity is present in the "catch activities" list, you should schedule the catch activity.

E.g.

Suggested change
{
var catchContext = workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Activity.Id == catchAvtivity!.Id);
if (catchContext == null)
{
catchContext = workflowExecutionContext.CreateActivityExecutionContext(catchAvtivity!, options);
workflowExecutionContext.AddActivityExecutionContext(catchContext);
await catchContext.EvaluateInputPropertiesAsync();
}
var scheduleWorkOptions = new ScheduleWorkOptions
{
CompletionCallback = OnChildCompletedAsync,
ExistingActivityExecutionContext = catchContext
};
var catchAll = catchAvtivity!.CatchAll.Get(catchContext);
if (catchAll)
{
var connections = Connections.Descendants(catchAvtivity).ToList();
if (!connections.Any(x => x.Target.Activity == exceptionActivity || x.Source.Activity == exceptionActivity))
await flowchartContext.ScheduleActivityAsync(catchAvtivity, scheduleWorkOptions);
}
else
{
var catchActivities = catchAvtivity.CatchActivities?.Get(catchContext);
if (catchActivities != null && catchActivities.Contains(exceptionActivity.Id))
await flowchartContext.ScheduleActivityAsync(catchAvtivity, scheduleWorkOptions);
}
}
}
{
var catchContext = workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Activity.Id == catchAvtivity!.Id);
if (catchContext == null)
{
catchContext = workflowExecutionContext.CreateActivityExecutionContext(catchAvtivity!, options);
workflowExecutionContext.AddActivityExecutionContext(catchContext);
await catchContext.EvaluateInputPropertiesAsync();
}
var scheduleWorkOptions = new ScheduleWorkOptions
{
CompletionCallback = OnChildCompletedAsync,
ExistingActivityExecutionContext = catchContext
};
var catchAll = catchActivity!.CatchAll.Get(catchContext);
var catchActivities = catchActivity.CatchActivities?.Get(catchContext);
var catchFaultingActivity = catchActivities?.Contains(exceptionActivity.Id) == true;
var shouldCatch = catchAll || catchFaultingActivity;
if(shouldCatch)
await flowchartContext.ScheduleActivityAsync(catchActivity, scheduleWorkOptions);
}
}

Please note that I haven't tested it, so I could be totally off here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When catchAll is true, we need to consider whether abnormal activities are executed after catch activities. Otherwise, an infinite loop may occur.

Comment on lines 64 to +66
strategy.HandleIncident(context);

await context.SendSignalAsync(new ExceptionSignal(activity, e));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm questioning whether we should report an incident if an exception was handled. What are your thoughts on that?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good question. I think if the user chooses Continue With Incidents Strategy, does he still need to add Catch Activity.If Catch Activity is not added, there will be no impact. If it is added, it means he needs this report.

namespace Elsa.Workflows.Signals;

/// <summary>
/// /// Sent by ExceptionHandlingMiddleware to notify their composite container that it has exception.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// /// Sent by ExceptionHandlingMiddleware to notify their composite container that it has exception.
/// Sent by ExceptionHandlingMiddleware to notify their composite container that an exception occurred.

@fanslead
Copy link
Author

fanslead commented Aug 14, 2024

All in all, I can see that the Catch activity could be interesting for handling exceptions centrally.

However, I think that we could do something nicer that is similar to BPMN's boundary event mechanisms.

Although it definitely doesn't have to be the same, I think that we might do the following:

  1. Introduce a new activity called Try

  2. The Try activity exposes two outcomes:

    1. Done
    2. Catch
  3. The Try activity has one embedded port called Body and is of type IActivity - effectively making this a composite activity.

  4. Similar what you did with Flowchart, the Try activity would listen for exception signals.

  5. When an exception signal is received, it completes with the Catch outcome.

  6. Otherwise, it completes with the Done outcome.

This way, we have a kind of a "boundary event" in the form of the "Catch" outcome on the Try activity, achieving the same centrally managed exception handling but without having to update the Flowchart activity.

What do you think?

@sfmskywalker that is a good idea. But I think composite activity will to some extent affect the readability of the flow. In my eyes, Catch activities should be detached from the main workflow and receive the signal from the main workflow. That is more like to BPMN's boundary event mechanisms. Of course, we can implement both options and leave it to users to choose and use.Just like the Switch Activity and the Switch(flow) Activity.

fanslead and others added 6 commits August 14, 2024 12:12
…core into feature/catchflow

* 'feature/catchflow' of https://github.com/fanslead/elsa-core: (200 commits)
  [Fix] Replace "bundles" with "apps" in the content of README.md to align with current source code structure. (elsa-workflows#5885)
  Rename Model to ServiceProfile across the codebase.
  Add TenantId to workflow definition and skip multitenancy test
  Fix background activity completion (elsa-workflows#5882)
  Update persistence feature base classes to include self-type
  Refactor journal data access and improve byte[] handling (elsa-workflows#5878)
  Changed RegisterClassMap to TryRegisterClassMap on SerializedKeyValuePair classmap registration (elsa-workflows#5877)
  Add Orchard Core Integration + Agents Module (elsa-workflows#5871)
  Add customizable DB exception handlers
  Refactor workflow execution and pipeline handling
  Update elsa-v3-avatar.png
  Add new Elsa v3 avatar to design and update README
  Add MyEvent activity to Elsa services
  Add TODO
  Fix logger type
  Update ElsaStudioVersion to 3.2.0-rc4.473
  Fix incorrect serializer reference
  Update pre-release version in GitHub Actions workflow
  Rework Workflow Context Feature (elsa-workflows#5861)
  Add engine exception handling middleware
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants