diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/Worker.cs b/sdk-dotnet/Examples/ExceptionsHandlerExample/MyWorker.cs similarity index 100% rename from sdk-dotnet/Examples/ExceptionsHandlerExample/Worker.cs rename to sdk-dotnet/Examples/ExceptionsHandlerExample/MyWorker.cs diff --git a/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj b/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj index cb535cc93..b68386455 100644 --- a/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj +++ b/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj @@ -1,6 +1,7 @@ + Exe net8.0 enable enable diff --git a/sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs b/sdk-dotnet/Examples/MaskedFieldsExample/MyWorker.cs similarity index 100% rename from sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs rename to sdk-dotnet/Examples/MaskedFieldsExample/MyWorker.cs diff --git a/sdk-dotnet/Examples/WorkerContextExample/MyWorker.cs b/sdk-dotnet/Examples/WorkerContextExample/MyWorker.cs new file mode 100644 index 000000000..612225a2b --- /dev/null +++ b/sdk-dotnet/Examples/WorkerContextExample/MyWorker.cs @@ -0,0 +1,21 @@ +using LittleHorse.Sdk.Worker; + +namespace WorkerContextExample; + +public class MyWorker +{ + [LHTaskMethod("task")] + public void ProcessTask(long requestTime, LHWorkerContext context) + { + context.Log("ProcessPayment"); + Console.WriteLine($"Processing request time: {requestTime}"); + Console.WriteLine($"The Workflow Run Id is: {context.GetWfRunId()}"); + Console.WriteLine($"The Node Run Id is: {context.GetNodeRunId()}"); + Console.WriteLine($"The Task Run Id is: {context.GetTaskRunId()}"); + Console.WriteLine($"The Idempotency Key is: {context.GetIdempotencyKey()}"); + Console.WriteLine($"The Attempt Number is: {context.GetAttemptNumber()}"); + Console.WriteLine($"The Scheduled Time is: {context.GetScheduledTime()}"); + Console.WriteLine($"The User Group is: {context.GetUserGroup()}"); + Console.WriteLine($"The User Id is: {context.GetUserId()}"); + } +} \ No newline at end of file diff --git a/sdk-dotnet/Examples/WorkerContextExample/Program.cs b/sdk-dotnet/Examples/WorkerContextExample/Program.cs new file mode 100644 index 000000000..880850a0a --- /dev/null +++ b/sdk-dotnet/Examples/WorkerContextExample/Program.cs @@ -0,0 +1,65 @@ +using LittleHorse.Sdk; +using LittleHorse.Sdk.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace WorkerContextExample; + +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, "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/WorkerContextExample/README.md b/sdk-dotnet/Examples/WorkerContextExample/README.md new file mode 100644 index 000000000..10509c7bf --- /dev/null +++ b/sdk-dotnet/Examples/WorkerContextExample/README.md @@ -0,0 +1,45 @@ +## Running WorkerContextExample + +This example shows how to get access to the context when executing a task. +Go to the class `MyWorker`. + +Let's run the example in `WorkerContextExample` + +``` +dotnet build +dotnet run +``` + +In another terminal, use `lhctl` to run the workflow: + +``` +lhctl run example-worker-context +``` + +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 +``` + +## Considerations + +If you need get access to the context you have to add a `LHWorkerContext` +parameter to the signature of your task: + +``` + [LHTaskMethod("task")] + public void ProcessTask(long requestTime, LHWorkerContext context) + { + ... + } +``` + +The `WorkerContext` should be the last parameter. \ No newline at end of file diff --git a/sdk-dotnet/Examples/WorkerContextExample/WorkerContextExample.csproj b/sdk-dotnet/Examples/WorkerContextExample/WorkerContextExample.csproj new file mode 100644 index 000000000..1cb816f31 --- /dev/null +++ b/sdk-dotnet/Examples/WorkerContextExample/WorkerContextExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/sdk-dotnet/LittleHorse.Sdk/Helper/LHHelper.cs b/sdk-dotnet/LittleHorse.Sdk/Helper/LHHelper.cs new file mode 100644 index 000000000..76208d884 --- /dev/null +++ b/sdk-dotnet/LittleHorse.Sdk/Helper/LHHelper.cs @@ -0,0 +1,41 @@ +using System.Text; +using LittleHorse.Common.Proto; + +namespace LittleHorse.Sdk.Helper +{ + public static class LHHelper + { + public static WfRunId GetWFRunId(TaskRunSource taskRunSource) + { + switch (taskRunSource.TaskRunSourceCase) + { + case TaskRunSource.TaskRunSourceOneofCase.TaskNode: + return taskRunSource.TaskNode.NodeRunId.WfRunId; + case TaskRunSource.TaskRunSourceOneofCase.UserTaskTrigger: + return taskRunSource.UserTaskTrigger.NodeRunId.WfRunId; + default: + return null; + } + } + + public static string TaskRunIdToString(TaskRunId taskRunId) + { + return $"{taskRunId.WfRunId}/{taskRunId.TaskGuid}"; + } + + private static string ParseWfRunIdToString(WfRunId wfRunId) { + var output = new StringBuilder(); + if (wfRunId.ParentWfRunId != null) { + output.Append(ParseWfRunIdToString(wfRunId.ParentWfRunId)); + output.Append("_"); + } + output.Append(wfRunId.Id); + + return output.ToString(); + } + + public static String ParseTaskRunIdToString(TaskRunId taskRunId) { + return ParseWfRunIdToString(taskRunId.WfRunId) + "/" + taskRunId.TaskGuid; + } + } +} diff --git a/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs b/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs index 9705fc8a4..da4738bcf 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs @@ -201,7 +201,7 @@ private static Double GetFloatingValue(object obj) }; } - public static bool IsInt64Type(Type type) + internal static bool IsInt64Type(Type type) { return type.IsAssignableFrom(typeof(Int64)) || type.IsAssignableFrom(typeof(UInt64)) @@ -209,7 +209,7 @@ public static bool IsInt64Type(Type type) || type.IsAssignableFrom(typeof(ulong)); } - public static LHErrorType GetFailureCodeFor(TaskStatus status) + internal static LHErrorType GetFailureCodeFor(TaskStatus status) { switch (status) { case TaskStatus.TaskFailed: diff --git a/sdk-dotnet/LittleHorse.Sdk/Helper/LHWorkerHelper.cs b/sdk-dotnet/LittleHorse.Sdk/Helper/LHWorkerHelper.cs deleted file mode 100644 index 44c29c9ab..000000000 --- a/sdk-dotnet/LittleHorse.Sdk/Helper/LHWorkerHelper.cs +++ /dev/null @@ -1,25 +0,0 @@ -using LittleHorse.Common.Proto; - -namespace LittleHorse.Sdk.Helper -{ - public static class LHWorkerHelper - { - public static string? GetWFRunId(TaskRunSource taskRunSource) - { - switch (taskRunSource.TaskRunSourceCase) - { - case TaskRunSource.TaskRunSourceOneofCase.TaskNode: - return taskRunSource.TaskNode.NodeRunId.WfRunId.ToString(); - case TaskRunSource.TaskRunSourceOneofCase.UserTaskTrigger: - return taskRunSource.UserTaskTrigger.NodeRunId.WfRunId.ToString(); - default: - return null; - } - } - - public static string TaskRunIdToString(TaskRunId taskRunId) - { - return $"{taskRunId.WfRunId}/{taskRunId.TaskGuid}"; - } - } -} diff --git a/sdk-dotnet/LittleHorse.Sdk/README.md b/sdk-dotnet/LittleHorse.Sdk/README.md index a062881e2..a93e8faa6 100644 --- a/sdk-dotnet/LittleHorse.Sdk/README.md +++ b/sdk-dotnet/LittleHorse.Sdk/README.md @@ -31,7 +31,7 @@ brew install dotnet ### Build ``` -cd sdk-dotnet/LittleHorse.Sdk +cd sdk-dotnet dotnet build ./LittleHorse.Sdk ``` ### Build and Run tests @@ -41,10 +41,13 @@ dotnet build ./LittleHorse.Sdk dotnet test ./LittleHorse.Sdk.Tests ``` -### Run Example +### Run Examples ``` dotnet run --project ./Examples/BasicExample +dotnet run --project ./Examples/ExceptionsHandlerExample +dotnet run --project ./Examples/MaskedFieldsExample +dotnet run --project ./Examples/WorkerContextExample ``` ### Self-signed TLS certificate diff --git a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnection.cs b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnection.cs index 671fe26db..774119c37 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnection.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnection.cs @@ -48,12 +48,12 @@ private async Task RequestMoreWorkAsync() if (taskToDo.Result != null) { var scheduledTask = taskToDo.Result; - var wFRunId = LHWorkerHelper.GetWFRunId(scheduledTask.Source); - _logger?.LogDebug($"Received task schedule request for wfRun {wFRunId}"); + var wFRunId = LHHelper.GetWFRunId(scheduledTask.Source); + _logger?.LogDebug($"Received task schedule request for wfRun {wFRunId.Id}"); _connectionManager.SubmitTaskForExecution(scheduledTask, _client); - _logger?.LogDebug($"Scheduled task on threadpool for wfRun {wFRunId}"); + _logger?.LogDebug($"Scheduled task on threadpool for wfRun {wFRunId.Id}"); } else { diff --git a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs index e50882420..381a3d70e 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs @@ -158,20 +158,20 @@ private void DoTask(ScheduledTask scheduledTask, LittleHorseClient client) ReportTaskRun result = ExecuteTask(scheduledTask, LHMappingHelper.MapDateTimeFromProtoTimeStamp(scheduledTask.CreatedAt)); _semaphore.Release(); - var wfRunId = LHWorkerHelper.GetWFRunId(scheduledTask.Source); + var wfRunId = LHHelper.GetWFRunId(scheduledTask.Source); try { var retriesLeft = MAX_REPORT_RETRIES; - _logger?.LogDebug($"Going to report task for wfRun {wfRunId}"); + _logger?.LogDebug($"Going to report task for wfRun {wfRunId.Id}"); Policy.Handle().WaitAndRetry(MAX_REPORT_RETRIES, retryAttempt => TimeSpan.FromSeconds(5), onRetry: (exception, timeSpan, retryCount, context) => { --retriesLeft; _logger?.LogDebug($"Failed to report task for wfRun {wfRunId}: {exception.Message}. Retries left: {retriesLeft}"); - _logger?.LogDebug($"Retrying reportTask rpc on taskRun {LHWorkerHelper.TaskRunIdToString(result.TaskRunId)}"); + _logger?.LogDebug($"Retrying reportTask rpc on taskRun {LHHelper.TaskRunIdToString(result.TaskRunId)}"); }).Execute(() => RunReportTask(result)); } catch (Exception ex) diff --git a/sdk-dotnet/LittleHorse.Sdk/Worker/LHWorkerContext.cs b/sdk-dotnet/LittleHorse.Sdk/Worker/LHWorkerContext.cs index 4ed6b658a..4850f59b7 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/LHWorkerContext.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/LHWorkerContext.cs @@ -1,4 +1,6 @@ -using LittleHorse.Common.Proto; +using System.Text; +using LittleHorse.Common.Proto; +using LittleHorse.Sdk.Helper; namespace LittleHorse.Sdk.Worker { @@ -24,6 +26,60 @@ public LHWorkerContext(ScheduledTask scheduleTask, DateTime? scheduleDateTime) _scheduleTask = scheduleTask; _scheduleDateTime = scheduleDateTime; } + + /// + /// Returns the Id of the WfRun for the NodeRun that's being executed. + /// + /// + /// the Id of the WfRun for the NodeRun that's being executed. + /// + public WfRunId GetWfRunId() + { + return LHHelper.GetWFRunId(_scheduleTask.Source); + } + + /// + /// Returns the NodeRun ID for the Task that was just scheduled. + /// + /// + /// @return a `NodeRunIdPb` protobuf class with the ID from the executed NodeRun. + /// + public NodeRunId GetNodeRunId() + { + TaskRunSource source = _scheduleTask.Source; + switch (source.TaskRunSourceCase) { + case TaskRunSource.TaskRunSourceOneofCase.TaskNode: + return source.TaskNode.NodeRunId; + case TaskRunSource.TaskRunSourceOneofCase.UserTaskTrigger: + return source.UserTaskTrigger.NodeRunId; + } + return null; + } + + /// + /// Returns the attemptNumber of the NodeRun that's being executed. If this is the first attempt, + /// returns zero. If this is the first retry, returns 1, and so on. + /// + /// + /// @return the attempt number of the NodeRun that's being executed. + /// + public int GetAttemptNumber() + { + return _scheduleTask.AttemptNumber; + } + + /// + /// Returns the time at which the task was scheduled by the processor. May be useful in certain + /// customer edge cases, eg. to determine whether it's too late to actually perform an action, + /// when (now() - getScheduledTime()) is above some threshold, etc. + /// + /// + /// @return the time at which the current NodeRun was scheduled. + /// + public DateTime? GetScheduledTime() + { + return _scheduleDateTime; + } /// /// Provides a way to push data into the log output. Any object may be passed in; its string @@ -41,7 +97,69 @@ public void Log(object item) LogOutput += "null"; } } + + /// + /// Returns the TaskRunId of this TaskRun. + /// + /// + /// @return the associated TaskRunId. + /// + public TaskRunId GetTaskRunId() + { + return _scheduleTask.TaskRunId; + } + private UserTaskTriggerReference? GetUserTaskTrigger() + { + return _scheduleTask.Source.UserTaskTrigger; + } + + /// + /// If this TaskRun is a User Task Reminder TaskRun, then this method returns the + /// UserId of the user who the associated UserTask is assigned to. Returns + /// null if: + /// - this TaskRun is not a Reminder Task + /// - this TaskRun is a Reminder Task, but the UserTaskRun does not have an assigned + /// user id. + /// + /// + /// @return the id of the user that the associated UserTask is assigned to. + /// + public string? GetUserId() + { + UserTaskTriggerReference? uttr = GetUserTaskTrigger(); + if (uttr == null) return null; + return uttr.HasUserId ? uttr.UserId : null; + } + + /// + /// If this TaskRun is a User Task Reminder TaskRun, then this method returns the + /// UserGroup that the associated UserTask is assigned to. Returns null if: + /// - this TaskRun is not a Reminder Task + /// - this TaskRun is a Reminder Task, but the UserTaskRun does not have an + /// associated User Group + /// + /// + /// @return the id of the User Group that the associated UserTask is assigned to. + /// + public string? GetUserGroup() { + UserTaskTriggerReference? uttr = GetUserTaskTrigger(); + if (uttr == null) return null; + + return uttr.HasUserGroup ? uttr.UserGroup : null; + } + + /// + /// Returns an idempotency key that can be used to make calls to upstream api's idempotent across + /// TaskRun Retries. + /// + /// + /// @return an idempotency key. + /// + public String GetIdempotencyKey() + { + return LHHelper.ParseTaskRunIdToString(GetTaskRunId()); + } } } diff --git a/sdk-dotnet/Littlehorse.sln b/sdk-dotnet/Littlehorse.sln index 23959b58f..066e17336 100644 --- a/sdk-dotnet/Littlehorse.sln +++ b/sdk-dotnet/Littlehorse.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleHorse.Sdk.Tests", "Li EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExceptionsHandlerExample", "Examples\ExceptionsHandlerExample\ExceptionsHandlerExample.csproj", "{6B7A8034-21C0-465D-8708-4F47A1780972}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkerContextExample", "Examples\WorkerContextExample\WorkerContextExample.csproj", "{93DB9DF9-A48B-436F-8892-AE4CAD42323E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,10 +46,15 @@ Global {6B7A8034-21C0-465D-8708-4F47A1780972}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B7A8034-21C0-465D-8708-4F47A1780972}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B7A8034-21C0-465D-8708-4F47A1780972}.Release|Any CPU.Build.0 = Release|Any CPU + {93DB9DF9-A48B-436F-8892-AE4CAD42323E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93DB9DF9-A48B-436F-8892-AE4CAD42323E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93DB9DF9-A48B-436F-8892-AE4CAD42323E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93DB9DF9-A48B-436F-8892-AE4CAD42323E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {88CB23B3-1174-45F5-9F72-46AB57BAB661} = {EEC3BB78-BFA5-4525-A965-4EAA3455BBAE} {AA9767C3-9F03-4414-A4C2-A0C3761A975C} = {EEC3BB78-BFA5-4525-A965-4EAA3455BBAE} {6B7A8034-21C0-465D-8708-4F47A1780972} = {EEC3BB78-BFA5-4525-A965-4EAA3455BBAE} + {93DB9DF9-A48B-436F-8892-AE4CAD42323E} = {EEC3BB78-BFA5-4525-A965-4EAA3455BBAE} EndGlobalSection EndGlobal diff --git a/sdk-dotnet/README.md b/sdk-dotnet/README.md index 17214520a..2bb7dba8e 100644 --- a/sdk-dotnet/README.md +++ b/sdk-dotnet/README.md @@ -31,7 +31,7 @@ brew install dotnet ### Build ``` -cd sdk-dotnet/LittleHorse.Sdk +cd sdk-dotnet dotnet build ./LittleHorse.Sdk ``` ### Build and Run tests @@ -41,10 +41,13 @@ dotnet build ./LittleHorse.Sdk dotnet test ./LittleHorse.Sdk.Tests ``` -### Run Example +### Run Examples ``` dotnet run --project ./Examples/BasicExample +dotnet run --project ./Examples/ExceptionsHandlerExample +dotnet run --project ./Examples/MaskedFieldsExample +dotnet run --project ./Examples/WorkerContextExample ``` ### Self-signed TLS certificate