diff --git a/.gitignore b/.gitignore index dfd1ec023..98c5d40eb 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ dist/ # dotnet global.json -*DotSettings.user \ No newline at end of file +*DotSettings.user +!sdk-dotnet/Littlehorse.sln +*.sln \ No newline at end of file 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..810abaaa3 --- /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..4f84d0919 100644 --- a/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs +++ b/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs @@ -1,8 +1,9 @@ -using Examples.BasicExample; using LittleHorse.Sdk; using LittleHorse.Sdk.Worker; -public class Program +namespace MaskedFieldsExample; + +public abstract class Program { private static ServiceProvider? _serviceProvider; private static void SetupApplication() @@ -26,6 +27,19 @@ private static LHConfig GetLHConfig(string[] args, ILoggerFactory loggerFactory) return config; } + + private static List> GetTaskWorkers(LHConfig config) + { + MyWorker executableExceptionHandling = new MyWorker(); + var workers = new List> + { + new(executableExceptionHandling, "create-greet", config), + new(executableExceptionHandling, "update-greet", config), + new(executableExceptionHandling, "delete-greet", config) + }; + + return workers; + } static void Main(string[] args) { @@ -34,23 +48,18 @@ static void Main(string[] args) { var loggerFactory = _serviceProvider.GetRequiredService(); var config = GetLHConfig(args, loggerFactory); + var workers = GetTaskWorkers(config); + foreach (var worker in workers) + { + worker.RegisterTaskDef(); + } - MyWorker executableCreateGreet = new MyWorker(); - var taskWorkerCreate = new LHTaskWorker(executableCreateGreet, "create-greet", config); - MyWorker executableUpdateGreet = new MyWorker(); - var taskWorkerUpdate = new LHTaskWorker(executableUpdateGreet, "update-greet", config); - MyWorker executableDeleteGreet = new MyWorker(); - var taskWorkerDelete = new LHTaskWorker(executableDeleteGreet, "delete-greet", config); - - taskWorkerCreate.RegisterTaskDef(); - taskWorkerUpdate.RegisterTaskDef(); - taskWorkerDelete.RegisterTaskDef(); + Thread.Sleep(300); - Thread.Sleep(1000); - - taskWorkerCreate.Start(); - taskWorkerUpdate.Start(); - taskWorkerDelete.Start(); + foreach (var worker in workers) + { + worker.Start(); + } } } -} +} \ No newline at end of file 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..e9cea017c 100644 --- a/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs +++ b/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs @@ -70,6 +70,16 @@ public void LHHelper_WithSystemBytesVariableType_ShouldReturnLHVariableBytesType Assert.True(result == VariableType.Bytes); } + [Fact] + public void LHHelper_WithSystemVoidVariableType_ShouldReturnLHVariableJsonObjType() + { + var type = typeof(void); + + var result = LHMappingHelper.MapDotNetTypeToLHVariableType(type); + + Assert.True(result == VariableType.JsonObj); + } + [Fact] public void LHHelper_WithSystemArrayObjectVariableType_ShouldReturnLHVariableJsonArrType() { @@ -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 new file mode 100644 index 000000000..2c7b8b57e --- /dev/null +++ b/sdk-dotnet/LittleHorse.Sdk/Exceptions/LHTaskException.cs @@ -0,0 +1,22 @@ +using LittleHorse.Common.Proto; + +namespace LittleHorse.Sdk.Exceptions; + +public class LHTaskException: Exception +{ + public string Name { get; } + + public VariableValue Content { get; } + + public LHTaskException(String name, String message): base(message) + { + Name = name; + Content = new VariableValue(); + } + + public LHTaskException(String name, String message, VariableValue content): base(message) + { + 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..39da40464 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs @@ -1,10 +1,12 @@ 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 TaskStatus = LittleHorse.Common.Proto.TaskStatus; using Type = System.Type; namespace LittleHorse.Sdk.Helper @@ -17,34 +19,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 +78,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 +212,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/LittleHorse.Sdk.sln b/sdk-dotnet/LittleHorse.Sdk/LittleHorse.Sdk.sln deleted file mode 100644 index cc965db66..000000000 --- a/sdk-dotnet/LittleHorse.Sdk/LittleHorse.Sdk.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LittleHorse.Sdk", "LittleHorse.Sdk.csproj", "{F47CBA48-D52D-4DD8-8D17-4F413C3CE469}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F47CBA48-D52D-4DD8-8D17-4F413C3CE469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F47CBA48-D52D-4DD8-8D17-4F413C3CE469}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F47CBA48-D52D-4DD8-8D17-4F413C3CE469}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F47CBA48-D52D-4DD8-8D17-4F413C3CE469}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {BBFC2829-71EA-411A-AD2D-D4791D38201D} - EndGlobalSection -EndGlobal diff --git a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs index 10d3f9bc3..e50882420 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Polly; using static LittleHorse.Common.Proto.LittleHorse; +using LHTaskException = LittleHorse.Sdk.Exceptions.LHTaskException; using TaskStatus = LittleHorse.Common.Proto.TaskStatus; namespace LittleHorse.Sdk.Worker.Internal @@ -218,18 +219,56 @@ 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.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) { _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..02e958833 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs @@ -32,7 +32,7 @@ public VariableMapping(TaskDef taskDef, int position, Type type, string? paramNa public object? Assign(ScheduledTask taskInstance, LHWorkerContext workerContext) { - if (_type.GetType() == typeof(LHWorkerContext)) + if (_type == typeof(LHWorkerContext)) { return workerContext; } @@ -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; }