Skip to content

Try-catch clause #525

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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions WorkflowCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.DSL", "src\Wor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample18", "src\samples\WorkflowCore.Sample18\WorkflowCore.Sample18.csproj", "{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowCore.Sample19", "src\samples\WorkflowCore.Sample19\WorkflowCore.Sample19.csproj", "{7F8775E5-30AD-415C-B66D-1C76472C77D2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -343,6 +345,10 @@ Global
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.Build.0 = Release|Any CPU
{7F8775E5-30AD-415C-B66D-1C76472C77D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F8775E5-30AD-415C-B66D-1C76472C77D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F8775E5-30AD-415C-B66D-1C76472C77D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F8775E5-30AD-415C-B66D-1C76472C77D2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -400,6 +406,7 @@ Global
{78217204-B873-40B9-8875-E3925B2FBCEC} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB}
{20B98905-08CB-4854-8E2C-A31A078383E9} = {EF47161E-E399-451C-BDE8-E92AAD3BD761}
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84} = {5080DB09-CBE8-4C45-9957-C3BB7651755E}
{7F8775E5-30AD-415C-B66D-1C76472C77D2} = {5080DB09-CBE8-4C45-9957-C3BB7651755E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC0FA8D3-6449-4FDA-BB46-ECF58FAD23B4}
Expand Down
9 changes: 9 additions & 0 deletions src/WorkflowCore/Interface/ICatchStepBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace WorkflowCore.Interface
{
public interface ICatchStepBuilder<TData, TStepBody> : IStepBuilder<TData, TStepBody>, ITryStepBuilder<TData, TStepBody>
Copy link
Owner

Choose a reason for hiding this comment

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

Do we need an empty interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This interface gives a builder full functionality of IStepBuilder and also allows to Catch() if we want to have multiple catches after one Try(). It is used as a return type for builder after the first Catch(), whereas after Try() we want to allow only to Catch().

where TStepBody : IStepBody
{
}
}
4 changes: 3 additions & 1 deletion src/WorkflowCore/Interface/IExecutionPointerFactory.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using WorkflowCore.Models;
using System;
using WorkflowCore.Models;

namespace WorkflowCore.Interface
{
public interface IExecutionPointerFactory
{
ExecutionPointer BuildGenesisPointer(WorkflowDefinition def);
ExecutionPointer BuildCompensationPointer(WorkflowDefinition def, ExecutionPointer pointer, ExecutionPointer exceptionPointer, int compensationStepId);
ExecutionPointer BuildCatchPointer(WorkflowDefinition def, ExecutionPointer pointer, ExecutionPointer exceptionPointer, int catchStepId, Exception exception);
ExecutionPointer BuildNextPointer(WorkflowDefinition def, ExecutionPointer pointer, IStepOutcome outcomeTarget);
ExecutionPointer BuildChildPointer(WorkflowDefinition def, ExecutionPointer pointer, int childDefinitionId, object branch);
}
Expand Down
2 changes: 2 additions & 0 deletions src/WorkflowCore/Interface/IStepBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ public interface IStepBuilder<TData, TStepBody>
/// <returns></returns>
IStepBuilder<TData, Sequence> Saga(Action<IWorkflowBuilder<TData>> builder);

ITryStepBuilder<TData, Sequence> Try(Action<IWorkflowBuilder<TData>> builder);

/// <summary>
/// Schedule a block of steps to execute in parallel sometime in the future
/// </summary>
Expand Down
7 changes: 5 additions & 2 deletions src/WorkflowCore/Interface/IStepExecutionContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using WorkflowCore.Models;
using System;
using WorkflowCore.Models;

namespace WorkflowCore.Interface
{
Expand All @@ -12,6 +13,8 @@ public interface IStepExecutionContext

WorkflowStep Step { get; set; }

WorkflowInstance Workflow { get; set; }
WorkflowInstance Workflow { get; set; }

SerializableException CurrentException { get; set; }
}
}
21 changes: 21 additions & 0 deletions src/WorkflowCore/Interface/ITryStepBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using WorkflowCore.Models;

namespace WorkflowCore.Interface
{
public interface ITryStepBuilder<TData, TStepBody>
where TStepBody : IStepBody
{
ICatchStepBuilder<TData, TStepBody> Catch<TStep>(IEnumerable<Type> exceptionTypes, Action<IStepBuilder<TData, TStep>> stepSetup = null)
where TStep : IStepBody;

ICatchStepBuilder<TData, TStepBody> Catch(IEnumerable<Type> exceptionTypes, Action<IStepExecutionContext> body);

ICatchStepBuilder<TData, TStepBody> Catch(IEnumerable<Type> exceptionTypes,
Func<IStepExecutionContext, ExecutionResult> body);

ICatchStepBuilder<TData, TStepBody> CatchWithSequence(IEnumerable<Type> exceptionTypes,
Action<IWorkflowBuilder<TData>> builder);
}
}
9 changes: 9 additions & 0 deletions src/WorkflowCore/Interface/IWorkflowDefinitionValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using WorkflowCore.Models;

namespace WorkflowCore.Interface
{
public interface IWorkflowDefinitionValidator
{
bool IsDefinitionValid(WorkflowDefinition definition);
Copy link
Owner

Choose a reason for hiding this comment

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

is this part of the try/catch feature? or something else?

Copy link
Contributor Author

@kirsanium kirsanium Mar 1, 2020

Choose a reason for hiding this comment

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

Right now this only checks that every Try() has corresponding Catch().

}
}
2 changes: 2 additions & 0 deletions src/WorkflowCore/Models/ExecutionPointer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class ExecutionPointer
public object Outcome { get; set; }

public PointerStatus Status { get; set; } = PointerStatus.Legacy;

public SerializableException CurrentException { get; set; }

public IReadOnlyCollection<string> Scope
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace WorkflowCore.Models.LifeCycleEvents
{
public class WorkflowTerminated : LifeCycleEvent
{
public SerializableException Exception { get; set; }
}
}
20 changes: 20 additions & 0 deletions src/WorkflowCore/Models/SerializableException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace WorkflowCore.Models
{
public class SerializableException
{
public string FullTypeName { get; private set; }

public string Message { get; private set; }

public string StackTrace { get; private set; }

public SerializableException(Exception exception)
{
FullTypeName = exception.GetType().FullName;
Message = exception.Message;
StackTrace = exception.StackTrace;
}
}
}
5 changes: 4 additions & 1 deletion src/WorkflowCore/Models/StepExecutionContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using WorkflowCore.Interface;
using System;
using WorkflowCore.Interface;

namespace WorkflowCore.Models
{
Expand All @@ -13,5 +14,7 @@ public class StepExecutionContext : IStepExecutionContext
public object PersistenceData { get; set; }

public object Item { get; set; }

public SerializableException CurrentException { get; set; }
}
}
3 changes: 2 additions & 1 deletion src/WorkflowCore/Models/WorkflowDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum WorkflowErrorHandling
Retry = 0,
Suspend = 1,
Terminate = 2,
Compensate = 3
Compensate = 3,
Catch = 4
}
}
4 changes: 3 additions & 1 deletion src/WorkflowCore/Models/WorkflowStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ public abstract class WorkflowStep

