diff --git a/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadErrorsAndExceptionsTest.cs b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadErrorsAndExceptionsTest.cs index fa0c5c242..a50b34cc8 100644 --- a/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadErrorsAndExceptionsTest.cs +++ b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadErrorsAndExceptionsTest.cs @@ -44,59 +44,96 @@ void EntryPointAction(WorkflowThread wf) var compiledWfThread = workflowThread.Compile(); - var expectedSpec = new ThreadSpec(); - var entrypoint = new Node - { - Entrypoint = new EntrypointNode(), - OutgoingEdges = + var expectedSpec = GetExpectedThreadSpec( + new FailureHandlerDef { - new Edge { SinkNodeName = "1-fail-TASK" } - } - }; + HandlerSpecName = "exn-handler-1-fail-TASK-FAILURE_TYPE_ERROR", + AnyFailureOfType = FailureHandlerDef.Types.LHFailureType.FailureTypeError + }); - var failTask = new Node + var expectedNumberOfNodes = numberOfEntrypointNodes + numberOfExitNodes + numberOfTasks; + Assert.Equal(expectedNumberOfNodes, compiledWfThread.Nodes.Count); + Assert.Equal(expectedSpec, compiledWfThread); + } + + [Fact] + public void WfThread_WithSpecificError_ShouldCompileErrorHandling() + { + var numberOfExitNodes = 1; + var numberOfEntrypointNodes = 1; + var numberOfTasks = 2; + var workflowName = "TestWorkflow"; + var mockParentWorkflow = new Mock(workflowName, _action); + + void EntryPointAction(WorkflowThread wf) { - Task = new TaskNode - { - TaskDefId = new TaskDefId { Name = "fail" } - }, - OutgoingEdges = { new Edge { SinkNodeName = "2-my-task-TASK" } }, - FailureHandlers = - { - new FailureHandlerDef + NodeOutput node = wf.Execute("fail"); + wf.HandleError( + node, + LHErrorType.Timeout, + handler => { - HandlerSpecName = "exn-handler-1-fail-TASK-FAILURE_TYPE_ERROR", - AnyFailureOfType = FailureHandlerDef.Types.LHFailureType.FailureTypeError + handler.Execute("my-task"); } - } - }; + ); + wf.Execute("my-task"); + } + var workflowThread = new WorkflowThread(mockParentWorkflow.Object, EntryPointAction); - var myTask = new Node - { - Task = new TaskNode + var compiledWfThread = workflowThread.Compile(); + + var expectedSpec = GetExpectedThreadSpec( + new FailureHandlerDef { - TaskDefId = new TaskDefId { Name = "my-task" } - }, - OutgoingEdges = { new Edge { SinkNodeName = "3-exit-EXIT" } } - }; + HandlerSpecName = "exn-handler-1-fail-TASK-TIMEOUT", + SpecificFailure = "TIMEOUT" + }); - var exitNode = new Node + var expectedNumberOfNodes = numberOfEntrypointNodes + numberOfExitNodes + numberOfTasks; + Assert.Equal(expectedNumberOfNodes, compiledWfThread.Nodes.Count); + Assert.Equal(expectedSpec, compiledWfThread); + } + + [Fact] + public void WfThread_WithExceptionName_ShouldCompileExceptionHandling() + { + var numberOfExitNodes = 1; + var numberOfEntrypointNodes = 1; + var numberOfTasks = 2; + var workflowName = "TestWorkflow"; + var mockParentWorkflow = new Mock(workflowName, _action); + + void EntryPointAction(WorkflowThread wf) { - Exit = new ExitNode() - }; + NodeOutput node = wf.Execute("fail"); + wf.HandleException( + node, + "any-business-exception", + handler => + { + handler.Execute("my-task"); + } + ); + wf.Execute("my-task"); + } + var workflowThread = new WorkflowThread(mockParentWorkflow.Object, EntryPointAction); - expectedSpec.Nodes.Add("0-entrypoint-ENTRYPOINT", entrypoint); - expectedSpec.Nodes.Add("1-fail-TASK", failTask); - expectedSpec.Nodes.Add("2-my-task-TASK", myTask); - expectedSpec.Nodes.Add("3-exit-EXIT", exitNode); + var compiledWfThread = workflowThread.Compile(); + + var expectedSpec = GetExpectedThreadSpec( + new FailureHandlerDef + { + HandlerSpecName = "exn-handler-1-fail-TASK-any-business-exception", + SpecificFailure = "any-business-exception" + }); var expectedNumberOfNodes = numberOfEntrypointNodes + numberOfExitNodes + numberOfTasks; Assert.Equal(expectedNumberOfNodes, compiledWfThread.Nodes.Count); Assert.Equal(expectedSpec, compiledWfThread); - } - + } + [Fact] - public void WfThread_WithSpecificError_ShouldCompileErrorHandling() + public void WfThread_WithoutExceptionName_ShouldCompileExceptionHandling() { var numberOfExitNodes = 1; var numberOfEntrypointNodes = 1; @@ -107,9 +144,8 @@ public void WfThread_WithSpecificError_ShouldCompileErrorHandling() void EntryPointAction(WorkflowThread wf) { NodeOutput node = wf.Execute("fail"); - wf.HandleError( + wf.HandleException( node, - LHErrorType.Timeout, handler => { handler.Execute("my-task"); @@ -120,7 +156,57 @@ void EntryPointAction(WorkflowThread wf) var workflowThread = new WorkflowThread(mockParentWorkflow.Object, EntryPointAction); var compiledWfThread = workflowThread.Compile(); + + var expectedSpec = GetExpectedThreadSpec( + new FailureHandlerDef + { + HandlerSpecName = "exn-handler-1-fail-TASK", + AnyFailureOfType = FailureHandlerDef.Types.LHFailureType.FailureTypeException + }); + + var expectedNumberOfNodes = numberOfEntrypointNodes + numberOfExitNodes + numberOfTasks; + Assert.Equal(expectedNumberOfNodes, compiledWfThread.Nodes.Count); + Assert.Equal(expectedSpec, compiledWfThread); + } + + [Fact] + public void WfThread_WithBusinessException_ShouldCompileAnyFailureHandling() + { + var numberOfExitNodes = 1; + var numberOfEntrypointNodes = 1; + var numberOfTasks = 2; + var workflowName = "TestWorkflow"; + var mockParentWorkflow = new Mock(workflowName, _action); + + void EntryPointAction(WorkflowThread wf) + { + NodeOutput node = wf.Execute("fail"); + wf.HandleAnyFailure( + node, + handler => + { + handler.Execute("my-task"); + } + ); + wf.Execute("my-task"); + } + var workflowThread = new WorkflowThread(mockParentWorkflow.Object, EntryPointAction); + var compiledWfThread = workflowThread.Compile(); + + var expectedSpec = GetExpectedThreadSpec( + new FailureHandlerDef + { + HandlerSpecName = "exn-handler-1-fail-TASK-any-failure", + }); + + var expectedNumberOfNodes = numberOfEntrypointNodes + numberOfExitNodes + numberOfTasks; + Assert.Equal(expectedNumberOfNodes, compiledWfThread.Nodes.Count); + Assert.Equal(expectedSpec, compiledWfThread); + } + + private ThreadSpec GetExpectedThreadSpec(FailureHandlerDef failureHandlerDef) + { var expectedSpec = new ThreadSpec(); var entrypoint = new Node { @@ -140,11 +226,7 @@ void EntryPointAction(WorkflowThread wf) OutgoingEdges = { new Edge { SinkNodeName = "2-my-task-TASK" } }, FailureHandlers = { - new FailureHandlerDef - { - HandlerSpecName = "exn-handler-1-fail-TASK-TIMEOUT", - SpecificFailure = "TIMEOUT" - } + failureHandlerDef } }; @@ -167,8 +249,6 @@ void EntryPointAction(WorkflowThread wf) expectedSpec.Nodes.Add("2-my-task-TASK", myTask); expectedSpec.Nodes.Add("3-exit-EXIT", exitNode); - var expectedNumberOfNodes = numberOfEntrypointNodes + numberOfExitNodes + numberOfTasks; - Assert.Equal(expectedNumberOfNodes, compiledWfThread.Nodes.Count); - Assert.Equal(expectedSpec, compiledWfThread); + return expectedSpec; } } \ No newline at end of file diff --git a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs index 7a1ba576e..0345d66af 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs @@ -661,9 +661,10 @@ public void Fail(string failureName, string message) Fail(null, failureName, message); } - private FailureHandlerDef BuildFailureHandlerDef(NodeOutput node, string error, Action handler) + private FailureHandlerDef BuildFailureHandlerDef(NodeOutput node, string error, Action handler) { - string threadName = $"exn-handler-{node.NodeName}-{error}"; + string suffix = !string.IsNullOrEmpty(error) ? $"-{error}" : string.Empty; + string threadName = $"exn-handler-{node.NodeName}{suffix}"; threadName = _parent.AddSubThread(threadName, handler); @@ -677,4 +678,61 @@ private void AddFailureHandlerDef(FailureHandlerDef handlerDef, NodeOutput node) lastNode.FailureHandlers.Add(handlerDef); } + + /// + /// Attaches an Exception Handler to the specified NodeOutput, enabling it to handle specific + /// types of exceptions as defined by the 'exceptionName' parameter. If 'exceptionName' is null, + /// the handler will catch all exceptions. + /// + /// + /// The NodeOutput instance to which the Exception Handler will be attached. + /// + /// + /// The name of the specific exception to handle. If set to null, the handler will catch all exceptions. + /// + /// + /// A ThreadFunction defining a ThreadSpec that specifies how to handle the exception. + /// + public void HandleException(NodeOutput node, String exceptionName, Action handler) + { + CheckIfWorkflowThreadIsActive(); + var handlerDef = BuildFailureHandlerDef(node, exceptionName, handler); + handlerDef.SpecificFailure = exceptionName; + AddFailureHandlerDef(handlerDef, node); + } + + /// + /// Attaches an Exception Handler to the specified NodeOutput, enabling it to handle any + /// types of exceptions. + /// + /// + /// The NodeOutput instance to which the Exception Handler will be attached. + /// + /// + /// A ThreadFunction defining a ThreadSpec that specifies how to handle the exception. + /// + public void HandleException(NodeOutput node, Action handler) + { + CheckIfWorkflowThreadIsActive(); + var handlerDef = BuildFailureHandlerDef(node, null!, handler); + handlerDef.AnyFailureOfType = FailureHandlerDef.Types.LHFailureType.FailureTypeException; + AddFailureHandlerDef(handlerDef, node); + } + + /// + /// Attaches a Failure Handler to the specified NodeOutput, allowing it manages any type of errors or exceptions. + /// types of exceptions. + /// + /// + /// The NodeOutput instance to which the Error Handler will be attached. + /// + /// + /// A ThreadFunction defining a ThreadSpec that specifies how to handle the error. + /// + public void HandleAnyFailure(NodeOutput node, Action handler) + { + CheckIfWorkflowThreadIsActive(); + var handlerDef = BuildFailureHandlerDef(node, "any-failure", handler); + AddFailureHandlerDef(handlerDef, node); + } } \ No newline at end of file