From 8ab426e189784de8316da9de24d340f2df8cbf91 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 12 Jul 2022 16:14:32 -0700 Subject: [PATCH 01/40] enable external sdk --- .../Commands/InvokeDurableActivityCommand.cs | 2 - .../SetFunctionInvocationContextCommand.cs | 1 + src/DurableSDK/DurableTaskHandler.cs | 36 +++++- src/DurableSDK/ExternalInvoker.cs | 26 ++++ src/DurableSDK/IExternalInvoker.cs | 16 +++ src/DurableSDK/IOrchestrationInvoker.cs | 1 + src/DurableSDK/IPowerShellServices.cs | 11 +- .../OrchestrationActionCollector.cs | 1 + src/DurableSDK/OrchestrationInvoker.cs | 115 +++++++++++------ src/DurableSDK/PowerShellExtensions.cs | 69 ++++++++++ src/DurableSDK/PowerShellServices.cs | 118 ++++++++++++++++-- .../Tasks/ActivityInvocationTask.cs | 25 ---- src/DurableWorker/DurableController.cs | 49 +++----- src/DurableWorker/DurableFunctionInfo.cs | 3 + ...soft.Azure.Functions.PowerShellWorker.psm1 | 2 +- src/PowerShell/PowerShellManager.cs | 53 ++++---- src/Utility/TypeExtensions.cs | 2 +- src/Utility/Utils.cs | 1 + test/E2E/TestFunctionApp/profile.ps1 | 22 ++++ .../Durable/ActivityInvocationTaskTests.cs | 53 -------- test/Unit/Durable/DurableControllerTests.cs | 111 +++++++--------- .../Unit/Durable/OrchestrationInvokerTests.cs | 8 ++ 22 files changed, 473 insertions(+), 252 deletions(-) create mode 100644 src/DurableSDK/ExternalInvoker.cs create mode 100644 src/DurableSDK/IExternalInvoker.cs create mode 100644 src/DurableSDK/PowerShellExtensions.cs create mode 100644 test/E2E/TestFunctionApp/profile.ps1 diff --git a/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs b/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs index ba9b429f..4cb8639e 100644 --- a/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs +++ b/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs @@ -43,10 +43,8 @@ protected override void EndProcessing() { var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey]; - var loadedFunctions = FunctionLoader.GetLoadedFunctions(); var task = new ActivityInvocationTask(FunctionName, Input, RetryOptions); - ActivityInvocationTask.ValidateTask(task, loadedFunctions); _durableTaskHandler.StopAndInitiateDurableTaskOrReplay( task, context, NoWait.IsPresent, diff --git a/src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs b/src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs index 943e8362..3430be16 100644 --- a/src/DurableSDK/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/DurableSDK/DurableTaskHandler.cs b/src/DurableSDK/DurableTaskHandler.cs index c65b1275..a35546b8 100644 --- a/src/DurableSDK/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 { @@ -83,7 +85,7 @@ public void StopAndInitiateDurableTaskOrReplay( retryOptions.MaxNumberOfAttempts, onSuccess: result => { - output(TypeExtensions.ConvertFromJson(result)); + output(ConvertFromJson(result)); }, onFailure); @@ -232,15 +234,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/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/DurableSDK/IOrchestrationInvoker.cs b/src/DurableSDK/IOrchestrationInvoker.cs index 7e80aba3..36011b56 100644 --- a/src/DurableSDK/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/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index a8cf897b..cdd850bc 100644 --- a/src/DurableSDK/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/DurableSDK/OrchestrationActionCollector.cs b/src/DurableSDK/OrchestrationActionCollector.cs index b62fbc4b..1542c2bf 100644 --- a/src/DurableSDK/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/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index fef557ba..b71116b4 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -11,58 +11,98 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Linq; using System.Management.Automation; - using PowerShellWorker.Utility; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions; internal class OrchestrationInvoker : IOrchestrationInvoker { - public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowerShellServices pwsh) + private IExternalInvoker _externalInvoker; + internal static string isOrchestrationFailureKey = "IsOrchestrationFailure"; + + public Hashtable Invoke( + OrchestrationBindingInfo orchestrationBindingInfo, + IPowerShellServices powerShellServices) { try { - var outputBuffer = new PSDataCollection(); - var context = orchestrationBindingInfo.Context; + if (powerShellServices.UseExternalDurableSDK()) + { + return InvokeExternalDurableSDK(powerShellServices); + } + return InvokeInternalDurableSDK(orchestrationBindingInfo, powerShellServices); + } + catch (Exception ex) + { + ex.Data.Add(isOrchestrationFailureKey, true); + throw; + } + finally + { + powerShellServices.ClearStreamsAndCommands(); + } + } + + public Hashtable InvokeExternalDurableSDK(IPowerShellServices powerShellServices) + { + return _externalInvoker.Invoke(powerShellServices); + } + + public Hashtable InvokeInternalDurableSDK( + OrchestrationBindingInfo orchestrationBindingInfo, + IPowerShellServices powerShellServices) + { + var outputBuffer = new PSDataCollection(); + var context = orchestrationBindingInfo.Context; + + // context.History should never be null when initializing CurrentUtcDateTime + var orchestrationStart = context.History.First( + e => e.EventType == HistoryEventType.OrchestratorStarted); + context.CurrentUtcDateTime = orchestrationStart.Timestamp.ToUniversalTime(); - // context.History should never be null when initializing CurrentUtcDateTime - var orchestrationStart = context.History.First( - e => e.EventType == HistoryEventType.OrchestratorStarted); - context.CurrentUtcDateTime = orchestrationStart.Timestamp.ToUniversalTime(); + // Marks the first OrchestratorStarted event as processed + orchestrationStart.IsProcessed = true; - // Marks the first OrchestratorStarted event as processed - orchestrationStart.IsProcessed = true; - - var asyncResult = pwsh.BeginInvoke(outputBuffer); + // Finish initializing the Function invocation + powerShellServices.AddParameter(orchestrationBindingInfo.ParameterName, context); + powerShellServices.TracePipelineObject(); - var (shouldStop, actions) = - orchestrationBindingInfo.Context.OrchestrationActionCollector.WaitForActions(asyncResult.AsyncWaitHandle); + var asyncResult = powerShellServices.BeginInvoke(outputBuffer); - if (shouldStop) + var (shouldStop, actions) = + orchestrationBindingInfo.Context.OrchestrationActionCollector.WaitForActions(asyncResult.AsyncWaitHandle); + + if (shouldStop) + { + // The orchestration function should be stopped and restarted + powerShellServices.StopInvoke(); + // return (Hashtable)orchestrationBindingInfo.Context.OrchestrationActionCollector.output; + return CreateOrchestrationResult(isDone: false, actions, output: null, context.CustomStatus); + } + else + { + try { - // The orchestration function should be stopped and restarted - pwsh.StopInvoke(); - return CreateOrchestrationResult(isDone: false, actions, output: null, context.CustomStatus); + // The orchestration function completed + powerShellServices.EndInvoke(asyncResult); + var result = CreateReturnValueFromFunctionOutput(outputBuffer); + return CreateOrchestrationResult(isDone: true, actions, output: result, context.CustomStatus); } - else + catch (Exception e) { - try - { - // The orchestration function completed - pwsh.EndInvoke(asyncResult); - var result = FunctionReturnValueBuilder.CreateReturnValueFromFunctionOutput(outputBuffer); - return CreateOrchestrationResult(isDone: true, actions, output: result, context.CustomStatus); - } - catch (Exception e) - { - // The orchestrator code has thrown an unhandled exception: - // this should be treated as an entire orchestration failure - throw new OrchestrationFailureException(actions, context.CustomStatus, e); - } + // The orchestrator code has thrown an unhandled exception: + // this should be treated as an entire orchestration failure + throw new OrchestrationFailureException(actions, context.CustomStatus, e); } } - finally + } + + public static object CreateReturnValueFromFunctionOutput(IList pipelineItems) + { + if (pipelineItems == null || pipelineItems.Count <= 0) { - pwsh.ClearStreamsAndCommands(); + return null; } + + return pipelineItems.Count == 1 ? pipelineItems[0] : pipelineItems.ToArray(); } private static Hashtable CreateOrchestrationResult( @@ -72,7 +112,12 @@ private static Hashtable CreateOrchestrationResult( object customStatus) { var orchestrationMessage = new OrchestrationMessage(isDone, actions, output, customStatus); - return new Hashtable { { AzFunctionInfo.DollarReturn, orchestrationMessage } }; + return new Hashtable { { "$return", orchestrationMessage } }; + } + + public void SetExternalInvoker(IExternalInvoker externalInvoker) + { + _externalInvoker = externalInvoker; } } } 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/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 0efb681d..fda8d80d 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -6,43 +6,132 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System; + using System.Collections.ObjectModel; + using System.Linq; using System.Management.Automation; - using PowerShell; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; + using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + using Newtonsoft.Json; internal class PowerShellServices : IPowerShellServices { - private const string SetFunctionInvocationContextCommand = - "Microsoft.Azure.Functions.PowerShellWorker\\Set-FunctionInvocationContext"; + private readonly string SetFunctionInvocationContextCommand; + private const string ExternalDurableSDKName = "DurableSDK"; + private const string InternalDurableSDKName = "Microsoft.Azure.Functions.PowerShellWorker"; private readonly PowerShell _pwsh; - private bool _hasSetOrchestrationContext = false; + private bool _hasInitializedDurableFunction = false; + private readonly bool _useExternalDurableSDK = false; public PowerShellServices(PowerShell pwsh) { + //This logic will be commented out until the external SDK is published on the PS Gallery + + // We attempt to import the external SDK upon construction of the PowerShellServices object. + // We maintain the boolean member _useExternalDurableSDK in this object rather than + // DurableController because the expected input and functionality of SetFunctionInvocationContextCommand + // may differ between the internal and external implementations. + + try + { + pwsh.AddCommand(Utils.ImportModuleCmdletInfo) + .AddParameter("Name", ExternalDurableSDKName) + .AddParameter("ErrorAction", ActionPreference.Stop) + .InvokeAndClearCommands(); + _useExternalDurableSDK = true; + } + catch (Exception e) + { + // Check to see if ExternalDurableSDK is among the modules imported or + // available to be imported: if it is, then something went wrong with + // the Import-Module statement and we should throw an Exception. + // Otherwise, we use the InternalDurableSDK + var availableModules = pwsh.AddCommand(Utils.GetModuleCmdletInfo) + .AddParameter("Name", ExternalDurableSDKName) + .InvokeAndClearCommands(); + if (availableModules.Count() > 0) + { + // TODO: evaluate if there is a better error message or exception type to be throwing. + // Ideally, this should never happen. + throw new InvalidOperationException("The external Durable SDK was detected, but unable to be imported.", e); + } + _useExternalDurableSDK = false; + } + //_useExternalDurableSDK = false; + + if (_useExternalDurableSDK) + { + SetFunctionInvocationContextCommand = $"{ExternalDurableSDKName}\\Set-FunctionInvocationContext"; + } + else + { + SetFunctionInvocationContextCommand = $"{InternalDurableSDKName}\\Set-FunctionInvocationContext"; + } _pwsh = pwsh; } + public bool UseExternalDurableSDK() + { + return _useExternalDurableSDK; + } + + public PowerShell GetPowerShell() + { + return this._pwsh; + } + public void SetDurableClient(object durableClient) { _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); - - _hasSetOrchestrationContext = true; + _hasInitializedDurableFunction = true; } - public void SetOrchestrationContext(OrchestrationContext orchestrationContext) + public OrchestrationBindingInfo SetOrchestrationContext( + ParameterBinding context, + out IExternalInvoker externalInvoker) { - _pwsh.AddCommand(SetFunctionInvocationContextCommand) - .AddParameter("OrchestrationContext", orchestrationContext) - .InvokeAndClearCommands(); + externalInvoker = null; + OrchestrationBindingInfo orchestrationBindingInfo = new OrchestrationBindingInfo( + context.Name, + JsonConvert.DeserializeObject(context.Data.String)); + + if (_useExternalDurableSDK) + { + Collection> output = _pwsh.AddCommand(SetFunctionInvocationContextCommand) + // The external SetFunctionInvocationContextCommand expects a .json string to deserialize + // and writes an invoker function to the output pipeline. + .AddParameter("OrchestrationContext", context.Data.String) + .InvokeAndClearCommands>(); + if (output.Count() == 1) + { + externalInvoker = new ExternalInvoker(output[0]); + } + else + { + throw new InvalidOperationException($"Only a single output was expected for an invocation of {SetFunctionInvocationContextCommand}"); + } + } + else + { + _pwsh.AddCommand(SetFunctionInvocationContextCommand) + .AddParameter("OrchestrationContext", orchestrationBindingInfo.Context) + .InvokeAndClearCommands(); + } + _hasInitializedDurableFunction = true; + return orchestrationBindingInfo; + } + - _hasSetOrchestrationContext = true; + public void AddParameter(string name, object value) + { + _pwsh.AddParameter(name, value); } public void ClearOrchestrationContext() { - if (_hasSetOrchestrationContext) + if (_hasInitializedDurableFunction) { _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("Clear", true) @@ -50,6 +139,11 @@ public void ClearOrchestrationContext() } } + public void TracePipelineObject() + { + _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); + } + public IAsyncResult BeginInvoke(PSDataCollection output) { return _pwsh.BeginInvoke(input: null, output); diff --git a/src/DurableSDK/Tasks/ActivityInvocationTask.cs b/src/DurableSDK/Tasks/ActivityInvocationTask.cs index 35b31fd2..5f9f6a33 100644 --- a/src/DurableSDK/Tasks/ActivityInvocationTask.cs +++ b/src/DurableSDK/Tasks/ActivityInvocationTask.cs @@ -7,13 +7,7 @@ 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; @@ -62,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/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 7b7f1b4e..3d756e15 100644 --- a/src/DurableWorker/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; @@ -48,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) { @@ -59,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); } } @@ -88,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/DurableWorker/DurableFunctionInfo.cs b/src/DurableWorker/DurableFunctionInfo.cs index 8665cfe8..05c3650d 100644 --- a/src/DurableWorker/DurableFunctionInfo.cs +++ b/src/DurableWorker/DurableFunctionInfo.cs @@ -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/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 index c87f08b6..be368a48 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 { 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/Utility/TypeExtensions.cs b/src/Utility/TypeExtensions.cs index 2f3c4186..b3bcb9e3 100644 --- a/src/Utility/TypeExtensions.cs +++ b/src/Utility/TypeExtensions.cs @@ -142,7 +142,7 @@ public static object ConvertFromJson(string json) private static string ConvertToJson(object fromObj) { var context = new JsonObject.ConvertToJsonContext( - maxDepth: 4, + maxDepth: 10, //TODO: fix enumsAsStrings: false, compressOutput: true); 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/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 530711ca..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)] diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 5ec3244c..b57cb767 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -23,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"); @@ -39,7 +42,7 @@ public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction( _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData); _mockPowerShellServices.Verify( _ => _.SetDurableClient( @@ -48,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] @@ -112,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] @@ -133,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/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(); } From c90c0706e6109bba1df46d3727e0eff11730f158 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 18 Jul 2022 13:39:05 -0700 Subject: [PATCH 02/40] improve readability of PR --- .../Commands/InvokeDurableActivityCommand.cs | 2 ++ src/DurableSDK/ExternalInvoker.cs | 6 ++++- ...er.cs => IExternalOrchestrationInvoker.cs} | 6 ++--- src/DurableSDK/IOrchestrationInvoker.cs | 2 +- src/DurableSDK/IPowerShellServices.cs | 4 ++-- src/DurableSDK/OrchestrationInvoker.cs | 10 ++++---- src/DurableSDK/PowerShellServices.cs | 4 ++-- .../Tasks/ActivityInvocationTask.cs | 23 +++++++++++++++++++ src/DurableWorker/DurableController.cs | 2 +- ...soft.Azure.Functions.PowerShellWorker.psm1 | 2 +- src/PowerShell/PowerShellManager.cs | 4 ++-- test/Unit/Durable/DurableControllerTests.cs | 18 +++++++-------- .../Unit/Durable/OrchestrationInvokerTests.cs | 8 +++---- 13 files changed, 60 insertions(+), 31 deletions(-) rename src/DurableSDK/{IExternalInvoker.cs => IExternalOrchestrationInvoker.cs} (60%) diff --git a/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs b/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs index 4cb8639e..ba9b429f 100644 --- a/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs +++ b/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs @@ -43,8 +43,10 @@ protected override void EndProcessing() { var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey]; + var loadedFunctions = FunctionLoader.GetLoadedFunctions(); var task = new ActivityInvocationTask(FunctionName, Input, RetryOptions); + ActivityInvocationTask.ValidateTask(task, loadedFunctions); _durableTaskHandler.StopAndInitiateDurableTaskOrReplay( task, context, NoWait.IsPresent, diff --git a/src/DurableSDK/ExternalInvoker.cs b/src/DurableSDK/ExternalInvoker.cs index fa8a31c6..e2abf11e 100644 --- a/src/DurableSDK/ExternalInvoker.cs +++ b/src/DurableSDK/ExternalInvoker.cs @@ -9,15 +9,19 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Collections; using System.Management.Automation; - internal class ExternalInvoker : IExternalInvoker + + // Contract for the orchestration invoker in external the Durable Functions SDK + internal class ExternalInvoker : IExternalOrchestrationInvoker { private readonly Func _externalSDKInvokerFunction; + public ExternalInvoker(Func invokerFunction) { _externalSDKInvokerFunction = invokerFunction; } + // Invokes an orchestration using the external Durable SDK public Hashtable Invoke(IPowerShellServices powerShellServices) { return (Hashtable)_externalSDKInvokerFunction.Invoke(powerShellServices.GetPowerShell()); diff --git a/src/DurableSDK/IExternalInvoker.cs b/src/DurableSDK/IExternalOrchestrationInvoker.cs similarity index 60% rename from src/DurableSDK/IExternalInvoker.cs rename to src/DurableSDK/IExternalOrchestrationInvoker.cs index 3a703f3d..91e15033 100644 --- a/src/DurableSDK/IExternalInvoker.cs +++ b/src/DurableSDK/IExternalOrchestrationInvoker.cs @@ -7,10 +7,10 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System.Collections; - // Represents a contract for the - internal interface IExternalInvoker + // Contract interface for the orchestration invoker in external the Durable Functions SDK + internal interface IExternalOrchestrationInvoker { - // Method to invoke an orchestration using the external Durable SDK + // Invokes an orchestration using the external Durable SDK Hashtable Invoke(IPowerShellServices powerShellServices); } } diff --git a/src/DurableSDK/IOrchestrationInvoker.cs b/src/DurableSDK/IOrchestrationInvoker.cs index 36011b56..8e83c7b9 100644 --- a/src/DurableSDK/IOrchestrationInvoker.cs +++ b/src/DurableSDK/IOrchestrationInvoker.cs @@ -10,6 +10,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal interface IOrchestrationInvoker { Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowerShellServices pwsh); - void SetExternalInvoker(IExternalInvoker externalInvoker); + void SetExternalInvoker(IExternalOrchestrationInvoker externalInvoker); } } diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index cdd850bc..2fbe322d 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -13,11 +13,11 @@ internal interface IPowerShellServices { PowerShell GetPowerShell(); - bool UseExternalDurableSDK(); + bool HasExternalDurableSDK(); void SetDurableClient(object durableClient); - OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context, out IExternalInvoker externalInvoker); + OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context, out IExternalOrchestrationInvoker externalInvoker); void ClearOrchestrationContext(); diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index b71116b4..c67ebfaf 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -15,7 +15,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal class OrchestrationInvoker : IOrchestrationInvoker { - private IExternalInvoker _externalInvoker; + private IExternalOrchestrationInvoker externalInvoker; internal static string isOrchestrationFailureKey = "IsOrchestrationFailure"; public Hashtable Invoke( @@ -24,7 +24,7 @@ public Hashtable Invoke( { try { - if (powerShellServices.UseExternalDurableSDK()) + if (powerShellServices.HasExternalDurableSDK()) { return InvokeExternalDurableSDK(powerShellServices); } @@ -43,7 +43,7 @@ public Hashtable Invoke( public Hashtable InvokeExternalDurableSDK(IPowerShellServices powerShellServices) { - return _externalInvoker.Invoke(powerShellServices); + return externalInvoker.Invoke(powerShellServices); } public Hashtable InvokeInternalDurableSDK( @@ -115,9 +115,9 @@ private static Hashtable CreateOrchestrationResult( return new Hashtable { { "$return", orchestrationMessage } }; } - public void SetExternalInvoker(IExternalInvoker externalInvoker) + public void SetExternalInvoker(IExternalOrchestrationInvoker externalInvoker) { - _externalInvoker = externalInvoker; + this.externalInvoker = externalInvoker; } } } diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index fda8d80d..f5a80cc4 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -70,7 +70,7 @@ public PowerShellServices(PowerShell pwsh) _pwsh = pwsh; } - public bool UseExternalDurableSDK() + public bool HasExternalDurableSDK() { return _useExternalDurableSDK; } @@ -90,7 +90,7 @@ public void SetDurableClient(object durableClient) public OrchestrationBindingInfo SetOrchestrationContext( ParameterBinding context, - out IExternalInvoker externalInvoker) + out IExternalOrchestrationInvoker externalInvoker) { externalInvoker = null; OrchestrationBindingInfo orchestrationBindingInfo = new OrchestrationBindingInfo( diff --git a/src/DurableSDK/Tasks/ActivityInvocationTask.cs b/src/DurableSDK/Tasks/ActivityInvocationTask.cs index 5f9f6a33..00105295 100644 --- a/src/DurableSDK/Tasks/ActivityInvocationTask.cs +++ b/src/DurableSDK/Tasks/ActivityInvocationTask.cs @@ -7,10 +7,13 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks { + using System; + using System.Collections.Generic; using System.Linq; using Microsoft.Azure.Functions.PowerShellWorker.Durable; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions; using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; + using Microsoft.Azure.WebJobs.Script.Grpc.Messages; public class ActivityInvocationTask : DurableTask { @@ -56,5 +59,25 @@ 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/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 3d756e15..2e23bcf6 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -69,7 +69,7 @@ public void InitializeBindings(IList inputData) { _orchestrationBindingInfo = _powerShellServices.SetOrchestrationContext( inputData[0], - out IExternalInvoker externalInvoker); + out IExternalOrchestrationInvoker externalInvoker); _orchestrationInvoker.SetExternalInvoker(externalInvoker); } } 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 be368a48..c87f08b6 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 ($null -eq $PrivateData -or $null -eq $PrivateData['DurableClient']) { + if ($PrivateData -eq $null -or $PrivateData['DurableClient'] -eq $null) { throw "No binding of the type 'durableClient' was defined." } else { diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 0344c05d..cf980087 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -263,7 +263,7 @@ public Hashtable InvokeFunction( private void SetInputBindingParameterValues( AzFunctionInfo functionInfo, IEnumerable inputData, - string orchParamName, + string orchestratorParameter, Hashtable triggerMetadata, TraceContext traceContext, RetryContext retryContext) @@ -272,7 +272,7 @@ private void SetInputBindingParameterValues( { if (functionInfo.FuncParameters.TryGetValue(binding.Name, out var paramInfo)) { - if (string.CompareOrdinal(binding.Name, orchParamName) != 0) + if (string.CompareOrdinal(binding.Name, orchestratorParameter) != 0) { var bindingInfo = functionInfo.InputBindings[binding.Name]; var valueToUse = Utils.TransformInBindingValueAsNeeded(paramInfo, bindingInfo, binding.Data.ToObject()); diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index b57cb767..803bb4a9 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -60,16 +60,16 @@ public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction }; _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), - out It.Ref.IsAny)) + out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); - _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData); _mockPowerShellServices.Verify( _ => _.SetOrchestrationContext( It.Is(c => c.Data.ToString().Contains(_orchestrationContext.InstanceId)), - out It.Ref.IsAny), + out It.Ref.IsAny), Times.Once); } @@ -121,9 +121,9 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame }; _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( It.IsAny(), - out It.Ref.IsAny)) + out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); - _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData); Assert.True(durableController.TryGetInputBindingParameterValue(_contextParameterName, out var value)); @@ -143,9 +143,9 @@ internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrat _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( It.IsAny(), - out It.Ref.IsAny)) + out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); - _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData); Assert.False(durableController.TryGetInputBindingParameterValue(_contextParameterName, out var value)); @@ -160,9 +160,9 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( It.IsAny(), - out It.Ref.IsAny)) + out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); - _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData); var expectedResult = new Hashtable(); diff --git a/test/Unit/Durable/OrchestrationInvokerTests.cs b/test/Unit/Durable/OrchestrationInvokerTests.cs index 2a2db694..50197b53 100644 --- a/test/Unit/Durable/OrchestrationInvokerTests.cs +++ b/test/Unit/Durable/OrchestrationInvokerTests.cs @@ -34,7 +34,7 @@ public void InvocationRunsToCompletionIfNotStopped() { var invocationAsyncResult = DurableTestUtilities.CreateInvocationResult(completed: true); DurableTestUtilities.ExpectBeginInvoke(_mockPowerShellServices, invocationAsyncResult); - _mockPowerShellServices.Setup(_ => _.UseExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); _orchestrationInvoker.Invoke(_orchestrationBindingInfo, _mockPowerShellServices.Object); @@ -43,7 +43,7 @@ public void InvocationRunsToCompletionIfNotStopped() _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.Verify(_ => _.HasExternalDurableSDK(), Times.Once); _mockPowerShellServices.VerifyNoOtherCalls(); } @@ -51,14 +51,14 @@ public void InvocationRunsToCompletionIfNotStopped() public void InvocationStopsOnStopEvent() { InvokeOrchestration(completed: false); - _mockPowerShellServices.Setup(_ => _.UseExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).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.Verify(_ => _.HasExternalDurableSDK(), Times.Once); _mockPowerShellServices.VerifyNoOtherCalls(); } From f2e291ca0e73636e9fbbac4ae75d8996b5f94840 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 18 Jul 2022 14:27:22 -0700 Subject: [PATCH 03/40] improve readability further --- src/DurableSDK/ExternalInvoker.cs | 2 +- .../IExternalOrchestrationInvoker.cs | 2 +- src/DurableSDK/OrchestrationInvoker.cs | 2 +- src/DurableSDK/PowerShellServices.cs | 31 ++++++++++--------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/DurableSDK/ExternalInvoker.cs b/src/DurableSDK/ExternalInvoker.cs index e2abf11e..fb760b5c 100644 --- a/src/DurableSDK/ExternalInvoker.cs +++ b/src/DurableSDK/ExternalInvoker.cs @@ -10,7 +10,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Management.Automation; - // Contract for the orchestration invoker in external the Durable Functions SDK + // Contract for the orchestration invoker in the external Durable Functions SDK internal class ExternalInvoker : IExternalOrchestrationInvoker { private readonly Func _externalSDKInvokerFunction; diff --git a/src/DurableSDK/IExternalOrchestrationInvoker.cs b/src/DurableSDK/IExternalOrchestrationInvoker.cs index 91e15033..99b8c2ca 100644 --- a/src/DurableSDK/IExternalOrchestrationInvoker.cs +++ b/src/DurableSDK/IExternalOrchestrationInvoker.cs @@ -7,7 +7,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System.Collections; - // Contract interface for the orchestration invoker in external the Durable Functions SDK + // Contract interface for the orchestration invoker in the external Durable Functions SDK internal interface IExternalOrchestrationInvoker { // Invokes an orchestration using the external Durable SDK diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index c67ebfaf..46748213 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -112,7 +112,7 @@ private static Hashtable CreateOrchestrationResult( object customStatus) { var orchestrationMessage = new OrchestrationMessage(isDone, actions, output, customStatus); - return new Hashtable { { "$return", orchestrationMessage } }; + return new Hashtable { { AzFunctionInfo.DollarReturn, orchestrationMessage } }; } public void SetExternalInvoker(IExternalOrchestrationInvoker externalInvoker) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index f5a80cc4..a7caf27e 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -20,15 +20,13 @@ internal class PowerShellServices : IPowerShellServices private const string InternalDurableSDKName = "Microsoft.Azure.Functions.PowerShellWorker"; private readonly PowerShell _pwsh; - private bool _hasInitializedDurableFunction = false; - private readonly bool _useExternalDurableSDK = false; + private bool hasInitializedDurableFunctions = false; + private readonly bool hasExternalDurableSDK = false; public PowerShellServices(PowerShell pwsh) { - //This logic will be commented out until the external SDK is published on the PS Gallery - // We attempt to import the external SDK upon construction of the PowerShellServices object. - // We maintain the boolean member _useExternalDurableSDK in this object rather than + // We maintain the boolean member hasExternalDurableSDK in this object rather than // DurableController because the expected input and functionality of SetFunctionInvocationContextCommand // may differ between the internal and external implementations. @@ -38,7 +36,7 @@ public PowerShellServices(PowerShell pwsh) .AddParameter("Name", ExternalDurableSDKName) .AddParameter("ErrorAction", ActionPreference.Stop) .InvokeAndClearCommands(); - _useExternalDurableSDK = true; + hasExternalDurableSDK = true; } catch (Exception e) { @@ -55,11 +53,10 @@ public PowerShellServices(PowerShell pwsh) // Ideally, this should never happen. throw new InvalidOperationException("The external Durable SDK was detected, but unable to be imported.", e); } - _useExternalDurableSDK = false; + hasExternalDurableSDK = false; } - //_useExternalDurableSDK = false; - if (_useExternalDurableSDK) + if (hasExternalDurableSDK) { SetFunctionInvocationContextCommand = $"{ExternalDurableSDKName}\\Set-FunctionInvocationContext"; } @@ -72,7 +69,7 @@ public PowerShellServices(PowerShell pwsh) public bool HasExternalDurableSDK() { - return _useExternalDurableSDK; + return hasExternalDurableSDK; } public PowerShell GetPowerShell() @@ -85,7 +82,7 @@ public void SetDurableClient(object durableClient) _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); - _hasInitializedDurableFunction = true; + hasInitializedDurableFunctions = true; } public OrchestrationBindingInfo SetOrchestrationContext( @@ -97,14 +94,18 @@ public OrchestrationBindingInfo SetOrchestrationContext( context.Name, JsonConvert.DeserializeObject(context.Data.String)); - if (_useExternalDurableSDK) + if (hasExternalDurableSDK) { Collection> output = _pwsh.AddCommand(SetFunctionInvocationContextCommand) // The external SetFunctionInvocationContextCommand expects a .json string to deserialize // and writes an invoker function to the output pipeline. .AddParameter("OrchestrationContext", context.Data.String) .InvokeAndClearCommands>(); - if (output.Count() == 1) + + // If more than 1 element is present in the output pipeline, we cannot trust that we have + // obtained the external orchestrator invoker; i.e the output contract is not met. + var outputContractIsMet = output.Count() == 1; + if (outputContractIsMet) { externalInvoker = new ExternalInvoker(output[0]); } @@ -119,7 +120,7 @@ public OrchestrationBindingInfo SetOrchestrationContext( .AddParameter("OrchestrationContext", orchestrationBindingInfo.Context) .InvokeAndClearCommands(); } - _hasInitializedDurableFunction = true; + hasInitializedDurableFunctions = true; return orchestrationBindingInfo; } @@ -131,7 +132,7 @@ public void AddParameter(string name, object value) public void ClearOrchestrationContext() { - if (_hasInitializedDurableFunction) + if (hasInitializedDurableFunctions) { _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("Clear", true) From 419ae378d21e0a8147a258a97f1b508d5fa78935 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 18 Jul 2022 14:39:21 -0700 Subject: [PATCH 04/40] clean up PR further --- src/DurableSDK/DurableTaskHandler.cs | 36 ++-------- .../OrchestrationActionCollector.cs | 1 - src/DurableSDK/PowerShellExtensions.cs | 69 ------------------- src/DurableSDK/PowerShellServices.cs | 1 + .../Tasks/ActivityInvocationTask.cs | 7 +- src/DurableWorker/DurableController.cs | 1 - src/DurableWorker/DurableFunctionInfo.cs | 5 +- 7 files changed, 11 insertions(+), 109 deletions(-) delete mode 100644 src/DurableSDK/PowerShellExtensions.cs diff --git a/src/DurableSDK/DurableTaskHandler.cs b/src/DurableSDK/DurableTaskHandler.cs index a35546b8..c65b1275 100644 --- a/src/DurableSDK/DurableTaskHandler.cs +++ b/src/DurableSDK/DurableTaskHandler.cs @@ -6,12 +6,10 @@ 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 Microsoft.PowerShell.Commands; + using Utility; internal class DurableTaskHandler { @@ -85,7 +83,7 @@ public void StopAndInitiateDurableTaskOrReplay( retryOptions.MaxNumberOfAttempts, onSuccess: result => { - output(ConvertFromJson(result)); + output(TypeExtensions.ConvertFromJson(result)); }, onFailure); @@ -234,41 +232,15 @@ private static object GetEventResult(HistoryEvent historyEvent) if (historyEvent.EventType == HistoryEventType.TaskCompleted) { - return ConvertFromJson(historyEvent.Result); + return TypeExtensions.ConvertFromJson(historyEvent.Result); } else if (historyEvent.EventType == HistoryEventType.EventRaised) { - return ConvertFromJson(historyEvent.Input); + return TypeExtensions.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/OrchestrationActionCollector.cs b/src/DurableSDK/OrchestrationActionCollector.cs index 1542c2bf..b62fbc4b 100644 --- a/src/DurableSDK/OrchestrationActionCollector.cs +++ b/src/DurableSDK/OrchestrationActionCollector.cs @@ -11,7 +11,6 @@ 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/DurableSDK/PowerShellExtensions.cs b/src/DurableSDK/PowerShellExtensions.cs deleted file mode 100644 index a5225188..00000000 --- a/src/DurableSDK/PowerShellExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -// -// 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/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index a7caf27e..b021767b 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation; + using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using Newtonsoft.Json; diff --git a/src/DurableSDK/Tasks/ActivityInvocationTask.cs b/src/DurableSDK/Tasks/ActivityInvocationTask.cs index 00105295..73447526 100644 --- a/src/DurableSDK/Tasks/ActivityInvocationTask.cs +++ b/src/DurableSDK/Tasks/ActivityInvocationTask.cs @@ -8,12 +8,14 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks { using System; - using System.Collections.Generic; using System.Linq; + using System.Collections.Generic; + + using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + using Microsoft.Azure.Functions.PowerShellWorker.Durable; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions; using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; - using Microsoft.Azure.WebJobs.Script.Grpc.Messages; public class ActivityInvocationTask : DurableTask { @@ -60,7 +62,6 @@ internal override OrchestrationAction CreateOrchestrationAction() : new CallActivityWithRetryAction(FunctionName, Input, RetryOptions); } - internal static void ValidateTask(ActivityInvocationTask task, IEnumerable loadedFunctions) { var functionInfo = loadedFunctions.FirstOrDefault(fi => fi.FuncName == task.FunctionName); diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 2e23bcf6..761f7add 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -63,7 +63,6 @@ public void InitializeBindings(IList inputData) .Data.ToObject(); _powerShellServices.SetDurableClient(durableClient); - } else if (_durableFunctionInfo.IsOrchestrationFunction) { diff --git a/src/DurableWorker/DurableFunctionInfo.cs b/src/DurableWorker/DurableFunctionInfo.cs index 05c3650d..d0181bc4 100644 --- a/src/DurableWorker/DurableFunctionInfo.cs +++ b/src/DurableWorker/DurableFunctionInfo.cs @@ -13,12 +13,11 @@ 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 bool IsActivityFunction => Type == DurableFunctionType.ActivityFunction; public string DurableClientBindingName { get; } From 049c4f77e5cb6f1e2f6cc414be240faf33b3e121 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 18 Jul 2022 14:48:56 -0700 Subject: [PATCH 05/40] further cleanup --- .../SetFunctionInvocationContextCommand.cs | 1 - src/DurableSDK/ExternalInvoker.cs | 2 - .../Tasks/ActivityInvocationTask.cs | 2 +- src/Utility/FunctionReturnValueBuilder.cs | 1 - .../Durable/ActivityInvocationTaskTests.cs | 53 +++++++++++++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs b/src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs index 3430be16..943e8362 100644 --- a/src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs +++ b/src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs @@ -9,7 +9,6 @@ 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/DurableSDK/ExternalInvoker.cs b/src/DurableSDK/ExternalInvoker.cs index fb760b5c..6b3d3a13 100644 --- a/src/DurableSDK/ExternalInvoker.cs +++ b/src/DurableSDK/ExternalInvoker.cs @@ -9,13 +9,11 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Collections; using System.Management.Automation; - // Contract for the orchestration invoker in the external Durable Functions SDK internal class ExternalInvoker : IExternalOrchestrationInvoker { private readonly Func _externalSDKInvokerFunction; - public ExternalInvoker(Func invokerFunction) { _externalSDKInvokerFunction = invokerFunction; diff --git a/src/DurableSDK/Tasks/ActivityInvocationTask.cs b/src/DurableSDK/Tasks/ActivityInvocationTask.cs index 73447526..6f54182c 100644 --- a/src/DurableSDK/Tasks/ActivityInvocationTask.cs +++ b/src/DurableSDK/Tasks/ActivityInvocationTask.cs @@ -11,7 +11,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks using System.Linq; using System.Collections.Generic; - using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + using WebJobs.Script.Grpc.Messages; using Microsoft.Azure.Functions.PowerShellWorker.Durable; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions; diff --git a/src/Utility/FunctionReturnValueBuilder.cs b/src/Utility/FunctionReturnValueBuilder.cs index 34b4a5e7..8f6b9a47 100644 --- a/src/Utility/FunctionReturnValueBuilder.cs +++ b/src/Utility/FunctionReturnValueBuilder.cs @@ -16,7 +16,6 @@ public static object CreateReturnValueFromFunctionOutput(IList pipelineI { return null; } - return pipelineItems.Count == 1 ? pipelineItems[0] : pipelineItems.ToArray(); } } diff --git a/test/Unit/Durable/ActivityInvocationTaskTests.cs b/test/Unit/Durable/ActivityInvocationTaskTests.cs index 96e27571..530711ca 100644 --- a/test/Unit/Durable/ActivityInvocationTaskTests.cs +++ b/test/Unit/Durable/ActivityInvocationTaskTests.cs @@ -163,6 +163,59 @@ 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)] From 1297298d2fb74e95509ad9c9949adbb1ca0d90aa Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 18 Jul 2022 15:07:58 -0700 Subject: [PATCH 06/40] remove unecessary file --- test/E2E/TestFunctionApp/profile.ps1 | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 test/E2E/TestFunctionApp/profile.ps1 diff --git a/test/E2E/TestFunctionApp/profile.ps1 b/test/E2E/TestFunctionApp/profile.ps1 deleted file mode 100644 index 9afdf1da..00000000 --- a/test/E2E/TestFunctionApp/profile.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -# 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 From 0d1cc1254769fa3ee196d7d26076040324887635 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 18 Jul 2022 15:33:16 -0700 Subject: [PATCH 07/40] increase JSON depth limit --- src/Utility/TypeExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utility/TypeExtensions.cs b/src/Utility/TypeExtensions.cs index b3bcb9e3..7fc3baf0 100644 --- a/src/Utility/TypeExtensions.cs +++ b/src/Utility/TypeExtensions.cs @@ -142,7 +142,7 @@ public static object ConvertFromJson(string json) private static string ConvertToJson(object fromObj) { var context = new JsonObject.ConvertToJsonContext( - maxDepth: 10, //TODO: fix + maxDepth: 50, enumsAsStrings: false, compressOutput: true); From 88dcc98b86cefb044a845b85a669df6d4bf6c7be Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 22 Jul 2022 14:54:08 -0700 Subject: [PATCH 08/40] respond to some PR feedback --- src/DurableSDK/OrchestrationInvoker.cs | 1 - src/DurableSDK/PowerShellServices.cs | 30 +++++++++------------- src/Utility/TypeExtensions.cs | 5 ++++ src/resources/PowerShellWorkerStrings.resx | 15 +++++++++++ 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index 46748213..2cbd146c 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -74,7 +74,6 @@ public Hashtable InvokeInternalDurableSDK( { // The orchestration function should be stopped and restarted powerShellServices.StopInvoke(); - // return (Hashtable)orchestrationBindingInfo.Context.OrchestrationActionCollector.output; return CreateOrchestrationResult(isDone: false, actions, output: null, context.CustomStatus); } else diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index b021767b..be177495 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -17,8 +17,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal class PowerShellServices : IPowerShellServices { private readonly string SetFunctionInvocationContextCommand; - private const string ExternalDurableSDKName = "DurableSDK"; - private const string InternalDurableSDKName = "Microsoft.Azure.Functions.PowerShellWorker"; private readonly PowerShell _pwsh; private bool hasInitializedDurableFunctions = false; @@ -34,7 +32,7 @@ public PowerShellServices(PowerShell pwsh) try { pwsh.AddCommand(Utils.ImportModuleCmdletInfo) - .AddParameter("Name", ExternalDurableSDKName) + .AddParameter("Name", PowerShellWorkerStrings.ExternalDurableSDKName) .AddParameter("ErrorAction", ActionPreference.Stop) .InvokeAndClearCommands(); hasExternalDurableSDK = true; @@ -46,26 +44,21 @@ public PowerShellServices(PowerShell pwsh) // the Import-Module statement and we should throw an Exception. // Otherwise, we use the InternalDurableSDK var availableModules = pwsh.AddCommand(Utils.GetModuleCmdletInfo) - .AddParameter("Name", ExternalDurableSDKName) + .AddParameter("Name", PowerShellWorkerStrings.ExternalDurableSDKName) .InvokeAndClearCommands(); if (availableModules.Count() > 0) { - // TODO: evaluate if there is a better error message or exception type to be throwing. - // Ideally, this should never happen. - throw new InvalidOperationException("The external Durable SDK was detected, but unable to be imported.", e); + var exceptionMessage = string.Format(PowerShellWorkerStrings.FailedToImportModule, PowerShellWorkerStrings.ExternalDurableSDKName, ""); + throw new InvalidOperationException(exceptionMessage, e); } hasExternalDurableSDK = false; } - if (hasExternalDurableSDK) - { - SetFunctionInvocationContextCommand = $"{ExternalDurableSDKName}\\Set-FunctionInvocationContext"; - } - else - { - SetFunctionInvocationContextCommand = $"{InternalDurableSDKName}\\Set-FunctionInvocationContext"; - } - _pwsh = pwsh; + var templatedSetFunctionInvocationContextCommand = "{0}\\Set-FunctionInvocationContext"; + var prefix = hasExternalDurableSDK ? PowerShellWorkerStrings.ExternalDurableSDKName : PowerShellWorkerStrings.InternalDurableSDKName; + SetFunctionInvocationContextCommand = string.Format(templatedSetFunctionInvocationContextCommand, prefix); + + _pwsh = pwsh; } public bool HasExternalDurableSDK() @@ -112,7 +105,8 @@ public OrchestrationBindingInfo SetOrchestrationContext( } else { - throw new InvalidOperationException($"Only a single output was expected for an invocation of {SetFunctionInvocationContextCommand}"); + var exceptionMessage = string.Format(PowerShellWorkerStrings.UnexpectedOutputInExternalDurableCommand, SetFunctionInvocationContextCommand); + throw new InvalidOperationException(exceptionMessage); } } else @@ -143,7 +137,7 @@ public void ClearOrchestrationContext() public void TracePipelineObject() { - _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); + _pwsh.AddCommand(PowerShellWorkerStrings.TracePipelineObjectCommand); } public IAsyncResult BeginInvoke(PSDataCollection output) diff --git a/src/Utility/TypeExtensions.cs b/src/Utility/TypeExtensions.cs index 7fc3baf0..5d5082fa 100644 --- a/src/Utility/TypeExtensions.cs +++ b/src/Utility/TypeExtensions.cs @@ -141,6 +141,11 @@ public static object ConvertFromJson(string json) private static string ConvertToJson(object fromObj) { + /* we set the max-depth to 50 because the Durable Functions Extension + * may produce deeply nested JSON-Objects when callig its + * WhenAll/WhenAny APIs. The value 50 is arbitrarily chosen to be + * "deep enough" for the vast majority of cases. + */ var context = new JsonObject.ConvertToJsonContext( maxDepth: 50, enumsAsStrings: false, diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 2d0228d7..9ec6de6d 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -352,4 +352,19 @@ Dependency snapshot '{0}' does not contain acceptable module versions. + + DurableSDK + + + Failed to import the '{0}' module. Please make sure this module is available in the PowerShell worker path. For instructions on how to install this module, please see '{1}'. + + + Microsoft.Azure.Functions.PowerShellWorker + + + Microsoft.Azure.Functions.PowerShellWorker\Trace-PipelineObject + + + Only a single output was expected for an invocation of '{0}'. + \ No newline at end of file From 1ac8d499814862ce21e47215a1bb46cf9b9bd71d Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 22 Jul 2022 16:21:21 -0700 Subject: [PATCH 09/40] patch tests --- src/DurableWorker/DurableController.cs | 3 ++- src/PowerShell/PowerShellManager.cs | 3 ++- src/resources/PowerShellWorkerStrings.resx | 3 +++ test/Unit/Durable/DurableControllerTests.cs | 14 +++++++------- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 761f7add..9226ceb7 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -52,7 +52,7 @@ public string GetOrchestrationParameterName() return _orchestrationBindingInfo?.ParameterName; } - public void InitializeBindings(IList inputData) + public void InitializeBindings(IList inputData, out bool hasExternalSDK) { // If the function is an durable client, then we set the DurableClient // in the module context for the 'Start-DurableOrchestration' function to use. @@ -71,6 +71,7 @@ public void InitializeBindings(IList inputData) out IExternalOrchestrationInvoker externalInvoker); _orchestrationInvoker.SetExternalInvoker(externalInvoker); } + hasExternalSDK = _powerShellServices.HasExternalDurableSDK(); } public void AfterFunctionInvocation() diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index cf980087..4aa357bc 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -209,7 +209,8 @@ public Hashtable InvokeFunction( try { - durableFunctionsUtils.InitializeBindings(inputData); + durableFunctionsUtils.InitializeBindings(inputData, out bool hasExternalDFsdk); + Logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.UtilizingExternalDurableSDK, hasExternalDFsdk)); AddEntryPointInvocationCommand(functionInfo); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.FunctionCodeReady); diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 9ec6de6d..a28aa38a 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -367,4 +367,7 @@ Only a single output was expected for an invocation of '{0}'. + + Utilizing external Durable Functions SDK: '{0}'. + \ No newline at end of file diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 803bb4a9..f4ce9a86 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -42,7 +42,7 @@ public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); - durableController.InitializeBindings(inputData); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); _mockPowerShellServices.Verify( _ => _.SetDurableClient( @@ -64,7 +64,7 @@ public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction .Returns(_orchestrationBindingInfo); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); - durableController.InitializeBindings(inputData); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); _mockPowerShellServices.Verify( _ => _.SetOrchestrationContext( @@ -79,7 +79,7 @@ public void InitializeBindings_Throws_OnOrchestrationFunctionWithoutContextParam var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); var inputData = new ParameterBinding[0]; - Assert.ThrowsAny(() => durableController.InitializeBindings(inputData)); + Assert.ThrowsAny(() => durableController.InitializeBindings(inputData, out bool hasExternalSDK)); } [Theory] @@ -94,7 +94,7 @@ internal void InitializeBindings_DoesNothing_ForNonOrchestrationFunction(Durable CreateParameterBinding("ParameterName", _orchestrationContext) }; - durableController.InitializeBindings(inputData); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); } [Theory] @@ -124,7 +124,7 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); - durableController.InitializeBindings(inputData); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); Assert.True(durableController.TryGetInputBindingParameterValue(_contextParameterName, out var value)); Assert.Equal(_orchestrationContext.InstanceId, ((OrchestrationContext)value).InstanceId); @@ -146,7 +146,7 @@ internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrat out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); - durableController.InitializeBindings(inputData); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); Assert.False(durableController.TryGetInputBindingParameterValue(_contextParameterName, out var value)); Assert.Null(value); @@ -163,7 +163,7 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); - durableController.InitializeBindings(inputData); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); var expectedResult = new Hashtable(); _mockOrchestrationInvoker.Setup( From d7fe44f28b4c7e68f313104e243222d96d6025fe Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 25 Jul 2022 10:40:28 -0700 Subject: [PATCH 10/40] mock HasExternalSDK in tests --- test/Unit/Durable/DurableControllerTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index f4ce9a86..2ba12721 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -41,6 +41,7 @@ public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() }; _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -63,6 +64,7 @@ public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -123,6 +125,8 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame It.IsAny(), out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -145,6 +149,8 @@ internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrat It.IsAny(), out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -162,7 +168,10 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() It.IsAny(), out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); var expectedResult = new Hashtable(); From 6f2d6546b0eadb9972ea74ea529928fd7dd715dd Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 25 Jul 2022 10:59:45 -0700 Subject: [PATCH 11/40] add return-false to mocked hasExternalDurableSDK --- test/Unit/Durable/DurableControllerTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 2ba12721..c9770a58 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -41,7 +41,7 @@ public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() }; _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); - _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -64,7 +64,7 @@ public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); - _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -125,7 +125,7 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame It.IsAny(), out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); - _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -149,7 +149,7 @@ internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrat It.IsAny(), out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); - _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -168,7 +168,7 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() It.IsAny(), out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); - _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); From 65913c3fa9434431538a09022380f9c7a13d7368 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 25 Jul 2022 11:09:14 -0700 Subject: [PATCH 12/40] fully mock hasExternalSDK --- test/Unit/Durable/DurableControllerTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index c9770a58..74319fe6 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -95,6 +95,7 @@ internal void InitializeBindings_DoesNothing_ForNonOrchestrationFunction(Durable // Even if a parameter similar to orchestration context is passed: CreateParameterBinding("ParameterName", _orchestrationContext) }; + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); durableController.InitializeBindings(inputData, out bool hasExternalSDK); } From 00bbf2e5e4fba96925d4b130c89b49a373b239ad Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 25 Jul 2022 11:21:17 -0700 Subject: [PATCH 13/40] turn IsOrchestrationFailure into constant string in string-resource file --- src/DurableSDK/OrchestrationInvoker.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index 2cbd146c..7f362e83 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -16,7 +16,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal class OrchestrationInvoker : IOrchestrationInvoker { private IExternalOrchestrationInvoker externalInvoker; - internal static string isOrchestrationFailureKey = "IsOrchestrationFailure"; public Hashtable Invoke( OrchestrationBindingInfo orchestrationBindingInfo, @@ -32,7 +31,7 @@ public Hashtable Invoke( } catch (Exception ex) { - ex.Data.Add(isOrchestrationFailureKey, true); + ex.Data.Add(PowerShellWorkerStrings.isOrchestrationFailureKey, true); throw; } finally From 1bff0cb02025253233887acb3ba4be62063bc344 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 25 Jul 2022 11:27:17 -0700 Subject: [PATCH 14/40] make project build again --- src/PowerShell/PowerShellManager.cs | 2 +- src/resources/PowerShellWorkerStrings.resx | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 4aa357bc..6c3f01ef 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -246,7 +246,7 @@ public Hashtable InvokeFunction( } catch (Exception e) { - if (e.Data.Contains(OrchestrationInvoker.isOrchestrationFailureKey) && e.InnerException is IContainsErrorRecord inner) + if (e.Data.Contains(PowerShellWorkerStrings.isOrchestrationFailureKey) && e.InnerException is IContainsErrorRecord inner) { Logger.Log(isUserOnlyLog: true, LogLevel.Error, GetFunctionExceptionMessage(inner)); } diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index a28aa38a..74af7d84 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -370,4 +370,7 @@ Utilizing external Durable Functions SDK: '{0}'. + + IsOrchestrationFailure + \ No newline at end of file From a793c75287e0749086183a78f0bb76deabd7da42 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 25 Jul 2022 12:13:20 -0700 Subject: [PATCH 15/40] clean up PR --- src/DurableWorker/DurableController.cs | 15 +++++++++++++++ src/RequestProcessor.cs | 2 +- src/Utility/TypeExtensions.cs | 16 ++++++++-------- src/resources/PowerShellWorkerStrings.resx | 3 +++ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 9226ceb7..0986efe3 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -16,6 +16,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using PowerShellWorker.Utility; using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; + using System; /// /// The main entry point for durable functions support. @@ -66,6 +67,20 @@ public void InitializeBindings(IList inputData, out bool hasEx } else if (_durableFunctionInfo.IsOrchestrationFunction) { + var numBindings = inputData.Count; + if (numBindings != 1) + { + /* 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 exceptionMessage = string.Format(PowerShellWorkerStrings.UnableToInitializeOrchestrator, numBindings); + throw new InvalidOperationException(); + } + _orchestrationBindingInfo = _powerShellServices.SetOrchestrationContext( inputData[0], out IExternalOrchestrationInvoker externalInvoker); diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index c9c10d2f..7a160358 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -500,7 +500,7 @@ private static void BindOutputFromResult(InvocationResponse response, AzFunction if (functionInfo.DurableFunctionInfo.ProvidesForcedDollarReturnValue) { - response.ReturnValue = results[AzFunctionInfo.DollarReturn].ToTypedData(); + response.ReturnValue = results[AzFunctionInfo.DollarReturn].ToTypedData(isDurableData: true); } } diff --git a/src/Utility/TypeExtensions.cs b/src/Utility/TypeExtensions.cs index 5d5082fa..43151edf 100644 --- a/src/Utility/TypeExtensions.cs +++ b/src/Utility/TypeExtensions.cs @@ -139,15 +139,15 @@ public static object ConvertFromJson(string json) return retObj; } - private static string ConvertToJson(object fromObj) + private static string ConvertToJson(object fromObj, bool isDurableData) { - /* we set the max-depth to 50 because the Durable Functions Extension - * may produce deeply nested JSON-Objects when callig its - * WhenAll/WhenAny APIs. The value 50 is arbitrarily chosen to be - * "deep enough" for the vast majority of cases. + /* For Durable Functions, we set the max-depth to 100 because + * the Durable Functions Extension may produce deeply nested JSON-Objects + * when calling its WhenAll/WhenAny APIs. The value 100 is arbitrarily + * chosen to be "deep enough" for the vast majority of cases. */ var context = new JsonObject.ConvertToJsonContext( - maxDepth: 50, + maxDepth: isDurableData ? 100 : 10, enumsAsStrings: false, compressOutput: true); @@ -203,7 +203,7 @@ private static string DeriveContentType(HttpResponseContext httpResponseContext, : TextPlainMediaType); } - internal static TypedData ToTypedData(this object value) + internal static TypedData ToTypedData(this object value, bool isDurableData = false) { if (value is TypedData self) { @@ -249,7 +249,7 @@ internal static TypedData ToTypedData(this object value) if (IsValidJson(str)) { typedData.Json = str; } else { typedData.String = str; } break; default: - typedData.Json = ConvertToJson(originalValue); + typedData.Json = ConvertToJson(originalValue, isDurableData); break; } return typedData; diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 74af7d84..3085b063 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -373,4 +373,7 @@ IsOrchestrationFailure + + Unable to initialize orchestrator function due to presence of other bindings. Orchestrator Functions should never use any input or output bindings other than the orchestration trigger itself. See: https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-bindings + \ No newline at end of file From 635a341a7e7734e549bf38836e0aea5d4eb6680c Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 25 Jul 2022 12:29:52 -0700 Subject: [PATCH 16/40] add missing mock for HasExternalDurableSDK --- test/Unit/Durable/DurableControllerTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 74319fe6..a1ddd3c4 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -80,6 +80,7 @@ public void InitializeBindings_Throws_OnOrchestrationFunctionWithoutContextParam { var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); var inputData = new ParameterBinding[0]; + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); Assert.ThrowsAny(() => durableController.InitializeBindings(inputData, out bool hasExternalSDK)); } From d4b508403a01d9a09c09a103e43dcdfecf010ddd Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 25 Jul 2022 12:39:05 -0700 Subject: [PATCH 17/40] add exception-message to InvalidOperation --- src/DurableWorker/DurableController.cs | 2 +- src/resources/PowerShellWorkerStrings.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 0986efe3..d063ad99 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -78,7 +78,7 @@ public void InitializeBindings(IList inputData, out bool hasEx * Therefore, it's by design that input data contains only one item, which is the metadata of the orchestration context. */ var exceptionMessage = string.Format(PowerShellWorkerStrings.UnableToInitializeOrchestrator, numBindings); - throw new InvalidOperationException(); + throw new InvalidOperationException(exceptionMessage); } _orchestrationBindingInfo = _powerShellServices.SetOrchestrationContext( diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 3085b063..8084c2b6 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -374,6 +374,6 @@ IsOrchestrationFailure - Unable to initialize orchestrator function due to presence of other bindings. Orchestrator Functions should never use any input or output bindings other than the orchestration trigger itself. See: https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-bindings + Unable to initialize orchestrator function due to presence of other bindings. Total number of bindings found is '{0}'. Orchestrator Functions should never use any input or output bindings other than the orchestration trigger itself. See: https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-bindings \ No newline at end of file From 3f3dea6256283fbb11591b2f24f3b65e7d46a74f Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 25 Jul 2022 12:44:56 -0700 Subject: [PATCH 18/40] change exception type on initializeBindings --- src/DurableWorker/DurableController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index d063ad99..670a5833 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -78,7 +78,7 @@ public void InitializeBindings(IList inputData, out bool hasEx * Therefore, it's by design that input data contains only one item, which is the metadata of the orchestration context. */ var exceptionMessage = string.Format(PowerShellWorkerStrings.UnableToInitializeOrchestrator, numBindings); - throw new InvalidOperationException(exceptionMessage); + throw new ArgumentException(exceptionMessage); } _orchestrationBindingInfo = _powerShellServices.SetOrchestrationContext( From 8dd5ebbe86dec210d106d561631856d4c77c5b85 Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 2 Dec 2022 17:02:16 -0800 Subject: [PATCH 19/40] apply feedback. comments are pending --- src/DurableSDK/PowerShellServices.cs | 64 +++++++++++++++------ src/DurableWorker/DurableController.cs | 13 +++-- src/PowerShell/PowerShellManager.cs | 2 +- src/resources/PowerShellWorkerStrings.resx | 20 ++++++- test/Unit/Durable/DurableControllerTests.cs | 5 +- 5 files changed, 80 insertions(+), 24 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index be177495..c7f924c4 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -12,7 +12,10 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + using Microsoft.Extensions.Logging; using Newtonsoft.Json; + using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level; + internal class PowerShellServices : IPowerShellServices { @@ -21,37 +24,64 @@ internal class PowerShellServices : IPowerShellServices private readonly PowerShell _pwsh; private bool hasInitializedDurableFunctions = false; private readonly bool hasExternalDurableSDK = false; + private readonly ErrorRecordFormatter _errorRecordFormatter = new ErrorRecordFormatter(); - public PowerShellServices(PowerShell pwsh) - { - // We attempt to import the external SDK upon construction of the PowerShellServices object. - // We maintain the boolean member hasExternalDurableSDK in this object rather than - // DurableController because the expected input and functionality of SetFunctionInvocationContextCommand - // may differ between the internal and external implementations. + private bool tryImportingDurableSDK(PowerShell pwsh, Utility.ILogger logger) + { + var importSucceeded = false; try { pwsh.AddCommand(Utils.ImportModuleCmdletInfo) .AddParameter("Name", PowerShellWorkerStrings.ExternalDurableSDKName) .AddParameter("ErrorAction", ActionPreference.Stop) .InvokeAndClearCommands(); - hasExternalDurableSDK = true; + importSucceeded = true; } catch (Exception e) { - // Check to see if ExternalDurableSDK is among the modules imported or - // available to be imported: if it is, then something went wrong with - // the Import-Module statement and we should throw an Exception. - // Otherwise, we use the InternalDurableSDK - var availableModules = pwsh.AddCommand(Utils.GetModuleCmdletInfo) - .AddParameter("Name", PowerShellWorkerStrings.ExternalDurableSDKName) - .InvokeAndClearCommands(); - if (availableModules.Count() > 0) + var errorMessage = e.ToString(); + if (e.InnerException is IContainsErrorRecord inner) { - var exceptionMessage = string.Format(PowerShellWorkerStrings.FailedToImportModule, PowerShellWorkerStrings.ExternalDurableSDKName, ""); - throw new InvalidOperationException(exceptionMessage, e); + errorMessage = _errorRecordFormatter.Format(inner.ErrorRecord); + } + logger.Log(isUserOnlyLog: true, LogLevel.Error, string.Format( + PowerShellWorkerStrings.ErrorImportingDurableSDK, + PowerShellWorkerStrings.ExternalDurableSDKName,errorMessage)); + + } + return importSucceeded; + + } + + public PowerShellServices(PowerShell pwsh, Utility.ILogger logger) + { + // We attempt to import the external SDK upon construction of the PowerShellServices object. + // We maintain the boolean member hasExternalDurableSDK in this object rather than + // DurableController because the expected input and functionality of SetFunctionInvocationContextCommand + // may differ between the internal and external implementations. + + var availableModules = pwsh.AddCommand(Utils.GetModuleCmdletInfo) + .AddParameter("Name", PowerShellWorkerStrings.ExternalDurableSDKName) + .InvokeAndClearCommands(); + + var numModulesFound = availableModules.Count(); + if (numModulesFound != 1) + { + // default to internal SDK hasExternalDurableSDK = false; + logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.FailedToImportExternalDurableSDK, PowerShellWorkerStrings.ExternalDurableSDKName, numModulesFound)); + } + + else + { + + var externalSDKInfo = availableModules[0]; + logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.LoadingDurableSDK, externalSDKInfo.Name,externalSDKInfo.Version)); + + hasExternalDurableSDK = tryImportingDurableSDK(pwsh, logger); + logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.UtilizingExternalDurableSDK, hasExternalDurableSDK)); } var templatedSetFunctionInvocationContextCommand = "{0}\\Set-FunctionInvocationContext"; diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 670a5833..8cbd6cc4 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -27,25 +27,30 @@ internal class DurableController private readonly IPowerShellServices _powerShellServices; private readonly IOrchestrationInvoker _orchestrationInvoker; private OrchestrationBindingInfo _orchestrationBindingInfo; + private ILogger _logger; public DurableController( DurableFunctionInfo durableDurableFunctionInfo, - PowerShell pwsh) + PowerShell pwsh, + ILogger logger) : this( durableDurableFunctionInfo, - new PowerShellServices(pwsh), - new OrchestrationInvoker()) + new PowerShellServices(pwsh, logger), + new OrchestrationInvoker(), + logger) { } internal DurableController( DurableFunctionInfo durableDurableFunctionInfo, IPowerShellServices powerShellServices, - IOrchestrationInvoker orchestrationInvoker) + IOrchestrationInvoker orchestrationInvoker, + ILogger logger) { _durableFunctionInfo = durableDurableFunctionInfo; _powerShellServices = powerShellServices; _orchestrationInvoker = orchestrationInvoker; + _logger = logger; } public string GetOrchestrationParameterName() diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 6c3f01ef..2bb09bd8 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -205,7 +205,7 @@ public Hashtable InvokeFunction( FunctionInvocationPerformanceStopwatch stopwatch) { var outputBindings = FunctionMetadata.GetOutputBindingHashtable(_pwsh.Runspace.InstanceId); - var durableFunctionsUtils = new DurableController(functionInfo.DurableFunctionInfo, _pwsh); + var durableFunctionsUtils = new DurableController(functionInfo.DurableFunctionInfo, _pwsh, Logger); try { diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 8084c2b6..1f168678 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -353,7 +353,7 @@ Dependency snapshot '{0}' does not contain acceptable module versions. - DurableSDK + AzureFunctions.PowerShell.Durable.SDK Failed to import the '{0}' module. Please make sure this module is available in the PowerShell worker path. For instructions on how to install this module, please see '{1}'. @@ -376,4 +376,22 @@ Unable to initialize orchestrator function due to presence of other bindings. Total number of bindings found is '{0}'. Orchestrator Functions should never use any input or output bindings other than the orchestration trigger itself. See: https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-bindings + + AzureFunctions.PowerShell.Durable.SDK + + + Attempted to find 1 instance of '{0}' on the worker path but found '{0}', defaulting to internal Durable Functions SDK. + + + Failed to import the '{0}' module. Please make sure this module is available in the PowerShell worker path. Defaulting to built-in SDK. + + + Attempting to load '{0}', version '{1}', as the Durable Functions SDK + + + + + + An unexpected error ocurred while importing '{0}': '{1}' + \ No newline at end of file diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index a1ddd3c4..1f436d01 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -26,6 +26,8 @@ public class DurableControllerTests 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); + private static readonly ILogger testLogger = new ConsoleLogger(); + [Fact] public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() @@ -247,7 +249,8 @@ private DurableController CreateDurableController( return new DurableController( durableFunctionInfo, _mockPowerShellServices.Object, - _mockOrchestrationInvoker.Object); + _mockOrchestrationInvoker.Object, + testLogger); } private static ParameterBinding CreateParameterBinding(string parameterName, object value) From 4668b8def73971c5ffb4ce88e85339abbc25a144 Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 2 Dec 2022 17:25:23 -0800 Subject: [PATCH 20/40] add comments --- src/DurableSDK/PowerShellServices.cs | 60 ++++++++++++++++------------ 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index c7f924c4..34af5ed3 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -12,26 +12,26 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; - using Microsoft.Extensions.Logging; using Newtonsoft.Json; - using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level; - + using LogLevel = WebJobs.Script.Grpc.Messages.RpcLog.Types.Level; internal class PowerShellServices : IPowerShellServices { private readonly string SetFunctionInvocationContextCommand; private readonly PowerShell _pwsh; - private bool hasInitializedDurableFunctions = false; - private readonly bool hasExternalDurableSDK = false; + private bool _hasInitializedDurableFunctions = false; + private readonly bool _usesExternalDurableSDK = false; private readonly ErrorRecordFormatter _errorRecordFormatter = new ErrorRecordFormatter(); - private bool tryImportingDurableSDK(PowerShell pwsh, Utility.ILogger logger) + private bool tryImportingDurableSDK(PowerShell pwsh, ILogger logger) { + // Try to load/import the external Durable Functions SDK. If an error occurs, it is logged. var importSucceeded = false; try { + // attempt to load pwsh.AddCommand(Utils.ImportModuleCmdletInfo) .AddParameter("Name", PowerShellWorkerStrings.ExternalDurableSDKName) .AddParameter("ErrorAction", ActionPreference.Stop) @@ -40,7 +40,10 @@ private bool tryImportingDurableSDK(PowerShell pwsh, Utility.ILogger logger) } catch (Exception e) { + // If an error ocurred, we try to log the exception. var errorMessage = e.ToString(); + + // If a PowerShell error record is available through Get-Error, we log that instead. if (e.InnerException is IContainsErrorRecord inner) { errorMessage = _errorRecordFormatter.Format(inner.ErrorRecord); @@ -55,37 +58,42 @@ private bool tryImportingDurableSDK(PowerShell pwsh, Utility.ILogger logger) } - public PowerShellServices(PowerShell pwsh, Utility.ILogger logger) + public PowerShellServices(PowerShell pwsh, ILogger logger) { - // We attempt to import the external SDK upon construction of the PowerShellServices object. - // We maintain the boolean member hasExternalDurableSDK in this object rather than + // We attempt to import the external DF SDK upon construction of the PowerShellServices object. + // We also maintain the boolean member hasExternalDurableSDK in this object rather than // DurableController because the expected input and functionality of SetFunctionInvocationContextCommand // may differ between the internal and external implementations. - var availableModules = pwsh.AddCommand(Utils.GetModuleCmdletInfo) + // First, we search for the external DF SDK in the available modules + var matchingModules = pwsh.AddCommand(Utils.GetModuleCmdletInfo) .AddParameter("Name", PowerShellWorkerStrings.ExternalDurableSDKName) .InvokeAndClearCommands(); - var numModulesFound = availableModules.Count(); - if (numModulesFound != 1) + // To load the external SDK, we expect there to be a single matching module. + var numCandidates = matchingModules.Count(); + if (numCandidates != 1) { - // default to internal SDK - hasExternalDurableSDK = false; - logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.FailedToImportExternalDurableSDK, PowerShellWorkerStrings.ExternalDurableSDKName, numModulesFound)); + // If we do not find exactly one matching module, we default to the built-in SDK. + // IAlthough it is unlikely (or impossible), if there were more than 1 result, we do not want to determine the "right" module. + _usesExternalDurableSDK = false; + logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( + PowerShellWorkerStrings.FailedToImportExternalDurableSDK, + PowerShellWorkerStrings.ExternalDurableSDKName, numCandidates)); } else { + // We found a singular instance of the external DF SDK. We log its name and version, and try to load it. + var externalSDKInfo = matchingModules[0]; + logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.LoadingDurableSDK, externalSDKInfo.Name, externalSDKInfo.Version)); - var externalSDKInfo = availableModules[0]; - logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.LoadingDurableSDK, externalSDKInfo.Name,externalSDKInfo.Version)); - - hasExternalDurableSDK = tryImportingDurableSDK(pwsh, logger); - logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.UtilizingExternalDurableSDK, hasExternalDurableSDK)); + _usesExternalDurableSDK = tryImportingDurableSDK(pwsh, logger); + logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.UtilizingExternalDurableSDK, _usesExternalDurableSDK)); } var templatedSetFunctionInvocationContextCommand = "{0}\\Set-FunctionInvocationContext"; - var prefix = hasExternalDurableSDK ? PowerShellWorkerStrings.ExternalDurableSDKName : PowerShellWorkerStrings.InternalDurableSDKName; + var prefix = _usesExternalDurableSDK ? PowerShellWorkerStrings.ExternalDurableSDKName : PowerShellWorkerStrings.InternalDurableSDKName; SetFunctionInvocationContextCommand = string.Format(templatedSetFunctionInvocationContextCommand, prefix); _pwsh = pwsh; @@ -93,7 +101,7 @@ public PowerShellServices(PowerShell pwsh, Utility.ILogger logger) public bool HasExternalDurableSDK() { - return hasExternalDurableSDK; + return _usesExternalDurableSDK; } public PowerShell GetPowerShell() @@ -106,7 +114,7 @@ public void SetDurableClient(object durableClient) _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); - hasInitializedDurableFunctions = true; + _hasInitializedDurableFunctions = true; } public OrchestrationBindingInfo SetOrchestrationContext( @@ -118,7 +126,7 @@ public OrchestrationBindingInfo SetOrchestrationContext( context.Name, JsonConvert.DeserializeObject(context.Data.String)); - if (hasExternalDurableSDK) + if (_usesExternalDurableSDK) { Collection> output = _pwsh.AddCommand(SetFunctionInvocationContextCommand) // The external SetFunctionInvocationContextCommand expects a .json string to deserialize @@ -145,7 +153,7 @@ public OrchestrationBindingInfo SetOrchestrationContext( .AddParameter("OrchestrationContext", orchestrationBindingInfo.Context) .InvokeAndClearCommands(); } - hasInitializedDurableFunctions = true; + _hasInitializedDurableFunctions = true; return orchestrationBindingInfo; } @@ -157,7 +165,7 @@ public void AddParameter(string name, object value) public void ClearOrchestrationContext() { - if (hasInitializedDurableFunctions) + if (_hasInitializedDurableFunctions) { _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("Clear", true) From 765b963106867a33cb7e004cc3a40d64a344cd57 Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 2 Dec 2022 17:34:59 -0800 Subject: [PATCH 21/40] fix typo --- src/DurableSDK/PowerShellServices.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 34af5ed3..695866bd 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -75,7 +75,7 @@ public PowerShellServices(PowerShell pwsh, ILogger logger) if (numCandidates != 1) { // If we do not find exactly one matching module, we default to the built-in SDK. - // IAlthough it is unlikely (or impossible), if there were more than 1 result, we do not want to determine the "right" module. + // Although it is unlikely (or impossible), if there were more than 1 result, we do not want to determine the "right" module. _usesExternalDurableSDK = false; logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( PowerShellWorkerStrings.FailedToImportExternalDurableSDK, From b24b6ec044639a89cf23174783136c004ef68715 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 6 Dec 2022 12:01:02 -0800 Subject: [PATCH 22/40] refactor code, add logging --- src/DurableSDK/PowerShellServices.cs | 76 +++++++++++++--------- src/resources/PowerShellWorkerStrings.resx | 14 +++- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 695866bd..5965fc83 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -31,11 +31,29 @@ private bool tryImportingDurableSDK(PowerShell pwsh, ILogger logger) var importSucceeded = false; try { - // attempt to load - pwsh.AddCommand(Utils.ImportModuleCmdletInfo) - .AddParameter("Name", PowerShellWorkerStrings.ExternalDurableSDKName) + // attempt to import SDK + logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( + PowerShellWorkerStrings.LoadingDurableSDK,PowerShellWorkerStrings.ExternalDurableSDKName)); + + var results = pwsh.AddCommand(Utils.ImportModuleCmdletInfo) + .AddParameter("FullyQualifiedName", PowerShellWorkerStrings.ExternalDurableSDKName) .AddParameter("ErrorAction", ActionPreference.Stop) - .InvokeAndClearCommands(); + .AddParameter("PassThru") + .InvokeAndClearCommands(); + + var numResults = results.Count(); + var numExpectedResults = 1; + if (numResults != numExpectedResults) + { + logger.Log(isUserOnlyLog: false, LogLevel.Warning, string.Format( + PowerShellWorkerStrings.UnexpectedResultCount, "Import Durable SDK", numExpectedResults, numResults)); + } + + // log that import succeeded + var moduleInfo = results[0]; + logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( + PowerShellWorkerStrings.ImportSucceeded, moduleInfo.Name, moduleInfo.Version)); + importSucceeded = true; } catch (Exception e) @@ -51,52 +69,48 @@ private bool tryImportingDurableSDK(PowerShell pwsh, ILogger logger) } logger.Log(isUserOnlyLog: true, LogLevel.Error, string.Format( PowerShellWorkerStrings.ErrorImportingDurableSDK, - PowerShellWorkerStrings.ExternalDurableSDKName,errorMessage)); + PowerShellWorkerStrings.ExternalDurableSDKName, errorMessage)); } return importSucceeded; } - public PowerShellServices(PowerShell pwsh, ILogger logger) + private bool configureDurableSDK(PowerShell pwsh, ILogger logger) { - // We attempt to import the external DF SDK upon construction of the PowerShellServices object. - // We also maintain the boolean member hasExternalDurableSDK in this object rather than - // DurableController because the expected input and functionality of SetFunctionInvocationContextCommand - // may differ between the internal and external implementations. + var useExternalSDK = false; - // First, we search for the external DF SDK in the available modules + // Search for the external DF SDK in the available modules var matchingModules = pwsh.AddCommand(Utils.GetModuleCmdletInfo) - .AddParameter("Name", PowerShellWorkerStrings.ExternalDurableSDKName) + .AddParameter("FullyQualifiedName", PowerShellWorkerStrings.ExternalDurableSDKName) .InvokeAndClearCommands(); - // To load the external SDK, we expect there to be a single matching module. + // If we get at least one result, we attempt to load it var numCandidates = matchingModules.Count(); - if (numCandidates != 1) + if (numCandidates > 0) { - // If we do not find exactly one matching module, we default to the built-in SDK. - // Although it is unlikely (or impossible), if there were more than 1 result, we do not want to determine the "right" module. - _usesExternalDurableSDK = false; - logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( - PowerShellWorkerStrings.FailedToImportExternalDurableSDK, - PowerShellWorkerStrings.ExternalDurableSDKName, numCandidates)); + // try to import the external DF SDK + useExternalSDK = tryImportingDurableSDK(pwsh, logger); } - else { - // We found a singular instance of the external DF SDK. We log its name and version, and try to load it. - var externalSDKInfo = matchingModules[0]; - logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.LoadingDurableSDK, externalSDKInfo.Name, externalSDKInfo.Version)); - - _usesExternalDurableSDK = tryImportingDurableSDK(pwsh, logger); - logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.UtilizingExternalDurableSDK, _usesExternalDurableSDK)); + // Log that the module was not found in worker path + logger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format( + PowerShellWorkerStrings.DurableNotInWorkerPath, PowerShellWorkerStrings.ExternalDurableSDKName)); } - var templatedSetFunctionInvocationContextCommand = "{0}\\Set-FunctionInvocationContext"; + logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.UtilizingExternalDurableSDK, useExternalSDK)); + return useExternalSDK; + } + + public PowerShellServices(PowerShell pwsh, ILogger logger) + { + _pwsh = pwsh; + _usesExternalDurableSDK = configureDurableSDK(_pwsh, logger); + + // Configure FunctionInvocationContext command, based on the select DF SDK var prefix = _usesExternalDurableSDK ? PowerShellWorkerStrings.ExternalDurableSDKName : PowerShellWorkerStrings.InternalDurableSDKName; - SetFunctionInvocationContextCommand = string.Format(templatedSetFunctionInvocationContextCommand, prefix); - - _pwsh = pwsh; + SetFunctionInvocationContextCommand = string.Format(PowerShellWorkerStrings.SetFunctionInvocationContextCmdLetTemplate, prefix); } public bool HasExternalDurableSDK() diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index f17ce79c..b8853723 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -386,7 +386,7 @@ Failed to import the '{0}' module. Please make sure this module is available in the PowerShell worker path. Defaulting to built-in SDK. - Attempting to load '{0}', version '{1}', as the Durable Functions SDK + Attempting to load '{0}', as the Durable Functions SDK An unexpected error ocurred while importing '{0}': '{1}' @@ -394,4 +394,16 @@ Worker init request completed in {0} ms. + + {0}\Set-FunctionInvocationContext + + + The Durable Functions SDK, '{0}', could not be found. Please make sure this module is available in the PowerShell worker path. + + + Import of module '{0}' at version '{1}' succeeded. + + + Operation '{0}' expected '{1}' result(s) but received '{2}'. + \ No newline at end of file From 7e98f7d7ca76378db8587528068386002f33204b Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 6 Dec 2022 12:05:29 -0800 Subject: [PATCH 23/40] trim strings resource file --- src/resources/PowerShellWorkerStrings.resx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index b8853723..e3c710f4 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -355,9 +355,6 @@ AzureFunctions.PowerShell.Durable.SDK - - Failed to import the '{0}' module. Please make sure this module is available in the PowerShell worker path. For instructions on how to install this module, please see '{1}'. - Microsoft.Azure.Functions.PowerShellWorker @@ -379,12 +376,6 @@ AzureFunctions.PowerShell.Durable.SDK - - Attempted to find 1 instance of '{0}' on the worker path but found '{0}', defaulting to internal Durable Functions SDK. - - - Failed to import the '{0}' module. Please make sure this module is available in the PowerShell worker path. Defaulting to built-in SDK. - Attempting to load '{0}', as the Durable Functions SDK From 71070b5f88401d34a9ad285e287854a35052bcad Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 6 Dec 2022 14:11:58 -0800 Subject: [PATCH 24/40] remove unused strings --- src/DurableSDK/PowerShellServices.cs | 7 +++++-- src/resources/PowerShellWorkerStrings.resx | 8 +------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 5965fc83..e625e1a6 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -150,14 +150,17 @@ public OrchestrationBindingInfo SetOrchestrationContext( // If more than 1 element is present in the output pipeline, we cannot trust that we have // obtained the external orchestrator invoker; i.e the output contract is not met. - var outputContractIsMet = output.Count() == 1; + var numResults = output.Count(); + var numExpectedResults = 1; + var outputContractIsMet = output.Count() == numExpectedResults; if (outputContractIsMet) { externalInvoker = new ExternalInvoker(output[0]); } else { - var exceptionMessage = string.Format(PowerShellWorkerStrings.UnexpectedOutputInExternalDurableCommand, SetFunctionInvocationContextCommand); + var exceptionMessage = string.Format(PowerShellWorkerStrings.UnexpectedResultCount, + SetFunctionInvocationContextCommand, numExpectedResults, numResults); throw new InvalidOperationException(exceptionMessage); } } diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index e3c710f4..930cf096 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -361,9 +361,6 @@ Microsoft.Azure.Functions.PowerShellWorker\Trace-PipelineObject - - Only a single output was expected for an invocation of '{0}'. - Utilizing external Durable Functions SDK: '{0}'. @@ -373,11 +370,8 @@ Unable to initialize orchestrator function due to presence of other bindings. Total number of bindings found is '{0}'. Orchestrator Functions should never use any input or output bindings other than the orchestration trigger itself. See: https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-bindings - - AzureFunctions.PowerShell.Durable.SDK - - Attempting to load '{0}', as the Durable Functions SDK + Attempting to load '{0}' as the Durable Functions SDK An unexpected error ocurred while importing '{0}': '{1}' From 42cdca662af4ea26010dbbf7263888ce63ac5df1 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 6 Dec 2022 16:28:35 -0800 Subject: [PATCH 25/40] hide external SDK logic behind feature flag --- src/DurableSDK/PowerShellServices.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index e625e1a6..e266deb2 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -24,6 +24,9 @@ internal class PowerShellServices : IPowerShellServices private readonly bool _usesExternalDurableSDK = false; private readonly ErrorRecordFormatter _errorRecordFormatter = new ErrorRecordFormatter(); + private bool EnableExternalDurableSDK { get; } = + PowerShellWorkerConfiguration.GetBoolean("ExternalDurablePowerShellSDK") ?? false; + private bool tryImportingDurableSDK(PowerShell pwsh, ILogger logger) { @@ -80,6 +83,12 @@ private bool configureDurableSDK(PowerShell pwsh, ILogger logger) { var useExternalSDK = false; + // If the user has not opted in to the external Durable SDK, we immediately default to the built-in SDK + if (!EnableExternalDurableSDK) + { + return useExternalSDK; + } + // Search for the external DF SDK in the available modules var matchingModules = pwsh.AddCommand(Utils.GetModuleCmdletInfo) .AddParameter("FullyQualifiedName", PowerShellWorkerStrings.ExternalDurableSDKName) From 73fcc5ea041341ac91f28441c094163c92da69b6 Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 8 Dec 2022 15:25:10 -0800 Subject: [PATCH 26/40] refactor to facilitate testing --- src/DurableSDK/IPowerShellServices.cs | 2 + src/DurableSDK/PowerShellServices.cs | 41 ++++++++++----------- src/DurableWorker/DurableController.cs | 4 +- test/Unit/Durable/DurableControllerTests.cs | 5 ++- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index 2fbe322d..294013fb 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -15,6 +15,8 @@ internal interface IPowerShellServices bool HasExternalDurableSDK(); + void tryEnablingExternalDurableSDK(); + void SetDurableClient(object durableClient); OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context, out IExternalOrchestrationInvoker externalInvoker); diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index e266deb2..33b1c36a 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -21,24 +21,26 @@ internal class PowerShellServices : IPowerShellServices private readonly PowerShell _pwsh; private bool _hasInitializedDurableFunctions = false; - private readonly bool _usesExternalDurableSDK = false; + private bool _usesExternalDurableSDK = false; private readonly ErrorRecordFormatter _errorRecordFormatter = new ErrorRecordFormatter(); + private readonly ILogger _logger; + private bool EnableExternalDurableSDK { get; } = PowerShellWorkerConfiguration.GetBoolean("ExternalDurablePowerShellSDK") ?? false; - private bool tryImportingDurableSDK(PowerShell pwsh, ILogger logger) + private bool tryImportingDurableSDK() { // Try to load/import the external Durable Functions SDK. If an error occurs, it is logged. var importSucceeded = false; try { // attempt to import SDK - logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( - PowerShellWorkerStrings.LoadingDurableSDK,PowerShellWorkerStrings.ExternalDurableSDKName)); + _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( + PowerShellWorkerStrings.LoadingDurableSDK, PowerShellWorkerStrings.ExternalDurableSDKName)); - var results = pwsh.AddCommand(Utils.ImportModuleCmdletInfo) + var results = _pwsh.AddCommand(Utils.ImportModuleCmdletInfo) .AddParameter("FullyQualifiedName", PowerShellWorkerStrings.ExternalDurableSDKName) .AddParameter("ErrorAction", ActionPreference.Stop) .AddParameter("PassThru") @@ -48,13 +50,13 @@ private bool tryImportingDurableSDK(PowerShell pwsh, ILogger logger) var numExpectedResults = 1; if (numResults != numExpectedResults) { - logger.Log(isUserOnlyLog: false, LogLevel.Warning, string.Format( + _logger.Log(isUserOnlyLog: false, LogLevel.Warning, string.Format( PowerShellWorkerStrings.UnexpectedResultCount, "Import Durable SDK", numExpectedResults, numResults)); } // log that import succeeded var moduleInfo = results[0]; - logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( + _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( PowerShellWorkerStrings.ImportSucceeded, moduleInfo.Name, moduleInfo.Version)); importSucceeded = true; @@ -70,7 +72,7 @@ private bool tryImportingDurableSDK(PowerShell pwsh, ILogger logger) errorMessage = _errorRecordFormatter.Format(inner.ErrorRecord); } - logger.Log(isUserOnlyLog: true, LogLevel.Error, string.Format( + _logger.Log(isUserOnlyLog: true, LogLevel.Error, string.Format( PowerShellWorkerStrings.ErrorImportingDurableSDK, PowerShellWorkerStrings.ExternalDurableSDKName, errorMessage)); @@ -79,18 +81,16 @@ private bool tryImportingDurableSDK(PowerShell pwsh, ILogger logger) } - private bool configureDurableSDK(PowerShell pwsh, ILogger logger) + public void tryEnablingExternalDurableSDK() { - var useExternalSDK = false; - - // If the user has not opted in to the external Durable SDK, we immediately default to the built-in SDK - if (!EnableExternalDurableSDK) + // If the user has not opted-in to the external SDK experience, exit + if (EnableExternalDurableSDK) { - return useExternalSDK; + return; } // Search for the external DF SDK in the available modules - var matchingModules = pwsh.AddCommand(Utils.GetModuleCmdletInfo) + var matchingModules = _pwsh.AddCommand(Utils.GetModuleCmdletInfo) .AddParameter("FullyQualifiedName", PowerShellWorkerStrings.ExternalDurableSDKName) .InvokeAndClearCommands(); @@ -99,23 +99,22 @@ private bool configureDurableSDK(PowerShell pwsh, ILogger logger) if (numCandidates > 0) { // try to import the external DF SDK - useExternalSDK = tryImportingDurableSDK(pwsh, logger); + _usesExternalDurableSDK = tryImportingDurableSDK(); } else { // Log that the module was not found in worker path - logger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format( + _logger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format( PowerShellWorkerStrings.DurableNotInWorkerPath, PowerShellWorkerStrings.ExternalDurableSDKName)); } - logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.UtilizingExternalDurableSDK, useExternalSDK)); - return useExternalSDK; + _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.UtilizingExternalDurableSDK, _usesExternalDurableSDK)); } public PowerShellServices(PowerShell pwsh, ILogger logger) { _pwsh = pwsh; - _usesExternalDurableSDK = configureDurableSDK(_pwsh, logger); + _logger = logger; // Configure FunctionInvocationContext command, based on the select DF SDK var prefix = _usesExternalDurableSDK ? PowerShellWorkerStrings.ExternalDurableSDKName : PowerShellWorkerStrings.InternalDurableSDKName; @@ -182,7 +181,7 @@ public OrchestrationBindingInfo SetOrchestrationContext( _hasInitializedDurableFunctions = true; return orchestrationBindingInfo; } - + public void AddParameter(string name, object value) { diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 8cbd6cc4..690a231e 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -27,7 +27,6 @@ internal class DurableController private readonly IPowerShellServices _powerShellServices; private readonly IOrchestrationInvoker _orchestrationInvoker; private OrchestrationBindingInfo _orchestrationBindingInfo; - private ILogger _logger; public DurableController( DurableFunctionInfo durableDurableFunctionInfo, @@ -50,7 +49,6 @@ internal DurableController( _durableFunctionInfo = durableDurableFunctionInfo; _powerShellServices = powerShellServices; _orchestrationInvoker = orchestrationInvoker; - _logger = logger; } public string GetOrchestrationParameterName() @@ -60,6 +58,8 @@ public string GetOrchestrationParameterName() public void InitializeBindings(IList inputData, out bool hasExternalSDK) { + _powerShellServices.tryEnablingExternalDurableSDK(); + // 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) diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 1f436d01..9a9ec388 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -26,7 +26,7 @@ public class DurableControllerTests 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); - private static readonly ILogger testLogger = new ConsoleLogger(); + private static readonly ILogger _testLogger = new ConsoleLogger(); [Fact] @@ -195,6 +195,7 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() Times.Once); } + [Fact] public void AddPipelineOutputIfNecessary_AddsDollarReturn_ForActivityFunction() { @@ -250,7 +251,7 @@ private DurableController CreateDurableController( durableFunctionInfo, _mockPowerShellServices.Object, _mockOrchestrationInvoker.Object, - testLogger); + _testLogger); } private static ParameterBinding CreateParameterBinding(string parameterName, object value) From cb2a17c9564dc79d634df3dc28acd8a1430d8d96 Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 8 Dec 2022 16:18:05 -0800 Subject: [PATCH 27/40] pass tests --- test/Unit/Durable/DurableControllerTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 9a9ec388..c5fd573e 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -44,6 +44,8 @@ public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -67,6 +69,7 @@ public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction .Returns(_orchestrationBindingInfo); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -83,6 +86,7 @@ public void InitializeBindings_Throws_OnOrchestrationFunctionWithoutContextParam var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); var inputData = new ParameterBinding[0]; _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); Assert.ThrowsAny(() => durableController.InitializeBindings(inputData, out bool hasExternalSDK)); } @@ -99,6 +103,7 @@ internal void InitializeBindings_DoesNothing_ForNonOrchestrationFunction(Durable CreateParameterBinding("ParameterName", _orchestrationContext) }; _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); durableController.InitializeBindings(inputData, out bool hasExternalSDK); } @@ -130,6 +135,7 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -154,6 +160,7 @@ internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrat out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -173,6 +180,7 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); From 9ab5b097e45f4f55f10d0339f809092e54160beb Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 8 Dec 2022 17:15:37 -0800 Subject: [PATCH 28/40] Add unit tests --- src/DurableSDK/PowerShellServices.cs | 11 --- src/DurableWorker/DurableController.cs | 9 +- test/Unit/Durable/DurableControllerTests.cs | 94 +++++++++++++++++++++ 3 files changed, 102 insertions(+), 12 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 33b1c36a..209c8f76 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -25,11 +25,6 @@ internal class PowerShellServices : IPowerShellServices private readonly ErrorRecordFormatter _errorRecordFormatter = new ErrorRecordFormatter(); private readonly ILogger _logger; - - private bool EnableExternalDurableSDK { get; } = - PowerShellWorkerConfiguration.GetBoolean("ExternalDurablePowerShellSDK") ?? false; - - private bool tryImportingDurableSDK() { // Try to load/import the external Durable Functions SDK. If an error occurs, it is logged. @@ -83,12 +78,6 @@ private bool tryImportingDurableSDK() public void tryEnablingExternalDurableSDK() { - // If the user has not opted-in to the external SDK experience, exit - if (EnableExternalDurableSDK) - { - return; - } - // Search for the external DF SDK in the available modules var matchingModules = _pwsh.AddCommand(Utils.GetModuleCmdletInfo) .AddParameter("FullyQualifiedName", PowerShellWorkerStrings.ExternalDurableSDKName) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 690a231e..4b73d18f 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -28,6 +28,9 @@ internal class DurableController private readonly IOrchestrationInvoker _orchestrationInvoker; private OrchestrationBindingInfo _orchestrationBindingInfo; + private bool EnableExternalDurableSDK { get; } = + PowerShellWorkerConfiguration.GetBoolean("ExternalDurablePowerShellSDK") ?? false; + public DurableController( DurableFunctionInfo durableDurableFunctionInfo, PowerShell pwsh, @@ -58,7 +61,11 @@ public string GetOrchestrationParameterName() public void InitializeBindings(IList inputData, out bool hasExternalSDK) { - _powerShellServices.tryEnablingExternalDurableSDK(); + // Enable external SDK only when customer has opted-in + if (EnableExternalDurableSDK) + { + _powerShellServices.tryEnablingExternalDurableSDK(); + } // If the function is an durable client, then we set the DurableClient // in the module context for the 'Start-DurableOrchestration' function to use. diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index c5fd573e..78e2de33 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -18,6 +18,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test.Durable using Moq; using Xunit; + using Grpc.Core; public class DurableControllerTests { @@ -249,6 +250,76 @@ internal void SuppressPipelineTracesForActivityFunctionOnly(DurableFunctionType Assert.Equal(shouldSuppressPipelineTraces, durableController.ShouldSuppressPipelineTraces()); } + [Theory] + [InlineData(DurableFunctionType.None)] + [InlineData(DurableFunctionType.OrchestrationFunction)] + internal void ExternalDurableSdkIsNotConfiguredByDefault(DurableFunctionType durableFunctionType) + { + var durableController = CreateDurableController(durableFunctionType); + var inputData = GetDurableBindings(durableFunctionType); + + if (durableFunctionType == DurableFunctionType.None) + { + _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); + } + else + { + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( + It.IsAny(), + out It.Ref.IsAny)).Returns(_orchestrationBindingInfo); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + } + + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()).Throws(new Exception("should not be called")); + durableController.InitializeBindings(inputData, out var hasExternalSDK); + + Assert.False(hasExternalSDK); + _mockPowerShellServices.Verify(_ => _.tryEnablingExternalDurableSDK(), Times.Never); + } + + [Theory] + [InlineData(DurableFunctionType.None)] + [InlineData(DurableFunctionType.OrchestrationFunction)] + internal void ExternalDurableSdkCanBeEnabled(DurableFunctionType durableFunctionType) + { + try + { + // opt-in to external DF SDK + Environment.SetEnvironmentVariable("ExternalDurablePowerShellSDK", "true"); + + var durableController = CreateDurableController(durableFunctionType); + var inputData = GetDurableBindings(durableFunctionType); + + if (durableFunctionType == DurableFunctionType.None) + { + _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); + } + else + { + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( + It.IsAny(), + out It.Ref.IsAny)).Returns(_orchestrationBindingInfo); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + } + + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(true); + _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + durableController.InitializeBindings(inputData, out var hasExternalSDK); + + Assert.True(hasExternalSDK); + _mockPowerShellServices.Verify(_ => _.tryEnablingExternalDurableSDK(), Times.Once); + + } + finally + { + Environment.SetEnvironmentVariable("ExternalDurablePowerShellSDK", "false"); + + } + + + } + private DurableController CreateDurableController( DurableFunctionType durableFunctionType, string durableClientBindingName = null) @@ -273,5 +344,28 @@ private static ParameterBinding CreateParameterBinding(string parameterName, obj } }; } + + private static IList GetDurableBindings(DurableFunctionType durableFunctionType) + { + ParameterBinding[] bindings; + if (durableFunctionType == DurableFunctionType.None) // DF Client case + { + var durableClient = new { FakeClientProperty = "FakeClientPropertyValue" }; + bindings = new[] + { + CreateParameterBinding("AnotherParameter", "IgnoredValue"), + CreateParameterBinding("DurableClientBindingName", durableClient), + CreateParameterBinding("YetAnotherParameter", "IgnoredValue") + }; + } + else //valid for orchestrators and acitvities + { + bindings = new[] +{ + CreateParameterBinding("ParameterName", _orchestrationContext) + }; + } + return bindings; + } } } From 55114645804346956bab4c867637ba6e624b5610 Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 9 Dec 2022 14:31:49 -0800 Subject: [PATCH 29/40] minor edits --- src/DurableSDK/PowerShellServices.cs | 35 +++++++++++++-------- test/Unit/Durable/DurableControllerTests.cs | 3 -- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 209c8f76..f3e52c92 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -17,14 +17,27 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal class PowerShellServices : IPowerShellServices { - private readonly string SetFunctionInvocationContextCommand; - private readonly PowerShell _pwsh; private bool _hasInitializedDurableFunctions = false; private bool _usesExternalDurableSDK = false; private readonly ErrorRecordFormatter _errorRecordFormatter = new ErrorRecordFormatter(); private readonly ILogger _logger; + // uses built-in SDK by default + private string SetFunctionInvocationContextCommand = string.Format( + PowerShellWorkerStrings.SetFunctionInvocationContextCmdLetTemplate, + PowerShellWorkerStrings.InternalDurableSDKName); + + public PowerShellServices(PowerShell pwsh, ILogger logger) + { + _pwsh = pwsh; + _logger = logger; + + // Configure FunctionInvocationContext command, based on the select DF SDK + var prefix = PowerShellWorkerStrings.InternalDurableSDKName; + SetFunctionInvocationContextCommand = string.Format(PowerShellWorkerStrings.SetFunctionInvocationContextCmdLetTemplate, prefix); + } + private bool tryImportingDurableSDK() { // Try to load/import the external Durable Functions SDK. If an error occurs, it is logged. @@ -80,6 +93,7 @@ public void tryEnablingExternalDurableSDK() { // Search for the external DF SDK in the available modules var matchingModules = _pwsh.AddCommand(Utils.GetModuleCmdletInfo) + .AddParameter("ListAvailable") .AddParameter("FullyQualifiedName", PowerShellWorkerStrings.ExternalDurableSDKName) .InvokeAndClearCommands(); @@ -97,17 +111,12 @@ public void tryEnablingExternalDurableSDK() PowerShellWorkerStrings.DurableNotInWorkerPath, PowerShellWorkerStrings.ExternalDurableSDKName)); } - _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format(PowerShellWorkerStrings.UtilizingExternalDurableSDK, _usesExternalDurableSDK)); - } - - public PowerShellServices(PowerShell pwsh, ILogger logger) - { - _pwsh = pwsh; - _logger = logger; - - // Configure FunctionInvocationContext command, based on the select DF SDK - var prefix = _usesExternalDurableSDK ? PowerShellWorkerStrings.ExternalDurableSDKName : PowerShellWorkerStrings.InternalDurableSDKName; - SetFunctionInvocationContextCommand = string.Format(PowerShellWorkerStrings.SetFunctionInvocationContextCmdLetTemplate, prefix); + // assign SetFunctionInvocationContextCommand to the corresponding external SDK's CmdLet + SetFunctionInvocationContextCommand = string.Format( + PowerShellWorkerStrings.SetFunctionInvocationContextCmdLetTemplate, + PowerShellWorkerStrings.ExternalDurableSDKName); + _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( + PowerShellWorkerStrings.UtilizingExternalDurableSDK, _usesExternalDurableSDK)); } public bool HasExternalDurableSDK() diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 78e2de33..9f118d79 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -314,10 +314,7 @@ internal void ExternalDurableSdkCanBeEnabled(DurableFunctionType durableFunction finally { Environment.SetEnvironmentVariable("ExternalDurablePowerShellSDK", "false"); - } - - } private DurableController CreateDurableController( From bf4b2f2a1930cb76121ea375f0085285fcb91e12 Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 9 Dec 2022 14:37:48 -0800 Subject: [PATCH 30/40] remove unecessary log --- src/DurableSDK/PowerShellServices.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index f3e52c92..5c5b6f8f 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -115,8 +115,6 @@ public void tryEnablingExternalDurableSDK() SetFunctionInvocationContextCommand = string.Format( PowerShellWorkerStrings.SetFunctionInvocationContextCmdLetTemplate, PowerShellWorkerStrings.ExternalDurableSDKName); - _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( - PowerShellWorkerStrings.UtilizingExternalDurableSDK, _usesExternalDurableSDK)); } public bool HasExternalDurableSDK() From 7c24933ef941879b617fd69be90814cc5c3e45fe Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 12 Dec 2022 13:46:28 -0800 Subject: [PATCH 31/40] apply feedback --- src/DurableSDK/OrchestrationInvoker.cs | 2 +- src/DurableSDK/PowerShellServices.cs | 28 +++++++++------------- src/PowerShell/PowerShellManager.cs | 2 +- src/resources/PowerShellWorkerStrings.resx | 10 ++------ 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index 7f362e83..b06a0acd 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -31,7 +31,7 @@ public Hashtable Invoke( } catch (Exception ex) { - ex.Data.Add(PowerShellWorkerStrings.isOrchestrationFailureKey, true); + ex.Data.Add(PowerShellWorkerStrings.IsOrchestrationFailureKey, true); throw; } finally diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 5c5b6f8f..226fe5ee 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation; + using System.Reflection.Metadata; using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; @@ -22,10 +23,11 @@ internal class PowerShellServices : IPowerShellServices private bool _usesExternalDurableSDK = false; private readonly ErrorRecordFormatter _errorRecordFormatter = new ErrorRecordFormatter(); private readonly ILogger _logger; + private const string _setFunctionInvocationContextCommandTemplate = "{0}\\Set-FunctionInvocationContext"; // uses built-in SDK by default private string SetFunctionInvocationContextCommand = string.Format( - PowerShellWorkerStrings.SetFunctionInvocationContextCmdLetTemplate, + _setFunctionInvocationContextCommandTemplate, PowerShellWorkerStrings.InternalDurableSDKName); public PowerShellServices(PowerShell pwsh, ILogger logger) @@ -35,7 +37,7 @@ public PowerShellServices(PowerShell pwsh, ILogger logger) // Configure FunctionInvocationContext command, based on the select DF SDK var prefix = PowerShellWorkerStrings.InternalDurableSDKName; - SetFunctionInvocationContextCommand = string.Format(PowerShellWorkerStrings.SetFunctionInvocationContextCmdLetTemplate, prefix); + SetFunctionInvocationContextCommand = string.Format(_setFunctionInvocationContextCommandTemplate, prefix); } private bool tryImportingDurableSDK() @@ -54,15 +56,7 @@ private bool tryImportingDurableSDK() .AddParameter("PassThru") .InvokeAndClearCommands(); - var numResults = results.Count(); - var numExpectedResults = 1; - if (numResults != numExpectedResults) - { - _logger.Log(isUserOnlyLog: false, LogLevel.Warning, string.Format( - PowerShellWorkerStrings.UnexpectedResultCount, "Import Durable SDK", numExpectedResults, numResults)); - } - - // log that import succeeded + // Given how the command above is constructed, only 1 result should be possible var moduleInfo = results[0]; _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( PowerShellWorkerStrings.ImportSucceeded, moduleInfo.Name, moduleInfo.Version)); @@ -78,15 +72,13 @@ private bool tryImportingDurableSDK() if (e.InnerException is IContainsErrorRecord inner) { errorMessage = _errorRecordFormatter.Format(inner.ErrorRecord); - } - _logger.Log(isUserOnlyLog: true, LogLevel.Error, string.Format( + _logger.Log(isUserOnlyLog: false, LogLevel.Error, string.Format( PowerShellWorkerStrings.ErrorImportingDurableSDK, PowerShellWorkerStrings.ExternalDurableSDKName, errorMessage)); } return importSucceeded; - } public void tryEnablingExternalDurableSDK() @@ -107,13 +99,15 @@ public void tryEnablingExternalDurableSDK() else { // Log that the module was not found in worker path + var workerPathContents = PowerShellWorkerConfiguration.GetString("PSModulePath"); _logger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format( - PowerShellWorkerStrings.DurableNotInWorkerPath, PowerShellWorkerStrings.ExternalDurableSDKName)); + PowerShellWorkerStrings.DurableNotInWorkerPath, PowerShellWorkerStrings.ExternalDurableSDKName, + workerPathContents)); } // assign SetFunctionInvocationContextCommand to the corresponding external SDK's CmdLet SetFunctionInvocationContextCommand = string.Format( - PowerShellWorkerStrings.SetFunctionInvocationContextCmdLetTemplate, + _setFunctionInvocationContextCommandTemplate, PowerShellWorkerStrings.ExternalDurableSDKName); } @@ -196,7 +190,7 @@ public void ClearOrchestrationContext() public void TracePipelineObject() { - _pwsh.AddCommand(PowerShellWorkerStrings.TracePipelineObjectCommand); + _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); } public IAsyncResult BeginInvoke(PSDataCollection output) diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 2bb09bd8..b1cda913 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -246,7 +246,7 @@ public Hashtable InvokeFunction( } catch (Exception e) { - if (e.Data.Contains(PowerShellWorkerStrings.isOrchestrationFailureKey) && e.InnerException is IContainsErrorRecord inner) + if (e.Data.Contains(PowerShellWorkerStrings.IsOrchestrationFailureKey) && e.InnerException is IContainsErrorRecord inner) { Logger.Log(isUserOnlyLog: true, LogLevel.Error, GetFunctionExceptionMessage(inner)); } diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 930cf096..9eb4ed38 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -358,13 +358,10 @@ Microsoft.Azure.Functions.PowerShellWorker - - Microsoft.Azure.Functions.PowerShellWorker\Trace-PipelineObject - Utilizing external Durable Functions SDK: '{0}'. - + IsOrchestrationFailure @@ -379,11 +376,8 @@ Worker init request completed in {0} ms. - - {0}\Set-FunctionInvocationContext - - The Durable Functions SDK, '{0}', could not be found. Please make sure this module is available in the PowerShell worker path. + The Durable Functions SDK, '{0}', could not be found. Please make sure this module is available in the PowerShell worker path. PowerShell worker path contents: '{1}' Import of module '{0}' at version '{1}' succeeded. From dee9ad93323327c7517e575b914b78885ed9cb0e Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 13 Dec 2022 09:56:21 -0800 Subject: [PATCH 32/40] remove module names from strings file --- src/DurableSDK/PowerShellServices.cs | 19 +++++++++++-------- src/resources/PowerShellWorkerStrings.resx | 6 ------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 226fe5ee..cbc23d13 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -23,12 +23,15 @@ internal class PowerShellServices : IPowerShellServices private bool _usesExternalDurableSDK = false; private readonly ErrorRecordFormatter _errorRecordFormatter = new ErrorRecordFormatter(); private readonly ILogger _logger; + private const string _setFunctionInvocationContextCommandTemplate = "{0}\\Set-FunctionInvocationContext"; + private const string _internalDurableSdkName = "Microsoft.Azure.Functions.PowerShellWorker"; + private const string _externalDurableSdkName = "AzureFunctions.PowerShell.Durable.SDK"; // uses built-in SDK by default private string SetFunctionInvocationContextCommand = string.Format( _setFunctionInvocationContextCommandTemplate, - PowerShellWorkerStrings.InternalDurableSDKName); + _internalDurableSdkName); public PowerShellServices(PowerShell pwsh, ILogger logger) { @@ -36,7 +39,7 @@ public PowerShellServices(PowerShell pwsh, ILogger logger) _logger = logger; // Configure FunctionInvocationContext command, based on the select DF SDK - var prefix = PowerShellWorkerStrings.InternalDurableSDKName; + var prefix = _internalDurableSdkName; SetFunctionInvocationContextCommand = string.Format(_setFunctionInvocationContextCommandTemplate, prefix); } @@ -48,10 +51,10 @@ private bool tryImportingDurableSDK() { // attempt to import SDK _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( - PowerShellWorkerStrings.LoadingDurableSDK, PowerShellWorkerStrings.ExternalDurableSDKName)); + PowerShellWorkerStrings.LoadingDurableSDK, _externalDurableSdkName)); var results = _pwsh.AddCommand(Utils.ImportModuleCmdletInfo) - .AddParameter("FullyQualifiedName", PowerShellWorkerStrings.ExternalDurableSDKName) + .AddParameter("FullyQualifiedName", _externalDurableSdkName) .AddParameter("ErrorAction", ActionPreference.Stop) .AddParameter("PassThru") .InvokeAndClearCommands(); @@ -75,7 +78,7 @@ private bool tryImportingDurableSDK() } _logger.Log(isUserOnlyLog: false, LogLevel.Error, string.Format( PowerShellWorkerStrings.ErrorImportingDurableSDK, - PowerShellWorkerStrings.ExternalDurableSDKName, errorMessage)); + _externalDurableSdkName, errorMessage)); } return importSucceeded; @@ -86,7 +89,7 @@ public void tryEnablingExternalDurableSDK() // Search for the external DF SDK in the available modules var matchingModules = _pwsh.AddCommand(Utils.GetModuleCmdletInfo) .AddParameter("ListAvailable") - .AddParameter("FullyQualifiedName", PowerShellWorkerStrings.ExternalDurableSDKName) + .AddParameter("FullyQualifiedName", _externalDurableSdkName) .InvokeAndClearCommands(); // If we get at least one result, we attempt to load it @@ -101,14 +104,14 @@ public void tryEnablingExternalDurableSDK() // Log that the module was not found in worker path var workerPathContents = PowerShellWorkerConfiguration.GetString("PSModulePath"); _logger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format( - PowerShellWorkerStrings.DurableNotInWorkerPath, PowerShellWorkerStrings.ExternalDurableSDKName, + PowerShellWorkerStrings.DurableNotInWorkerPath, _externalDurableSdkName, workerPathContents)); } // assign SetFunctionInvocationContextCommand to the corresponding external SDK's CmdLet SetFunctionInvocationContextCommand = string.Format( _setFunctionInvocationContextCommandTemplate, - PowerShellWorkerStrings.ExternalDurableSDKName); + _externalDurableSdkName); } public bool HasExternalDurableSDK() diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 9eb4ed38..fc0fccd2 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -352,12 +352,6 @@ Dependency snapshot '{0}' does not contain acceptable module versions. - - AzureFunctions.PowerShell.Durable.SDK - - - Microsoft.Azure.Functions.PowerShellWorker - Utilizing external Durable Functions SDK: '{0}'. From 6f7fa8a37bd88e45397ee375e8da54d9f7d1c3c7 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 13 Dec 2022 10:28:10 -0800 Subject: [PATCH 33/40] modify log to mention PSWorkerPath explicitly --- src/resources/PowerShellWorkerStrings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index fc0fccd2..8941c24c 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -371,7 +371,7 @@ Worker init request completed in {0} ms. - The Durable Functions SDK, '{0}', could not be found. Please make sure this module is available in the PowerShell worker path. PowerShell worker path contents: '{1}' + The Durable Functions SDK, '{0}', could not be found. Please make sure this module is available in the PowerShell worker path. PowerShell worker PSModulePath: '{1}' Import of module '{0}' at version '{1}' succeeded. From 889f65f610cdcad97ea567e0fdf35ab1d9278a89 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 13 Dec 2022 18:13:58 -0800 Subject: [PATCH 34/40] replace importing external DF SDK for simply validating it is in session --- src/DurableSDK/IPowerShellServices.cs | 4 +- src/DurableSDK/PowerShellServices.cs | 93 ++++++++------------- src/DurableWorker/DurableController.cs | 29 +++++-- src/Utility/Utils.cs | 3 + src/resources/PowerShellWorkerStrings.resx | 16 +++- test/Unit/Durable/DurableControllerTests.cs | 31 ++++--- 6 files changed, 97 insertions(+), 79 deletions(-) diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index 294013fb..3b7b49f3 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -15,7 +15,9 @@ internal interface IPowerShellServices bool HasExternalDurableSDK(); - void tryEnablingExternalDurableSDK(); + bool isExternalDurableSdkLoaded(); + + void EnableExternalDurableSDK(); void SetDurableClient(object durableClient); diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index cbc23d13..28ed64e4 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -25,13 +25,11 @@ internal class PowerShellServices : IPowerShellServices private readonly ILogger _logger; private const string _setFunctionInvocationContextCommandTemplate = "{0}\\Set-FunctionInvocationContext"; - private const string _internalDurableSdkName = "Microsoft.Azure.Functions.PowerShellWorker"; - private const string _externalDurableSdkName = "AzureFunctions.PowerShell.Durable.SDK"; // uses built-in SDK by default private string SetFunctionInvocationContextCommand = string.Format( _setFunctionInvocationContextCommandTemplate, - _internalDurableSdkName); + Utils.InternalDurableSdkName); public PowerShellServices(PowerShell pwsh, ILogger logger) { @@ -39,79 +37,54 @@ public PowerShellServices(PowerShell pwsh, ILogger logger) _logger = logger; // Configure FunctionInvocationContext command, based on the select DF SDK - var prefix = _internalDurableSdkName; + var prefix = Utils.InternalDurableSdkName; SetFunctionInvocationContextCommand = string.Format(_setFunctionInvocationContextCommandTemplate, prefix); } - private bool tryImportingDurableSDK() + public bool isExternalDurableSdkLoaded() { - // Try to load/import the external Durable Functions SDK. If an error occurs, it is logged. - var importSucceeded = false; - try - { - // attempt to import SDK - _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( - PowerShellWorkerStrings.LoadingDurableSDK, _externalDurableSdkName)); - - var results = _pwsh.AddCommand(Utils.ImportModuleCmdletInfo) - .AddParameter("FullyQualifiedName", _externalDurableSdkName) - .AddParameter("ErrorAction", ActionPreference.Stop) - .AddParameter("PassThru") - .InvokeAndClearCommands(); - - // Given how the command above is constructed, only 1 result should be possible - var moduleInfo = results[0]; - _logger.Log(isUserOnlyLog: false, LogLevel.Trace, String.Format( - PowerShellWorkerStrings.ImportSucceeded, moduleInfo.Name, moduleInfo.Version)); - - importSucceeded = true; - } - catch (Exception e) - { - // If an error ocurred, we try to log the exception. - var errorMessage = e.ToString(); + // Search for the external DF SDK in the current session + var matchingModules = _pwsh.AddCommand(Utils.GetModuleCmdletInfo) + .AddParameter("FullyQualifiedName", Utils.ExternalDurableSdkName) + .InvokeAndClearCommands(); - // If a PowerShell error record is available through Get-Error, we log that instead. - if (e.InnerException is IContainsErrorRecord inner) + // If we get at least one result, we know the external SDK was imported + var numCandidates = matchingModules.Count(); + var isModuleInCurrentSession = numCandidates > 0; + + if (isModuleInCurrentSession) + { + var candidatesInfo = matchingModules.Select(module => string.Format( + PowerShellWorkerStrings.FoundExternalDurableSdkInSession, module.Name, module.Version, module.Path)); + var externalSDKModuleInfo = string.Join('\n', candidatesInfo); + + if (numCandidates > 1) + { + // If there's more than 1 result, there may be runtime conflicts + // warn user of potential conflicts + _logger.Log(isUserOnlyLog: false, LogLevel.Warning, String.Format( + PowerShellWorkerStrings.MultipleExternalSDKsInSession, + numCandidates, Utils.ExternalDurableSdkName, externalSDKModuleInfo)); + } + else { - errorMessage = _errorRecordFormatter.Format(inner.ErrorRecord); + // a single external SDK is in session. Report its metadata + _logger.Log(isUserOnlyLog: false, LogLevel.Trace, externalSDKModuleInfo); } - _logger.Log(isUserOnlyLog: false, LogLevel.Error, string.Format( - PowerShellWorkerStrings.ErrorImportingDurableSDK, - _externalDurableSdkName, errorMessage)); } - return importSucceeded; + + return isModuleInCurrentSession; } - public void tryEnablingExternalDurableSDK() + public void EnableExternalDurableSDK() { - // Search for the external DF SDK in the available modules - var matchingModules = _pwsh.AddCommand(Utils.GetModuleCmdletInfo) - .AddParameter("ListAvailable") - .AddParameter("FullyQualifiedName", _externalDurableSdkName) - .InvokeAndClearCommands(); - - // If we get at least one result, we attempt to load it - var numCandidates = matchingModules.Count(); - if (numCandidates > 0) - { - // try to import the external DF SDK - _usesExternalDurableSDK = tryImportingDurableSDK(); - } - else - { - // Log that the module was not found in worker path - var workerPathContents = PowerShellWorkerConfiguration.GetString("PSModulePath"); - _logger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format( - PowerShellWorkerStrings.DurableNotInWorkerPath, _externalDurableSdkName, - workerPathContents)); - } + _usesExternalDurableSDK = true; // assign SetFunctionInvocationContextCommand to the corresponding external SDK's CmdLet SetFunctionInvocationContextCommand = string.Format( _setFunctionInvocationContextCommandTemplate, - _externalDurableSdkName); + Utils.ExternalDurableSdkName); } public bool HasExternalDurableSDK() diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 4b73d18f..9b789af4 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -11,12 +11,13 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Linq; using System.Management.Automation; - using Newtonsoft.Json; using WebJobs.Script.Grpc.Messages; using PowerShellWorker.Utility; using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; using System; + using LogLevel = WebJobs.Script.Grpc.Messages.RpcLog.Types.Level; + /// /// The main entry point for durable functions support. @@ -27,8 +28,9 @@ internal class DurableController private readonly IPowerShellServices _powerShellServices; private readonly IOrchestrationInvoker _orchestrationInvoker; private OrchestrationBindingInfo _orchestrationBindingInfo; + private readonly ILogger _logger; - private bool EnableExternalDurableSDK { get; } = + private bool isExternalDFSdkEnabled { get; } = PowerShellWorkerConfiguration.GetBoolean("ExternalDurablePowerShellSDK") ?? false; public DurableController( @@ -52,6 +54,7 @@ internal DurableController( _durableFunctionInfo = durableDurableFunctionInfo; _powerShellServices = powerShellServices; _orchestrationInvoker = orchestrationInvoker; + _logger = logger; } public string GetOrchestrationParameterName() @@ -61,12 +64,28 @@ public string GetOrchestrationParameterName() public void InitializeBindings(IList inputData, out bool hasExternalSDK) { - // Enable external SDK only when customer has opted-in - if (EnableExternalDurableSDK) + var isExternalSdkLoaded = _powerShellServices.isExternalDurableSdkLoaded(); + + if (isExternalDFSdkEnabled) { - _powerShellServices.tryEnablingExternalDurableSDK(); + if (isExternalSdkLoaded) + { + // Enable external SDK only when customer has opted-in + _powerShellServices.EnableExternalDurableSDK(); + } + else + { + // Customer attempted to enable external SDK but the module not in session. Default to built-in SDK. + _logger.Log(isUserOnlyLog: false, LogLevel.Error, string.Format(PowerShellWorkerStrings.ExternalSDKWasNotLoaded, Utils.ExternalDurableSdkName)); + } + } + else if (isExternalSdkLoaded) + { + // External SDK is in session, but customer does not mean to enable it. Report potential clashes + _logger.Log(isUserOnlyLog: false, LogLevel.Error, String.Format(PowerShellWorkerStrings.PotentialDurableSDKClash, Utils.ExternalDurableSdkName)); } + // 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) diff --git a/src/Utility/Utils.cs b/src/Utility/Utils.cs index 21ec68b8..e80babc9 100644 --- a/src/Utility/Utils.cs +++ b/src/Utility/Utils.cs @@ -23,6 +23,9 @@ internal class Utils internal readonly static CmdletInfo RemoveJobCmdletInfo = new CmdletInfo("Remove-Job", typeof(RemoveJobCommand)); internal readonly static CmdletInfo OutStringCmdletInfo = new CmdletInfo("Out-String", typeof(OutStringCommand)); internal readonly static CmdletInfo WriteInformationCmdletInfo = new CmdletInfo("Write-Information", typeof(WriteInformationCommand)); + + internal const string InternalDurableSdkName = "Microsoft.Azure.Functions.PowerShellWorker"; + internal const string ExternalDurableSdkName = "AzureFunctions.PowerShell.Durable.SDK"; internal readonly static object BoxedTrue = (object)true; internal readonly static object BoxedFalse = (object)false; diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 8941c24c..fb234b44 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -352,8 +352,8 @@ Dependency snapshot '{0}' does not contain acceptable module versions. - - Utilizing external Durable Functions SDK: '{0}'. + + Found External Durable Functions SDK in session: Name='{0}', Version='{1}', Path='{2}'. IsOrchestrationFailure @@ -379,4 +379,16 @@ Operation '{0}' expected '{1}' result(s) but received '{2}'. + + The external Durable Functions SDK is enabled but it cannot be used because it was not loaded onto the current PowerShell session. Please add `Import-Module`'{0}' to your profile.ps1 so it may be used. Defaulting to built-in SDK. + + + Get-Module returned '{0}' instances of '{1}' in the PowerShell session, but only 1 or 0 are expected. This may create runtime errors. Please ensure your script only imports a single version of '{0}'. The modules currently in session are:\n '{2}'. + + + The external Durable Functions SDK is not enabled but '{0}' has been imported to the PowerShell session. This may create runtime conflicts between the built-in and external Durable Functions CommandLets. If you mean to use the external Durable Functions SDK, please set the enviroment variable '{1}' to "true" + + + Utilizing external Durable Functions SDK: '{0}'. + \ No newline at end of file diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 9f118d79..065232c7 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -45,7 +45,8 @@ public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); - _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -70,7 +71,8 @@ public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction .Returns(_orchestrationBindingInfo); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); - _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -87,7 +89,8 @@ public void InitializeBindings_Throws_OnOrchestrationFunctionWithoutContextParam var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); var inputData = new ParameterBinding[0]; _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); - _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); Assert.ThrowsAny(() => durableController.InitializeBindings(inputData, out bool hasExternalSDK)); } @@ -104,7 +107,8 @@ internal void InitializeBindings_DoesNothing_ForNonOrchestrationFunction(Durable CreateParameterBinding("ParameterName", _orchestrationContext) }; _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); - _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); durableController.InitializeBindings(inputData, out bool hasExternalSDK); } @@ -136,7 +140,8 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); - _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -161,7 +166,8 @@ internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrat out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); - _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData, out bool hasExternalSDK); @@ -181,7 +187,8 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() out It.Ref.IsAny)) .Returns(_orchestrationBindingInfo); _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); - _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); @@ -271,11 +278,12 @@ internal void ExternalDurableSdkIsNotConfiguredByDefault(DurableFunctionType dur } _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); - _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()).Throws(new Exception("should not be called")); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()).Throws(new Exception("should not be called")); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); durableController.InitializeBindings(inputData, out var hasExternalSDK); Assert.False(hasExternalSDK); - _mockPowerShellServices.Verify(_ => _.tryEnablingExternalDurableSDK(), Times.Never); + _mockPowerShellServices.Verify(_ => _.EnableExternalDurableSDK(), Times.Never); } [Theory] @@ -304,11 +312,12 @@ internal void ExternalDurableSdkCanBeEnabled(DurableFunctionType durableFunction } _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(true); - _mockPowerShellServices.Setup(_ => _.tryEnablingExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(true); durableController.InitializeBindings(inputData, out var hasExternalSDK); Assert.True(hasExternalSDK); - _mockPowerShellServices.Verify(_ => _.tryEnablingExternalDurableSDK(), Times.Once); + _mockPowerShellServices.Verify(_ => _.EnableExternalDurableSDK(), Times.Once); } finally From a1e59931eeaadc9898e14938200e8b7090c68376 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 13 Dec 2022 18:36:03 -0800 Subject: [PATCH 35/40] remove unused strings --- src/DurableSDK/PowerShellServices.cs | 6 ++---- src/DurableWorker/DurableController.cs | 7 +++++-- src/resources/PowerShellWorkerStrings.resx | 12 ------------ 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 28ed64e4..354d981f 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -52,8 +52,8 @@ public bool isExternalDurableSdkLoaded() var numCandidates = matchingModules.Count(); var isModuleInCurrentSession = numCandidates > 0; - if (isModuleInCurrentSession) - { + if (isModuleInCurrentSession) + { var candidatesInfo = matchingModules.Select(module => string.Format( PowerShellWorkerStrings.FoundExternalDurableSdkInSession, module.Name, module.Version, module.Path)); var externalSDKModuleInfo = string.Join('\n', candidatesInfo); @@ -71,9 +71,7 @@ public bool isExternalDurableSdkLoaded() // a single external SDK is in session. Report its metadata _logger.Log(isUserOnlyLog: false, LogLevel.Trace, externalSDKModuleInfo); } - } - return isModuleInCurrentSession; } diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 9b789af4..7f0e583b 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -62,10 +62,9 @@ public string GetOrchestrationParameterName() return _orchestrationBindingInfo?.ParameterName; } - public void InitializeBindings(IList inputData, out bool hasExternalSDK) + private void tryEnablingExternalSDK() { var isExternalSdkLoaded = _powerShellServices.isExternalDurableSdkLoaded(); - if (isExternalDFSdkEnabled) { if (isExternalSdkLoaded) @@ -84,7 +83,11 @@ public void InitializeBindings(IList inputData, out bool hasEx // External SDK is in session, but customer does not mean to enable it. Report potential clashes _logger.Log(isUserOnlyLog: false, LogLevel.Error, String.Format(PowerShellWorkerStrings.PotentialDurableSDKClash, Utils.ExternalDurableSdkName)); } + } + public void InitializeBindings(IList inputData, out bool hasExternalSDK) + { + this.tryEnablingExternalSDK(); // If the function is an durable client, then we set the DurableClient // in the module context for the 'Start-DurableOrchestration' function to use. diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index fb234b44..7b4409ad 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -361,21 +361,9 @@ Unable to initialize orchestrator function due to presence of other bindings. Total number of bindings found is '{0}'. Orchestrator Functions should never use any input or output bindings other than the orchestration trigger itself. See: https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-bindings - - Attempting to load '{0}' as the Durable Functions SDK - - - An unexpected error ocurred while importing '{0}': '{1}' - Worker init request completed in {0} ms. - - The Durable Functions SDK, '{0}', could not be found. Please make sure this module is available in the PowerShell worker path. PowerShell worker PSModulePath: '{1}' - - - Import of module '{0}' at version '{1}' succeeded. - Operation '{0}' expected '{1}' result(s) but received '{2}'. From 8fe3d36be79b1bf610e090d4c3445bfed1f0d882 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 13 Dec 2022 18:45:52 -0800 Subject: [PATCH 36/40] remove typos --- src/DurableWorker/DurableController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 7f0e583b..59c6f6b6 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -74,13 +74,13 @@ private void tryEnablingExternalSDK() } else { - // Customer attempted to enable external SDK but the module not in session. Default to built-in SDK. + // Customer attempted to enable external SDK but the module is not in the session. Default to built-in SDK. _logger.Log(isUserOnlyLog: false, LogLevel.Error, string.Format(PowerShellWorkerStrings.ExternalSDKWasNotLoaded, Utils.ExternalDurableSdkName)); } } else if (isExternalSdkLoaded) { - // External SDK is in session, but customer does not mean to enable it. Report potential clashes + // External SDK is in the session, but customer did not explicitly enable it. Report the potential of runtime errors. _logger.Log(isUserOnlyLog: false, LogLevel.Error, String.Format(PowerShellWorkerStrings.PotentialDurableSDKClash, Utils.ExternalDurableSdkName)); } } From 6b36ee112d45362f3dd70f33cc7c0d24caa08b76 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 13 Dec 2022 18:48:02 -0800 Subject: [PATCH 37/40] patch resource strings --- src/resources/PowerShellWorkerStrings.resx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 7b4409ad..abea2179 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -353,7 +353,7 @@ Dependency snapshot '{0}' does not contain acceptable module versions. - Found External Durable Functions SDK in session: Name='{0}', Version='{1}', Path='{2}'. + Found external Durable Functions SDK in session: Name='{0}', Version='{1}', Path='{2}'. IsOrchestrationFailure @@ -368,7 +368,7 @@ Operation '{0}' expected '{1}' result(s) but received '{2}'. - The external Durable Functions SDK is enabled but it cannot be used because it was not loaded onto the current PowerShell session. Please add `Import-Module`'{0}' to your profile.ps1 so it may be used. Defaulting to built-in SDK. + The external Durable Functions SDK is enabled but it cannot be used because it was not loaded onto the current PowerShell session. Please add `Import-Module '{0}'` to your profile.ps1 so it may be used. Defaulting to built-in SDK. Get-Module returned '{0}' instances of '{1}' in the PowerShell session, but only 1 or 0 are expected. This may create runtime errors. Please ensure your script only imports a single version of '{0}'. The modules currently in session are:\n '{2}'. From 6e1e97f2340a62bdfa579b6047ee644781ebe0db Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 16 Dec 2022 15:05:49 -0800 Subject: [PATCH 38/40] apply feedback --- src/DurableSDK/OrchestrationInvoker.cs | 3 ++- src/DurableSDK/PowerShellServices.cs | 2 +- src/PowerShell/PowerShellManager.cs | 6 +++--- src/Utility/Utils.cs | 2 ++ src/resources/PowerShellWorkerStrings.resx | 7 ++----- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index b06a0acd..1ce69bea 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -12,6 +12,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Management.Automation; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; internal class OrchestrationInvoker : IOrchestrationInvoker { @@ -31,7 +32,7 @@ public Hashtable Invoke( } catch (Exception ex) { - ex.Data.Add(PowerShellWorkerStrings.IsOrchestrationFailureKey, true); + ex.Data.Add(Utils.IsOrchestrationFailureKey, true); throw; } finally diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 354d981f..98e48783 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -164,7 +164,7 @@ public void ClearOrchestrationContext() public void TracePipelineObject() { - _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); + _pwsh.AddCommand(Utils.TracePipelineObjectCmdlet); } public IAsyncResult BeginInvoke(PSDataCollection output) diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index b1cda913..5a0a4db7 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -168,7 +168,7 @@ internal void InvokeProfile(string profilePath) // Import-Module on a .ps1 file will evaluate the script in the global scope. _pwsh.AddCommand(Utils.ImportModuleCmdletInfo) .AddParameter("Name", profilePath) - .AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject") + .AddCommand(Utils.TracePipelineObjectCmdlet) .InvokeAndClearCommands(); profileExecutionHadErrors = _pwsh.HadErrors; @@ -233,7 +233,7 @@ public Hashtable InvokeFunction( var isActivityFunction = functionInfo.DurableFunctionInfo.IsActivityFunction; if (!isActivityFunction) { - _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); + _pwsh.AddCommand(Utils.TracePipelineObjectCmdlet); } return ExecuteUserCode(isActivityFunction, outputBindings); } @@ -246,7 +246,7 @@ public Hashtable InvokeFunction( } catch (Exception e) { - if (e.Data.Contains(PowerShellWorkerStrings.IsOrchestrationFailureKey) && e.InnerException is IContainsErrorRecord inner) + if (e.Data.Contains(Utils.IsOrchestrationFailureKey) && e.InnerException is IContainsErrorRecord inner) { Logger.Log(isUserOnlyLog: true, LogLevel.Error, GetFunctionExceptionMessage(inner)); } diff --git a/src/Utility/Utils.cs b/src/Utility/Utils.cs index e80babc9..df068e5b 100644 --- a/src/Utility/Utils.cs +++ b/src/Utility/Utils.cs @@ -26,6 +26,8 @@ internal class Utils internal const string InternalDurableSdkName = "Microsoft.Azure.Functions.PowerShellWorker"; internal const string ExternalDurableSdkName = "AzureFunctions.PowerShell.Durable.SDK"; + internal const string IsOrchestrationFailureKey = "IsOrchestrationFailure"; + internal const string TracePipelineObjectCmdlet = "Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"; internal readonly static object BoxedTrue = (object)true; internal readonly static object BoxedFalse = (object)false; diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index abea2179..891a1d55 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -355,11 +355,8 @@ Found external Durable Functions SDK in session: Name='{0}', Version='{1}', Path='{2}'. - - IsOrchestrationFailure - - Unable to initialize orchestrator function due to presence of other bindings. Total number of bindings found is '{0}'. Orchestrator Functions should never use any input or output bindings other than the orchestration trigger itself. See: https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-bindings + Unable to initialize orchestrator function due to presence of other bindings. Total number of bindings found is '{0}'. Orchestrator Functions should never use any input or output bindings other than the orchestration trigger itself. See: aka.ms/df-bindings Worker init request completed in {0} ms. @@ -368,7 +365,7 @@ Operation '{0}' expected '{1}' result(s) but received '{2}'. - The external Durable Functions SDK is enabled but it cannot be used because it was not loaded onto the current PowerShell session. Please add `Import-Module '{0}'` to your profile.ps1 so it may be used. Defaulting to built-in SDK. + The external Durable Functions SDK is enabled but it cannot be used because it was not loaded onto the current PowerShell session. Please add `Import-Module -Name {0} -ErrorAction Stop` to your profile.ps1 so it may be used. Defaulting to built-in SDK. Get-Module returned '{0}' instances of '{1}' in the PowerShell session, but only 1 or 0 are expected. This may create runtime errors. Please ensure your script only imports a single version of '{0}'. The modules currently in session are:\n '{2}'. From 8e0e103d4ffa7abd758eab6581f9e96809049533 Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 6 Jan 2023 15:38:48 -0800 Subject: [PATCH 39/40] update error message to specify that DF SDK should only be imported in the profile.ps1 file --- src/resources/PowerShellWorkerStrings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 891a1d55..d61e825d 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -368,7 +368,7 @@ The external Durable Functions SDK is enabled but it cannot be used because it was not loaded onto the current PowerShell session. Please add `Import-Module -Name {0} -ErrorAction Stop` to your profile.ps1 so it may be used. Defaulting to built-in SDK. - Get-Module returned '{0}' instances of '{1}' in the PowerShell session, but only 1 or 0 are expected. This may create runtime errors. Please ensure your script only imports a single version of '{0}'. The modules currently in session are:\n '{2}'. + Get-Module returned '{0}' instances of '{1}' in the PowerShell session, but only 1 or 0 are expected. This may create runtime errors. Please ensure your script only imports a single version of '{0}' in your profile.ps1; do not import '{0}' anywhere else in your code. The modules currently in session are:\n '{2}'. The external Durable Functions SDK is not enabled but '{0}' has been imported to the PowerShell session. This may create runtime conflicts between the built-in and external Durable Functions CommandLets. If you mean to use the external Durable Functions SDK, please set the enviroment variable '{1}' to "true" From e85c19e8070e27596d23a6af56630343d02b16ab Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 9 Jan 2023 09:31:22 -0800 Subject: [PATCH 40/40] simplify instruction to import external SDK --- src/resources/PowerShellWorkerStrings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index d61e825d..89563720 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -368,7 +368,7 @@ The external Durable Functions SDK is enabled but it cannot be used because it was not loaded onto the current PowerShell session. Please add `Import-Module -Name {0} -ErrorAction Stop` to your profile.ps1 so it may be used. Defaulting to built-in SDK. - Get-Module returned '{0}' instances of '{1}' in the PowerShell session, but only 1 or 0 are expected. This may create runtime errors. Please ensure your script only imports a single version of '{0}' in your profile.ps1; do not import '{0}' anywhere else in your code. The modules currently in session are:\n '{2}'. + Get-Module returned '{0}' instances of '{1}' in the PowerShell session, but only 1 or 0 are expected. This may create runtime errors. Please ensure your script only imports a single version of '{0}' in your profile.ps1. The modules currently in session are:\n '{2}'. The external Durable Functions SDK is not enabled but '{0}' has been imported to the PowerShell session. This may create runtime conflicts between the built-in and external Durable Functions CommandLets. If you mean to use the external Durable Functions SDK, please set the enviroment variable '{1}' to "true"