public virtual WorkflowErrorHandling? ErrorBehavior { get; set; }

public virtual TimeSpan? RetryInterval { get; set; }
public virtual TimeSpan? RetryInterval { get; set; }

public virtual int? CompensationStepId { get; set; }

public virtual Queue<KeyValuePair<Type, int>> CatchStepsQueue { get; set; } = new Queue<KeyValuePair<Type, int>>();

public virtual bool ResumeChildrenAfterCompensation => true;

Expand Down
2 changes: 2 additions & 0 deletions src/WorkflowCore/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A
services.AddTransient<IWorkflowErrorHandler, RetryHandler>();
services.AddTransient<IWorkflowErrorHandler, TerminateHandler>();
services.AddTransient<IWorkflowErrorHandler, SuspendHandler>();
services.AddTransient<IWorkflowErrorHandler, CatchHandler>();

services.AddSingleton<IWorkflowController, WorkflowController>();
services.AddSingleton<IActivityController, ActivityController>();
Expand All @@ -58,6 +59,7 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A
services.AddTransient<IDateTimeProvider, DateTimeProvider>();
services.AddTransient<IExecutionResultProcessor, ExecutionResultProcessor>();
services.AddTransient<IExecutionPointerFactory, ExecutionPointerFactory>();
services.AddTransient<IWorkflowDefinitionValidator, WorkflowDefinitionValidator>();

