diff --git a/src/Durable/Actions/ActionType.cs b/src/DurableSDK/Actions/ActionType.cs similarity index 100% rename from src/Durable/Actions/ActionType.cs rename to src/DurableSDK/Actions/ActionType.cs diff --git a/src/Durable/Actions/CallActivityAction.cs b/src/DurableSDK/Actions/CallActivityAction.cs similarity index 100% rename from src/Durable/Actions/CallActivityAction.cs rename to src/DurableSDK/Actions/CallActivityAction.cs diff --git a/src/Durable/Actions/CallActivityWithRetryAction.cs b/src/DurableSDK/Actions/CallActivityWithRetryAction.cs similarity index 100% rename from src/Durable/Actions/CallActivityWithRetryAction.cs rename to src/DurableSDK/Actions/CallActivityWithRetryAction.cs diff --git a/src/Durable/Actions/CreateDurableTimerAction.cs b/src/DurableSDK/Actions/CreateDurableTimerAction.cs similarity index 100% rename from src/Durable/Actions/CreateDurableTimerAction.cs rename to src/DurableSDK/Actions/CreateDurableTimerAction.cs diff --git a/src/Durable/Actions/ExternalEventAction.cs b/src/DurableSDK/Actions/ExternalEventAction.cs similarity index 100% rename from src/Durable/Actions/ExternalEventAction.cs rename to src/DurableSDK/Actions/ExternalEventAction.cs diff --git a/src/Durable/Actions/OrchestrationAction.cs b/src/DurableSDK/Actions/OrchestrationAction.cs similarity index 100% rename from src/Durable/Actions/OrchestrationAction.cs rename to src/DurableSDK/Actions/OrchestrationAction.cs diff --git a/src/Durable/ActivityFailureException.cs b/src/DurableSDK/ActivityFailureException.cs similarity index 100% rename from src/Durable/ActivityFailureException.cs rename to src/DurableSDK/ActivityFailureException.cs diff --git a/src/DurableSDK/Commands/GetDurableTaskResult.cs b/src/DurableSDK/Commands/GetDurableTaskResult.cs new file mode 100644 index 00000000..74f5493b --- /dev/null +++ b/src/DurableSDK/Commands/GetDurableTaskResult.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +#pragma warning disable 1591 // Missing XML comment for publicly visible type or member 'member' + +namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Commands +{ + using System.Collections; + using System.Management.Automation; + using Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks; + + [Cmdlet("Get", "DurableTaskResult")] + public class GetDurableTaskResultCommand : PSCmdlet + { + [Parameter(Mandatory = true)] + [ValidateNotNull] + public DurableTask[] Task { get; set; } + + private readonly DurableTaskHandler _durableTaskHandler = new DurableTaskHandler(); + + protected override void EndProcessing() + { + var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; + var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey]; + + _durableTaskHandler.GetTaskResult(Task, context, WriteObject); + } + + protected override void StopProcessing() + { + _durableTaskHandler.Stop(); + } + } +} diff --git a/src/Durable/Commands/InvokeDurableActivityCommand.cs b/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs similarity index 100% rename from src/Durable/Commands/InvokeDurableActivityCommand.cs rename to src/DurableSDK/Commands/InvokeDurableActivityCommand.cs index ba9b429f..6291c666 100644 --- a/src/Durable/Commands/InvokeDurableActivityCommand.cs +++ b/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs @@ -43,8 +43,8 @@ protected override void EndProcessing() { var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey]; - var loadedFunctions = FunctionLoader.GetLoadedFunctions(); + var loadedFunctions = FunctionLoader.GetLoadedFunctions(); var task = new ActivityInvocationTask(FunctionName, Input, RetryOptions); ActivityInvocationTask.ValidateTask(task, loadedFunctions); diff --git a/src/Durable/Commands/SetDurableCustomStatusCommand.cs b/src/DurableSDK/Commands/SetDurableCustomStatusCommand.cs similarity index 100% rename from src/Durable/Commands/SetDurableCustomStatusCommand.cs rename to src/DurableSDK/Commands/SetDurableCustomStatusCommand.cs diff --git a/src/Durable/Commands/SetFunctionInvocationContextCommand.cs b/src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs similarity index 97% rename from src/Durable/Commands/SetFunctionInvocationContextCommand.cs rename to src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs index 943e8362..3430be16 100644 --- a/src/Durable/Commands/SetFunctionInvocationContextCommand.cs +++ b/src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Commands { using System.Collections; using System.Management.Automation; + using Microsoft.PowerShell.Commands; /// /// Set the orchestration context. diff --git a/src/Durable/Commands/StartDurableExternalEventListenerCommand.cs b/src/DurableSDK/Commands/StartDurableExternalEventListenerCommand.cs similarity index 100% rename from src/Durable/Commands/StartDurableExternalEventListenerCommand.cs rename to src/DurableSDK/Commands/StartDurableExternalEventListenerCommand.cs diff --git a/src/Durable/Commands/StartDurableTimerCommand.cs b/src/DurableSDK/Commands/StartDurableTimerCommand.cs similarity index 100% rename from src/Durable/Commands/StartDurableTimerCommand.cs rename to src/DurableSDK/Commands/StartDurableTimerCommand.cs diff --git a/src/Durable/Commands/StopDurableTimerTaskCommand.cs b/src/DurableSDK/Commands/StopDurableTimerTaskCommand.cs similarity index 100% rename from src/Durable/Commands/StopDurableTimerTaskCommand.cs rename to src/DurableSDK/Commands/StopDurableTimerTaskCommand.cs diff --git a/src/Durable/Commands/WaitDurableTaskCommand.cs b/src/DurableSDK/Commands/WaitDurableTaskCommand.cs similarity index 100% rename from src/Durable/Commands/WaitDurableTaskCommand.cs rename to src/DurableSDK/Commands/WaitDurableTaskCommand.cs diff --git a/src/Durable/CurrentUtcDateTimeUpdater.cs b/src/DurableSDK/CurrentUtcDateTimeUpdater.cs similarity index 100% rename from src/Durable/CurrentUtcDateTimeUpdater.cs rename to src/DurableSDK/CurrentUtcDateTimeUpdater.cs diff --git a/src/Durable/DurableActivityErrorHandler.cs b/src/DurableSDK/DurableActivityErrorHandler.cs similarity index 100% rename from src/Durable/DurableActivityErrorHandler.cs rename to src/DurableSDK/DurableActivityErrorHandler.cs diff --git a/src/Durable/DurableTaskHandler.cs b/src/DurableSDK/DurableTaskHandler.cs similarity index 76% rename from src/Durable/DurableTaskHandler.cs rename to src/DurableSDK/DurableTaskHandler.cs index 3981b686..30b5573b 100644 --- a/src/Durable/DurableTaskHandler.cs +++ b/src/DurableSDK/DurableTaskHandler.cs @@ -6,10 +6,12 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System; + using System.Collections; using System.Collections.Generic; + using System.Management.Automation; using System.Threading; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks; - using Utility; + using Microsoft.PowerShell.Commands; internal class DurableTaskHandler { @@ -47,6 +49,7 @@ public void StopAndInitiateDurableTaskOrReplay( } completedHistoryEvent.IsProcessed = true; + context.IsReplaying = completedHistoryEvent.IsPlayed; switch (completedHistoryEvent.EventType) { @@ -57,6 +60,13 @@ public void StopAndInitiateDurableTaskOrReplay( output(eventResult); } break; + case HistoryEventType.EventRaised: + var eventRaisedResult = GetEventResult(completedHistoryEvent); + if (eventRaisedResult != null) + { + output(eventRaisedResult); + } + break; case HistoryEventType.TaskFailed: if (retryOptions == null) @@ -76,7 +86,7 @@ public void StopAndInitiateDurableTaskOrReplay( retryOptions.MaxNumberOfAttempts, onSuccess: result => { - output(TypeExtensions.ConvertFromJson(result)); + output(ConvertFromJson(result)); }, onFailure); @@ -126,6 +136,7 @@ public void WaitAll( var allTasksCompleted = completedEvents.Count == tasksToWaitFor.Count; if (allTasksCompleted) { + context.IsReplaying = completedEvents.Count == 0 ? false : completedEvents[0].IsPlayed; CurrentUtcDateTimeUpdater.UpdateCurrentUtcDateTime(context); foreach (var completedHistoryEvent in completedEvents) @@ -164,6 +175,7 @@ public void WaitAny( if (scheduledHistoryEvent != null) { scheduledHistoryEvent.IsProcessed = true; + scheduledHistoryEvent.IsPlayed = true; } if (completedHistoryEvent != null) @@ -179,12 +191,14 @@ public void WaitAny( } completedHistoryEvent.IsProcessed = true; + completedHistoryEvent.IsPlayed = true; } } var anyTaskCompleted = completedTasks.Count > 0; if (anyTaskCompleted) { + context.IsReplaying = context.History[firstCompletedHistoryEventIndex].IsPlayed; CurrentUtcDateTimeUpdater.UpdateCurrentUtcDateTime(context); // Return a reference to the first completed task output(firstCompletedTask); @@ -195,6 +209,21 @@ public void WaitAny( } } + public void GetTaskResult( + IReadOnlyCollection tasksToQueryResultFor, + OrchestrationContext context, + Action output) + { + foreach (var task in tasksToQueryResultFor) { + var scheduledHistoryEvent = task.GetScheduledHistoryEvent(context, true); + var processedHistoryEvent = task.GetCompletedHistoryEvent(context, scheduledHistoryEvent, true); + if (processedHistoryEvent != null) + { + output(GetEventResult(processedHistoryEvent)); + } + } + } + public void Stop() { _waitForStop.Set(); @@ -206,15 +235,41 @@ private static object GetEventResult(HistoryEvent historyEvent) if (historyEvent.EventType == HistoryEventType.TaskCompleted) { - return TypeExtensions.ConvertFromJson(historyEvent.Result); + return ConvertFromJson(historyEvent.Result); } else if (historyEvent.EventType == HistoryEventType.EventRaised) { - return TypeExtensions.ConvertFromJson(historyEvent.Input); + return ConvertFromJson(historyEvent.Input); } return null; } + public static object ConvertFromJson(string json) + { + object retObj = JsonObject.ConvertFromJson(json, returnHashtable: true, error: out _); + + if (retObj is PSObject psObj) + { + retObj = psObj.BaseObject; + } + + if (retObj is Hashtable hashtable) + { + try + { + // ConvertFromJson returns case-sensitive Hashtable by design -- JSON may contain keys that only differ in case. + // We try casting the Hashtable to a case-insensitive one, but if that fails, we keep using the original one. + retObj = new Hashtable(hashtable, StringComparer.OrdinalIgnoreCase); + } + catch + { + retObj = hashtable; + } + } + + return retObj; + } + private void InitiateAndWaitForStop(OrchestrationContext context) { context.OrchestrationActionCollector.Stop(); diff --git a/src/DurableSDK/ExternalInvoker.cs b/src/DurableSDK/ExternalInvoker.cs new file mode 100644 index 00000000..fa8a31c6 --- /dev/null +++ b/src/DurableSDK/ExternalInvoker.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +{ + using System; + using System.Collections; + using System.Management.Automation; + + internal class ExternalInvoker : IExternalInvoker + { + private readonly Func _externalSDKInvokerFunction; + + public ExternalInvoker(Func invokerFunction) + { + _externalSDKInvokerFunction = invokerFunction; + } + + public Hashtable Invoke(IPowerShellServices powerShellServices) + { + return (Hashtable)_externalSDKInvokerFunction.Invoke(powerShellServices.GetPowerShell()); + } + } +} diff --git a/src/Durable/HistoryEvent.cs b/src/DurableSDK/HistoryEvent.cs similarity index 100% rename from src/Durable/HistoryEvent.cs rename to src/DurableSDK/HistoryEvent.cs diff --git a/src/Durable/HistoryEventType.cs b/src/DurableSDK/HistoryEventType.cs similarity index 100% rename from src/Durable/HistoryEventType.cs rename to src/DurableSDK/HistoryEventType.cs diff --git a/src/DurableSDK/IExternalInvoker.cs b/src/DurableSDK/IExternalInvoker.cs new file mode 100644 index 00000000..3a703f3d --- /dev/null +++ b/src/DurableSDK/IExternalInvoker.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +{ + using System.Collections; + + // Represents a contract for the + internal interface IExternalInvoker + { + // Method to invoke an orchestration using the external Durable SDK + Hashtable Invoke(IPowerShellServices powerShellServices); + } +} diff --git a/src/Durable/IOrchestrationInvoker.cs b/src/DurableSDK/IOrchestrationInvoker.cs similarity index 86% rename from src/Durable/IOrchestrationInvoker.cs rename to src/DurableSDK/IOrchestrationInvoker.cs index 7e80aba3..36011b56 100644 --- a/src/Durable/IOrchestrationInvoker.cs +++ b/src/DurableSDK/IOrchestrationInvoker.cs @@ -10,5 +10,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal interface IOrchestrationInvoker { Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowerShellServices pwsh); + void SetExternalInvoker(IExternalInvoker externalInvoker); } } diff --git a/src/Durable/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs similarity index 64% rename from src/Durable/IPowerShellServices.cs rename to src/DurableSDK/IPowerShellServices.cs index a8cf897b..cdd850bc 100644 --- a/src/Durable/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -5,17 +5,26 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { + using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using System; using System.Management.Automation; internal interface IPowerShellServices { + PowerShell GetPowerShell(); + + bool UseExternalDurableSDK(); + void SetDurableClient(object durableClient); - void SetOrchestrationContext(OrchestrationContext orchestrationContext); + OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context, out IExternalInvoker externalInvoker); void ClearOrchestrationContext(); + void TracePipelineObject(); + + void AddParameter(string name, object value); + IAsyncResult BeginInvoke(PSDataCollection output); void EndInvoke(IAsyncResult asyncResult); diff --git a/src/Durable/OrchestrationActionCollector.cs b/src/DurableSDK/OrchestrationActionCollector.cs similarity index 98% rename from src/Durable/OrchestrationActionCollector.cs rename to src/DurableSDK/OrchestrationActionCollector.cs index b62fbc4b..1542c2bf 100644 --- a/src/Durable/OrchestrationActionCollector.cs +++ b/src/DurableSDK/OrchestrationActionCollector.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Threading; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions; + using Newtonsoft.Json; internal class OrchestrationActionCollector { diff --git a/src/Durable/OrchestrationBindingInfo.cs b/src/DurableSDK/OrchestrationBindingInfo.cs similarity index 100% rename from src/Durable/OrchestrationBindingInfo.cs rename to src/DurableSDK/OrchestrationBindingInfo.cs diff --git a/src/Durable/OrchestrationContext.cs b/src/DurableSDK/OrchestrationContext.cs similarity index 91% rename from src/Durable/OrchestrationContext.cs rename to src/DurableSDK/OrchestrationContext.cs index 0d11acee..27f082db 100644 --- a/src/Durable/OrchestrationContext.cs +++ b/src/DurableSDK/OrchestrationContext.cs @@ -20,13 +20,13 @@ public class OrchestrationContext public object Input { get; internal set; } [DataMember] - internal string InstanceId { get; set; } + public string InstanceId { get; set; } [DataMember] internal string ParentInstanceId { get; set; } [DataMember] - internal bool IsReplaying { get; set; } + public bool IsReplaying { get; set; } [DataMember] internal HistoryEvent[] History { get; set; } diff --git a/src/Durable/OrchestrationFailureException.cs b/src/DurableSDK/OrchestrationFailureException.cs similarity index 100% rename from src/Durable/OrchestrationFailureException.cs rename to src/DurableSDK/OrchestrationFailureException.cs diff --git a/src/Durable/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs similarity index 100% rename from src/Durable/OrchestrationInvoker.cs rename to src/DurableSDK/OrchestrationInvoker.cs diff --git a/src/Durable/OrchestrationMessage.cs b/src/DurableSDK/OrchestrationMessage.cs similarity index 100% rename from src/Durable/OrchestrationMessage.cs rename to src/DurableSDK/OrchestrationMessage.cs diff --git a/src/DurableSDK/PowerShellExtensions.cs b/src/DurableSDK/PowerShellExtensions.cs new file mode 100644 index 00000000..a5225188 --- /dev/null +++ b/src/DurableSDK/PowerShellExtensions.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections; +using System.Collections.ObjectModel; + +namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +{ + using System.Management.Automation; + + internal static class PowerShellExtensions + { + public static void InvokeAndClearCommands(this PowerShell pwsh) + { + try + { + pwsh.Invoke(); + } + finally + { + pwsh.Streams.ClearStreams(); + pwsh.Commands.Clear(); + } + } + + public static void InvokeAndClearCommands(this PowerShell pwsh, IEnumerable input) + { + try + { + pwsh.Invoke(input); + } + finally + { + pwsh.Streams.ClearStreams(); + pwsh.Commands.Clear(); + } + } + + public static Collection InvokeAndClearCommands(this PowerShell pwsh) + { + try + { + var result = pwsh.Invoke(); + return result; + } + finally + { + pwsh.Streams.ClearStreams(); + pwsh.Commands.Clear(); + } + } + + public static Collection InvokeAndClearCommands(this PowerShell pwsh, IEnumerable input) + { + try + { + var result = pwsh.Invoke(input); + return result; + } + finally + { + pwsh.Streams.ClearStreams(); + pwsh.Commands.Clear(); + } + } + } +} diff --git a/src/Durable/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs similarity index 100% rename from src/Durable/PowerShellServices.cs rename to src/DurableSDK/PowerShellServices.cs diff --git a/src/Durable/RetryOptions.cs b/src/DurableSDK/RetryOptions.cs similarity index 100% rename from src/Durable/RetryOptions.cs rename to src/DurableSDK/RetryOptions.cs diff --git a/src/Durable/RetryProcessor.cs b/src/DurableSDK/RetryProcessor.cs similarity index 100% rename from src/Durable/RetryProcessor.cs rename to src/DurableSDK/RetryProcessor.cs diff --git a/src/Durable/Tasks/ActivityInvocationTask.cs b/src/DurableSDK/Tasks/ActivityInvocationTask.cs similarity index 62% rename from src/Durable/Tasks/ActivityInvocationTask.cs rename to src/DurableSDK/Tasks/ActivityInvocationTask.cs index 87bf61c2..5f9f6a33 100644 --- a/src/Durable/Tasks/ActivityInvocationTask.cs +++ b/src/DurableSDK/Tasks/ActivityInvocationTask.cs @@ -7,15 +7,10 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks { - using System; using System.Linq; - using System.Collections.Generic; - - using WebJobs.Script.Grpc.Messages; - - using Microsoft.Azure.Functions.PowerShellWorker; using Microsoft.Azure.Functions.PowerShellWorker.Durable; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions; + using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; public class ActivityInvocationTask : DurableTask { @@ -37,15 +32,15 @@ internal ActivityInvocationTask(string functionName, object functionInput) { } - internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context) + internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed) { return context.History.FirstOrDefault( e => e.EventType == HistoryEventType.TaskScheduled && e.Name == FunctionName && - !e.IsProcessed); + e.IsProcessed == processed); } - internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent) + internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent, bool processed) { return scheduledHistoryEvent == null ? null @@ -61,24 +56,5 @@ internal override OrchestrationAction CreateOrchestrationAction() ? new CallActivityAction(FunctionName, Input) : new CallActivityWithRetryAction(FunctionName, Input, RetryOptions); } - - internal static void ValidateTask(ActivityInvocationTask task, IEnumerable loadedFunctions) - { - var functionInfo = loadedFunctions.FirstOrDefault(fi => fi.FuncName == task.FunctionName); - if (functionInfo == null) - { - var message = string.Format(PowerShellWorkerStrings.FunctionNotFound, task.FunctionName); - throw new InvalidOperationException(message); - } - - var activityTriggerBinding = functionInfo.InputBindings.FirstOrDefault( - entry => DurableBindings.IsActivityTrigger(entry.Value.Type) - && entry.Value.Direction == BindingInfo.Types.Direction.In); - if (activityTriggerBinding.Key == null) - { - var message = string.Format(PowerShellWorkerStrings.FunctionDoesNotHaveProperActivityFunctionBinding, task.FunctionName); - throw new InvalidOperationException(message); - } - } } } diff --git a/src/Durable/Tasks/DurableTask.cs b/src/DurableSDK/Tasks/DurableTask.cs similarity index 89% rename from src/Durable/Tasks/DurableTask.cs rename to src/DurableSDK/Tasks/DurableTask.cs index af71f36d..228e769b 100644 --- a/src/Durable/Tasks/DurableTask.cs +++ b/src/DurableSDK/Tasks/DurableTask.cs @@ -11,9 +11,9 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks public abstract class DurableTask { - internal abstract HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context); + internal abstract HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed = false); - internal abstract HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent); + internal abstract HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent, bool processed = false); internal abstract OrchestrationAction CreateOrchestrationAction(); } diff --git a/src/Durable/Tasks/DurableTimerTask.cs b/src/DurableSDK/Tasks/DurableTimerTask.cs similarity index 97% rename from src/Durable/Tasks/DurableTimerTask.cs rename to src/DurableSDK/Tasks/DurableTimerTask.cs index 86a75abe..4bb357ef 100644 --- a/src/Durable/Tasks/DurableTimerTask.cs +++ b/src/DurableSDK/Tasks/DurableTimerTask.cs @@ -28,7 +28,7 @@ internal DurableTimerTask( Action = new CreateDurableTimerAction(FireAt); } - internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context) + internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed) { return context.History.FirstOrDefault( e => e.EventType == HistoryEventType.TimerCreated && @@ -36,7 +36,7 @@ internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext con !e.IsProcessed); } - internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent) + internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent, bool processed) { return scheduledHistoryEvent == null ? null diff --git a/src/Durable/Tasks/ExternalEventTask.cs b/src/DurableSDK/Tasks/ExternalEventTask.cs similarity index 88% rename from src/Durable/Tasks/ExternalEventTask.cs rename to src/DurableSDK/Tasks/ExternalEventTask.cs index 1bd1fc58..5d12ad66 100644 --- a/src/Durable/Tasks/ExternalEventTask.cs +++ b/src/DurableSDK/Tasks/ExternalEventTask.cs @@ -21,17 +21,17 @@ public ExternalEventTask(string externalEventName) } // There is no corresponding history event for an expected external event - internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context) + internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed) { return null; } - internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent taskScheduled) + internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent taskScheduled, bool processed) { return context.History.FirstOrDefault( e => e.EventType == HistoryEventType.EventRaised && e.Name == ExternalEventName && - !e.IsProcessed); + e.IsPlayed == processed); } internal override OrchestrationAction CreateOrchestrationAction() diff --git a/src/Durable/DurableBindings.cs b/src/DurableWorker/DurableBindings.cs similarity index 95% rename from src/Durable/DurableBindings.cs rename to src/DurableWorker/DurableBindings.cs index 6bdf2468..d46a4adc 100644 --- a/src/Durable/DurableBindings.cs +++ b/src/DurableWorker/DurableBindings.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +namespace Microsoft.Azure.Functions.PowerShellWorker.DurableWorker { using System; diff --git a/src/Durable/DurableController.cs b/src/DurableWorker/DurableController.cs similarity index 64% rename from src/Durable/DurableController.cs rename to src/DurableWorker/DurableController.cs index 6b1f31bb..3d756e15 100644 --- a/src/Durable/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -5,7 +5,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { - using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -16,6 +15,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using WebJobs.Script.Grpc.Messages; using PowerShellWorker.Utility; + using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; /// /// The main entry point for durable functions support. @@ -47,9 +47,14 @@ internal DurableController( _orchestrationInvoker = orchestrationInvoker; } - public void BeforeFunctionInvocation(IList inputData) + public string GetOrchestrationParameterName() { - // If the function is an orchestration client, then we set the DurableClient + return _orchestrationBindingInfo?.ParameterName; + } + + public void InitializeBindings(IList inputData) + { + // If the function is an durable client, then we set the DurableClient // in the module context for the 'Start-DurableOrchestration' function to use. if (_durableFunctionInfo.IsDurableClient) { @@ -58,11 +63,14 @@ public void BeforeFunctionInvocation(IList inputData) .Data.ToObject(); _powerShellServices.SetDurableClient(durableClient); + } else if (_durableFunctionInfo.IsOrchestrationFunction) { - _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); - _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); + _orchestrationBindingInfo = _powerShellServices.SetOrchestrationContext( + inputData[0], + out IExternalInvoker externalInvoker); + _orchestrationInvoker.SetExternalInvoker(externalInvoker); } } @@ -87,46 +95,22 @@ public bool TryGetInputBindingParameterValue(string bindingName, out object valu public void AddPipelineOutputIfNecessary(Collection pipelineItems, Hashtable result) { - var shouldAddPipelineOutput = - _durableFunctionInfo.Type == DurableFunctionType.ActivityFunction; - - if (shouldAddPipelineOutput) + + if (ShouldSuppressPipelineTraces()) { var returnValue = FunctionReturnValueBuilder.CreateReturnValueFromFunctionOutput(pipelineItems); result.Add(AzFunctionInfo.DollarReturn, returnValue); } } - public bool TryInvokeOrchestrationFunction(out Hashtable result) + public Hashtable InvokeOrchestrationFunction() { - if (!_durableFunctionInfo.IsOrchestrationFunction) - { - result = null; - return false; - } - - result = _orchestrationInvoker.Invoke(_orchestrationBindingInfo, _powerShellServices); - return true; + return _orchestrationInvoker.Invoke(_orchestrationBindingInfo, _powerShellServices); } public bool ShouldSuppressPipelineTraces() { return _durableFunctionInfo.Type == DurableFunctionType.ActivityFunction; } - - private static OrchestrationBindingInfo CreateOrchestrationBindingInfo(IList inputData) - { - // Quote from https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-bindings: - // - // "Orchestrator functions should never use any input or output bindings other than the orchestration trigger binding. - // Doing so has the potential to cause problems with the Durable Task extension because those bindings may not obey the single-threading and I/O rules." - // - // Therefore, it's by design that input data contains only one item, which is the metadata of the orchestration context. - var context = inputData[0]; - - return new OrchestrationBindingInfo( - context.Name, - JsonConvert.DeserializeObject(context.Data.String)); - } } } diff --git a/src/Durable/DurableFunctionInfo.cs b/src/DurableWorker/DurableFunctionInfo.cs similarity index 84% rename from src/Durable/DurableFunctionInfo.cs rename to src/DurableWorker/DurableFunctionInfo.cs index a245ac67..05c3650d 100644 --- a/src/Durable/DurableFunctionInfo.cs +++ b/src/DurableWorker/DurableFunctionInfo.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +namespace Microsoft.Azure.Functions.PowerShellWorker.DurableWorker { internal class DurableFunctionInfo { @@ -13,9 +13,12 @@ public DurableFunctionInfo(DurableFunctionType type, string durableClientBinding DurableClientBindingName = durableClientBindingName; } + public bool IsActivityFunction => Type == DurableFunctionType.ActivityFunction; + public bool IsDurableClient => DurableClientBindingName != null; public bool IsOrchestrationFunction => Type == DurableFunctionType.OrchestrationFunction; + public string DurableClientBindingName { get; } diff --git a/src/Durable/DurableFunctionInfoFactory.cs b/src/DurableWorker/DurableFunctionInfoFactory.cs similarity index 96% rename from src/Durable/DurableFunctionInfoFactory.cs rename to src/DurableWorker/DurableFunctionInfoFactory.cs index 3e65b9f7..0cc08d27 100644 --- a/src/Durable/DurableFunctionInfoFactory.cs +++ b/src/DurableWorker/DurableFunctionInfoFactory.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +namespace Microsoft.Azure.Functions.PowerShellWorker.DurableWorker { using System.Linq; diff --git a/src/Durable/DurableFunctionType.cs b/src/DurableWorker/DurableFunctionType.cs similarity index 80% rename from src/Durable/DurableFunctionType.cs rename to src/DurableWorker/DurableFunctionType.cs index 73feb504..2ecacc17 100644 --- a/src/Durable/DurableFunctionType.cs +++ b/src/DurableWorker/DurableFunctionType.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +namespace Microsoft.Azure.Functions.PowerShellWorker.DurableWorker { internal enum DurableFunctionType { diff --git a/src/FunctionInfo.cs b/src/FunctionInfo.cs index 2aea62ec..091eecde 100644 --- a/src/FunctionInfo.cs +++ b/src/FunctionInfo.cs @@ -15,7 +15,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { - using Durable; + using DurableWorker; /// /// This type represents the metadata of an Azure PowerShell Function. diff --git a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psd1 b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psd1 index 7ae51c2b..7bed18b9 100644 --- a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psd1 +++ b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psd1 @@ -59,6 +59,7 @@ FunctionsToExport = @( # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( 'Get-OutputBinding', + 'Get-DurableTaskResult' 'Invoke-DurableActivity', 'Push-OutputBinding', 'Set-DurableCustomStatus', diff --git a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 index 1d4d00ad..d0a285e0 100644 --- a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 +++ b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 @@ -11,7 +11,7 @@ Set-Alias -Name Start-NewOrchestration -Value Start-DurableOrchestration function GetDurableClientFromModulePrivateData { $PrivateData = $PSCmdlet.MyInvocation.MyCommand.Module.PrivateData - if ($PrivateData -eq $null -or $PrivateData['DurableClient'] -eq $null) { + if ($null -eq $PrivateData -or $null -eq $PrivateData['DurableClient']) { throw "No binding of the type 'durableClient' was defined." } else { @@ -98,7 +98,11 @@ function Start-DurableOrchestration { [Parameter( ValueFromPipelineByPropertyName=$true)] - [object] $DurableClient + [object] $DurableClient, + + [Parameter( + ValueFromPipelineByPropertyName=$true)] + [string] $InstanceId ) $ErrorActionPreference = 'Stop' @@ -107,7 +111,10 @@ function Start-DurableOrchestration { $DurableClient = GetDurableClientFromModulePrivateData } - $InstanceId = (New-Guid).Guid + # TODO: Port this change to the External SDK + if (-not $InstanceId) { + $InstanceId = (New-Guid).Guid + } $Uri = if ($DurableClient.rpcBaseUrl) { @@ -235,6 +242,8 @@ function New-DurableOrchestrationCheckStatusResponse { The TaskHubName of the orchestration instance that will handle the external event. .PARAMETER ConnectionName The name of the connection string associated with TaskHubName +.PARAMETER AppCode + The Azure Functions system key #> function Send-DurableExternalEvent { [CmdletBinding()] @@ -263,12 +272,16 @@ function Send-DurableExternalEvent { [Parameter( ValueFromPipelineByPropertyName=$true)] - [string] $ConnectionName + [string] $ConnectionName, + + [Parameter( + ValueFromPipelineByPropertyName=$true)] + [string] $AppCode ) $DurableClient = GetDurableClientFromModulePrivateData - $RequestUrl = GetRaiseEventUrl -DurableClient $DurableClient -InstanceId $InstanceId -EventName $EventName -TaskHubName $TaskHubName -ConnectionName $ConnectionName + $RequestUrl = GetRaiseEventUrl -DurableClient $DurableClient -InstanceId $InstanceId -EventName $EventName -TaskHubName $TaskHubName -ConnectionName $ConnectionName -AppCode $AppCode $Body = $EventData | ConvertTo-Json -Compress @@ -280,7 +293,8 @@ function GetRaiseEventUrl( [string] $InstanceId, [string] $EventName, [string] $TaskHubName, - [string] $ConnectionName) { + [string] $ConnectionName, + [string] $AppCode) { $RequestUrl = $DurableClient.BaseUrl + "/instances/$InstanceId/raiseEvent/$EventName" @@ -291,6 +305,9 @@ function GetRaiseEventUrl( if ($null -eq $ConnectionName) { $query += "connection=$ConnectionName" } + if ($null -eq $AppCode) { + $query += "code=$AppCode" + } if ($query.Count -gt 0) { $RequestUrl += "?" + [string]::Join("&", $query) } diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 7e458914..0344c05d 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -15,6 +15,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell { + using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; using System.Management.Automation; using System.Text; @@ -204,32 +205,37 @@ public Hashtable InvokeFunction( FunctionInvocationPerformanceStopwatch stopwatch) { var outputBindings = FunctionMetadata.GetOutputBindingHashtable(_pwsh.Runspace.InstanceId); - - var durableController = new DurableController(functionInfo.DurableFunctionInfo, _pwsh); + var durableFunctionsUtils = new DurableController(functionInfo.DurableFunctionInfo, _pwsh); try { - durableController.BeforeFunctionInvocation(inputData); + durableFunctionsUtils.InitializeBindings(inputData); AddEntryPointInvocationCommand(functionInfo); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.FunctionCodeReady); - SetInputBindingParameterValues(functionInfo, inputData, durableController, triggerMetadata, traceContext, retryContext); + var orchestrationParamName = durableFunctionsUtils.GetOrchestrationParameterName(); + SetInputBindingParameterValues(functionInfo, inputData, orchestrationParamName, triggerMetadata, traceContext, retryContext); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.InputBindingValuesReady); - if (!durableController.ShouldSuppressPipelineTraces()) - { - _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); - } - stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.InvokingFunctionCode); Logger.Log(isUserOnlyLog: false, LogLevel.Trace, CreateInvocationPerformanceReportMessage(functionInfo.FuncName, stopwatch)); try { - return durableController.TryInvokeOrchestrationFunction(out var result) - ? result - : InvokeNonOrchestrationFunction(durableController, outputBindings); + if(functionInfo.DurableFunctionInfo.IsOrchestrationFunction) + { + return durableFunctionsUtils.InvokeOrchestrationFunction(); + } + else + { + var isActivityFunction = functionInfo.DurableFunctionInfo.IsActivityFunction; + if (!isActivityFunction) + { + _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); + } + return ExecuteUserCode(isActivityFunction, outputBindings); + } } catch (RuntimeException e) { @@ -237,9 +243,9 @@ public Hashtable InvokeFunction( Logger.Log(isUserOnlyLog: true, LogLevel.Error, GetFunctionExceptionMessage(e)); throw; } - catch (OrchestrationFailureException e) + catch (Exception e) { - if (e.InnerException is IContainsErrorRecord inner) + if (e.Data.Contains(OrchestrationInvoker.isOrchestrationFailureKey) && e.InnerException is IContainsErrorRecord inner) { Logger.Log(isUserOnlyLog: true, LogLevel.Error, GetFunctionExceptionMessage(inner)); } @@ -248,7 +254,7 @@ public Hashtable InvokeFunction( } finally { - durableController.AfterFunctionInvocation(); + durableFunctionsUtils.AfterFunctionInvocation(); outputBindings.Clear(); ResetRunspace(); } @@ -257,7 +263,7 @@ public Hashtable InvokeFunction( private void SetInputBindingParameterValues( AzFunctionInfo functionInfo, IEnumerable inputData, - DurableController durableController, + string orchParamName, Hashtable triggerMetadata, TraceContext traceContext, RetryContext retryContext) @@ -266,13 +272,12 @@ private void SetInputBindingParameterValues( { if (functionInfo.FuncParameters.TryGetValue(binding.Name, out var paramInfo)) { - if (!durableController.TryGetInputBindingParameterValue(binding.Name, out var valueToUse)) + if (string.CompareOrdinal(binding.Name, orchParamName) != 0) { var bindingInfo = functionInfo.InputBindings[binding.Name]; - valueToUse = Utils.TransformInBindingValueAsNeeded(paramInfo, bindingInfo, binding.Data.ToObject()); + var valueToUse = Utils.TransformInBindingValueAsNeeded(paramInfo, bindingInfo, binding.Data.ToObject()); + _pwsh.AddParameter(binding.Name, valueToUse); } - - _pwsh.AddParameter(binding.Name, valueToUse); } } @@ -296,11 +301,15 @@ private void SetInputBindingParameterValues( /// /// Execution a function fired by a trigger or an activity function scheduled by an orchestration. /// - private Hashtable InvokeNonOrchestrationFunction(DurableController durableController, IDictionary outputBindings) + private Hashtable ExecuteUserCode(bool addPipelineOutput, IDictionary outputBindings) { var pipelineItems = _pwsh.InvokeAndClearCommands(); var result = new Hashtable(outputBindings, StringComparer.OrdinalIgnoreCase); - durableController.AddPipelineOutputIfNecessary(pipelineItems, result); + if (addPipelineOutput) + { + var returnValue = FunctionReturnValueBuilder.CreateReturnValueFromFunctionOutput(pipelineItems); + result.Add(AzFunctionInfo.DollarReturn, returnValue); + } return result; } diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index 89b4c7e9..c9c10d2f 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -13,7 +13,7 @@ using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement; -using Microsoft.Azure.Functions.PowerShellWorker.Durable; +using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; namespace Microsoft.Azure.Functions.PowerShellWorker diff --git a/src/Utility/Utils.cs b/src/Utility/Utils.cs index 549ba5de..21ec68b8 100644 --- a/src/Utility/Utils.cs +++ b/src/Utility/Utils.cs @@ -17,6 +17,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Utility internal class Utils { + internal readonly static CmdletInfo GetModuleCmdletInfo = new CmdletInfo("Get-Module", typeof(GetModuleCommand)); internal readonly static CmdletInfo ImportModuleCmdletInfo = new CmdletInfo("Import-Module", typeof(ImportModuleCommand)); internal readonly static CmdletInfo RemoveModuleCmdletInfo = new CmdletInfo("Remove-Module", typeof(RemoveModuleCommand)); internal readonly static CmdletInfo RemoveJobCmdletInfo = new CmdletInfo("Remove-Job", typeof(RemoveJobCommand)); diff --git a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs index ddc20c5e..067e9fba 100644 --- a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs +++ b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs @@ -147,6 +147,112 @@ public async Task LegacyDurableCommandNamesStillWork() } } + [Fact] + public async Task OrchestratationContextHasAllExpectedProperties() + { + var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClientOrchContextProperties", queryString: string.Empty); + Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode); + + var initialResponseBody = await initialResponse.Content.ReadAsStringAsync(); + dynamic initialResponseBodyObject = JsonConvert.DeserializeObject(initialResponseBody); + var statusQueryGetUri = (string)initialResponseBodyObject.statusQueryGetUri; + + var startTime = DateTime.UtcNow; + + using (var httpClient = new HttpClient()) + { + while (true) + { + var statusResponse = await httpClient.GetAsync(statusQueryGetUri); + switch (statusResponse.StatusCode) + { + case HttpStatusCode.Accepted: + { + var statusResponseBody = await GetResponseBodyAsync(statusResponse); + var runtimeStatus = (string)statusResponseBody.runtimeStatus; + Assert.True( + runtimeStatus == "Running" || runtimeStatus == "Pending", + $"Unexpected runtime status: {runtimeStatus}"); + + if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout) + { + Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}"); + } + + await Task.Delay(TimeSpan.FromSeconds(2)); + break; + } + + case HttpStatusCode.OK: + { + var statusResponseBody = await GetResponseBodyAsync(statusResponse); + Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus); + Assert.Equal("True", statusResponseBody.output[0].ToString()); + Assert.Equal("Hello myInstanceId", statusResponseBody.output[1].ToString()); + Assert.Equal("False", statusResponseBody.output[2].ToString()); + return; + } + + default: + Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}"); + break; + } + } + } + } + + [Fact] + public async Task OrchestratationCanAlwaysObtainTaskResult() + { + var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClient", queryString: "?FunctionName=DurableOrchestratorGetTaskResult"); + Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode); + + var initialResponseBody = await initialResponse.Content.ReadAsStringAsync(); + dynamic initialResponseBodyObject = JsonConvert.DeserializeObject(initialResponseBody); + var statusQueryGetUri = (string)initialResponseBodyObject.statusQueryGetUri; + + var startTime = DateTime.UtcNow; + + using (var httpClient = new HttpClient()) + { + while (true) + { + var statusResponse = await httpClient.GetAsync(statusQueryGetUri); + switch (statusResponse.StatusCode) + { + case HttpStatusCode.Accepted: + { + var statusResponseBody = await GetResponseBodyAsync(statusResponse); + var runtimeStatus = (string)statusResponseBody.runtimeStatus; + Assert.True( + runtimeStatus == "Running" || runtimeStatus == "Pending", + $"Unexpected runtime status: {runtimeStatus}"); + + if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout) + { + Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}"); + } + + await Task.Delay(TimeSpan.FromSeconds(2)); + break; + } + + case HttpStatusCode.OK: + { + var statusResponseBody = await GetResponseBodyAsync(statusResponse); + Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus); + Assert.Equal("Hello world", statusResponseBody.output.ToString()); + return; + } + + default: + Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}"); + break; + } + } + } + } + [Fact] public async Task ActivityCanHaveQueueBinding() { diff --git a/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/function.json b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/function.json new file mode 100644 index 00000000..ce618d34 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/function.json @@ -0,0 +1,24 @@ +{ + "bindings": [ + { + "authLevel": "function", + "name": "Request", + "type": "httpTrigger", + "direction": "in", + "methods": [ + "post", + "get" + ] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + }, + { + "name": "starter", + "type": "durableClient", + "direction": "in" + } + ] +} \ No newline at end of file diff --git a/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 new file mode 100644 index 00000000..228d57f6 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 @@ -0,0 +1,9 @@ +using namespace System.Net + +param($Request, $TriggerMetadata) + +$InstanceId = Start-DurableOrchestration -FunctionName "DurableOrchestratorAccessContextProps" -InstanceId "myInstanceId" +Write-Host "Started orchestration with ID = '$InstanceId'" + +$Response = New-DurableOrchestrationCheckStatusResponse -Request $Request -InstanceId $InstanceId +Push-OutputBinding -Name Response -Value $Response diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/function.json b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/function.json new file mode 100644 index 00000000..336f5a18 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/function.json @@ -0,0 +1,9 @@ +{ + "bindings": [ + { + "name": "Context", + "type": "orchestrationTrigger", + "direction": "in" + } + ] +} \ No newline at end of file diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 new file mode 100644 index 00000000..031a5492 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 @@ -0,0 +1,8 @@ +param($Context) + +$output = @() + +$output += $Context.IsReplaying +$output += Invoke-DurableActivity -FunctionName 'DurableActivity' -Input $Context.InstanceId +$output += $Context.IsReplaying +$output diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/function.json b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/function.json new file mode 100644 index 00000000..336f5a18 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/function.json @@ -0,0 +1,9 @@ +{ + "bindings": [ + { + "name": "Context", + "type": "orchestrationTrigger", + "direction": "in" + } + ] +} \ No newline at end of file diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 new file mode 100644 index 00000000..23380916 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 @@ -0,0 +1,8 @@ +param($Context) + +$output = @() + +$task = Invoke-DurableActivity -FunctionName 'DurableActivity' -Input "world" -NoWait +$firstTask = Wait-DurableTask -Task @($task) -Any +$output += Get-DurableTaskResult -Task @($firstTask) +$output diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/function.json b/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/function.json new file mode 100644 index 00000000..336f5a18 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/function.json @@ -0,0 +1,9 @@ +{ + "bindings": [ + { + "name": "Context", + "type": "orchestrationTrigger", + "direction": "in" + } + ] +} \ No newline at end of file diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/run.ps1 new file mode 100644 index 00000000..d4a75a7f --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/run.ps1 @@ -0,0 +1,9 @@ +param($Context) + +$output = @() + +$output += Invoke-DurableActivity -FunctionName 'Hello' -Input 'Tokyo' +$output += Invoke-DurableActivity -FunctionName 'Hello' -Input 'Seattle' +$output += Invoke-DurableActivity -FunctionName 'Hello' -Input 'London' + +$output diff --git a/test/E2E/TestFunctionApp/profile.ps1 b/test/E2E/TestFunctionApp/profile.ps1 new file mode 100644 index 00000000..9afdf1da --- /dev/null +++ b/test/E2E/TestFunctionApp/profile.ps1 @@ -0,0 +1,22 @@ +# Azure Functions profile.ps1 +# +# This profile.ps1 will get executed every "cold start" of your Function App. +# "cold start" occurs when: +# +# * A Function App starts up for the very first time +# * A Function App starts up after being de-allocated due to inactivity +# +# You can define helper functions, run commands, or specify environment variables +# NOTE: any variables defined that are not environment variables will get reset after the first execution + +# Authenticate with Azure PowerShell using MSI. +# Remove this if you are not planning on using MSI or Azure PowerShell. +if ($env:MSI_SECRET) { + Disable-AzContextAutosave -Scope Process | Out-Null + Connect-AzAccount -Identity +} + +# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. +# Enable-AzureRmAlias + +# You can also define functions or aliases that can be referenced in any of your PowerShell functions. \ No newline at end of file diff --git a/test/Unit/Durable/ActivityInvocationTaskTests.cs b/test/Unit/Durable/ActivityInvocationTaskTests.cs index c96cc755..96e27571 100644 --- a/test/Unit/Durable/ActivityInvocationTaskTests.cs +++ b/test/Unit/Durable/ActivityInvocationTaskTests.cs @@ -163,59 +163,6 @@ public void StopAndInitiateDurableTaskOrReplay_OutputsActivityInvocationTask_Whe Assert.Equal(FunctionName, allOutput.Single().FunctionName); } - [Fact] - public void ValidateTask_Throws_WhenActivityFunctionDoesNotExist() - { - var history = CreateHistory(scheduled: false, completed: false, failed: false, output: InvocationResultJson); - var orchestrationContext = new OrchestrationContext { History = history }; - - var loadedFunctions = new[] - { - DurableTestUtilities.CreateFakeAzFunctionInfo(FunctionName, "fakeTriggerBindingName", ActivityTriggerBindingType, BindingInfo.Types.Direction.In) - }; - - const string wrongFunctionName = "AnotherFunction"; - - var durableTaskHandler = new DurableTaskHandler(); - - var exception = - Assert.Throws( - () => ActivityInvocationTask.ValidateTask( - new ActivityInvocationTask(wrongFunctionName, FunctionInput), loadedFunctions)); - - Assert.Contains(wrongFunctionName, exception.Message); - Assert.DoesNotContain(ActivityTriggerBindingType, exception.Message); - - DurableTestUtilities.VerifyNoActionAdded(orchestrationContext); - } - - [Theory] - [InlineData("IncorrectBindingType", BindingInfo.Types.Direction.In)] - [InlineData(ActivityTriggerBindingType, BindingInfo.Types.Direction.Out)] - public void ValidateTask_Throws_WhenActivityFunctionHasNoProperBinding( - string bindingType, BindingInfo.Types.Direction bindingDirection) - { - var history = CreateHistory(scheduled: false, completed: false, failed: false, output: InvocationResultJson); - var orchestrationContext = new OrchestrationContext { History = history }; - - var loadedFunctions = new[] - { - DurableTestUtilities.CreateFakeAzFunctionInfo(FunctionName, "fakeTriggerBindingName", bindingType, bindingDirection) - }; - - var durableTaskHandler = new DurableTaskHandler(); - - var exception = - Assert.Throws( - () => ActivityInvocationTask.ValidateTask( - new ActivityInvocationTask(FunctionName, FunctionInput), loadedFunctions)); - - Assert.Contains(FunctionName, exception.Message); - Assert.Contains(ActivityTriggerBindingType, exception.Message); - - DurableTestUtilities.VerifyNoActionAdded(orchestrationContext); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -225,8 +172,8 @@ public void GetCompletedHistoryEvent_ReturnsTaskCompletedOrTaskFailed(bool succe var orchestrationContext = new OrchestrationContext { History = history }; var task = new ActivityInvocationTask(FunctionName, FunctionInput); - var scheduledEvent = task.GetScheduledHistoryEvent(orchestrationContext); - var completedEvent = task.GetCompletedHistoryEvent(orchestrationContext, scheduledEvent); + var scheduledEvent = task.GetScheduledHistoryEvent(orchestrationContext, false); + var completedEvent = task.GetCompletedHistoryEvent(orchestrationContext, scheduledEvent, false); Assert.Equal(scheduledEvent.EventId, completedEvent.TaskScheduledId); } diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 68531f7c..b57cb767 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test.Durable using System.Collections.ObjectModel; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; using Microsoft.Azure.Functions.PowerShellWorker.Durable; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Newtonsoft.Json; @@ -22,9 +23,12 @@ public class DurableControllerTests { private readonly Mock _mockPowerShellServices = new Mock(MockBehavior.Strict); private readonly Mock _mockOrchestrationInvoker = new Mock(MockBehavior.Strict); + private const string _contextParameterName = "ParameterName"; + private static readonly OrchestrationContext _orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; + private static readonly OrchestrationBindingInfo _orchestrationBindingInfo = new OrchestrationBindingInfo(_contextParameterName, _orchestrationContext); [Fact] - public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction() + public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() { var durableController = CreateDurableController(DurableFunctionType.None, "DurableClientBindingName"); @@ -38,7 +42,7 @@ public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction( _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData); _mockPowerShellServices.Verify( _ => _.SetDurableClient( @@ -47,50 +51,50 @@ public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction( } [Fact] - public void BeforeFunctionInvocation_SetsOrchestrationContext_ForOrchestrationFunction() + public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction() { var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); - - var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; var inputData = new[] { - CreateParameterBinding("ParameterName", orchestrationContext) + CreateParameterBinding("ParameterName", _orchestrationContext) }; + + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), + out It.Ref.IsAny)) + .Returns(_orchestrationBindingInfo); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData); _mockPowerShellServices.Verify( _ => _.SetOrchestrationContext( - It.Is(c => c.InstanceId == orchestrationContext.InstanceId)), + It.Is(c => c.Data.ToString().Contains(_orchestrationContext.InstanceId)), + out It.Ref.IsAny), Times.Once); } [Fact] - public void BeforeFunctionInvocation_Throws_OnOrchestrationFunctionWithoutContextParameter() + public void InitializeBindings_Throws_OnOrchestrationFunctionWithoutContextParameter() { var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); var inputData = new ParameterBinding[0]; - Assert.ThrowsAny(() => durableController.BeforeFunctionInvocation(inputData)); + Assert.ThrowsAny(() => durableController.InitializeBindings(inputData)); } [Theory] [InlineData(DurableFunctionType.None)] [InlineData(DurableFunctionType.ActivityFunction)] - internal void BeforeFunctionInvocation_DoesNothing_ForNonOrchestrationFunction(DurableFunctionType durableFunctionType) + internal void InitializeBindings_DoesNothing_ForNonOrchestrationFunction(DurableFunctionType durableFunctionType) { var durableController = CreateDurableController(durableFunctionType); - var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; - var inputData = new[] { // Even if a parameter similar to orchestration context is passed: - CreateParameterBinding("ParameterName", orchestrationContext) + CreateParameterBinding("ParameterName", _orchestrationContext) }; - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData); } [Theory] @@ -111,19 +115,19 @@ internal void AfterFunctionInvocation_ClearsOrchestrationContext(DurableFunction public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParameter_ForOrchestrationFunction() { var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); - - var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; - const string contextParameterName = "ParameterName"; var inputData = new[] { - CreateParameterBinding(contextParameterName, orchestrationContext) + CreateParameterBinding(_contextParameterName, _orchestrationContext) }; - - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); - - Assert.True(durableController.TryGetInputBindingParameterValue(contextParameterName, out var value)); - Assert.Equal(orchestrationContext.InstanceId, ((OrchestrationContext)value).InstanceId); + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( + It.IsAny(), + out It.Ref.IsAny)) + .Returns(_orchestrationBindingInfo); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + durableController.InitializeBindings(inputData); + + Assert.True(durableController.TryGetInputBindingParameterValue(_contextParameterName, out var value)); + Assert.Equal(_orchestrationContext.InstanceId, ((OrchestrationContext)value).InstanceId); } [Theory] @@ -132,70 +136,52 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrationFunction(DurableFunctionType durableFunctionType) { var durableController = CreateDurableController(durableFunctionType); - - var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; - const string contextParameterName = "ParameterName"; var inputData = new[] { - CreateParameterBinding(contextParameterName, orchestrationContext) + CreateParameterBinding(_contextParameterName, _orchestrationContext) }; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( + It.IsAny(), + out It.Ref.IsAny)) + .Returns(_orchestrationBindingInfo); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + durableController.InitializeBindings(inputData); - Assert.False(durableController.TryGetInputBindingParameterValue(contextParameterName, out var value)); + Assert.False(durableController.TryGetInputBindingParameterValue(_contextParameterName, out var value)); Assert.Null(value); } [Fact] public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() { - var contextParameterName = "ParameterName"; - var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; - var inputData = new[] { CreateParameterBinding(contextParameterName, orchestrationContext) }; - + var inputData = new[] { CreateParameterBinding(_contextParameterName, _orchestrationContext) }; var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( + It.IsAny(), + out It.Ref.IsAny)) + .Returns(_orchestrationBindingInfo); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + durableController.InitializeBindings(inputData); var expectedResult = new Hashtable(); _mockOrchestrationInvoker.Setup( _ => _.Invoke(It.IsAny(), It.IsAny())) .Returns(expectedResult); - var invoked = durableController.TryInvokeOrchestrationFunction(out var actualResult); - Assert.True(invoked); + var actualResult = durableController.InvokeOrchestrationFunction(); Assert.Same(expectedResult, actualResult); _mockOrchestrationInvoker.Verify( _ => _.Invoke( It.Is( - bindingInfo => bindingInfo.Context.InstanceId == orchestrationContext.InstanceId - && bindingInfo.ParameterName == contextParameterName), + bindingInfo => bindingInfo.Context.InstanceId == _orchestrationContext.InstanceId + && bindingInfo.ParameterName == _contextParameterName), _mockPowerShellServices.Object), Times.Once); } - [Theory] - [InlineData(DurableFunctionType.None)] - [InlineData(DurableFunctionType.ActivityFunction)] - internal void TryInvokeOrchestrationFunction_DoesNotInvokeNonOrchestrationFunction(DurableFunctionType durableFunctionType) - { - var contextParameterName = "ParameterName"; - var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; - var inputData = new[] { CreateParameterBinding(contextParameterName, orchestrationContext) }; - - var durableController = CreateDurableController(durableFunctionType); - - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); - - var invoked = durableController.TryInvokeOrchestrationFunction(out var actualResult); - Assert.False(invoked); - Assert.Null(actualResult); - } - [Fact] public void AddPipelineOutputIfNecessary_AddsDollarReturn_ForActivityFunction() { diff --git a/test/Unit/Durable/DurableFunctionInfoFactoryTests.cs b/test/Unit/Durable/DurableFunctionInfoFactoryTests.cs index 09cea266..2c898316 100644 --- a/test/Unit/Durable/DurableFunctionInfoFactoryTests.cs +++ b/test/Unit/Durable/DurableFunctionInfoFactoryTests.cs @@ -10,7 +10,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test.Durable using Xunit; - using Microsoft.Azure.Functions.PowerShellWorker.Durable; + using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; public class DurableFunctionInfoFactoryTests { diff --git a/test/Unit/Durable/OrchestrationInvokerTests.cs b/test/Unit/Durable/OrchestrationInvokerTests.cs index 65c4b24d..2a2db694 100644 --- a/test/Unit/Durable/OrchestrationInvokerTests.cs +++ b/test/Unit/Durable/OrchestrationInvokerTests.cs @@ -34,12 +34,16 @@ public void InvocationRunsToCompletionIfNotStopped() { var invocationAsyncResult = DurableTestUtilities.CreateInvocationResult(completed: true); DurableTestUtilities.ExpectBeginInvoke(_mockPowerShellServices, invocationAsyncResult); + _mockPowerShellServices.Setup(_ => _.UseExternalDurableSDK()).Returns(false); _orchestrationInvoker.Invoke(_orchestrationBindingInfo, _mockPowerShellServices.Object); _mockPowerShellServices.Verify(_ => _.BeginInvoke(It.IsAny>()), Times.Once); _mockPowerShellServices.Verify(_ => _.EndInvoke(invocationAsyncResult), Times.Once); _mockPowerShellServices.Verify(_ => _.ClearStreamsAndCommands(), Times.Once); + _mockPowerShellServices.Verify(_ => _.TracePipelineObject(), Times.Once); + _mockPowerShellServices.Verify(_ => _.AddParameter(It.IsAny(), It.IsAny()), Times.Once); + _mockPowerShellServices.Verify(_ => _.UseExternalDurableSDK(), Times.Once); _mockPowerShellServices.VerifyNoOtherCalls(); } @@ -47,10 +51,14 @@ public void InvocationRunsToCompletionIfNotStopped() public void InvocationStopsOnStopEvent() { InvokeOrchestration(completed: false); + _mockPowerShellServices.Setup(_ => _.UseExternalDurableSDK()).Returns(false); _mockPowerShellServices.Verify(_ => _.BeginInvoke(It.IsAny>()), Times.Once); _mockPowerShellServices.Verify(_ => _.StopInvoke(), Times.Once); _mockPowerShellServices.Verify(_ => _.ClearStreamsAndCommands(), Times.Once); + _mockPowerShellServices.Verify(_ => _.TracePipelineObject(), Times.Once); + _mockPowerShellServices.Verify(_ => _.AddParameter(It.IsAny(), It.IsAny()), Times.Once); + _mockPowerShellServices.Verify(_ => _.UseExternalDurableSDK(), Times.Once); _mockPowerShellServices.VerifyNoOtherCalls(); } diff --git a/test/Unit/PowerShell/PowerShellManagerTests.cs b/test/Unit/PowerShell/PowerShellManagerTests.cs index 5d4faf42..36588611 100644 --- a/test/Unit/PowerShell/PowerShellManagerTests.cs +++ b/test/Unit/PowerShell/PowerShellManagerTests.cs @@ -18,6 +18,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test using System.Collections.ObjectModel; using System.Management.Automation; using Microsoft.Azure.Functions.PowerShellWorker.Durable; + using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; using Newtonsoft.Json; internal class TestUtils