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;
}