services.AddTransient<IPooledObjectPolicy<IPersistenceProvider>, InjectedObjectPoolPolicy<IPersistenceProvider>>();
services.AddTransient<IPooledObjectPolicy<IWorkflowExecutor>, InjectedObjectPoolPolicy<IWorkflowExecutor>>();
Expand Down
88 changes: 88 additions & 0 deletions src/WorkflowCore/Services/ErrorHandlers/CatchHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
using WorkflowCore.Interface;
using WorkflowCore.Models;
using WorkflowCore.Models.LifeCycleEvents;

namespace WorkflowCore.Services.ErrorHandlers
{
public class CatchHandler : IWorkflowErrorHandler
Copy link
Owner

Choose a reason for hiding this comment

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

Could you implement a set of unit tests for this class that make it's behavior explicit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, will do it soon. By the way I have a question concerning behaviour of CompensateHandler. I don't understand what lines 41-45 of CatchHandler do as I copypasted them from lines 56-60 of CompensateHandler, hence I don't understand if they are needed at all. Could you please elaborate on this?

Copy link
Owner

Choose a reason for hiding this comment

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

I believe that was for bubbling up the call stack of nested containers

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I clearly have troubles understanding this part of bubbling up the call stack. Could you please go into deeper details? And please tell me if it's needed or not in case of Catchclause.

{
private readonly ILifeCycleEventPublisher _eventPublisher;
private readonly IExecutionPointerFactory _pointerFactory;
private readonly IDateTimeProvider _datetimeProvider;
private readonly WorkflowOptions _options;

public CatchHandler(IExecutionPointerFactory pointerFactory, ILifeCycleEventPublisher eventPublisher, IDateTimeProvider datetimeProvider, WorkflowOptions options)
{
_pointerFactory = pointerFactory;
_eventPublisher = eventPublisher;
_datetimeProvider = datetimeProvider;
_options = options;
}

public WorkflowErrorHandling Type => WorkflowErrorHandling.Catch;

public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer exceptionPointer, WorkflowStep step,
Exception exception, Queue<ExecutionPointer> bubbleUpQueue)
{
var scope = new Stack<string>(exceptionPointer.Scope.Reverse());
scope.Push(exceptionPointer.Id);

var exceptionCaught = false;

while (scope.Any())
{
var pointerId = scope.Pop();
var scopePointer = workflow.ExecutionPointers.FindById(pointerId);
var scopeStep = def.Steps.FindById(scopePointer.StepId);

if ((scopeStep.ErrorBehavior ?? WorkflowErrorHandling.Catch) != WorkflowErrorHandling.Catch)
{
bubbleUpQueue.Enqueue(scopePointer);
continue;
} //TODO: research if it's needed

scopePointer.Active = false;
scopePointer.EndTime = _datetimeProvider.Now.ToUniversalTime();
scopePointer.Status = PointerStatus.Failed;

while (scopeStep.CatchStepsQueue.Count != 0)
{
var nextCatchStepPair = scopeStep.CatchStepsQueue.Dequeue();
var exceptionType = nextCatchStepPair.Key;
var catchStepId = nextCatchStepPair.Value;
if (exceptionType.IsInstanceOfType(exception))
{
var catchPointer = _pointerFactory.BuildCatchPointer(def, scopePointer, exceptionPointer, catchStepId, exception);
workflow.ExecutionPointers.Add(catchPointer);

foreach (var outcomeTarget in scopeStep.Outcomes.Where(x => x.Matches(workflow.Data)))
workflow.ExecutionPointers.Add(_pointerFactory.BuildNextPointer(def, scopePointer, outcomeTarget));

exceptionCaught = true;

scopeStep.CatchStepsQueue.Clear();
scope.Clear();
break;
}
}
}

if (!exceptionCaught)
{
workflow.Status = WorkflowStatus.Terminated;
_eventPublisher.PublishNotification(new WorkflowTerminated()
{
EventTimeUtc = _datetimeProvider.UtcNow,
Reference = workflow.Reference,
WorkflowInstanceId = workflow.Id,
WorkflowDefinitionId = workflow.WorkflowDefinitionId,
Version = workflow.Version,
Exception = new SerializableException(exception)
});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionP
Reference = workflow.Reference,
WorkflowInstanceId = workflow.Id,
WorkflowDefinitionId = workflow.WorkflowDefinitionId,
Version = workflow.Version
Version = workflow.Version,
Exception = new SerializableException(exception)
});
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/WorkflowCore/Services/ExecutionPointerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ public ExecutionPointer BuildCompensationPointer(WorkflowDefinition def, Executi
Scope = new List<string>(pointer.Scope)
};
}

