From 581c30aafa68cdcd1747696519817c73bdbab08e Mon Sep 17 00:00:00 2001 From: KarlaCarvajal Date: Wed, 27 Nov 2024 13:08:03 -0500 Subject: [PATCH] (dotnet-sdk): Fix error and exception handling and add example to test escenarios --- .../ExceptionsHandlerExample.csproj | 14 ++++ .../ExceptionsHandlerExample/Program.cs | 70 +++++++++++++++++++ .../ExceptionsHandlerExample/README.md | 31 ++++++++ .../ExceptionsHandlerExample/Worker.cs | 56 +++++++++++++++ .../MaskedFieldsExample.csproj | 1 - .../Examples/MaskedFieldsExample/Program.cs | 2 +- .../Examples/MaskedFieldsExample/Worker.cs | 2 +- .../Helper/LHMappingHelperTest.cs | 17 +++-- .../Exceptions/LHTaskException.cs | 13 ++-- .../LittleHorse.Sdk/Helper/LHMappingHelper.cs | 57 +++++++++++---- .../Internal/LHServerConnectionManager.cs | 31 +++++++- .../LittleHorse.Sdk/Worker/VariableMapping.cs | 2 +- 12 files changed, 268 insertions(+), 28 deletions(-) create mode 100644 sdk-dotnet/Examples/ExceptionsHandlerExample/ExceptionsHandlerExample.csproj create mode 100644 sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs create mode 100644 sdk-dotnet/Examples/ExceptionsHandlerExample/README.md create mode 100644 sdk-dotnet/Examples/ExceptionsHandlerExample/Worker.cs diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/ExceptionsHandlerExample.csproj b/sdk-dotnet/Examples/ExceptionsHandlerExample/ExceptionsHandlerExample.csproj new file mode 100644 index 000000000..1cb816f31 --- /dev/null +++ b/sdk-dotnet/Examples/ExceptionsHandlerExample/ExceptionsHandlerExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs b/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs new file mode 100644 index 000000000..9a135dd8a --- /dev/null +++ b/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs @@ -0,0 +1,70 @@ +using ExceptionsHandler; +using LittleHorse.Sdk; +using LittleHorse.Sdk.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ExceptionsHandlerExample; + +public abstract class Program +{ + private static ServiceProvider? _serviceProvider; + + private static void SetupApplication() + { + _serviceProvider = new ServiceCollection() + .AddLogging(config => + { + config.AddConsole(); + config.SetMinimumLevel(LogLevel.Debug); + }) + .BuildServiceProvider(); + } + + private static LHConfig GetLHConfig(string[] args, ILoggerFactory loggerFactory) + { + var config = new LHConfig(loggerFactory); + + string filePath = Path.Combine(Directory.GetCurrentDirectory(), ".config/littlehorse.config"); + if (File.Exists(filePath)) + config = new LHConfig(filePath, loggerFactory); + + return config; + } + + private static List> GetTaskWorkers(LHConfig config) + { + MyWorker executableExceptionHandling = new MyWorker(); + var workers = new List> + { + new(executableExceptionHandling, "fail", config), + new(executableExceptionHandling, "fail-new-process", config), + new(executableExceptionHandling, "technical-failure", config), + new(executableExceptionHandling, "my-task", config) + }; + + return workers; + } + + static void Main(string[] args) + { + SetupApplication(); + if (_serviceProvider != null) + { + var loggerFactory = _serviceProvider.GetRequiredService(); + var config = GetLHConfig(args, loggerFactory); + var workers = GetTaskWorkers(config); + foreach (var worker in workers) + { + worker.RegisterTaskDef(); + } + + Thread.Sleep(300); + + foreach (var worker in workers) + { + worker.Start(); + } + } + } +} \ No newline at end of file diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/README.md b/sdk-dotnet/Examples/ExceptionsHandlerExample/README.md new file mode 100644 index 000000000..33c8e73ac --- /dev/null +++ b/sdk-dotnet/Examples/ExceptionsHandlerExample/README.md @@ -0,0 +1,31 @@ +## Running Exceptions Handler Example + +This is a simple demonstration of a workflow that handles the failure of a task with +the handleException() functionality, which spawns a child thread and then +resumes execution when the handler thread completes. + +Let's run the example in `ExceptionsHandlerExample` + +``` +dotnet build +dotnet run +``` + +In another terminal, use `lhctl` to run the workflow: + +``` +lhctl run example-exception-handler +``` + +In addition, you can check the result with: + +``` +# This call shows the result +lhctl get wfRun + +# This will show you all nodes in tha run +lhctl list nodeRun + +# This shows the task run information +lhctl get taskRun +``` diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/Worker.cs b/sdk-dotnet/Examples/ExceptionsHandlerExample/Worker.cs new file mode 100644 index 000000000..efc3c2961 --- /dev/null +++ b/sdk-dotnet/Examples/ExceptionsHandlerExample/Worker.cs @@ -0,0 +1,56 @@ +using LittleHorse.Common.Proto; +using LittleHorse.Sdk.Worker; +using LHTaskException = LittleHorse.Sdk.Exceptions.LHTaskException; + +namespace ExceptionsHandler +{ + public class MyWorker + { + [LHTaskMethod("fail")] + public void Fail() + { + Random random = new Random(); + int randomNumber = random.Next(6, 10); + var message = $"Throw New Failing Task {randomNumber}."; + if (randomNumber > 5) + { + throw new LHTaskException("Fail", message); + } + + Console.WriteLine(message); + } + + [LHTaskMethod("fail-new-process")] + public void FailNewProcess() + { + Random random = new Random(); + int randomNumber = random.Next(1, 10); + var message = $"Throw Other Failing Task {randomNumber}"; + if (randomNumber < 8) + { + VariableValue content = new VariableValue + { + Str = "This is a problem" + }; + throw new LHTaskException("Fail-New-Task", message, content); + } + + Console.WriteLine(message); + } + + [LHTaskMethod("technical-failure")] + public void FailForTechnicalReason() + { + String message = null!; + int result = message!.Length; + Console.WriteLine(result); + } + + [LHTaskMethod("my-task")] + public string PassingTask() + { + Console.WriteLine("Executing passing task."); + return "woohoo!"; + } + } +} \ No newline at end of file diff --git a/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj b/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj index f09e3ff5c..cb535cc93 100644 --- a/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj +++ b/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj @@ -4,7 +4,6 @@ net8.0 enable enable - enable diff --git a/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs b/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs index 7a378c616..b08038a39 100644 --- a/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs +++ b/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs @@ -1,6 +1,6 @@ -using Examples.BasicExample; using LittleHorse.Sdk; using LittleHorse.Sdk.Worker; +using MaskedFieldsExample; public class Program { diff --git a/sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs b/sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs index 9760e3363..c7f6f4e0f 100644 --- a/sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs +++ b/sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs @@ -1,6 +1,6 @@ using LittleHorse.Sdk.Worker; -namespace Examples.BasicExample +namespace MaskedFieldsExample { public class MyWorker { diff --git a/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs b/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs index 1a2691164..bfa10be77 100644 --- a/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs +++ b/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs @@ -72,6 +72,16 @@ public void LHHelper_WithSystemBytesVariableType_ShouldReturnLHVariableBytesType [Fact] public void LHHelper_WithSystemArrayObjectVariableType_ShouldReturnLHVariableJsonArrType() + { + var type = typeof(void); + + var result = LHMappingHelper.MapDotNetTypeToLHVariableType(type); + + Assert.True(result == VariableType.JsonObj); + } + + [Fact] + public void LHHelper_WithSystemVoidVariableType_ShouldReturnLHVariableJsonObjType() { var test_allowed_types = new List() { typeof(List), typeof(List), typeof(List)}; @@ -145,12 +155,11 @@ public void LHHelper_WithVariableValue_ShouldReturnSameValue() } [Fact] - public void LHHelper_WithNullLHVariableValue_ShouldThrowException() + public void LHHelper_WithNullLHVariableValue_ShouldReturnNewLHVariableValue() { - var exception = Assert.Throws - (() => LHMappingHelper.MapObjectToVariableValue(null)); + var result = LHMappingHelper.MapObjectToVariableValue(null); - Assert.Equal($"There is no object to be mapped.", exception.Message); + Assert.NotNull(result); } [Fact] diff --git a/sdk-dotnet/LittleHorse.Sdk/Exceptions/LHTaskException.cs b/sdk-dotnet/LittleHorse.Sdk/Exceptions/LHTaskException.cs index 013fdd748..2c7b8b57e 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Exceptions/LHTaskException.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Exceptions/LHTaskException.cs @@ -4,18 +4,19 @@ namespace LittleHorse.Sdk.Exceptions; public class LHTaskException: Exception { - private String name; - private VariableValue content; + public string Name { get; } + public VariableValue Content { get; } + public LHTaskException(String name, String message): base(message) { - this.name = name; - this.content = null; + Name = name; + Content = new VariableValue(); } public LHTaskException(String name, String message, VariableValue content): base(message) { - this.name = name; - this.content = content; + Name = name; + Content = content; } } \ No newline at end of file diff --git a/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs b/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs index 118be46cf..798cd7870 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs @@ -1,10 +1,13 @@ using System.Collections; +using System.Net; using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using LittleHorse.Common.Proto; using LittleHorse.Sdk.Exceptions; using LittleHorse.Sdk.Utils; using LittleHorse.Sdk.Worker; +using Microsoft.AspNetCore.Identity; +using TaskStatus = LittleHorse.Common.Proto.TaskStatus; using Type = System.Type; namespace LittleHorse.Sdk.Helper @@ -17,34 +20,43 @@ public static VariableType MapDotNetTypeToLHVariableType(Type type) { return VariableType.Int; } - else if (IsFloat(type)) + + if (IsFloat(type)) { return VariableType.Double; } - else if (type.IsAssignableFrom(typeof(string))) + + if (type.IsAssignableFrom(typeof(string))) { return VariableType.Str; } - else if (type.IsAssignableFrom(typeof(bool))) + + if (type.IsAssignableFrom(typeof(bool))) { return VariableType.Bool; } - else if (type.IsAssignableFrom(typeof(byte[]))) + + if (type.IsAssignableFrom(typeof(byte[]))) { return VariableType.Bytes; } - else if (typeof(IEnumerable).IsAssignableFrom(type)) + + if (typeof(IEnumerable).IsAssignableFrom(type)) { return VariableType.JsonArr; } - else if (!type.Namespace!.StartsWith("System")) + + if (!type.Namespace!.StartsWith("System")) { return VariableType.JsonObj; } - else + + if (type.IsAssignableFrom(typeof(void))) { - throw new Exception("Unaccepted variable type."); + return VariableType.JsonObj; } + + throw new Exception("Unaccepted variable type."); } public static DateTime? MapDateTimeFromProtoTimeStamp(Timestamp protoTimestamp) @@ -67,10 +79,7 @@ public static VariableValue MapObjectToVariableValue(object? obj) if (obj is VariableValue variableValue) return variableValue; var result = new VariableValue(); - if (obj == null) - { - throw new LHInputVarSubstitutionException("There is no object to be mapped."); - } + if (obj == null) {} else if (IsIntObject(obj)) { result.Int = GetIntegralValue(obj); @@ -204,12 +213,34 @@ private static Double GetFloatingValue(object obj) }; } - public static bool isInt64Type(Type type) + public static bool IsInt64Type(Type type) { return type.IsAssignableFrom(typeof(Int64)) || type.IsAssignableFrom(typeof(UInt64)) || type.IsAssignableFrom(typeof(long)) || type.IsAssignableFrom(typeof(ulong)); } + + public static LHErrorType GetFailureCodeFor(TaskStatus status) + { + switch (status) { + case TaskStatus.TaskFailed: + return LHErrorType.TaskFailure; + case TaskStatus.TaskTimeout: + return LHErrorType.Timeout; + case TaskStatus.TaskOutputSerializingError: + return LHErrorType.VarMutationError; + case TaskStatus.TaskInputVarSubError: + return LHErrorType.VarSubError; + case TaskStatus.TaskRunning: + case TaskStatus.TaskScheduled: + case TaskStatus.TaskSuccess: + case TaskStatus.TaskPending: + case TaskStatus.TaskException: // TASK_EXCEPTION is NOT a technical ERROR, so this fails. + break; + } + + throw new ArgumentException($"Unexpected task status: {status}");; + } } } diff --git a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs index 536725db6..e50882420 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs @@ -219,20 +219,45 @@ private ReportTaskRun ExecuteTask(ScheduledTask scheduledTask, DateTime? schedul _logger?.LogError(ex, "Failed calculating task input variables"); taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); taskResult.Status = TaskStatus.TaskInputVarSubError; + taskResult.Error = new LHTaskError + { + Message = ex.ToString(), Type = LHMappingHelper.GetFailureCodeFor(taskResult.Status) + }; } catch (LHSerdeException ex) { _logger?.LogError(ex, "Failed serializing Task Output"); taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); taskResult.Status = TaskStatus.TaskOutputSerializingError; + taskResult.Error = new LHTaskError + { + Message = ex.ToString(), Type = LHMappingHelper.GetFailureCodeFor(taskResult.Status) + }; } catch (TargetInvocationException ex) { - if (ex is LHTaskException) + if (ex.GetBaseException() is LHTaskException taskException) { _logger?.LogError(ex, "Task Method threw a Business Exception"); taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); taskResult.Status = TaskStatus.TaskException; + taskResult.Exception = new Common.Proto.LHTaskException + { + Name = taskException.Name, + Message = taskException.Message, + Content = taskException.Content + }; + } + else + { + _logger?.LogError(ex, "Task Method threw an exception"); + taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); + taskResult.Status = TaskStatus.TaskFailed; + taskResult.Error = new LHTaskError + { + Message = ex.InnerException!.ToString(), + Type = LHMappingHelper.GetFailureCodeFor(taskResult.Status) + }; } } catch (Exception ex) @@ -240,6 +265,10 @@ private ReportTaskRun ExecuteTask(ScheduledTask scheduledTask, DateTime? schedul _logger?.LogError(ex, "Unexpected exception during task execution"); taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); taskResult.Status = TaskStatus.TaskFailed; + taskResult.Error = new LHTaskError + { + Message = ex.ToString(), Type = LHMappingHelper.GetFailureCodeFor(taskResult.Status) + }; } taskResult.Time = Timestamp.FromDateTime(DateTime.UtcNow); diff --git a/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs b/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs index 36514461b..5890fe2ee 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs @@ -45,7 +45,7 @@ public VariableMapping(TaskDef taskDef, int position, Type type, string? paramNa switch (val.ValueCase) { case VariableValue.ValueOneofCase.Int: - if (LHMappingHelper.isInt64Type(_type)) + if (LHMappingHelper.IsInt64Type(_type)) { return val.Int; }