From 4214dab8b51b7d8d29f2d7f904368fd3102976a3 Mon Sep 17 00:00:00 2001 From: KarlaCarvajal Date: Mon, 17 Feb 2025 18:18:37 -0500 Subject: [PATCH 1/6] feat(sdk-dotnet): Implement timeouts in tasks and external events nodes --- .../Workflow/Spec/NodeOutput.cs | 7 ++++++ .../Workflow/Spec/WorkflowThread.cs | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/NodeOutput.cs b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/NodeOutput.cs index c03db0b89..c9ba23a62 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/NodeOutput.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/NodeOutput.cs @@ -23,4 +23,11 @@ public NodeOutput WithJsonPath(string path) return nodeOutput; } + + public NodeOutput WithTimeout(int timeoutSeconds) + { + Parent.AddTimeoutToExtEvt(this, timeoutSeconds); + + return this; + } } \ 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 aeaa7fbc9..1011f63ef 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs @@ -522,4 +522,27 @@ internal VariableAssignment AssignVariableHelper(object? value) return variableAssignment; } + + internal void AddTimeoutToExtEvt(NodeOutput node, int timeoutSeconds) { + CheckIfWorkflowThreadIsActive(); + Node newNode = FindNode(node.NodeName); + + var timeoutValue = new VariableAssignment + { + LiteralValue = new VariableValue { Int = timeoutSeconds } + }; + + if (newNode.NodeCase == Node.NodeOneofCase.Task) + { + newNode.Task.TimeoutSeconds = timeoutSeconds; + } + else if (newNode.NodeCase == Node.NodeOneofCase.ExternalEvent) + { + newNode.ExternalEvent.TimeoutSeconds = timeoutValue; + } + else + { + throw new Exception("Timeouts are only supported on ExternalEvent and Task nodes."); + } + } } \ No newline at end of file From 9408887a995ad72c0092c6d78503eed3736b5d90 Mon Sep 17 00:00:00 2001 From: KarlaCarvajal Date: Tue, 18 Feb 2025 13:56:33 -0500 Subject: [PATCH 2/6] feat(sdk-dotnet): Implement retries in task nodes and timeouts in external events --- .../Workflow/Spec/LHVariableAssigmentTest.cs | 2 +- .../Spec/WorkflowThreadTaskRetriesTest.cs | 66 +++++++++++++++++++ .../Workflow/Spec/WorkflowThreadTest.cs | 8 ++- .../Workflow/Spec/NodeOutput.cs | 6 +- .../Workflow/Spec/TaskNodeOutput.cs | 22 +++++++ .../Workflow/Spec/WorkflowThread.cs | 34 +++++++++- 6 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadTaskRetriesTest.cs create mode 100644 sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/TaskNodeOutput.cs diff --git a/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/LHVariableAssigmentTest.cs b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/LHVariableAssigmentTest.cs index 78e32830d..be3ccd1f4 100644 --- a/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/LHVariableAssigmentTest.cs +++ b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/LHVariableAssigmentTest.cs @@ -55,7 +55,7 @@ public void VariableAssigment_WithWfRunVariableContainingJson_ShouldAssignDetail public void VariableAssigment_WithNodeOutput_ShouldAssignNodeOutputToVariable() { var nodeOutput = new NodeOutput("wait-to-collect-order-data", _parentWfThread); - nodeOutput.JsonPath = "$.order"; + nodeOutput.WithJsonPath("$.order"); var variableAssigment = _parentWfThread.AssignVariableHelper(nodeOutput); diff --git a/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadTaskRetriesTest.cs b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadTaskRetriesTest.cs new file mode 100644 index 000000000..3f4c6e791 --- /dev/null +++ b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadTaskRetriesTest.cs @@ -0,0 +1,66 @@ +using System; +using LittleHorse.Sdk.Common.Proto; +using LittleHorse.Sdk.Workflow.Spec; +using Moq; +using Xunit; + +namespace LittleHorse.Sdk.Tests.Workflow.Spec; + +public class WorkflowThreadTaskRetriesTest +{ + private Action _action; + void ParentEntrypoint(WorkflowThread thread) + { + } + + public WorkflowThreadTaskRetriesTest() + { + LHLoggerFactoryProvider.Initialize(null); + _action = ParentEntrypoint; + } + + [Fact] + public void WfThread_WithRetriesInTaskNode_ShouldCompile() + { + var numberOfExitNodes = 1; + var numberOfEntrypointNodes = 1; + var numberOfTasks = 1; + var workflowName = "TestWorkflow"; + var mockParentWorkflow = new Mock(workflowName, _action); + void EntryPointAction(WorkflowThread wf) + { + wf.Execute("greet").WithRetries(2); + } + var workflowThread = new WorkflowThread(mockParentWorkflow.Object, EntryPointAction); + + var compiledWfThread = workflowThread.Compile(); + + var expectedSpec = new ThreadSpec(); + + var entrypoint = new Node + { + Entrypoint = new EntrypointNode(), + OutgoingEdges = { new Edge { SinkNodeName = "1-greet-TASK" } } + }; + + var greetTask = new Node + { + Task = new TaskNode + { + TaskDefId = new TaskDefId { Name = "greet" }, + Retries = 2 + }, + OutgoingEdges = { new Edge { SinkNodeName = "2-exit-EXIT" } } + }; + + var exitNode = new Node { Exit = new ExitNode() }; + + expectedSpec.Nodes.Add("0-entrypoint-ENTRYPOINT", entrypoint); + expectedSpec.Nodes.Add("1-greet-TASK", greetTask); + expectedSpec.Nodes.Add("2-exit-EXIT", exitNode); + + var expectedNumberOfNodes = numberOfEntrypointNodes + numberOfExitNodes + numberOfTasks; + Assert.Equal(expectedNumberOfNodes, compiledWfThread.Nodes.Count); + Assert.Equal(expectedSpec, compiledWfThread); + } +} \ No newline at end of file diff --git a/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadTest.cs b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadTest.cs index b4b133be5..048e07e53 100644 --- a/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadTest.cs +++ b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadTest.cs @@ -156,18 +156,19 @@ void EntryPointAction(WorkflowThread wf) } [Fact] - public void WfThread_WithExternalEvent_ShouldCompileItInTheWorkflowThread() + public void WfThread_WithExternalEvent_ShouldCompile() { var numberOfExitNodes = 1; var numberOfEntrypointNodes = 1; var numberOfExternalEvents = 1; var numberOfTasks = 1; var workflowName = "TestWorkflow"; + var timeoutInSeconds = 30; var mockParentWorkflow = new Mock(workflowName, _action); void EntryPointAction(WorkflowThread wf) { WfRunVariable name = wf.DeclareStr("name"); - name.Assign(wf.WaitForEvent("name-event")); + name.Assign(wf.WaitForEvent("name-event").WithTimeout(timeoutInSeconds)); wf.Execute("greet", name); } var workflowThread = new WorkflowThread(mockParentWorkflow.Object, EntryPointAction); @@ -188,7 +189,8 @@ void EntryPointAction(WorkflowThread wf) { ExternalEvent = new ExternalEventNode { - ExternalEventDefId = new ExternalEventDefId { Name = "name-event" } + ExternalEventDefId = new ExternalEventDefId { Name = "name-event" }, + TimeoutSeconds = new VariableAssignment { LiteralValue = new VariableValue { Int = timeoutInSeconds }} }, OutgoingEdges = { diff --git a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/NodeOutput.cs b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/NodeOutput.cs index c9ba23a62..76fad0eb2 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/NodeOutput.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/NodeOutput.cs @@ -2,9 +2,9 @@ namespace LittleHorse.Sdk.Workflow.Spec; public class NodeOutput { - public string NodeName { get; set; } - public WorkflowThread Parent { get; set; } - public string? JsonPath { get; set; } + public string NodeName { get; private set; } + public WorkflowThread Parent { get; private set; } + public string? JsonPath { get; private set; } public NodeOutput(string nodeName, WorkflowThread parent) { diff --git a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/TaskNodeOutput.cs b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/TaskNodeOutput.cs new file mode 100644 index 000000000..3dea0bf0c --- /dev/null +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/TaskNodeOutput.cs @@ -0,0 +1,22 @@ +using LittleHorse.Sdk.Common.Proto; + +namespace LittleHorse.Sdk.Workflow.Spec; + +public class TaskNodeOutput : NodeOutput +{ + public TaskNodeOutput(string nodeName, WorkflowThread parent) : base(nodeName, parent) + { + } + + public TaskNodeOutput WithExponentialBackoff(ExponentialBackoffRetryPolicy policy) + { + Parent.OverrideTaskExponentialBackoffPolicy(this, policy); + return this; + } + + public TaskNodeOutput WithRetries(int retries) + { + Parent.OverrideTaskRetries(this, retries); + return this; + } +} \ 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 1011f63ef..c4238fd5a 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs @@ -182,7 +182,7 @@ public WfRunVariable AddVariable(string name, Object typeOrDefaultVal) /// pass that literal value in. /// /// A NodeOutput for that TASK node. - public NodeOutput Execute(string taskName, params object[] args) + public TaskNodeOutput Execute(string taskName, params object[] args) { CheckIfWorkflowThreadIsActive(); _parent.AddTaskDefName(taskName); @@ -190,7 +190,7 @@ public NodeOutput Execute(string taskName, params object[] args) new TaskNode { TaskDefId = new TaskDefId { Name = taskName } }, args); string nodeName = AddNode(taskName, Node.NodeOneofCase.Task, taskNode); - return new NodeOutput(nodeName, this); + return new TaskNodeOutput(nodeName, this); } private VariableAssignment AssignVariable(Object variable) @@ -523,7 +523,8 @@ internal VariableAssignment AssignVariableHelper(object? value) return variableAssignment; } - internal void AddTimeoutToExtEvt(NodeOutput node, int timeoutSeconds) { + internal void AddTimeoutToExtEvt(NodeOutput node, int timeoutSeconds) + { CheckIfWorkflowThreadIsActive(); Node newNode = FindNode(node.NodeName); @@ -545,4 +546,31 @@ internal void AddTimeoutToExtEvt(NodeOutput node, int timeoutSeconds) { throw new Exception("Timeouts are only supported on ExternalEvent and Task nodes."); } } + + internal void OverrideTaskExponentialBackoffPolicy(TaskNodeOutput node, ExponentialBackoffRetryPolicy policy) + { + var newNode = CheckTaskNode(node); + + newNode.Task.ExponentialBackoff = policy; + } + + internal void OverrideTaskRetries(TaskNodeOutput node, int retries) + { + var newNode = CheckTaskNode(node); + + newNode.Task.Retries = retries; + } + + private Node CheckTaskNode(TaskNodeOutput node) + { + CheckIfWorkflowThreadIsActive(); + Node newNode = FindNode(node.NodeName); + + if (newNode.NodeCase != Node.NodeOneofCase.Task) + { + throw new InvalidOperationException("Impossible to not have task node here"); + } + + return newNode; + } } \ No newline at end of file From fb422375b18f87d7760b2c4e44b78a7d2140e1ba Mon Sep 17 00:00:00 2001 From: KarlaCarvajal Date: Wed, 19 Feb 2025 10:12:42 -0500 Subject: [PATCH 3/6] feat(sdk-dotnet): add error handling to workflow thread --- .../ExceptionsHandlerExample/Program.cs | 22 +++++++ .../LittleHorse.Sdk/Workflow/Spec/Workflow.cs | 3 +- .../Workflow/Spec/WorkflowThread.cs | 61 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs b/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs index 9a135dd8a..e252ba21f 100644 --- a/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs +++ b/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs @@ -1,6 +1,7 @@ using ExceptionsHandler; using LittleHorse.Sdk; using LittleHorse.Sdk.Worker; +using LittleHorse.Sdk.Workflow.Spec; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -46,6 +47,24 @@ private static List> GetTaskWorkers(LHConfig config) return workers; } + private static Workflow GetWorkflow() + { + void MyEntryPoint(WorkflowThread wf) + { + NodeOutput node = wf.Execute("fail"); + wf.HandleError( + node, + handler => + { + handler.Execute("my-task"); + } + ); + wf.Execute("my-task"); + } + + return new Workflow("example-exception-handler", MyEntryPoint); + } + static void Main(string[] args) { SetupApplication(); @@ -59,6 +78,9 @@ static void Main(string[] args) worker.RegisterTaskDef(); } + var workflow = GetWorkflow(); + workflow.RegisterWfSpec(config.GetGrpcClientInstance()); + Thread.Sleep(300); foreach (var worker in workers) diff --git a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/Workflow.cs b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/Workflow.cs index 7519a2634..b9ad0f66d 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/Workflow.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/Workflow.cs @@ -1,4 +1,5 @@ using LittleHorse.Sdk.Common.Proto; +using LittleHorse.Sdk.Exceptions; using LittleHorse.Sdk.Helper; using Microsoft.Extensions.Logging; @@ -51,7 +52,7 @@ public void RegisterWfSpec(LittleHorseClient client) _logger!.LogInformation(LHMappingHelper.ProtoToJson(client.PutWfSpec(Compile()))); } - private string AddSubThread(string subThreadName, Action subThreadAction) + internal string AddSubThread(string subThreadName, Action subThreadAction) { foreach (var threadPair in _threadActions) { diff --git a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs index c4238fd5a..fd49386a3 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs @@ -573,4 +573,65 @@ private Node CheckTaskNode(TaskNodeOutput node) return newNode; } + + /// + /// Attaches an Error Handler to the specified NodeOutput, allowing it to manage specific types of errors + /// as defined by the 'error' parameter. If 'error' is set to null, the handler will catch all errors. + /// + /// + /// The NodeOutput instance to which the Error Handler will be attached. + /// + /// + /// The type of error that the handler will manage. + /// + /// + /// A ThreadFunction defining a ThreadSpec that specifies how to handle the error. + /// + public void HandleError(NodeOutput node, LHErrorType error, Action handler) + { + CheckIfWorkflowThreadIsActive(); + var handlerDef = BuildFailureHandlerDef(node, + error.ToString(), + handler); + handlerDef.SpecificFailure = error.ToString(); + AddFailureHandlerDef(handlerDef, node); + } + + /// + /// Attaches an Error Handler to the specified NodeOutput, allowing it to manage any types of errors. + /// + /// + /// + /// TThe NodeOutput instance to which the Error Handler will be attached. + /// + /// + /// A ThreadFunction defining a ThreadSpec that specifies how to handle the error. + /// + public void HandleError(NodeOutput node, Action handler) + { + CheckIfWorkflowThreadIsActive(); + var handlerDef = BuildFailureHandlerDef(node, + "FAILURE_TYPE_ERROR", + handler); + handlerDef.AnyFailureOfType = FailureHandlerDef.Types.LHFailureType.FailureTypeError; + AddFailureHandlerDef(handlerDef, node); + } + + private FailureHandlerDef BuildFailureHandlerDef(NodeOutput node, string error, Action handler) + { + string threadName = $"exn-handler-{node.NodeName}-{error}"; + + threadName = _parent.AddSubThread(threadName, handler); + + return new FailureHandlerDef { HandlerSpecName = threadName }; + } + + private void AddFailureHandlerDef(FailureHandlerDef handlerDef, NodeOutput node) + { + // Add the failure handler to the most recent node + Node lastNode = FindNode(node.NodeName); + + lastNode.FailureHandlers.Add(handlerDef); + //_spec.Nodes.Add(node.NodeName, lastNode); + } } \ No newline at end of file From e3f1b10705189ef1566d18892d2909acd63f3468 Mon Sep 17 00:00:00 2001 From: KarlaCarvajal Date: Wed, 19 Feb 2025 10:21:25 -0500 Subject: [PATCH 4/6] feat(sdk-dotnet): remove comment --- sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs index fd49386a3..ec3974ae6 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs @@ -632,6 +632,5 @@ private void AddFailureHandlerDef(FailureHandlerDef handlerDef, NodeOutput node) Node lastNode = FindNode(node.NodeName); lastNode.FailureHandlers.Add(handlerDef); - //_spec.Nodes.Add(node.NodeName, lastNode); } } \ No newline at end of file From 2ae905adb484334514f49b60107a016ae7929954 Mon Sep 17 00:00:00 2001 From: KarlaCarvajal Date: Wed, 19 Feb 2025 12:48:49 -0500 Subject: [PATCH 5/6] feat(sdk-dotnet): add fail method to workflow thread --- .../Workflow/Spec/WorkflowThread.cs | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs index ec3974ae6..7a1ba576e 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Workflow/Spec/WorkflowThread.cs @@ -590,10 +590,11 @@ private Node CheckTaskNode(TaskNodeOutput node) public void HandleError(NodeOutput node, LHErrorType error, Action handler) { CheckIfWorkflowThreadIsActive(); + var errorFormatted = error.ToString().ToUpper(); var handlerDef = BuildFailureHandlerDef(node, - error.ToString(), + errorFormatted, handler); - handlerDef.SpecificFailure = error.ToString(); + handlerDef.SpecificFailure = errorFormatted; AddFailureHandlerDef(handlerDef, node); } @@ -602,7 +603,7 @@ public void HandleError(NodeOutput node, LHErrorType error, Action /// - /// TThe NodeOutput instance to which the Error Handler will be attached. + /// The NodeOutput instance to which the Error Handler will be attached. /// /// /// A ThreadFunction defining a ThreadSpec that specifies how to handle the error. @@ -617,6 +618,49 @@ public void HandleError(NodeOutput node, Action handler) AddFailureHandlerDef(handlerDef, node); } + /// + /// Adds an EXIT node with a Failure defined. This causes a ThreadRun to fail, and the resulting + /// Failure has the specified value, name, and human-readable message. + /// + /// + /// It is a literal value (cast to VariableValue by the Library) or a WfRunVariable. + /// The assigned value is the payload of the resulting Failure, which can be accessed by any + /// Failure Handler ThreadRuns. + /// + /// + /// It is the name of the failure to throw. + /// + /// + /// It is a human-readable message. + /// + public void Fail(object? output, string failureName, string? message) + { + CheckIfWorkflowThreadIsActive(); + var failureDef = new FailureDef(); + if (output != null) failureDef.Content = AssignVariable(output); + if (message != null) failureDef.Message = message; + failureDef.FailureName = failureName; + + ExitNode exitNode = new ExitNode { FailureDef = failureDef }; + + AddNode(failureName, Node.NodeOneofCase.Exit, exitNode); + } + + /// + /// Adds an EXIT node with a Failure defined. This causes a ThreadRun to fail, and the resulting + /// Failure has the specified name and human-readable message. + /// + /// + /// It is the name of the failure to throw. + /// + /// + /// It is a human-readable message. + /// + public void Fail(string failureName, string message) + { + Fail(null, failureName, message); + } + private FailureHandlerDef BuildFailureHandlerDef(NodeOutput node, string error, Action handler) { string threadName = $"exn-handler-{node.NodeName}-{error}"; From cffb9c1ca7e5f302673c2937a53be2f6cd3fd759 Mon Sep 17 00:00:00 2001 From: KarlaCarvajal Date: Wed, 19 Feb 2025 15:42:19 -0500 Subject: [PATCH 6/6] feat(sdk-dotnet): add unit test to handling errors in workflow thread --- .../WorkflowThreadErrorsAndExceptionsTest.cs | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadErrorsAndExceptionsTest.cs diff --git a/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadErrorsAndExceptionsTest.cs b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadErrorsAndExceptionsTest.cs new file mode 100644 index 000000000..fa0c5c242 --- /dev/null +++ b/sdk-dotnet/LittleHorse.Sdk.Tests/Workflow/Spec/WorkflowThreadErrorsAndExceptionsTest.cs @@ -0,0 +1,174 @@ +using System; +using LittleHorse.Sdk.Common.Proto; +using LittleHorse.Sdk.Workflow.Spec; +using Moq; +using Xunit; + +namespace LittleHorse.Sdk.Tests.Workflow.Spec; + +public class WorkflowThreadErrorsAndExceptionsTest +{ + private readonly Action _action; + void ParentEntrypoint(WorkflowThread thread) + { + } + + public WorkflowThreadErrorsAndExceptionsTest() + { + LHLoggerFactoryProvider.Initialize(null); + _action = ParentEntrypoint; + } + + [Fact] + public void WfThread_WithoutSpecificError_ShouldCompileErrorHandling() + { + 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.HandleError( + node, + handler => + { + handler.Execute("my-task"); + } + ); + wf.Execute("my-task"); + } + var workflowThread = new WorkflowThread(mockParentWorkflow.Object, EntryPointAction); + + var compiledWfThread = workflowThread.Compile(); + + var expectedSpec = new ThreadSpec(); + var entrypoint = new Node + { + Entrypoint = new EntrypointNode(), + OutgoingEdges = + { + new Edge { SinkNodeName = "1-fail-TASK" } + } + }; + + var failTask = new Node + { + Task = new TaskNode + { + TaskDefId = new TaskDefId { Name = "fail" } + }, + OutgoingEdges = { new Edge { SinkNodeName = "2-my-task-TASK" } }, + FailureHandlers = + { + new FailureHandlerDef + { + HandlerSpecName = "exn-handler-1-fail-TASK-FAILURE_TYPE_ERROR", + AnyFailureOfType = FailureHandlerDef.Types.LHFailureType.FailureTypeError + } + } + }; + + var myTask = new Node + { + Task = new TaskNode + { + TaskDefId = new TaskDefId { Name = "my-task" } + }, + OutgoingEdges = { new Edge { SinkNodeName = "3-exit-EXIT" } } + }; + + var exitNode = new Node + { + Exit = new ExitNode() + }; + + 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 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) + { + NodeOutput node = wf.Execute("fail"); + wf.HandleError( + node, + LHErrorType.Timeout, + handler => + { + handler.Execute("my-task"); + } + ); + wf.Execute("my-task"); + } + var workflowThread = new WorkflowThread(mockParentWorkflow.Object, EntryPointAction); + + var compiledWfThread = workflowThread.Compile(); + + var expectedSpec = new ThreadSpec(); + var entrypoint = new Node + { + Entrypoint = new EntrypointNode(), + OutgoingEdges = + { + new Edge { SinkNodeName = "1-fail-TASK" } + } + }; + + var failTask = new Node + { + Task = new TaskNode + { + TaskDefId = new TaskDefId { Name = "fail" } + }, + OutgoingEdges = { new Edge { SinkNodeName = "2-my-task-TASK" } }, + FailureHandlers = + { + new FailureHandlerDef + { + HandlerSpecName = "exn-handler-1-fail-TASK-TIMEOUT", + SpecificFailure = "TIMEOUT" + } + } + }; + + var myTask = new Node + { + Task = new TaskNode + { + TaskDefId = new TaskDefId { Name = "my-task" } + }, + OutgoingEdges = { new Edge { SinkNodeName = "3-exit-EXIT" } } + }; + + var exitNode = new Node + { + Exit = new ExitNode() + }; + + 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 expectedNumberOfNodes = numberOfEntrypointNodes + numberOfExitNodes + numberOfTasks; + Assert.Equal(expectedNumberOfNodes, compiledWfThread.Nodes.Count); + Assert.Equal(expectedSpec, compiledWfThread); + } +} \ No newline at end of file