public ExecutionPointer BuildCatchPointer(WorkflowDefinition def, ExecutionPointer pointer, ExecutionPointer exceptionPointer, int catchStepId, Exception exception)
{
var nextId = GenerateId();
return new ExecutionPointer()
{
Id = nextId,
PredecessorId = exceptionPointer.Id,
StepId = catchStepId,
Active = true,
ContextItem = pointer.ContextItem,
Status = PointerStatus.Pending,
StepName = def.Steps.FindById(catchStepId).Name,
Scope = new List<string>(pointer.Scope),
CurrentException = new SerializableException(exception)
};
}

private string GenerateId()
{
Expand Down
14 changes: 8 additions & 6 deletions src/WorkflowCore/Services/ExecutionResultProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,17 @@ public void HandleStepException(WorkflowInstance workflow, WorkflowDefinition de
{
var exceptionPointer = queue.Dequeue();
var exceptionStep = def.Steps.FindById(exceptionPointer.StepId);
var shouldCompensate = ShouldCompensate(workflow, def, exceptionPointer);
var errorOption = (exceptionStep.ErrorBehavior ?? (shouldCompensate ? WorkflowErrorHandling.Compensate : def.DefaultErrorBehavior));
var errorOption = (exceptionStep.ErrorBehavior ?? GetErrorHandling(workflow, def, exceptionPointer));

foreach (var handler in _errorHandlers.Where(x => x.Type == errorOption))
{
handler.Handle(workflow, def, exceptionPointer, exceptionStep, exception, queue);
}
}
}

private bool ShouldCompensate(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer currentPointer)

private WorkflowErrorHandling GetErrorHandling(WorkflowInstance workflow, WorkflowDefinition def,
ExecutionPointer currentPointer)
{
var scope = new Stack<string>(currentPointer.Scope);
scope.Push(currentPointer.Id);
Expand All @@ -132,11 +132,13 @@ private bool ShouldCompensate(WorkflowInstance workflow, WorkflowDefinition def,
var pointerId = scope.Pop();
var pointer = workflow.ExecutionPointers.FindById(pointerId);
var step = def.Steps.FindById(pointer.StepId);
if (step.CatchStepsQueue.Count != 0)
return WorkflowErrorHandling.Catch;
if ((step.CompensationStepId.HasValue) || (step.RevertChildrenAfterCompensation))
return true;
return WorkflowErrorHandling.Compensate;
}

return false;
return def.DefaultErrorBehavior;
}
}
}
Loading