diff --git a/src/DurableSDK/ExternalInvoker.cs b/src/DurableSDK/ExternalInvoker.cs new file mode 100644 index 00000000..6b3d3a13 --- /dev/null +++ b/src/DurableSDK/ExternalInvoker.cs @@ -0,0 +1,28 @@ +// +// 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; + + // 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; + } + + // Invokes an orchestration using the external Durable SDK + public Hashtable Invoke(IPowerShellServices powerShellServices) + { + return (Hashtable)_externalSDKInvokerFunction.Invoke(powerShellServices.GetPowerShell()); + } + } +} diff --git a/src/DurableSDK/IExternalOrchestrationInvoker.cs b/src/DurableSDK/IExternalOrchestrationInvoker.cs new file mode 100644 index 00000000..99b8c2ca --- /dev/null +++ b/src/DurableSDK/IExternalOrchestrationInvoker.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; + + // Contract interface for the orchestration invoker in the external Durable Functions SDK + internal interface IExternalOrchestrationInvoker + { + // 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 7e80aba3..8e83c7b9 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(IExternalOrchestrationInvoker externalInvoker); } } diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index a8cf897b..3b7b49f3 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -5,17 +5,30 @@ 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 HasExternalDurableSDK(); + + bool isExternalDurableSdkLoaded(); + + void EnableExternalDurableSDK(); + void SetDurableClient(object durableClient); - void SetOrchestrationContext(OrchestrationContext orchestrationContext); + OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context, out IExternalOrchestrationInvoker externalInvoker); void ClearOrchestrationContext(); + void TracePipelineObject(); + + void AddParameter(string name, object value); + IAsyncResult BeginInvoke(PSDataCollection output); void EndInvoke(IAsyncResult asyncResult); diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index fef557ba..1ce69bea 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -11,58 +11,97 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Linq; using System.Management.Automation; - using PowerShellWorker.Utility; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; internal class OrchestrationInvoker : IOrchestrationInvoker { - public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowerShellServices pwsh) + private IExternalOrchestrationInvoker externalInvoker; + + public Hashtable Invoke( + OrchestrationBindingInfo orchestrationBindingInfo, + IPowerShellServices powerShellServices) { try { - var outputBuffer = new PSDataCollection(); - var context = orchestrationBindingInfo.Context; + if (powerShellServices.HasExternalDurableSDK()) + { + return InvokeExternalDurableSDK(powerShellServices); + } + return InvokeInternalDurableSDK(orchestrationBindingInfo, powerShellServices); + } + catch (Exception ex) + { + ex.Data.Add(Utils.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 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( @@ -74,5 +113,10 @@ private static Hashtable CreateOrchestrationResult( var orchestrationMessage = new OrchestrationMessage(isDone, actions, output, customStatus); return new Hashtable { { AzFunctionInfo.DollarReturn, orchestrationMessage } }; } + + public void SetExternalInvoker(IExternalOrchestrationInvoker externalInvoker) + { + this.externalInvoker = externalInvoker; + } } } diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 0efb681d..98e48783 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -6,20 +6,93 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System; + using System.Collections.ObjectModel; + using System.Linq; using System.Management.Automation; - using PowerShell; + using System.Reflection.Metadata; + using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; + using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + using Newtonsoft.Json; + using LogLevel = WebJobs.Script.Grpc.Messages.RpcLog.Types.Level; internal class PowerShellServices : IPowerShellServices { - private const string SetFunctionInvocationContextCommand = - "Microsoft.Azure.Functions.PowerShellWorker\\Set-FunctionInvocationContext"; - private readonly PowerShell _pwsh; - private bool _hasSetOrchestrationContext = false; + private bool _hasInitializedDurableFunctions = false; + 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( + _setFunctionInvocationContextCommandTemplate, + Utils.InternalDurableSdkName); - public PowerShellServices(PowerShell pwsh) + public PowerShellServices(PowerShell pwsh, ILogger logger) { _pwsh = pwsh; + _logger = logger; + + // Configure FunctionInvocationContext command, based on the select DF SDK + var prefix = Utils.InternalDurableSdkName; + SetFunctionInvocationContextCommand = string.Format(_setFunctionInvocationContextCommandTemplate, prefix); + } + + public bool isExternalDurableSdkLoaded() + { + // Search for the external DF SDK in the current session + var matchingModules = _pwsh.AddCommand(Utils.GetModuleCmdletInfo) + .AddParameter("FullyQualifiedName", Utils.ExternalDurableSdkName) + .InvokeAndClearCommands(); + + // 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 + { + // a single external SDK is in session. Report its metadata + _logger.Log(isUserOnlyLog: false, LogLevel.Trace, externalSDKModuleInfo); + } + } + return isModuleInCurrentSession; + } + + public void EnableExternalDurableSDK() + { + _usesExternalDurableSDK = true; + + // assign SetFunctionInvocationContextCommand to the corresponding external SDK's CmdLet + SetFunctionInvocationContextCommand = string.Format( + _setFunctionInvocationContextCommandTemplate, + Utils.ExternalDurableSdkName); + } + + public bool HasExternalDurableSDK() + { + return _usesExternalDurableSDK; + } + + public PowerShell GetPowerShell() + { + return this._pwsh; } public void SetDurableClient(object durableClient) @@ -27,22 +100,61 @@ public void SetDurableClient(object durableClient) _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); - - _hasSetOrchestrationContext = true; + _hasInitializedDurableFunctions = true; } - public void SetOrchestrationContext(OrchestrationContext orchestrationContext) + public OrchestrationBindingInfo SetOrchestrationContext( + ParameterBinding context, + out IExternalOrchestrationInvoker externalInvoker) { - _pwsh.AddCommand(SetFunctionInvocationContextCommand) - .AddParameter("OrchestrationContext", orchestrationContext) - .InvokeAndClearCommands(); + externalInvoker = null; + OrchestrationBindingInfo orchestrationBindingInfo = new OrchestrationBindingInfo( + context.Name, + JsonConvert.DeserializeObject(context.Data.String)); - _hasSetOrchestrationContext = true; + if (_usesExternalDurableSDK) + { + 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 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 numResults = output.Count(); + var numExpectedResults = 1; + var outputContractIsMet = output.Count() == numExpectedResults; + if (outputContractIsMet) + { + externalInvoker = new ExternalInvoker(output[0]); + } + else + { + var exceptionMessage = string.Format(PowerShellWorkerStrings.UnexpectedResultCount, + SetFunctionInvocationContextCommand, numExpectedResults, numResults); + throw new InvalidOperationException(exceptionMessage); + } + } + else + { + _pwsh.AddCommand(SetFunctionInvocationContextCommand) + .AddParameter("OrchestrationContext", orchestrationBindingInfo.Context) + .InvokeAndClearCommands(); + } + _hasInitializedDurableFunctions = true; + return orchestrationBindingInfo; + } + + + public void AddParameter(string name, object value) + { + _pwsh.AddParameter(name, value); } public void ClearOrchestrationContext() { - if (_hasSetOrchestrationContext) + if (_hasInitializedDurableFunctions) { _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("Clear", true) @@ -50,6 +162,11 @@ public void ClearOrchestrationContext() } } + public void TracePipelineObject() + { + _pwsh.AddCommand(Utils.TracePipelineObjectCmdlet); + } + 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..6f54182c 100644 --- a/src/DurableSDK/Tasks/ActivityInvocationTask.cs +++ b/src/DurableSDK/Tasks/ActivityInvocationTask.cs @@ -13,7 +13,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks 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; diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 7b7f1b4e..59c6f6b6 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -5,18 +5,19 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { - using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; 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,30 +28,68 @@ internal class DurableController private readonly IPowerShellServices _powerShellServices; private readonly IOrchestrationInvoker _orchestrationInvoker; private OrchestrationBindingInfo _orchestrationBindingInfo; + private readonly ILogger _logger; + + private bool isExternalDFSdkEnabled { get; } = + PowerShellWorkerConfiguration.GetBoolean("ExternalDurablePowerShellSDK") ?? false; 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() + { + return _orchestrationBindingInfo?.ParameterName; + } + + private void tryEnablingExternalSDK() + { + var isExternalSdkLoaded = _powerShellServices.isExternalDurableSdkLoaded(); + if (isExternalDFSdkEnabled) + { + if (isExternalSdkLoaded) + { + // Enable external SDK only when customer has opted-in + _powerShellServices.EnableExternalDurableSDK(); + } + else + { + // 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 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)); + } } - public void BeforeFunctionInvocation(IList inputData) + public void InitializeBindings(IList inputData, out bool hasExternalSDK) { - // If the function is an orchestration client, then we set the DurableClient + 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. if (_durableFunctionInfo.IsDurableClient) { @@ -62,9 +101,26 @@ public void BeforeFunctionInvocation(IList inputData) } else if (_durableFunctionInfo.IsOrchestrationFunction) { - _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); - _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); + 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 ArgumentException(exceptionMessage); + } + + _orchestrationBindingInfo = _powerShellServices.SetOrchestrationContext( + inputData[0], + out IExternalOrchestrationInvoker externalInvoker); + _orchestrationInvoker.SetExternalInvoker(externalInvoker); } + hasExternalSDK = _powerShellServices.HasExternalDurableSDK(); } public void AfterFunctionInvocation() @@ -88,46 +144,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..d0181bc4 100644 --- a/src/DurableWorker/DurableFunctionInfo.cs +++ b/src/DurableWorker/DurableFunctionInfo.cs @@ -17,6 +17,8 @@ public DurableFunctionInfo(DurableFunctionType type, string durableClientBinding public bool IsOrchestrationFunction => Type == DurableFunctionType.OrchestrationFunction; + public bool IsActivityFunction => Type == DurableFunctionType.ActivityFunction; + public string DurableClientBindingName { get; } public DurableFunctionType Type diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 7e458914..5a0a4db7 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; @@ -167,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; @@ -204,32 +205,38 @@ 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, Logger); try { - durableController.BeforeFunctionInvocation(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); - 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(Utils.TracePipelineObjectCmdlet); + } + return ExecuteUserCode(isActivityFunction, outputBindings); + } } catch (RuntimeException e) { @@ -237,9 +244,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(Utils.IsOrchestrationFailureKey) && e.InnerException is IContainsErrorRecord inner) { Logger.Log(isUserOnlyLog: true, LogLevel.Error, GetFunctionExceptionMessage(inner)); } @@ -248,7 +255,7 @@ public Hashtable InvokeFunction( } finally { - durableController.AfterFunctionInvocation(); + durableFunctionsUtils.AfterFunctionInvocation(); outputBindings.Clear(); ResetRunspace(); } @@ -257,7 +264,7 @@ public Hashtable InvokeFunction( private void SetInputBindingParameterValues( AzFunctionInfo functionInfo, IEnumerable inputData, - DurableController durableController, + string orchestratorParameter, Hashtable triggerMetadata, TraceContext traceContext, RetryContext retryContext) @@ -266,13 +273,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, orchestratorParameter) != 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 +302,15 @@ private void SetInputBindingParameterValues( /// /// Execution a function fired by a trigger or an activity function scheduled by an orchestration. /// - private Hashtable InvokeNonOrchestrationFunction(DurableController durableController, IDictionary outputBindings) + private Hashtable ExecuteUserCode(bool addPipelineOutput, IDictionary outputBindings) { var pipelineItems = _pwsh.InvokeAndClearCommands(); var result = new Hashtable(outputBindings, StringComparer.OrdinalIgnoreCase); - durableController.AddPipelineOutputIfNecessary(pipelineItems, result); + if (addPipelineOutput) + { + var returnValue = FunctionReturnValueBuilder.CreateReturnValueFromFunctionOutput(pipelineItems); + result.Add(AzFunctionInfo.DollarReturn, returnValue); + } return result; } diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index b2f177a0..ad192aa0 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -521,7 +521,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/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/src/Utility/TypeExtensions.cs b/src/Utility/TypeExtensions.cs index 2f3c4186..43151edf 100644 --- a/src/Utility/TypeExtensions.cs +++ b/src/Utility/TypeExtensions.cs @@ -139,10 +139,15 @@ public static object ConvertFromJson(string json) return retObj; } - private static string ConvertToJson(object fromObj) + private static string ConvertToJson(object fromObj, bool isDurableData) { + /* 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: 4, + maxDepth: isDurableData ? 100 : 10, enumsAsStrings: false, compressOutput: true); @@ -198,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) { @@ -244,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/Utility/Utils.cs b/src/Utility/Utils.cs index 549ba5de..df068e5b 100644 --- a/src/Utility/Utils.cs +++ b/src/Utility/Utils.cs @@ -17,11 +17,17 @@ 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)); 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 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 07d6e78d..89563720 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -352,7 +352,28 @@ Dependency snapshot '{0}' does not contain acceptable module versions. + + Found external Durable Functions SDK in session: Name='{0}', Version='{1}', Path='{2}'. + + + 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. + + 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 -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. 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 5ec3244c..065232c7 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -18,14 +18,20 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test.Durable using Moq; using Xunit; + using Grpc.Core; 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); + private static readonly ILogger _testLogger = new ConsoleLogger(); + [Fact] - public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction() + public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() { var durableController = CreateDurableController(DurableFunctionType.None, "DurableClientBindingName"); @@ -38,8 +44,12 @@ public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction( }; _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); + - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); _mockPowerShellServices.Verify( _ => _.SetDurableClient( @@ -48,50 +58,59 @@ 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(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); _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]; + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); - Assert.ThrowsAny(() => durableController.BeforeFunctionInvocation(inputData)); + Assert.ThrowsAny(() => durableController.InitializeBindings(inputData, out bool hasExternalSDK)); } [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) }; + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); } [Theory] @@ -112,19 +131,23 @@ 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); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); + + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); + + Assert.True(durableController.TryGetInputBindingParameterValue(_contextParameterName, out var value)); + Assert.Equal(_orchestrationContext.InstanceId, ((OrchestrationContext)value).InstanceId); } [Theory] @@ -133,69 +156,61 @@ 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); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); - Assert.False(durableController.TryGetInputBindingParameterValue(contextParameterName, out var value)); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + durableController.InitializeBindings(inputData, out bool hasExternalSDK); + + 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); + _mockPowerShellServices.Setup(_ => _.HasExternalDurableSDK()).Returns(false); + _mockPowerShellServices.Setup(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); + + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); + + durableController.InitializeBindings(inputData, out bool hasExternalSDK); 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() @@ -242,6 +257,75 @@ 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(_ => _.EnableExternalDurableSDK()).Throws(new Exception("should not be called")); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(false); + durableController.InitializeBindings(inputData, out var hasExternalSDK); + + Assert.False(hasExternalSDK); + _mockPowerShellServices.Verify(_ => _.EnableExternalDurableSDK(), 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(_ => _.EnableExternalDurableSDK()); + _mockPowerShellServices.Setup(_ => _.isExternalDurableSdkLoaded()).Returns(true); + durableController.InitializeBindings(inputData, out var hasExternalSDK); + + Assert.True(hasExternalSDK); + _mockPowerShellServices.Verify(_ => _.EnableExternalDurableSDK(), Times.Once); + + } + finally + { + Environment.SetEnvironmentVariable("ExternalDurablePowerShellSDK", "false"); + } + } + private DurableController CreateDurableController( DurableFunctionType durableFunctionType, string durableClientBindingName = null) @@ -251,7 +335,8 @@ private DurableController CreateDurableController( return new DurableController( durableFunctionInfo, _mockPowerShellServices.Object, - _mockOrchestrationInvoker.Object); + _mockOrchestrationInvoker.Object, + _testLogger); } private static ParameterBinding CreateParameterBinding(string parameterName, object value) @@ -265,5 +350,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; + } } } diff --git a/test/Unit/Durable/OrchestrationInvokerTests.cs b/test/Unit/Durable/OrchestrationInvokerTests.cs index 65c4b24d..50197b53 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(_ => _.HasExternalDurableSDK()).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(_ => _.HasExternalDurableSDK(), Times.Once); _mockPowerShellServices.VerifyNoOtherCalls(); } @@ -47,10 +51,14 @@ public void InvocationRunsToCompletionIfNotStopped() public void InvocationStopsOnStopEvent() { InvokeOrchestration(completed: 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(_ => _.HasExternalDurableSDK(), Times.Once); _mockPowerShellServices.VerifyNoOtherCalls(); }