diff --git a/src/DurableSDK/ExternalInvoker.cs b/src/DurableSDK/ExternalInvoker.cs new file mode 100644 index 00000000..f0a73b86 --- /dev/null +++ b/src/DurableSDK/ExternalInvoker.cs @@ -0,0 +1,27 @@ +// +// 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.Management.Automation; + + internal class ExternalInvoker : IExternalInvoker + { + private readonly Func _externalSDKInvokerFunction; + private readonly IPowerShellServices _powerShellServices; + + public ExternalInvoker(Func invokerFunction, IPowerShellServices powerShellServices) + { + _externalSDKInvokerFunction = invokerFunction; + _powerShellServices = powerShellServices; + } + + public void Invoke() + { + _externalSDKInvokerFunction.Invoke(_powerShellServices.GetPowerShell()); + } + } +} diff --git a/src/DurableSDK/IExternalInvoker.cs b/src/DurableSDK/IExternalInvoker.cs new file mode 100644 index 00000000..16d17e23 --- /dev/null +++ b/src/DurableSDK/IExternalInvoker.cs @@ -0,0 +1,14 @@ +// +// 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 +{ + // Represents a contract for the + internal interface IExternalInvoker + { + // Method to invoke an orchestration using the external Durable SDK + void Invoke(); + } +} diff --git a/src/DurableSDK/IOrchestrationInvoker.cs b/src/DurableSDK/IOrchestrationInvoker.cs index 488266db..36011b56 100644 --- a/src/DurableSDK/IOrchestrationInvoker.cs +++ b/src/DurableSDK/IOrchestrationInvoker.cs @@ -5,13 +5,11 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { - using System; using System.Collections; - using System.Management.Automation; internal interface IOrchestrationInvoker { Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowerShellServices pwsh); - void SetExternalInvoker(Action externalInvoker); + void SetExternalInvoker(IExternalInvoker externalInvoker); } } diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index a13d998c..cdd850bc 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -17,7 +17,7 @@ internal interface IPowerShellServices void SetDurableClient(object durableClient); - OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding orchestrationContext, out Action externalInvoker); + OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context, out IExternalInvoker externalInvoker); void ClearOrchestrationContext(); diff --git a/src/DurableSDK/OrchestrationContext.cs b/src/DurableSDK/OrchestrationContext.cs index b4b76f8c..e7edec60 100644 --- a/src/DurableSDK/OrchestrationContext.cs +++ b/src/DurableSDK/OrchestrationContext.cs @@ -35,15 +35,16 @@ public class OrchestrationContext internal OrchestrationActionCollector OrchestrationActionCollector { get; } = new OrchestrationActionCollector(); - internal object ExternalResult; - internal bool ExternalIsError; + internal object ExternalSDKResult; + + internal bool ExternalSDKIsError; // Called by the External DF SDK to communicate its orchestration result // back to the worker. internal void SetExternalResult(object result, bool isError) { - this.ExternalResult = result; - this.ExternalIsError = isError; + this.ExternalSDKResult = result; + this.ExternalSDKIsError = isError; } internal object CustomStatus { get; set; } diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index 8f2bcbb6..4706c2c3 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -11,80 +11,95 @@ 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 { - private Action externalInvoker = null; + private IExternalInvoker _externalInvoker; - public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowerShellServices pwsh) + public Hashtable Invoke( + OrchestrationBindingInfo orchestrationBindingInfo, + IPowerShellServices powerShellServices) { - try { - if (pwsh.UseExternalDurableSDK()) + if (powerShellServices.UseExternalDurableSDK()) { - externalInvoker.Invoke(pwsh.GetPowerShell()); - var result = orchestrationBindingInfo.Context.ExternalResult; - var isError = orchestrationBindingInfo.Context.ExternalIsError; - if (isError) - { - throw (Exception)result; - } - else - { - return (Hashtable)result; - } + return InvokeExternalDurableSDK(orchestrationBindingInfo, powerShellServices); } + return InvokeInternalDurableSDK(orchestrationBindingInfo, powerShellServices); + } + finally + { + powerShellServices.ClearStreamsAndCommands(); + } + } - var outputBuffer = new PSDataCollection(); - var context = orchestrationBindingInfo.Context; + public Hashtable InvokeExternalDurableSDK( + OrchestrationBindingInfo orchestrationBindingInfo, + IPowerShellServices powerShellServices) + { - // context.History should never be null when initializing CurrentUtcDateTime - var orchestrationStart = context.History.First( - e => e.EventType == HistoryEventType.OrchestratorStarted); - context.CurrentUtcDateTime = orchestrationStart.Timestamp.ToUniversalTime(); + _externalInvoker.Invoke(); + var result = orchestrationBindingInfo.Context.ExternalSDKResult; + var isError = orchestrationBindingInfo.Context.ExternalSDKIsError; + if (isError) + { + throw (Exception)result; + } + else + { + return (Hashtable)result; + } + } + + public Hashtable InvokeInternalDurableSDK( + OrchestrationBindingInfo orchestrationBindingInfo, + IPowerShellServices powerShellServices) + { + var outputBuffer = new PSDataCollection(); + var context = orchestrationBindingInfo.Context; - // Marks the first OrchestratorStarted event as processed - orchestrationStart.IsProcessed = true; + // context.History should never be null when initializing CurrentUtcDateTime + var orchestrationStart = context.History.First( + e => e.EventType == HistoryEventType.OrchestratorStarted); + context.CurrentUtcDateTime = orchestrationStart.Timestamp.ToUniversalTime(); - // Finish initializing the Function invocation - pwsh.AddParameter(orchestrationBindingInfo.ParameterName, context); - pwsh.TracePipelineObject(); + // 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 = 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 - { - pwsh.ClearStreamsAndCommands(); - } } public static object CreateReturnValueFromFunctionOutput(IList pipelineItems) @@ -107,9 +122,9 @@ private static Hashtable CreateOrchestrationResult( return new Hashtable { { "$return", orchestrationMessage } }; } - public void SetExternalInvoker(Action externalInvoker) + public void SetExternalInvoker(IExternalInvoker externalInvoker) { - this.externalInvoker = externalInvoker; + _externalInvoker = externalInvoker; } } } diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 8baf4739..64355b25 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -25,7 +25,10 @@ internal class PowerShellServices : IPowerShellServices public PowerShellServices(PowerShell pwsh) { - // Attempt to import the external SDK + // 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) @@ -46,7 +49,7 @@ public PowerShellServices(PowerShell pwsh) if (availableModules.Count() > 0) { // TODO: evaluate if there is a better error message or exception type to be throwing. - // Ideally, this should never happen + // Ideally, this should never happen. throw new InvalidOperationException("The external Durable SDK was detected, but unable to be imported.", e); } _useExternalDurableSDK = false; @@ -78,38 +81,46 @@ public void SetDurableClient(object durableClient) _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); - + // TODO: is _hasSetOrchestrationContext properly named? _hasSetOrchestrationContext = true; } - public OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context, out Action externalInvoker) + public OrchestrationBindingInfo SetOrchestrationContext( + ParameterBinding context, + out IExternalInvoker externalInvoker) { externalInvoker = null; - var orchBindingInfo = new OrchestrationBindingInfo( + OrchestrationBindingInfo orchestrationBindingInfo = new OrchestrationBindingInfo( context.Name, JsonConvert.DeserializeObject(context.Data.String)); if (_useExternalDurableSDK) { - Collection> output = _pwsh.AddCommand(SetFunctionInvocationContextCommand) + 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) - .AddParameter("SetResult", (Action) orchBindingInfo.Context.SetExternalResult) - .InvokeAndClearCommands>(); + .AddParameter("SetResult", (Action) orchestrationBindingInfo.Context.SetExternalResult) + .InvokeAndClearCommands>(); if (output.Count() == 1) { - externalInvoker = output[0]; + externalInvoker = new ExternalInvoker(output[0], this); + } + else + { + throw new InvalidOperationException($"Only a single output was expected for an invocation of {SetFunctionInvocationContextCommand}"); } } else { _pwsh.AddCommand(SetFunctionInvocationContextCommand) - .AddParameter("OrchestrationContext", orchBindingInfo.Context) - .InvokeAndClearCommands>(); + .AddParameter("OrchestrationContext", orchestrationBindingInfo.Context) + .InvokeAndClearCommands(); } - _hasSetOrchestrationContext = true; - return orchBindingInfo; + return orchestrationBindingInfo; } + public void AddParameter(string name, object value) { diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 43b5538c..427fd9be 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -67,12 +67,10 @@ public void InitializeBindings(IList inputData) } else if (_durableFunctionInfo.IsOrchestrationFunction) { - var contextBindingData = inputData[0]; - _orchestrationBindingInfo = _powerShellServices.SetOrchestrationContext(contextBindingData, out var externalInvoker); - if (externalInvoker != null) - { - this._orchestrationInvoker.SetExternalInvoker(externalInvoker); - } + _orchestrationBindingInfo = _powerShellServices.SetOrchestrationContext( + inputData[0], + out IExternalInvoker 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 ec03059c..d0a285e0 100644 --- a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 +++ b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 @@ -11,7 +11,7 @@ Set-Alias -Name Start-NewOrchestration -Value Start-DurableOrchestration function GetDurableClientFromModulePrivateData { $PrivateData = $PSCmdlet.MyInvocation.MyCommand.Module.PrivateData - if ($PrivateData -eq $null -or $PrivateData['DurableClient'] -eq $null) { + if ($null -eq $PrivateData -or $null -eq $PrivateData['DurableClient']) { throw "No binding of the type 'durableClient' was defined." } else { diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 0e7cff64..e16f04d7 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -58,16 +58,18 @@ public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction { CreateParameterBinding("ParameterName", _orchestrationContext) }; - - Action externalInvoker; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)) - .Returns(_orchestrationBindingInfo); + + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), + out It.Ref.IsAny)) + .Returns(_orchestrationBindingInfo); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData); _mockPowerShellServices.Verify( _ => _.SetOrchestrationContext( - It.Is(c => c.Data.ToString().Contains(_orchestrationContext.InstanceId)), out externalInvoker), + It.Is(c => c.Data.ToString().Contains(_orchestrationContext.InstanceId)), + out It.Ref.IsAny), Times.Once); } @@ -118,9 +120,11 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame CreateParameterBinding(_contextParameterName, _orchestrationContext) }; - Action externalInvoker; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)) - .Returns(_orchestrationBindingInfo); + _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)); @@ -138,8 +142,11 @@ internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrat CreateParameterBinding(_contextParameterName, _orchestrationContext) }; - Action externalInvoker; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)); + _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)); @@ -152,9 +159,11 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() var inputData = new[] { CreateParameterBinding(_contextParameterName, _orchestrationContext) }; var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); - Action externalInvoker; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)) - .Returns(_orchestrationBindingInfo); + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( + It.IsAny(), + out It.Ref.IsAny)) + .Returns(_orchestrationBindingInfo); + _mockOrchestrationInvoker.Setup(_ => _.SetExternalInvoker(It.IsAny())); durableController.InitializeBindings(inputData); var expectedResult = new Hashtable();