-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
base: main
Are you sure you want to change the base?
Conversation
@dotnet-policy-service agree |
@sfmskywalker any suggestion for this pr? |
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. |
@sfmskywalker Hi, Do you have any thoughts about this PR? |
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. |
@sfmskywalker any update for this pr? |
There was a problem hiding this 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:
- Introduce a new activity called
Try
- The
Try
activity exposes two outcomes:Done
Catch
- The
Try
activity has one embedded port calledBody
and is of typeIActivity
- effectively making this a composite activity. - Similar what you did with
Flowchart
, theTry
activity would listen for exception signals. - When an exception signal is received, it completes with the
Catch
outcome. - 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); |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// 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")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Input(Description = "A value that is the tag a global catcher")] | |
[Input(Description = "A value indicating whether to catch all exceptions from all activities.")] |
[Input(Description = "The Activities will be catch", | ||
UIHint = InputUIHints.MultiText)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (avtivities.Count == 0) | |
if (activities.Count == 0) |
Owner = flowchartContext | ||
}; | ||
|
||
foreach (var catchAvtivity in avtivities) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
foreach (var catchAvtivity in avtivities) | |
foreach (var catchActivity in activities) |
{ | ||
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); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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.
{ | |
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.
There was a problem hiding this comment.
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.
strategy.HandleIncident(context); | ||
|
||
await context.SendSignalAsync(new ExceptionSignal(activity, e)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// /// Sent by ExceptionHandlingMiddleware to notify their composite container that it has exception. | |
/// Sent by ExceptionHandlingMiddleware to notify their composite container that an exception occurred. |
@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. |
…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 ...
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.
This change is