From 4423d13353b34a0a1255f43d734d5497bfec60ca Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 28 Jan 2022 19:20:04 -0800 Subject: [PATCH 01/43] separate DF SDK classes from DF worker classes --- .../Actions/ActionType.cs | 0 .../Actions/CallActivityAction.cs | 0 .../Actions/CallActivityWithRetryAction.cs | 0 .../Actions/CreateDurableTimerAction.cs | 0 .../Actions/ExternalEventAction.cs | 0 .../Actions/OrchestrationAction.cs | 0 .../ActivityFailureException.cs | 0 .../Commands/InvokeDurableActivityCommand.cs | 4 ++-- .../Commands/SetDurableCustomStatusCommand.cs | 0 .../SetFunctionInvocationContextCommand.cs | 0 ...tartDurableExternalEventListenerCommand.cs | 0 .../Commands/StartDurableTimerCommand.cs | 0 .../Commands/StopDurableTimerTaskCommand.cs | 0 .../Commands/WaitDurableTaskCommand.cs | 0 .../CurrentUtcDateTimeUpdater.cs | 0 .../DurableActivityErrorHandler.cs | 0 .../DurableTaskHandler.cs | 0 src/{Durable => DurableSDK}/HistoryEvent.cs | 0 .../HistoryEventType.cs | 0 .../IOrchestrationInvoker.cs | 0 .../IPowerShellServices.cs | 0 .../OrchestrationActionCollector.cs | 0 .../OrchestrationBindingInfo.cs | 0 .../OrchestrationContext.cs | 0 .../OrchestrationFailureException.cs | 0 .../OrchestrationInvoker.cs | 6 +++++ .../OrchestrationMessage.cs | 0 .../PowerShellServices.cs | 0 src/{Durable => DurableSDK}/RetryOptions.cs | 0 src/{Durable => DurableSDK}/RetryProcessor.cs | 0 .../Tasks/ActivityInvocationTask.cs | 4 ++-- .../Tasks/DurableTask.cs | 0 .../Tasks/DurableTimerTask.cs | 0 .../Tasks/ExternalEventTask.cs | 0 .../DurableBindings.cs | 2 +- .../DurableController.cs | 3 +++ .../DurableFunctionInfo.cs | 2 +- .../DurableFunctionInfoFactory.cs | 2 +- .../DurableFunctionType.cs | 2 +- src/FunctionInfo.cs | 2 +- src/RequestProcessor.cs | 2 +- test/E2E/TestFunctionApp/profile.ps1 | 22 +++++++++++++++++++ .../Durable/ActivityInvocationTaskTests.cs | 8 +++---- test/Unit/Durable/DurableControllerTests.cs | 1 + .../DurableFunctionInfoFactoryTests.cs | 2 +- .../Unit/PowerShell/PowerShellManagerTests.cs | 1 + 46 files changed, 48 insertions(+), 15 deletions(-) rename src/{Durable => DurableSDK}/Actions/ActionType.cs (100%) rename src/{Durable => DurableSDK}/Actions/CallActivityAction.cs (100%) rename src/{Durable => DurableSDK}/Actions/CallActivityWithRetryAction.cs (100%) rename src/{Durable => DurableSDK}/Actions/CreateDurableTimerAction.cs (100%) rename src/{Durable => DurableSDK}/Actions/ExternalEventAction.cs (100%) rename src/{Durable => DurableSDK}/Actions/OrchestrationAction.cs (100%) rename src/{Durable => DurableSDK}/ActivityFailureException.cs (100%) rename src/{Durable => DurableSDK}/Commands/InvokeDurableActivityCommand.cs (93%) rename src/{Durable => DurableSDK}/Commands/SetDurableCustomStatusCommand.cs (100%) rename src/{Durable => DurableSDK}/Commands/SetFunctionInvocationContextCommand.cs (100%) rename src/{Durable => DurableSDK}/Commands/StartDurableExternalEventListenerCommand.cs (100%) rename src/{Durable => DurableSDK}/Commands/StartDurableTimerCommand.cs (100%) rename src/{Durable => DurableSDK}/Commands/StopDurableTimerTaskCommand.cs (100%) rename src/{Durable => DurableSDK}/Commands/WaitDurableTaskCommand.cs (100%) rename src/{Durable => DurableSDK}/CurrentUtcDateTimeUpdater.cs (100%) rename src/{Durable => DurableSDK}/DurableActivityErrorHandler.cs (100%) rename src/{Durable => DurableSDK}/DurableTaskHandler.cs (100%) rename src/{Durable => DurableSDK}/HistoryEvent.cs (100%) rename src/{Durable => DurableSDK}/HistoryEventType.cs (100%) rename src/{Durable => DurableSDK}/IOrchestrationInvoker.cs (100%) rename src/{Durable => DurableSDK}/IPowerShellServices.cs (100%) rename src/{Durable => DurableSDK}/OrchestrationActionCollector.cs (100%) rename src/{Durable => DurableSDK}/OrchestrationBindingInfo.cs (100%) rename src/{Durable => DurableSDK}/OrchestrationContext.cs (100%) rename src/{Durable => DurableSDK}/OrchestrationFailureException.cs (100%) rename src/{Durable => DurableSDK}/OrchestrationInvoker.cs (86%) rename src/{Durable => DurableSDK}/OrchestrationMessage.cs (100%) rename src/{Durable => DurableSDK}/PowerShellServices.cs (100%) rename src/{Durable => DurableSDK}/RetryOptions.cs (100%) rename src/{Durable => DurableSDK}/RetryProcessor.cs (100%) rename src/{Durable => DurableSDK}/Tasks/ActivityInvocationTask.cs (96%) rename src/{Durable => DurableSDK}/Tasks/DurableTask.cs (100%) rename src/{Durable => DurableSDK}/Tasks/DurableTimerTask.cs (100%) rename src/{Durable => DurableSDK}/Tasks/ExternalEventTask.cs (100%) rename src/{Durable => DurableWorker}/DurableBindings.cs (95%) rename src/{Durable => DurableWorker}/DurableController.cs (95%) rename src/{Durable => DurableWorker}/DurableFunctionInfo.cs (92%) rename src/{Durable => DurableWorker}/DurableFunctionInfoFactory.cs (96%) rename src/{Durable => DurableWorker}/DurableFunctionType.cs (80%) create mode 100644 test/E2E/TestFunctionApp/profile.ps1 diff --git a/src/Durable/Actions/ActionType.cs b/src/DurableSDK/Actions/ActionType.cs similarity index 100% rename from src/Durable/Actions/ActionType.cs rename to src/DurableSDK/Actions/ActionType.cs diff --git a/src/Durable/Actions/CallActivityAction.cs b/src/DurableSDK/Actions/CallActivityAction.cs similarity index 100% rename from src/Durable/Actions/CallActivityAction.cs rename to src/DurableSDK/Actions/CallActivityAction.cs diff --git a/src/Durable/Actions/CallActivityWithRetryAction.cs b/src/DurableSDK/Actions/CallActivityWithRetryAction.cs similarity index 100% rename from src/Durable/Actions/CallActivityWithRetryAction.cs rename to src/DurableSDK/Actions/CallActivityWithRetryAction.cs diff --git a/src/Durable/Actions/CreateDurableTimerAction.cs b/src/DurableSDK/Actions/CreateDurableTimerAction.cs similarity index 100% rename from src/Durable/Actions/CreateDurableTimerAction.cs rename to src/DurableSDK/Actions/CreateDurableTimerAction.cs diff --git a/src/Durable/Actions/ExternalEventAction.cs b/src/DurableSDK/Actions/ExternalEventAction.cs similarity index 100% rename from src/Durable/Actions/ExternalEventAction.cs rename to src/DurableSDK/Actions/ExternalEventAction.cs diff --git a/src/Durable/Actions/OrchestrationAction.cs b/src/DurableSDK/Actions/OrchestrationAction.cs similarity index 100% rename from src/Durable/Actions/OrchestrationAction.cs rename to src/DurableSDK/Actions/OrchestrationAction.cs diff --git a/src/Durable/ActivityFailureException.cs b/src/DurableSDK/ActivityFailureException.cs similarity index 100% rename from src/Durable/ActivityFailureException.cs rename to src/DurableSDK/ActivityFailureException.cs diff --git a/src/Durable/Commands/InvokeDurableActivityCommand.cs b/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs similarity index 93% rename from src/Durable/Commands/InvokeDurableActivityCommand.cs rename to src/DurableSDK/Commands/InvokeDurableActivityCommand.cs index ba9b429f..4868b62c 100644 --- a/src/Durable/Commands/InvokeDurableActivityCommand.cs +++ b/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs @@ -43,10 +43,10 @@ protected override void EndProcessing() { var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey]; - var loadedFunctions = FunctionLoader.GetLoadedFunctions(); + // var loadedFunctions = FunctionLoader.GetLoadedFunctions(); var task = new ActivityInvocationTask(FunctionName, Input, RetryOptions); - ActivityInvocationTask.ValidateTask(task, loadedFunctions); + // ActivityInvocationTask.ValidateTask(task, loadedFunctions); _durableTaskHandler.StopAndInitiateDurableTaskOrReplay( task, context, NoWait.IsPresent, diff --git a/src/Durable/Commands/SetDurableCustomStatusCommand.cs b/src/DurableSDK/Commands/SetDurableCustomStatusCommand.cs similarity index 100% rename from src/Durable/Commands/SetDurableCustomStatusCommand.cs rename to src/DurableSDK/Commands/SetDurableCustomStatusCommand.cs diff --git a/src/Durable/Commands/SetFunctionInvocationContextCommand.cs b/src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs similarity index 100% rename from src/Durable/Commands/SetFunctionInvocationContextCommand.cs rename to src/DurableSDK/Commands/SetFunctionInvocationContextCommand.cs diff --git a/src/Durable/Commands/StartDurableExternalEventListenerCommand.cs b/src/DurableSDK/Commands/StartDurableExternalEventListenerCommand.cs similarity index 100% rename from src/Durable/Commands/StartDurableExternalEventListenerCommand.cs rename to src/DurableSDK/Commands/StartDurableExternalEventListenerCommand.cs diff --git a/src/Durable/Commands/StartDurableTimerCommand.cs b/src/DurableSDK/Commands/StartDurableTimerCommand.cs similarity index 100% rename from src/Durable/Commands/StartDurableTimerCommand.cs rename to src/DurableSDK/Commands/StartDurableTimerCommand.cs diff --git a/src/Durable/Commands/StopDurableTimerTaskCommand.cs b/src/DurableSDK/Commands/StopDurableTimerTaskCommand.cs similarity index 100% rename from src/Durable/Commands/StopDurableTimerTaskCommand.cs rename to src/DurableSDK/Commands/StopDurableTimerTaskCommand.cs diff --git a/src/Durable/Commands/WaitDurableTaskCommand.cs b/src/DurableSDK/Commands/WaitDurableTaskCommand.cs similarity index 100% rename from src/Durable/Commands/WaitDurableTaskCommand.cs rename to src/DurableSDK/Commands/WaitDurableTaskCommand.cs diff --git a/src/Durable/CurrentUtcDateTimeUpdater.cs b/src/DurableSDK/CurrentUtcDateTimeUpdater.cs similarity index 100% rename from src/Durable/CurrentUtcDateTimeUpdater.cs rename to src/DurableSDK/CurrentUtcDateTimeUpdater.cs diff --git a/src/Durable/DurableActivityErrorHandler.cs b/src/DurableSDK/DurableActivityErrorHandler.cs similarity index 100% rename from src/Durable/DurableActivityErrorHandler.cs rename to src/DurableSDK/DurableActivityErrorHandler.cs diff --git a/src/Durable/DurableTaskHandler.cs b/src/DurableSDK/DurableTaskHandler.cs similarity index 100% rename from src/Durable/DurableTaskHandler.cs rename to src/DurableSDK/DurableTaskHandler.cs diff --git a/src/Durable/HistoryEvent.cs b/src/DurableSDK/HistoryEvent.cs similarity index 100% rename from src/Durable/HistoryEvent.cs rename to src/DurableSDK/HistoryEvent.cs diff --git a/src/Durable/HistoryEventType.cs b/src/DurableSDK/HistoryEventType.cs similarity index 100% rename from src/Durable/HistoryEventType.cs rename to src/DurableSDK/HistoryEventType.cs diff --git a/src/Durable/IOrchestrationInvoker.cs b/src/DurableSDK/IOrchestrationInvoker.cs similarity index 100% rename from src/Durable/IOrchestrationInvoker.cs rename to src/DurableSDK/IOrchestrationInvoker.cs diff --git a/src/Durable/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs similarity index 100% rename from src/Durable/IPowerShellServices.cs rename to src/DurableSDK/IPowerShellServices.cs diff --git a/src/Durable/OrchestrationActionCollector.cs b/src/DurableSDK/OrchestrationActionCollector.cs similarity index 100% rename from src/Durable/OrchestrationActionCollector.cs rename to src/DurableSDK/OrchestrationActionCollector.cs diff --git a/src/Durable/OrchestrationBindingInfo.cs b/src/DurableSDK/OrchestrationBindingInfo.cs similarity index 100% rename from src/Durable/OrchestrationBindingInfo.cs rename to src/DurableSDK/OrchestrationBindingInfo.cs diff --git a/src/Durable/OrchestrationContext.cs b/src/DurableSDK/OrchestrationContext.cs similarity index 100% rename from src/Durable/OrchestrationContext.cs rename to src/DurableSDK/OrchestrationContext.cs diff --git a/src/Durable/OrchestrationFailureException.cs b/src/DurableSDK/OrchestrationFailureException.cs similarity index 100% rename from src/Durable/OrchestrationFailureException.cs rename to src/DurableSDK/OrchestrationFailureException.cs diff --git a/src/Durable/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs similarity index 86% rename from src/Durable/OrchestrationInvoker.cs rename to src/DurableSDK/OrchestrationInvoker.cs index fef557ba..b1a838bd 100644 --- a/src/Durable/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -31,6 +31,12 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe // Marks the first OrchestratorStarted event as processed orchestrationStart.IsProcessed = true; + // IDEA: + // This seems to be where the user-code is allowed to run. + // When using the new SDK, we'll want the user-code to send an `asyncResult` + // with a specific flag/signature that tells the worker to short-circuit + // its regular DF logic, and to return the value its been provided without further processing. + // All weed is to make the orchestrationBinding info viewable to the user-code. var asyncResult = pwsh.BeginInvoke(outputBuffer); var (shouldStop, actions) = diff --git a/src/Durable/OrchestrationMessage.cs b/src/DurableSDK/OrchestrationMessage.cs similarity index 100% rename from src/Durable/OrchestrationMessage.cs rename to src/DurableSDK/OrchestrationMessage.cs diff --git a/src/Durable/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs similarity index 100% rename from src/Durable/PowerShellServices.cs rename to src/DurableSDK/PowerShellServices.cs diff --git a/src/Durable/RetryOptions.cs b/src/DurableSDK/RetryOptions.cs similarity index 100% rename from src/Durable/RetryOptions.cs rename to src/DurableSDK/RetryOptions.cs diff --git a/src/Durable/RetryProcessor.cs b/src/DurableSDK/RetryProcessor.cs similarity index 100% rename from src/Durable/RetryProcessor.cs rename to src/DurableSDK/RetryProcessor.cs diff --git a/src/Durable/Tasks/ActivityInvocationTask.cs b/src/DurableSDK/Tasks/ActivityInvocationTask.cs similarity index 96% rename from src/Durable/Tasks/ActivityInvocationTask.cs rename to src/DurableSDK/Tasks/ActivityInvocationTask.cs index 87bf61c2..730b7727 100644 --- a/src/Durable/Tasks/ActivityInvocationTask.cs +++ b/src/DurableSDK/Tasks/ActivityInvocationTask.cs @@ -62,7 +62,7 @@ internal override OrchestrationAction CreateOrchestrationAction() : new CallActivityWithRetryAction(FunctionName, Input, RetryOptions); } - internal static void ValidateTask(ActivityInvocationTask task, IEnumerable loadedFunctions) + /*internal static void ValidateTask(ActivityInvocationTask task, IEnumerable loadedFunctions) { var functionInfo = loadedFunctions.FirstOrDefault(fi => fi.FuncName == task.FunctionName); if (functionInfo == null) @@ -79,6 +79,6 @@ internal static void ValidateTask(ActivityInvocationTask task, IEnumerable /// The main entry point for durable functions support. diff --git a/src/Durable/DurableFunctionInfo.cs b/src/DurableWorker/DurableFunctionInfo.cs similarity index 92% rename from src/Durable/DurableFunctionInfo.cs rename to src/DurableWorker/DurableFunctionInfo.cs index a245ac67..8665cfe8 100644 --- a/src/Durable/DurableFunctionInfo.cs +++ b/src/DurableWorker/DurableFunctionInfo.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +namespace Microsoft.Azure.Functions.PowerShellWorker.DurableWorker { internal class DurableFunctionInfo { diff --git a/src/Durable/DurableFunctionInfoFactory.cs b/src/DurableWorker/DurableFunctionInfoFactory.cs similarity index 96% rename from src/Durable/DurableFunctionInfoFactory.cs rename to src/DurableWorker/DurableFunctionInfoFactory.cs index 3e65b9f7..0cc08d27 100644 --- a/src/Durable/DurableFunctionInfoFactory.cs +++ b/src/DurableWorker/DurableFunctionInfoFactory.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +namespace Microsoft.Azure.Functions.PowerShellWorker.DurableWorker { using System.Linq; diff --git a/src/Durable/DurableFunctionType.cs b/src/DurableWorker/DurableFunctionType.cs similarity index 80% rename from src/Durable/DurableFunctionType.cs rename to src/DurableWorker/DurableFunctionType.cs index 73feb504..2ecacc17 100644 --- a/src/Durable/DurableFunctionType.cs +++ b/src/DurableWorker/DurableFunctionType.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.Azure.Functions.PowerShellWorker.Durable +namespace Microsoft.Azure.Functions.PowerShellWorker.DurableWorker { internal enum DurableFunctionType { diff --git a/src/FunctionInfo.cs b/src/FunctionInfo.cs index 2aea62ec..091eecde 100644 --- a/src/FunctionInfo.cs +++ b/src/FunctionInfo.cs @@ -15,7 +15,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker { - using Durable; + using DurableWorker; /// /// This type represents the metadata of an Azure PowerShell Function. diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index 6aabd3f3..93a1271f 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -13,7 +13,7 @@ using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement; -using Microsoft.Azure.Functions.PowerShellWorker.Durable; +using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; namespace Microsoft.Azure.Functions.PowerShellWorker diff --git a/test/E2E/TestFunctionApp/profile.ps1 b/test/E2E/TestFunctionApp/profile.ps1 new file mode 100644 index 00000000..9afdf1da --- /dev/null +++ b/test/E2E/TestFunctionApp/profile.ps1 @@ -0,0 +1,22 @@ +# Azure Functions profile.ps1 +# +# This profile.ps1 will get executed every "cold start" of your Function App. +# "cold start" occurs when: +# +# * A Function App starts up for the very first time +# * A Function App starts up after being de-allocated due to inactivity +# +# You can define helper functions, run commands, or specify environment variables +# NOTE: any variables defined that are not environment variables will get reset after the first execution + +# Authenticate with Azure PowerShell using MSI. +# Remove this if you are not planning on using MSI or Azure PowerShell. +if ($env:MSI_SECRET) { + Disable-AzContextAutosave -Scope Process | Out-Null + Connect-AzAccount -Identity +} + +# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. +# Enable-AzureRmAlias + +# You can also define functions or aliases that can be referenced in any of your PowerShell functions. \ No newline at end of file diff --git a/test/Unit/Durable/ActivityInvocationTaskTests.cs b/test/Unit/Durable/ActivityInvocationTaskTests.cs index c96cc755..f5083f88 100644 --- a/test/Unit/Durable/ActivityInvocationTaskTests.cs +++ b/test/Unit/Durable/ActivityInvocationTaskTests.cs @@ -163,7 +163,7 @@ public void StopAndInitiateDurableTaskOrReplay_OutputsActivityInvocationTask_Whe Assert.Equal(FunctionName, allOutput.Single().FunctionName); } - [Fact] + /*[Fact] public void ValidateTask_Throws_WhenActivityFunctionDoesNotExist() { var history = CreateHistory(scheduled: false, completed: false, failed: false, output: InvocationResultJson); @@ -187,9 +187,9 @@ public void ValidateTask_Throws_WhenActivityFunctionDoesNotExist() Assert.DoesNotContain(ActivityTriggerBindingType, exception.Message); DurableTestUtilities.VerifyNoActionAdded(orchestrationContext); - } + }*/ - [Theory] + /*[Theory] [InlineData("IncorrectBindingType", BindingInfo.Types.Direction.In)] [InlineData(ActivityTriggerBindingType, BindingInfo.Types.Direction.Out)] public void ValidateTask_Throws_WhenActivityFunctionHasNoProperBinding( @@ -214,7 +214,7 @@ public void ValidateTask_Throws_WhenActivityFunctionHasNoProperBinding( Assert.Contains(ActivityTriggerBindingType, exception.Message); DurableTestUtilities.VerifyNoActionAdded(orchestrationContext); - } + }*/ [Theory] [InlineData(false)] diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 68531f7c..5ec3244c 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test.Durable using System.Collections.ObjectModel; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; using Microsoft.Azure.Functions.PowerShellWorker.Durable; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Newtonsoft.Json; diff --git a/test/Unit/Durable/DurableFunctionInfoFactoryTests.cs b/test/Unit/Durable/DurableFunctionInfoFactoryTests.cs index 09cea266..2c898316 100644 --- a/test/Unit/Durable/DurableFunctionInfoFactoryTests.cs +++ b/test/Unit/Durable/DurableFunctionInfoFactoryTests.cs @@ -10,7 +10,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test.Durable using Xunit; - using Microsoft.Azure.Functions.PowerShellWorker.Durable; + using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; public class DurableFunctionInfoFactoryTests { diff --git a/test/Unit/PowerShell/PowerShellManagerTests.cs b/test/Unit/PowerShell/PowerShellManagerTests.cs index 5d4faf42..36588611 100644 --- a/test/Unit/PowerShell/PowerShellManagerTests.cs +++ b/test/Unit/PowerShell/PowerShellManagerTests.cs @@ -18,6 +18,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test using System.Collections.ObjectModel; using System.Management.Automation; using Microsoft.Azure.Functions.PowerShellWorker.Durable; + using Microsoft.Azure.Functions.PowerShellWorker.DurableWorker; using Newtonsoft.Json; internal class TestUtils From 9d73110b6a29ffdf8ce28934594d9fcf261644dc Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 28 Jan 2022 19:21:02 -0800 Subject: [PATCH 02/43] fix typo --- src/DurableSDK/OrchestrationInvoker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index b1a838bd..00fb2f13 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -36,7 +36,7 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe // When using the new SDK, we'll want the user-code to send an `asyncResult` // with a specific flag/signature that tells the worker to short-circuit // its regular DF logic, and to return the value its been provided without further processing. - // All weed is to make the orchestrationBinding info viewable to the user-code. + // All we need is to make the orchestrationBinding info viewable to the user-code. < This should be our next step var asyncResult = pwsh.BeginInvoke(outputBuffer); var (shouldStop, actions) = From 2fcbfae587ee97cd485f28718534ed54a84873f2 Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 28 Jan 2022 21:04:20 -0800 Subject: [PATCH 03/43] DurableSDK now compiles by itself --- src/DurableSDK/DurableTaskHandler.cs | 37 ++++++++-- src/DurableSDK/OrchestrationInvoker.cs | 16 ++++- src/DurableSDK/PowerShellExtensions.cs | 69 +++++++++++++++++++ src/DurableSDK/PowerShellServices.cs | 1 - .../Tasks/ActivityInvocationTask.cs | 2 +- 5 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 src/DurableSDK/PowerShellExtensions.cs diff --git a/src/DurableSDK/DurableTaskHandler.cs b/src/DurableSDK/DurableTaskHandler.cs index 3981b686..6c7ef73b 100644 --- a/src/DurableSDK/DurableTaskHandler.cs +++ b/src/DurableSDK/DurableTaskHandler.cs @@ -6,10 +6,13 @@ 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; + // using Utility; internal class DurableTaskHandler { @@ -76,7 +79,7 @@ public void StopAndInitiateDurableTaskOrReplay( retryOptions.MaxNumberOfAttempts, onSuccess: result => { - output(TypeExtensions.ConvertFromJson(result)); + output(ConvertFromJson(result)); }, onFailure); @@ -206,15 +209,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/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index 00fb2f13..cd275b79 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -11,7 +11,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Linq; using System.Management.Automation; - using PowerShellWorker.Utility; + // using PowerShellWorker.Utility; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions; internal class OrchestrationInvoker : IOrchestrationInvoker @@ -54,7 +54,7 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe { // The orchestration function completed pwsh.EndInvoke(asyncResult); - var result = FunctionReturnValueBuilder.CreateReturnValueFromFunctionOutput(outputBuffer); + var result = CreateReturnValueFromFunctionOutput(outputBuffer); return CreateOrchestrationResult(isDone: true, actions, output: result, context.CustomStatus); } catch (Exception e) @@ -71,6 +71,16 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe } } + public static object CreateReturnValueFromFunctionOutput(IList pipelineItems) + { + if (pipelineItems == null || pipelineItems.Count <= 0) + { + return null; + } + + return pipelineItems.Count == 1 ? pipelineItems[0] : pipelineItems.ToArray(); + } + private static Hashtable CreateOrchestrationResult( bool isDone, List> actions, @@ -78,7 +88,7 @@ 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 } }; } } } 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..b7f85d14 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -7,7 +7,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System; using System.Management.Automation; - using PowerShell; internal class PowerShellServices : IPowerShellServices { diff --git a/src/DurableSDK/Tasks/ActivityInvocationTask.cs b/src/DurableSDK/Tasks/ActivityInvocationTask.cs index 730b7727..627fc6a6 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 WebJobs.Script.Grpc.Messages; + // using WebJobs.Script.Grpc.Messages; using Microsoft.Azure.Functions.PowerShellWorker; using Microsoft.Azure.Functions.PowerShellWorker.Durable; From f43f350a853159ad6ddce6773ddc5be97a5530b8 Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 3 Feb 2022 15:35:09 -0800 Subject: [PATCH 04/43] Allow ExternalSDK to handle orchestration --- .../SetFunctionInvocationContextCommand.cs | 1 + src/DurableSDK/DurableTaskHandler.cs | 10 +++++++ src/DurableSDK/IOrchestrationInvoker.cs | 3 ++ src/DurableSDK/IPowerShellServices.cs | 1 + .../OrchestrationActionCollector.cs | 1 + src/DurableSDK/OrchestrationContext.cs | 13 ++++++-- src/DurableSDK/OrchestrationInvoker.cs | 30 +++++++++++++++---- src/DurableSDK/PowerShellServices.cs | 6 ++++ src/DurableWorker/DurableController.cs | 16 ++++++++++ ...soft.Azure.Functions.PowerShellWorker.psm1 | 2 +- src/PowerShell/PowerShellManager.cs | 2 +- 11 files changed, 75 insertions(+), 10 deletions(-) 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 6c7ef73b..68324483 100644 --- a/src/DurableSDK/DurableTaskHandler.cs +++ b/src/DurableSDK/DurableTaskHandler.cs @@ -50,6 +50,7 @@ public void StopAndInitiateDurableTaskOrReplay( } completedHistoryEvent.IsProcessed = true; + context.IsReplaying = completedHistoryEvent.IsPlayed; switch (completedHistoryEvent.EventType) { @@ -60,6 +61,13 @@ public void StopAndInitiateDurableTaskOrReplay( output(eventResult); } break; + case HistoryEventType.EventRaised: + var eventRaisedResult = GetEventResult(completedHistoryEvent); + if (eventRaisedResult != null) + { + output(eventRaisedResult); + } + break; case HistoryEventType.TaskFailed: if (retryOptions == null) @@ -129,6 +137,7 @@ public void WaitAll( var allTasksCompleted = completedEvents.Count == tasksToWaitFor.Count; if (allTasksCompleted) { + context.IsReplaying = completedEvents[0].IsPlayed; CurrentUtcDateTimeUpdater.UpdateCurrentUtcDateTime(context); foreach (var completedHistoryEvent in completedEvents) @@ -188,6 +197,7 @@ public void WaitAny( var anyTaskCompleted = completedTasks.Count > 0; if (anyTaskCompleted) { + context.IsReplaying = context.History[firstCompletedHistoryEventIndex].IsPlayed; CurrentUtcDateTimeUpdater.UpdateCurrentUtcDateTime(context); // Return a reference to the first completed task output(firstCompletedTask); diff --git a/src/DurableSDK/IOrchestrationInvoker.cs b/src/DurableSDK/IOrchestrationInvoker.cs index 7e80aba3..488266db 100644 --- a/src/DurableSDK/IOrchestrationInvoker.cs +++ b/src/DurableSDK/IOrchestrationInvoker.cs @@ -5,10 +5,13 @@ 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); } } diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index a8cf897b..f6fe3e59 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -10,6 +10,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal interface IPowerShellServices { + PowerShell GetPowerShell(); void SetDurableClient(object durableClient); void SetOrchestrationContext(OrchestrationContext orchestrationContext); 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/OrchestrationContext.cs b/src/DurableSDK/OrchestrationContext.cs index 0d11acee..6b051036 100644 --- a/src/DurableSDK/OrchestrationContext.cs +++ b/src/DurableSDK/OrchestrationContext.cs @@ -20,13 +20,13 @@ public class OrchestrationContext public object Input { get; internal set; } [DataMember] - internal string InstanceId { get; set; } + public string InstanceId { get; set; } [DataMember] internal string ParentInstanceId { get; set; } [DataMember] - internal bool IsReplaying { get; set; } + public bool IsReplaying { get; set; } [DataMember] internal HistoryEvent[] History { get; set; } @@ -35,6 +35,15 @@ public class OrchestrationContext internal OrchestrationActionCollector OrchestrationActionCollector { get; } = new OrchestrationActionCollector(); + internal object ExternalResult; + internal bool ExternalIsError; + + internal void SetExternalResult(object result, bool isError) + { + this.ExternalResult = result; + this.ExternalIsError = isError; + } + internal object CustomStatus { get; set; } } } diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index cd275b79..7ff387ea 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -16,6 +16,8 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal class OrchestrationInvoker : IOrchestrationInvoker { + private Action externalInvoker = null; + public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowerShellServices pwsh) { try @@ -30,13 +32,23 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe // Marks the first OrchestratorStarted event as processed orchestrationStart.IsProcessed = true; + + var useExternalSDK = externalInvoker != null; + if (useExternalSDK) + { + externalInvoker.Invoke(pwsh.GetPowerShell()); + var result = orchestrationBindingInfo.Context.ExternalResult; + var isError = orchestrationBindingInfo.Context.ExternalIsError; + if (isError) + { + throw (Exception)result; + } + else + { + return (Hashtable)result; + } + } - // IDEA: - // This seems to be where the user-code is allowed to run. - // When using the new SDK, we'll want the user-code to send an `asyncResult` - // with a specific flag/signature that tells the worker to short-circuit - // its regular DF logic, and to return the value its been provided without further processing. - // All we need is to make the orchestrationBinding info viewable to the user-code. < This should be our next step var asyncResult = pwsh.BeginInvoke(outputBuffer); var (shouldStop, actions) = @@ -46,6 +58,7 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe { // The orchestration function should be stopped and restarted pwsh.StopInvoke(); + // return (Hashtable)orchestrationBindingInfo.Context.OrchestrationActionCollector.output; return CreateOrchestrationResult(isDone: false, actions, output: null, context.CustomStatus); } else @@ -90,5 +103,10 @@ private static Hashtable CreateOrchestrationResult( var orchestrationMessage = new OrchestrationMessage(isDone, actions, output, customStatus); return new Hashtable { { "$return", orchestrationMessage } }; } + + public void SetExternalInvoker(Action externalInvoker) + { + this.externalInvoker = externalInvoker; + } } } diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index b7f85d14..8985eb92 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -21,12 +21,18 @@ public PowerShellServices(PowerShell pwsh) _pwsh = pwsh; } + public PowerShell GetPowerShell() + { + return this._pwsh; + } + public void SetDurableClient(object durableClient) { _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); + _hasSetOrchestrationContext = true; } diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index db50d20d..456506cc 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -29,6 +29,7 @@ internal class DurableController private readonly IPowerShellServices _powerShellServices; private readonly IOrchestrationInvoker _orchestrationInvoker; private OrchestrationBindingInfo _orchestrationBindingInfo; + private PowerShell pwsh; public DurableController( DurableFunctionInfo durableDurableFunctionInfo, @@ -38,6 +39,7 @@ public DurableController( new PowerShellServices(pwsh), new OrchestrationInvoker()) { + this.pwsh = pwsh; } internal DurableController( @@ -66,6 +68,20 @@ public void BeforeFunctionInvocation(IList inputData) { _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); + + // Bote: Cannot find the DurableSDK module here, somehow. + Collection output2 = this.pwsh.AddCommand("Get-Module") + .InvokeAndClearCommands(); + + var context = inputData[0]; + Collection> output = this.pwsh.AddCommand("Set-BindingData") + .AddParameter("Input", context.Data.String) + .AddParameter("SetResult", (Action)_orchestrationBindingInfo.Context.SetExternalResult) + .InvokeAndClearCommands>(); + if (output.Count() == 1) + { + this._orchestrationInvoker.SetExternalInvoker(output[0]); + } } } diff --git a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 index 1d4d00ad..a505f704 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 @@ -5,7 +5,7 @@ # Set aliases for cmdlets to export Set-Alias -Name Wait-ActivityFunction -Value Wait-DurableTask -Set-Alias -Name Invoke-ActivityFunction -Value Invoke-DurableActivity +# Set-Alias -Name Invoke-ActivityFunction -Value Invoke-DurableActivity Set-Alias -Name New-OrchestrationCheckStatusResponse -Value New-DurableOrchestrationCheckStatusResponse Set-Alias -Name Start-NewOrchestration -Value Start-DurableOrchestration diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 7e458914..168aeb26 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -204,7 +204,6 @@ public Hashtable InvokeFunction( FunctionInvocationPerformanceStopwatch stopwatch) { var outputBindings = FunctionMetadata.GetOutputBindingHashtable(_pwsh.Runspace.InstanceId); - var durableController = new DurableController(functionInfo.DurableFunctionInfo, _pwsh); try @@ -227,6 +226,7 @@ public Hashtable InvokeFunction( try { + return durableController.TryInvokeOrchestrationFunction(out var result) ? result : InvokeNonOrchestrationFunction(durableController, outputBindings); From 092e333d9569cfcd5446a4852f31a7f5573a9575 Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 3 Feb 2022 15:37:25 -0800 Subject: [PATCH 05/43] document next steps --- src/DurableWorker/DurableController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 456506cc..2fa1780e 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -143,6 +143,7 @@ private static OrchestrationBindingInfo CreateOrchestrationBindingInfo(IList(context.Data.String)); From 506e4f78564fe0b3d71bf1e7705c9bf522c17ce5 Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 3 Feb 2022 16:48:53 -0800 Subject: [PATCH 06/43] allow external SDK to set the user-code's input. Still need to refactor this logic for the worker to continue working with old SDK --- src/DurableWorker/DurableController.cs | 33 +++++++++++++++----------- src/PowerShell/PowerShellManager.cs | 14 ++++++++--- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 2fa1780e..4e32ea67 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -66,22 +66,27 @@ public void BeforeFunctionInvocation(IList inputData) } else if (_durableFunctionInfo.IsOrchestrationFunction) { - _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); - _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); - - // Bote: Cannot find the DurableSDK module here, somehow. - Collection output2 = this.pwsh.AddCommand("Get-Module") - .InvokeAndClearCommands(); - - var context = inputData[0]; - Collection> output = this.pwsh.AddCommand("Set-BindingData") - .AddParameter("Input", context.Data.String) - .AddParameter("SetResult", (Action)_orchestrationBindingInfo.Context.SetExternalResult) - .InvokeAndClearCommands>(); - if (output.Count() == 1) + try { - this._orchestrationInvoker.SetExternalInvoker(output[0]); + _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); + var context = inputData[0]; + Collection> output = this.pwsh.AddCommand("Set-BindingData") + .AddParameter("Input", context.Data.String) + .AddParameter("SetResult", (Action)_orchestrationBindingInfo.Context.SetExternalResult) + .InvokeAndClearCommands>(); + if (output.Count() == 1) + { + this._orchestrationInvoker.SetExternalInvoker(output[0]); + } + + _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); } + catch + { + _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); + _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); + } + } } diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 168aeb26..95feb796 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -208,6 +208,7 @@ public Hashtable InvokeFunction( try { + durableController.BeforeFunctionInvocation(inputData); AddEntryPointInvocationCommand(functionInfo); @@ -216,10 +217,11 @@ public Hashtable InvokeFunction( SetInputBindingParameterValues(functionInfo, inputData, durableController, triggerMetadata, traceContext, retryContext); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.InputBindingValuesReady); - if (!durableController.ShouldSuppressPipelineTraces()) + /* This has been moved to the DF SDK (although it should also be moved down within the worker) + * 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)); @@ -270,9 +272,15 @@ private void SetInputBindingParameterValues( { var bindingInfo = functionInfo.InputBindings[binding.Name]; valueToUse = Utils.TransformInBindingValueAsNeeded(paramInfo, bindingInfo, binding.Data.ToObject()); + _pwsh.AddParameter(binding.Name, valueToUse); + } + else + { + // move this further down in the worker + // _pwsh.AddParameter(binding.Name, valueToUse); + } - _pwsh.AddParameter(binding.Name, valueToUse); } } From 7a38ec8a10431a4e20151a3c484247e4d7459ddd Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 15 Feb 2022 16:30:05 -0800 Subject: [PATCH 07/43] add import module --- src/DurableWorker/DurableController.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 4e32ea67..664487e9 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -62,7 +62,20 @@ public void BeforeFunctionInvocation(IList inputData) inputData.First(item => item.Name == _durableFunctionInfo.DurableClientBindingName) .Data.ToObject(); + this.pwsh.AddCommand("Import-Module") + .AddParameter("Name", "DurableSDK") + .InvokeAndClearCommands>(); + + //this.pwsh.AddCommand("Import-Module") + // .AddParameter("Name", "C:\\Users\\dajusto\\source\\repos\\azure-functions-durable-powershell-private\\samples\\durableApp\\Modules\\DurableSDK\\bin\\Debug\\net6.0\\DurableSDK.dll") + // .InvokeAndClearCommands>(); + + //this.pwsh.AddCommand("Set-BindingData") + // .AddParameter("Input", "") + // .InvokeAndClearCommands>(); + _powerShellServices.SetDurableClient(durableClient); + } else if (_durableFunctionInfo.IsOrchestrationFunction) { From 9b2cf427734fe61616cfb2a09559483283d5adbe Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 15 Feb 2022 16:54:04 -0800 Subject: [PATCH 08/43] supress traces --- .../Commands/InvokeDurableActivityCommand.cs | 2 -- src/DurableSDK/DurableTaskHandler.cs | 1 - src/DurableSDK/OrchestrationInvoker.cs | 4 +++ .../Tasks/ActivityInvocationTask.cs | 25 ------------------- src/DurableWorker/DurableController.cs | 2 -- ...soft.Azure.Functions.PowerShellWorker.psm1 | 2 +- src/PowerShell/PowerShellManager.cs | 7 ------ 7 files changed, 5 insertions(+), 38 deletions(-) diff --git a/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs b/src/DurableSDK/Commands/InvokeDurableActivityCommand.cs index 4868b62c..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/DurableTaskHandler.cs b/src/DurableSDK/DurableTaskHandler.cs index 68324483..71a78f5b 100644 --- a/src/DurableSDK/DurableTaskHandler.cs +++ b/src/DurableSDK/DurableTaskHandler.cs @@ -12,7 +12,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable using System.Threading; using Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks; using Microsoft.PowerShell.Commands; - // using Utility; internal class DurableTaskHandler { diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index 7ff387ea..28fd3f68 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -20,6 +20,10 @@ internal class OrchestrationInvoker : IOrchestrationInvoker public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowerShellServices pwsh) { + + + pwsh.GetPowerShell().AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); + try { var outputBuffer = new PSDataCollection(); diff --git a/src/DurableSDK/Tasks/ActivityInvocationTask.cs b/src/DurableSDK/Tasks/ActivityInvocationTask.cs index 627fc6a6..30442083 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; @@ -61,24 +55,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 664487e9..b0e939bb 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -3,8 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -// TODO: Some of this functionality may need to go into the DurableSDK component. -// I expect this file will need to be split between DurableWorker and DurableSDK namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System; 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 a505f704..1d4d00ad 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 @@ -5,7 +5,7 @@ # Set aliases for cmdlets to export Set-Alias -Name Wait-ActivityFunction -Value Wait-DurableTask -# Set-Alias -Name Invoke-ActivityFunction -Value Invoke-DurableActivity +Set-Alias -Name Invoke-ActivityFunction -Value Invoke-DurableActivity Set-Alias -Name New-OrchestrationCheckStatusResponse -Value New-DurableOrchestrationCheckStatusResponse Set-Alias -Name Start-NewOrchestration -Value Start-DurableOrchestration diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 95feb796..7ce60e0e 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -217,18 +217,11 @@ public Hashtable InvokeFunction( SetInputBindingParameterValues(functionInfo, inputData, durableController, triggerMetadata, traceContext, retryContext); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.InputBindingValuesReady); - /* This has been moved to the DF SDK (although it should also be moved down within the worker) - * 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); From 47caee93b7e4af8b3ab236d6f009d0c6b2c264f3 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 15 Feb 2022 17:09:18 -0800 Subject: [PATCH 09/43] avoid nullptr --- src/DurableSDK/OrchestrationInvoker.cs | 2 +- .../Durable/ActivityInvocationTaskTests.cs | 53 ------------------- 2 files changed, 1 insertion(+), 54 deletions(-) diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index 28fd3f68..a8a881a8 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -22,7 +22,7 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe { - pwsh.GetPowerShell().AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); + pwsh.GetPowerShell()?.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); try { diff --git a/test/Unit/Durable/ActivityInvocationTaskTests.cs b/test/Unit/Durable/ActivityInvocationTaskTests.cs index f5083f88..65837321 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)] From 1010f974d621c1e99f658b0322551ee97e6be81e Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 16 Feb 2022 18:18:32 -0800 Subject: [PATCH 10/43] pass tests --- src/DurableSDK/DurableTaskHandler.cs | 2 +- src/DurableSDK/IPowerShellServices.cs | 6 +++ src/DurableSDK/OrchestrationInvoker.cs | 7 ++- src/DurableSDK/PowerShellServices.cs | 18 ++++++++ src/DurableWorker/DurableController.cs | 36 +++++----------- src/PowerShell/PowerShellManager.cs | 43 +++++++++++++------ test/Unit/Durable/DurableControllerTests.cs | 36 ++++------------ .../Unit/Durable/OrchestrationInvokerTests.cs | 4 ++ 8 files changed, 80 insertions(+), 72 deletions(-) diff --git a/src/DurableSDK/DurableTaskHandler.cs b/src/DurableSDK/DurableTaskHandler.cs index 71a78f5b..745ced86 100644 --- a/src/DurableSDK/DurableTaskHandler.cs +++ b/src/DurableSDK/DurableTaskHandler.cs @@ -136,7 +136,7 @@ public void WaitAll( var allTasksCompleted = completedEvents.Count == tasksToWaitFor.Count; if (allTasksCompleted) { - context.IsReplaying = completedEvents[0].IsPlayed; + context.IsReplaying = completedEvents.Count == 0 ? false : completedEvents[0].IsPlayed; CurrentUtcDateTimeUpdater.UpdateCurrentUtcDateTime(context); foreach (var completedHistoryEvent in completedEvents) diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index f6fe3e59..636c2b30 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -11,12 +11,18 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal interface IPowerShellServices { PowerShell GetPowerShell(); + + bool UsesExternalDurableSDK(); void SetDurableClient(object durableClient); void SetOrchestrationContext(OrchestrationContext orchestrationContext); void ClearOrchestrationContext(); + public void TracePipelineObject(); + public 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 a8a881a8..d2edd97e 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -21,9 +21,6 @@ internal class OrchestrationInvoker : IOrchestrationInvoker public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowerShellServices pwsh) { - - pwsh.GetPowerShell()?.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); - try { var outputBuffer = new PSDataCollection(); @@ -52,7 +49,9 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe return (Hashtable)result; } } - + pwsh.AddParameter(orchestrationBindingInfo.ParameterName, context); + pwsh.TracePipelineObject(); + var asyncResult = pwsh.BeginInvoke(outputBuffer); var (shouldStop, actions) = diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 8985eb92..9169cca2 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -26,6 +26,14 @@ public PowerShell GetPowerShell() return this._pwsh; } + public bool UsesExternalDurableSDK() + { + this._pwsh.AddCommand("Import-Module") + .AddParameter("Name", "DurableSDK") + .InvokeAndClearCommands>(); + return false; + } + public void SetDurableClient(object durableClient) { _pwsh.AddCommand(SetFunctionInvocationContextCommand) @@ -45,6 +53,11 @@ public void SetOrchestrationContext(OrchestrationContext orchestrationContext) _hasSetOrchestrationContext = true; } + public void AddParameter(string name, object value) + { + _pwsh.AddParameter(name, value); + } + public void ClearOrchestrationContext() { if (_hasSetOrchestrationContext) @@ -55,6 +68,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/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index b0e939bb..25866d71 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -50,7 +50,12 @@ internal DurableController( _orchestrationInvoker = orchestrationInvoker; } - public void BeforeFunctionInvocation(IList inputData) + public string GetOrchestrationParameterName() + { + return _orchestrationBindingInfo?.ParameterName; + } + + public void InitializeBindings(IList inputData) { // If the function is an orchestration client, then we set the DurableClient // in the module context for the 'Start-DurableOrchestration' function to use. @@ -60,18 +65,6 @@ public void BeforeFunctionInvocation(IList inputData) inputData.First(item => item.Name == _durableFunctionInfo.DurableClientBindingName) .Data.ToObject(); - this.pwsh.AddCommand("Import-Module") - .AddParameter("Name", "DurableSDK") - .InvokeAndClearCommands>(); - - //this.pwsh.AddCommand("Import-Module") - // .AddParameter("Name", "C:\\Users\\dajusto\\source\\repos\\azure-functions-durable-powershell-private\\samples\\durableApp\\Modules\\DurableSDK\\bin\\Debug\\net6.0\\DurableSDK.dll") - // .InvokeAndClearCommands>(); - - //this.pwsh.AddCommand("Set-BindingData") - // .AddParameter("Input", "") - // .InvokeAndClearCommands>(); - _powerShellServices.SetDurableClient(durableClient); } @@ -122,26 +115,17 @@ 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() diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 7ce60e0e..15b6d094 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,17 +205,17 @@ 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); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.InvokingFunctionCode); @@ -222,9 +223,20 @@ public Hashtable InvokeFunction( try { - return durableController.TryInvokeOrchestrationFunction(out var result) - ? result - : InvokeNonOrchestrationFunction(durableController, outputBindings); + if(functionInfo.DurableFunctionInfo.IsOrchestrationFunction) + { + return durableFunctionsUtils.InvokeOrchestrationFunction(); + } + else + { + var addPipelineOutput = functionInfo.DurableFunctionInfo.Type != DurableFunctionType.ActivityFunction; + if (addPipelineOutput) + { + _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); + } + return ExecuteUserCode(addPipelineOutput, outputBindings); + } + } catch (RuntimeException e) { @@ -243,7 +255,8 @@ public Hashtable InvokeFunction( } finally { - durableController.AfterFunctionInvocation(); + // TODO: determine if external SDK also needs this call + durableFunctionsUtils.AfterFunctionInvocation(); outputBindings.Clear(); ResetRunspace(); } @@ -252,7 +265,7 @@ public Hashtable InvokeFunction( private void SetInputBindingParameterValues( AzFunctionInfo functionInfo, IEnumerable inputData, - DurableController durableController, + string orchParamName, Hashtable triggerMetadata, TraceContext traceContext, RetryContext retryContext) @@ -261,10 +274,10 @@ 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); } else @@ -297,11 +310,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/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 5ec3244c..728a03a8 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -39,7 +39,7 @@ public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction( _mockPowerShellServices.Setup(_ => _.SetDurableClient(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData); _mockPowerShellServices.Verify( _ => _.SetDurableClient( @@ -60,7 +60,7 @@ public void BeforeFunctionInvocation_SetsOrchestrationContext_ForOrchestrationFu _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData); _mockPowerShellServices.Verify( _ => _.SetOrchestrationContext( @@ -74,7 +74,7 @@ public void BeforeFunctionInvocation_Throws_OnOrchestrationFunctionWithoutContex var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); var inputData = new ParameterBinding[0]; - Assert.ThrowsAny(() => durableController.BeforeFunctionInvocation(inputData)); + Assert.ThrowsAny(() => durableController.InitializeBindings(inputData)); } [Theory] @@ -91,7 +91,7 @@ internal void BeforeFunctionInvocation_DoesNothing_ForNonOrchestrationFunction(D CreateParameterBinding("ParameterName", orchestrationContext) }; - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData); } [Theory] @@ -121,7 +121,7 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame }; _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData); Assert.True(durableController.TryGetInputBindingParameterValue(contextParameterName, out var value)); Assert.Equal(orchestrationContext.InstanceId, ((OrchestrationContext)value).InstanceId); @@ -142,7 +142,7 @@ internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrat }; _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); + durableController.InitializeBindings(inputData); Assert.False(durableController.TryGetInputBindingParameterValue(contextParameterName, out var value)); Assert.Null(value); @@ -158,15 +158,14 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); - durableController.BeforeFunctionInvocation(inputData); + 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( @@ -178,25 +177,6 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() 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..167af701 100644 --- a/test/Unit/Durable/OrchestrationInvokerTests.cs +++ b/test/Unit/Durable/OrchestrationInvokerTests.cs @@ -40,6 +40,8 @@ public void InvocationRunsToCompletionIfNotStopped() _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.VerifyNoOtherCalls(); } @@ -51,6 +53,8 @@ public void InvocationStopsOnStopEvent() _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.VerifyNoOtherCalls(); } From d916d58ad70f1719fdf1868c257124ce27ee0f1f Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 17 Feb 2022 12:06:23 -0800 Subject: [PATCH 11/43] fix E2E tests --- src/DurableWorker/DurableController.cs | 6 +++--- src/PowerShell/PowerShellManager.cs | 13 +++---------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 25866d71..54311130 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -70,7 +70,7 @@ public void InitializeBindings(IList inputData) } else if (_durableFunctionInfo.IsOrchestrationFunction) { - try + /*try { _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); var context = inputData[0]; @@ -86,10 +86,10 @@ public void InitializeBindings(IList inputData) _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); } catch - { + {*/ _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); - } + //} } } diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 15b6d094..1f1b1e1e 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -229,12 +229,12 @@ public Hashtable InvokeFunction( } else { - var addPipelineOutput = functionInfo.DurableFunctionInfo.Type != DurableFunctionType.ActivityFunction; - if (addPipelineOutput) + var isActivityFunction = functionInfo.DurableFunctionInfo.Type == DurableFunctionType.ActivityFunction; + if (!isActivityFunction) { _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); } - return ExecuteUserCode(addPipelineOutput, outputBindings); + return ExecuteUserCode(isActivityFunction, outputBindings); } } @@ -280,13 +280,6 @@ private void SetInputBindingParameterValues( var valueToUse = Utils.TransformInBindingValueAsNeeded(paramInfo, bindingInfo, binding.Data.ToObject()); _pwsh.AddParameter(binding.Name, valueToUse); } - else - { - // move this further down in the worker - // _pwsh.AddParameter(binding.Name, valueToUse); - - } - } } From 946ea37badaf78c464f6cf716954ba876ff39f9f Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 17 Feb 2022 16:04:02 -0800 Subject: [PATCH 12/43] develop E2E tests --- src/DurableWorker/DurableController.cs | 5 ++ .../DurableEndToEndTests.cs | 52 +++++++++++++++++++ .../function.json | 24 +++++++++ .../run.ps1 | 10 ++++ .../function.json | 9 ++++ .../run.ps1 | 9 ++++ .../function.json | 9 ++++ .../DurableOrchestratorRaiseEvent/run.ps1 | 9 ++++ 8 files changed, 127 insertions(+) create mode 100644 test/E2E/TestFunctionApp/DurableClientOrchContextProperties/function.json create mode 100644 test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 create mode 100644 test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/function.json create mode 100644 test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 create mode 100644 test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/function.json create mode 100644 test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/run.ps1 diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 54311130..3ce54d24 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -57,6 +57,11 @@ public string GetOrchestrationParameterName() public void InitializeBindings(IList inputData) { + + this.pwsh.AddCommand("Import-Module") + .AddParameter("Name", "DurableSDK") + .InvokeAndClearCommands>(); + // If the function is an orchestration client, then we set the DurableClient // in the module context for the 'Start-DurableOrchestration' function to use. if (_durableFunctionInfo.IsDurableClient) diff --git a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs index ddc20c5e..c79a9228 100644 --- a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs +++ b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs @@ -147,6 +147,58 @@ public async Task LegacyDurableCommandNamesStillWork() } } + [Fact] + public async Task OrchestratationContextHasAllExpectedProperties() + { + var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClientOrchContextProperties", queryString: string.Empty); + Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode); + + var initialResponseBody = await initialResponse.Content.ReadAsStringAsync(); + dynamic initialResponseBodyObject = JsonConvert.DeserializeObject(initialResponseBody); + var statusQueryGetUri = (string)initialResponseBodyObject.statusQueryGetUri; + + var startTime = DateTime.UtcNow; + + using (var httpClient = new HttpClient()) + { + while (true) + { + var statusResponse = await httpClient.GetAsync(statusQueryGetUri); + switch (statusResponse.StatusCode) + { + case HttpStatusCode.Accepted: + { + var statusResponseBody = await GetResponseBodyAsync(statusResponse); + var runtimeStatus = (string)statusResponseBody.runtimeStatus; + Assert.True( + runtimeStatus == "Running" || runtimeStatus == "Pending", + $"Unexpected runtime status: {runtimeStatus}"); + + if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout) + { + Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}"); + } + + await Task.Delay(TimeSpan.FromSeconds(2)); + break; + } + + case HttpStatusCode.OK: + { + var statusResponseBody = await GetResponseBodyAsync(statusResponse); + Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus); + Assert.Equal("myInstanceId", statusResponseBody.output[0].ToString()); + return; + } + + default: + Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}"); + break; + } + } + } + } + [Fact] public async Task ActivityCanHaveQueueBinding() { diff --git a/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/function.json b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/function.json new file mode 100644 index 00000000..ce618d34 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/function.json @@ -0,0 +1,24 @@ +{ + "bindings": [ + { + "authLevel": "function", + "name": "Request", + "type": "httpTrigger", + "direction": "in", + "methods": [ + "post", + "get" + ] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + }, + { + "name": "starter", + "type": "durableClient", + "direction": "in" + } + ] +} \ No newline at end of file diff --git a/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 new file mode 100644 index 00000000..468e7e88 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 @@ -0,0 +1,10 @@ +using namespace System.Net + +param($Request, $TriggerMetadata) + +$FunctionName = $Request.Params.FunctionName +$InstanceId = Start-DurableOrchestration -FunctionName "DurableOrchestratorAccessContextProps" -InstanceId "myInstanceId" +Write-Host "Started orchestration with ID = '$InstanceId'" + +$Response = New-DurableOrchestrationCheckStatusResponse -Request $Request -InstanceId $InstanceId +Push-OutputBinding -Name Response -Value $Response diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/function.json b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/function.json new file mode 100644 index 00000000..336f5a18 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/function.json @@ -0,0 +1,9 @@ +{ + "bindings": [ + { + "name": "Context", + "type": "orchestrationTrigger", + "direction": "in" + } + ] +} \ No newline at end of file diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 new file mode 100644 index 00000000..f8eaa7fe --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 @@ -0,0 +1,9 @@ +param($Context) + +$output = @() + +$Context.InstanceId +$Context.IsReplaying +$output += Invoke-DurableActivity -FunctionName 'DurableActivity' -Input $Context.InstanceId +$Context.IsReplaying +$output diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/function.json b/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/function.json new file mode 100644 index 00000000..336f5a18 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/function.json @@ -0,0 +1,9 @@ +{ + "bindings": [ + { + "name": "Context", + "type": "orchestrationTrigger", + "direction": "in" + } + ] +} \ No newline at end of file diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/run.ps1 new file mode 100644 index 00000000..d4a75a7f --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/run.ps1 @@ -0,0 +1,9 @@ +param($Context) + +$output = @() + +$output += Invoke-DurableActivity -FunctionName 'Hello' -Input 'Tokyo' +$output += Invoke-DurableActivity -FunctionName 'Hello' -Input 'Seattle' +$output += Invoke-DurableActivity -FunctionName 'Hello' -Input 'London' + +$output From 64b6c49917a311e0d28d15bd466afbf3bb1fcd9d Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 17 Feb 2022 16:06:00 -0800 Subject: [PATCH 13/43] Enabled external durable client (#765) Co-authored-by: Michael Peng --- src/DurableSDK/PowerShellServices.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 9169cca2..e5359825 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -7,11 +7,14 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System; using System.Management.Automation; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; internal class PowerShellServices : IPowerShellServices { private const string SetFunctionInvocationContextCommand = "Microsoft.Azure.Functions.PowerShellWorker\\Set-FunctionInvocationContext"; + private const string SetFunctionInvocationContextExternalCommand = + "DurableSDK\\Set-FunctionInvocationContextExternal"; private readonly PowerShell _pwsh; private bool _hasSetOrchestrationContext = false; @@ -36,7 +39,9 @@ public bool UsesExternalDurableSDK() public void SetDurableClient(object durableClient) { - _pwsh.AddCommand(SetFunctionInvocationContextCommand) + _pwsh.AddCommand(Utils.ImportModuleCmdletInfo) + .AddParameter("Name", "DurableSDK"); + _pwsh.AddCommand(SetFunctionInvocationContextExternalCommand) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); @@ -46,7 +51,7 @@ public void SetDurableClient(object durableClient) public void SetOrchestrationContext(OrchestrationContext orchestrationContext) { - _pwsh.AddCommand(SetFunctionInvocationContextCommand) + _pwsh.AddCommand(SetFunctionInvocationContextExternalCommand) .AddParameter("OrchestrationContext", orchestrationContext) .InvokeAndClearCommands(); @@ -62,7 +67,7 @@ public void ClearOrchestrationContext() { if (_hasSetOrchestrationContext) { - _pwsh.AddCommand(SetFunctionInvocationContextCommand) + _pwsh.AddCommand(SetFunctionInvocationContextExternalCommand) .AddParameter("Clear", true) .InvokeAndClearCommands(); } From 9e7cf96724cf0711e43ca3ce0b9c56b7d639122a Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 17 Feb 2022 16:32:55 -0800 Subject: [PATCH 14/43] bindings work --- src/DurableSDK/PowerShellServices.cs | 4 ++-- src/DurableWorker/DurableController.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index e5359825..8837323a 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -51,7 +51,7 @@ public void SetDurableClient(object durableClient) public void SetOrchestrationContext(OrchestrationContext orchestrationContext) { - _pwsh.AddCommand(SetFunctionInvocationContextExternalCommand) + _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("OrchestrationContext", orchestrationContext) .InvokeAndClearCommands(); @@ -67,7 +67,7 @@ public void ClearOrchestrationContext() { if (_hasSetOrchestrationContext) { - _pwsh.AddCommand(SetFunctionInvocationContextExternalCommand) + _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("Clear", true) .InvokeAndClearCommands(); } diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 3ce54d24..56fbf791 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -75,7 +75,7 @@ public void InitializeBindings(IList inputData) } else if (_durableFunctionInfo.IsOrchestrationFunction) { - /*try + try { _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); var context = inputData[0]; @@ -91,10 +91,10 @@ public void InitializeBindings(IList inputData) _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); } catch - {*/ + { _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); - //} + } } } From 92f113ef9edffc305c3c8c1d28c45a2f171c5404 Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 17 Feb 2022 17:09:57 -0800 Subject: [PATCH 15/43] conditional binding intialization --- src/DurableSDK/IPowerShellServices.cs | 3 +- src/DurableSDK/PowerShellServices.cs | 64 ++++++++++++++++++++------ src/DurableWorker/DurableController.cs | 22 ++------- 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index 636c2b30..d01c2a4f 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -5,6 +5,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { + using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using System; using System.Management.Automation; @@ -15,7 +16,7 @@ internal interface IPowerShellServices bool UsesExternalDurableSDK(); void SetDurableClient(object durableClient); - void SetOrchestrationContext(OrchestrationContext orchestrationContext); + void SetOrchestrationContext(ParameterBinding orchestrationContext, out Action externalInvoker); void ClearOrchestrationContext(); diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 8837323a..e37cc8dc 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -6,8 +6,12 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System; + using System.Collections.ObjectModel; + using System.Linq; using System.Management.Automation; using Microsoft.Azure.Functions.PowerShellWorker.Utility; + using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + using Newtonsoft.Json; internal class PowerShellServices : IPowerShellServices { @@ -31,29 +35,57 @@ public PowerShell GetPowerShell() public bool UsesExternalDurableSDK() { - this._pwsh.AddCommand("Import-Module") - .AddParameter("Name", "DurableSDK") - .InvokeAndClearCommands>(); - return false; + try + { + this._pwsh.AddCommand("Import-Module") + .AddParameter("Name", "DurableSDK") + .InvokeAndClearCommands>(); + return true; + } + catch + { + return false; + } } public void SetDurableClient(object durableClient) { - _pwsh.AddCommand(Utils.ImportModuleCmdletInfo) - .AddParameter("Name", "DurableSDK"); - _pwsh.AddCommand(SetFunctionInvocationContextExternalCommand) + var cmdlet = SetFunctionInvocationContextCommand; + if (UsesExternalDurableSDK()) + { + cmdlet = SetFunctionInvocationContextExternalCommand; + } + + _pwsh.AddCommand(cmdlet) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); - _hasSetOrchestrationContext = true; } - public void SetOrchestrationContext(OrchestrationContext orchestrationContext) + public void SetOrchestrationContext(ParameterBinding context, out Action externalInvoker) { - _pwsh.AddCommand(SetFunctionInvocationContextCommand) - .AddParameter("OrchestrationContext", orchestrationContext) - .InvokeAndClearCommands(); + externalInvoker = null; + var cmdlet = SetFunctionInvocationContextCommand; + object arg = context.Data.String; + if (UsesExternalDurableSDK()) + { + cmdlet = SetFunctionInvocationContextExternalCommand; + } + else + { + arg = new OrchestrationBindingInfo( + context.Name, + JsonConvert.DeserializeObject(context.Data.String)); + } + + Collection> output = _pwsh.AddCommand(cmdlet) + .AddParameter("OrchestrationContext", arg) + .InvokeAndClearCommands>(); + if (output.Count() == 1) + { + externalInvoker = output[0]; + } _hasSetOrchestrationContext = true; } @@ -65,9 +97,15 @@ public void AddParameter(string name, object value) public void ClearOrchestrationContext() { + var cmdlet = SetFunctionInvocationContextCommand; + if (UsesExternalDurableSDK()) + { + cmdlet = SetFunctionInvocationContextExternalCommand; + } + if (_hasSetOrchestrationContext) { - _pwsh.AddCommand(SetFunctionInvocationContextCommand) + _pwsh.AddCommand(cmdlet) .AddParameter("Clear", true) .InvokeAndClearCommands(); } diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 56fbf791..63434da9 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -75,25 +75,11 @@ public void InitializeBindings(IList inputData) } else if (_durableFunctionInfo.IsOrchestrationFunction) { - try + var contextBindingData = inputData[0]; + _powerShellServices.SetOrchestrationContext(contextBindingData, out var externalInvoker); + if (externalInvoker != null) { - _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); - var context = inputData[0]; - Collection> output = this.pwsh.AddCommand("Set-BindingData") - .AddParameter("Input", context.Data.String) - .AddParameter("SetResult", (Action)_orchestrationBindingInfo.Context.SetExternalResult) - .InvokeAndClearCommands>(); - if (output.Count() == 1) - { - this._orchestrationInvoker.SetExternalInvoker(output[0]); - } - - _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); - } - catch - { - _orchestrationBindingInfo = CreateOrchestrationBindingInfo(inputData); - _powerShellServices.SetOrchestrationContext(_orchestrationBindingInfo.Context); + this._orchestrationInvoker.SetExternalInvoker(externalInvoker); } } From e5843ce251ac9c7dc4f02324189f493ed5d1d3a5 Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 17 Feb 2022 18:53:08 -0800 Subject: [PATCH 16/43] conditional import --- src/DurableSDK/IPowerShellServices.cs | 2 +- src/DurableSDK/OrchestrationInvoker.cs | 24 +++++++------- src/DurableSDK/PowerShellServices.cs | 35 ++++++++++++--------- src/DurableWorker/DurableController.cs | 6 +--- test/Unit/Durable/DurableControllerTests.cs | 14 ++++++--- 5 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index d01c2a4f..5eaa1b4a 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -16,7 +16,7 @@ internal interface IPowerShellServices bool UsesExternalDurableSDK(); void SetDurableClient(object durableClient); - void SetOrchestrationContext(ParameterBinding orchestrationContext, out Action externalInvoker); + OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding orchestrationContext, out Action externalInvoker); void ClearOrchestrationContext(); diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index d2edd97e..46178b13 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -23,17 +23,6 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe try { - 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(); - - // Marks the first OrchestratorStarted event as processed - orchestrationStart.IsProcessed = true; - var useExternalSDK = externalInvoker != null; if (useExternalSDK) { @@ -49,6 +38,19 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe return (Hashtable)result; } } + + 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(); + + // Marks the first OrchestratorStarted event as processed + orchestrationStart.IsProcessed = true; + + pwsh.AddParameter(orchestrationBindingInfo.ParameterName, context); pwsh.TracePipelineObject(); diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index e37cc8dc..7f62e420 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -39,8 +39,9 @@ public bool UsesExternalDurableSDK() { this._pwsh.AddCommand("Import-Module") .AddParameter("Name", "DurableSDK") + .AddParameter("ErrorAction", ActionPreference.Stop) .InvokeAndClearCommands>(); - return true; + return true; } catch { @@ -63,31 +64,35 @@ public void SetDurableClient(object durableClient) _hasSetOrchestrationContext = true; } - public void SetOrchestrationContext(ParameterBinding context, out Action externalInvoker) + public OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context, out Action externalInvoker) { externalInvoker = null; - var cmdlet = SetFunctionInvocationContextCommand; - object arg = context.Data.String; + var orchBindingInfo = new OrchestrationBindingInfo( + context.Name, + JsonConvert.DeserializeObject(context.Data.String)); + if (UsesExternalDurableSDK()) { - cmdlet = SetFunctionInvocationContextExternalCommand; + Collection> output = _pwsh.AddCommand(SetFunctionInvocationContextExternalCommand) + .AddParameter("OrchestrationContext", context.Data.String) + .AddParameter("SetResult", (Action) orchBindingInfo.Context.SetExternalResult) + .InvokeAndClearCommands>(); + if (output.Count() == 1) + { + externalInvoker = output[0]; + } } else { - arg = new OrchestrationBindingInfo( - context.Name, - JsonConvert.DeserializeObject(context.Data.String)); + _pwsh.AddCommand(SetFunctionInvocationContextCommand) + .AddParameter("OrchestrationContext", orchBindingInfo.Context) + .InvokeAndClearCommands>(); } - Collection> output = _pwsh.AddCommand(cmdlet) - .AddParameter("OrchestrationContext", arg) - .InvokeAndClearCommands>(); - if (output.Count() == 1) - { - externalInvoker = output[0]; - } + _hasSetOrchestrationContext = true; + return orchBindingInfo; } public void AddParameter(string name, object value) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 63434da9..42599ab0 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -58,10 +58,6 @@ public string GetOrchestrationParameterName() public void InitializeBindings(IList inputData) { - this.pwsh.AddCommand("Import-Module") - .AddParameter("Name", "DurableSDK") - .InvokeAndClearCommands>(); - // If the function is an orchestration client, then we set the DurableClient // in the module context for the 'Start-DurableOrchestration' function to use. if (_durableFunctionInfo.IsDurableClient) @@ -76,7 +72,7 @@ public void InitializeBindings(IList inputData) else if (_durableFunctionInfo.IsOrchestrationFunction) { var contextBindingData = inputData[0]; - _powerShellServices.SetOrchestrationContext(contextBindingData, out var externalInvoker); + _orchestrationBindingInfo = _powerShellServices.SetOrchestrationContext(contextBindingData, out var externalInvoker); if (externalInvoker != null) { this._orchestrationInvoker.SetExternalInvoker(externalInvoker); diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 728a03a8..97e5117b 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -58,13 +58,14 @@ public void BeforeFunctionInvocation_SetsOrchestrationContext_ForOrchestrationFu CreateParameterBinding("ParameterName", orchestrationContext) }; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); + Action externalInvoker; + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)); durableController.InitializeBindings(inputData); _mockPowerShellServices.Verify( _ => _.SetOrchestrationContext( - It.Is(c => c.InstanceId == orchestrationContext.InstanceId)), + It.Is(c => c.Data.ToString().Contains(orchestrationContext.InstanceId)), out externalInvoker), Times.Once); } @@ -120,7 +121,8 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame CreateParameterBinding(contextParameterName, orchestrationContext) }; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); + Action externalInvoker; + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)); durableController.InitializeBindings(inputData); Assert.True(durableController.TryGetInputBindingParameterValue(contextParameterName, out var value)); @@ -141,7 +143,8 @@ internal void TryGetInputBindingParameterValue_RetrievesNothing_ForNonOrchestrat CreateParameterBinding(contextParameterName, orchestrationContext) }; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); + Action externalInvoker; + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)); durableController.InitializeBindings(inputData); Assert.False(durableController.TryGetInputBindingParameterValue(contextParameterName, out var value)); @@ -157,7 +160,8 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny())); + Action externalInvoker; + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)); durableController.InitializeBindings(inputData); var expectedResult = new Hashtable(); From 2ee6b967b10994ff1a7464d41f2b741ebd55da97 Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Fri, 25 Feb 2022 15:37:45 -0800 Subject: [PATCH 17/43] Added exception handling logic --- src/DurableSDK/IPowerShellServices.cs | 3 +- src/DurableSDK/OrchestrationInvoker.cs | 3 +- src/DurableSDK/PowerShellServices.cs | 82 ++++++++++++++---------- src/DurableWorker/DurableController.cs | 7 +- src/DurableWorker/DurableFunctionInfo.cs | 3 + src/PowerShell/PowerShellManager.cs | 13 ++-- src/Utility/Utils.cs | 1 + 7 files changed, 61 insertions(+), 51 deletions(-) diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index 5eaa1b4a..ebe24fab 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -13,7 +13,8 @@ internal interface IPowerShellServices { PowerShell GetPowerShell(); - bool UsesExternalDurableSDK(); + bool UseExternalDurableSDK(); + void SetDurableClient(object durableClient); OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding orchestrationContext, out Action externalInvoker); diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index 46178b13..d01184a6 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -23,8 +23,7 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe try { - var useExternalSDK = externalInvoker != null; - if (useExternalSDK) + if (pwsh.UseExternalDurableSDK()) { externalInvoker.Invoke(pwsh.GetPowerShell()); var result = orchestrationBindingInfo.Context.ExternalResult; diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 7f62e420..e4337839 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -15,49 +15,67 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal class PowerShellServices : IPowerShellServices { - private const string SetFunctionInvocationContextCommand = - "Microsoft.Azure.Functions.PowerShellWorker\\Set-FunctionInvocationContext"; - private const string SetFunctionInvocationContextExternalCommand = - "DurableSDK\\Set-FunctionInvocationContextExternal"; + 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 readonly bool _useExternalDurableSDK = false; public PowerShellServices(PowerShell pwsh) { + // Attempt to import the external SDK + 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) + { + // throw new Exception(errorRecords); + // InvalidOperationException + throw e; + } + _useExternalDurableSDK = false; + } + + if (_useExternalDurableSDK) + { + SetFunctionInvocationContextCommand = $"{ExternalDurableSDKName}\\Set-FunctionInvocationContext"; + } + else + { + SetFunctionInvocationContextCommand = $"{InternalDurableSDKName}\\Set-FunctionInvocationContext"; + } _pwsh = pwsh; } - public PowerShell GetPowerShell() + public bool UseExternalDurableSDK() { - return this._pwsh; + return _useExternalDurableSDK; } - public bool UsesExternalDurableSDK() + public PowerShell GetPowerShell() { - try - { - this._pwsh.AddCommand("Import-Module") - .AddParameter("Name", "DurableSDK") - .AddParameter("ErrorAction", ActionPreference.Stop) - .InvokeAndClearCommands>(); - return true; - } - catch - { - return false; - } + return this._pwsh; } public void SetDurableClient(object durableClient) { - var cmdlet = SetFunctionInvocationContextCommand; - if (UsesExternalDurableSDK()) - { - cmdlet = SetFunctionInvocationContextExternalCommand; - } - - _pwsh.AddCommand(cmdlet) + _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); @@ -71,9 +89,9 @@ public OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context context.Name, JsonConvert.DeserializeObject(context.Data.String)); - if (UsesExternalDurableSDK()) + if (_useExternalDurableSDK) { - Collection> output = _pwsh.AddCommand(SetFunctionInvocationContextExternalCommand) + Collection> output = _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("OrchestrationContext", context.Data.String) .AddParameter("SetResult", (Action) orchBindingInfo.Context.SetExternalResult) .InvokeAndClearCommands>(); @@ -102,15 +120,9 @@ public void AddParameter(string name, object value) public void ClearOrchestrationContext() { - var cmdlet = SetFunctionInvocationContextCommand; - if (UsesExternalDurableSDK()) - { - cmdlet = SetFunctionInvocationContextExternalCommand; - } - if (_hasSetOrchestrationContext) { - _pwsh.AddCommand(cmdlet) + _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("Clear", true) .InvokeAndClearCommands(); } diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 42599ab0..43b5538c 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; @@ -27,7 +26,6 @@ internal class DurableController private readonly IPowerShellServices _powerShellServices; private readonly IOrchestrationInvoker _orchestrationInvoker; private OrchestrationBindingInfo _orchestrationBindingInfo; - private PowerShell pwsh; public DurableController( DurableFunctionInfo durableDurableFunctionInfo, @@ -37,7 +35,6 @@ public DurableController( new PowerShellServices(pwsh), new OrchestrationInvoker()) { - this.pwsh = pwsh; } internal DurableController( @@ -57,8 +54,7 @@ public string GetOrchestrationParameterName() public void InitializeBindings(IList inputData) { - - // If the function is an orchestration client, then we set the DurableClient + // 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) { @@ -77,7 +73,6 @@ public void InitializeBindings(IList inputData) { this._orchestrationInvoker.SetExternalInvoker(externalInvoker); } - } } 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/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 1f1b1e1e..c5ad60d3 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -205,16 +205,16 @@ public Hashtable InvokeFunction( FunctionInvocationPerformanceStopwatch stopwatch) { var outputBindings = FunctionMetadata.GetOutputBindingHashtable(_pwsh.Runspace.InstanceId); - var durableFunctionsUtils = new DurableController(functionInfo.DurableFunctionInfo, _pwsh); + var durableController = new DurableController(functionInfo.DurableFunctionInfo, _pwsh); try { - durableFunctionsUtils.InitializeBindings(inputData); + durableController.InitializeBindings(inputData); AddEntryPointInvocationCommand(functionInfo); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.FunctionCodeReady); - var orchestrationParamName = durableFunctionsUtils.GetOrchestrationParameterName(); + var orchestrationParamName = durableController.GetOrchestrationParameterName(); SetInputBindingParameterValues(functionInfo, inputData, orchestrationParamName, triggerMetadata, traceContext, retryContext); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.InputBindingValuesReady); @@ -225,18 +225,17 @@ public Hashtable InvokeFunction( { if(functionInfo.DurableFunctionInfo.IsOrchestrationFunction) { - return durableFunctionsUtils.InvokeOrchestrationFunction(); + return durableController.InvokeOrchestrationFunction(); } else { - var isActivityFunction = functionInfo.DurableFunctionInfo.Type == DurableFunctionType.ActivityFunction; + var isActivityFunction = functionInfo.DurableFunctionInfo.IsActivityFunction; if (!isActivityFunction) { _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Trace-PipelineObject"); } return ExecuteUserCode(isActivityFunction, outputBindings); } - } catch (RuntimeException e) { @@ -256,7 +255,7 @@ public Hashtable InvokeFunction( finally { // TODO: determine if external SDK also needs this call - durableFunctionsUtils.AfterFunctionInvocation(); + durableController.AfterFunctionInvocation(); outputBindings.Clear(); ResetRunspace(); } 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)); From ae28f6ea7392982e99762f6080f010e7d7ee05ab Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Mon, 28 Feb 2022 14:27:33 -0800 Subject: [PATCH 18/43] Ensure unit tests are functioning properly --- src/DurableSDK/PowerShellServices.cs | 2 -- test/Unit/Durable/DurableControllerTests.cs | 15 +++++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 7f62e420..d4415f61 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -89,8 +89,6 @@ public OrchestrationBindingInfo SetOrchestrationContext(ParameterBinding context .InvokeAndClearCommands>(); } - - _hasSetOrchestrationContext = true; return orchBindingInfo; } diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 97e5117b..1689cab3 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -24,6 +24,7 @@ public class DurableControllerTests private readonly Mock _mockPowerShellServices = new Mock(MockBehavior.Strict); private readonly Mock _mockOrchestrationInvoker = new Mock(MockBehavior.Strict); + // TODO: change the name of the method being tested to InitializeBindings [Fact] public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction() { @@ -47,19 +48,21 @@ public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction( Times.Once); } + // TODO: change the name of the method being tested to InitializeBindings [Fact] public void BeforeFunctionInvocation_SetsOrchestrationContext_ForOrchestrationFunction() { var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); - + const string contextParameterName = "ParameterName"; var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; + var orchestrationBindingInfo = new OrchestrationBindingInfo(contextParameterName, orchestrationContext); var inputData = new[] { CreateParameterBinding("ParameterName", orchestrationContext) }; Action externalInvoker; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)); + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)).Returns(orchestrationBindingInfo); durableController.InitializeBindings(inputData); @@ -69,6 +72,7 @@ public void BeforeFunctionInvocation_SetsOrchestrationContext_ForOrchestrationFu Times.Once); } + // TODO: change the name of the method being tested to InitializeBindings [Fact] public void BeforeFunctionInvocation_Throws_OnOrchestrationFunctionWithoutContextParameter() { @@ -78,6 +82,7 @@ public void BeforeFunctionInvocation_Throws_OnOrchestrationFunctionWithoutContex Assert.ThrowsAny(() => durableController.InitializeBindings(inputData)); } + // TODO: change the name of the method being tested to InitializeBindings [Theory] [InlineData(DurableFunctionType.None)] [InlineData(DurableFunctionType.ActivityFunction)] @@ -116,13 +121,14 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; const string contextParameterName = "ParameterName"; + var orchestrationBindingInfo = new OrchestrationBindingInfo(contextParameterName, orchestrationContext); var inputData = new[] { CreateParameterBinding(contextParameterName, orchestrationContext) }; Action externalInvoker; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)); + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)).Returns(orchestrationBindingInfo); durableController.InitializeBindings(inputData); Assert.True(durableController.TryGetInputBindingParameterValue(contextParameterName, out var value)); @@ -157,11 +163,12 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() var contextParameterName = "ParameterName"; var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; var inputData = new[] { CreateParameterBinding(contextParameterName, orchestrationContext) }; + var orchestrationBindingInfo = new OrchestrationBindingInfo(contextParameterName, orchestrationContext); var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); Action externalInvoker; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)); + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)).Returns(orchestrationBindingInfo); durableController.InitializeBindings(inputData); var expectedResult = new Hashtable(); From 94f995d2e6cc308982afb9ab9d1f45dafc57f49d Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Mon, 28 Feb 2022 14:31:36 -0800 Subject: [PATCH 19/43] Corrected unit test names --- test/Unit/Durable/DurableControllerTests.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index 1689cab3..e6da6111 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -24,9 +24,8 @@ public class DurableControllerTests private readonly Mock _mockPowerShellServices = new Mock(MockBehavior.Strict); private readonly Mock _mockOrchestrationInvoker = new Mock(MockBehavior.Strict); - // TODO: change the name of the method being tested to InitializeBindings [Fact] - public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction() + public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() { var durableController = CreateDurableController(DurableFunctionType.None, "DurableClientBindingName"); @@ -48,9 +47,8 @@ public void BeforeFunctionInvocation_SetsDurableClient_ForDurableClientFunction( Times.Once); } - // TODO: change the name of the method being tested to InitializeBindings [Fact] - public void BeforeFunctionInvocation_SetsOrchestrationContext_ForOrchestrationFunction() + public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction() { var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); const string contextParameterName = "ParameterName"; @@ -72,9 +70,8 @@ public void BeforeFunctionInvocation_SetsOrchestrationContext_ForOrchestrationFu Times.Once); } - // TODO: change the name of the method being tested to InitializeBindings [Fact] - public void BeforeFunctionInvocation_Throws_OnOrchestrationFunctionWithoutContextParameter() + public void InitializeBindings_Throws_OnOrchestrationFunctionWithoutContextParameter() { var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); var inputData = new ParameterBinding[0]; @@ -82,11 +79,10 @@ public void BeforeFunctionInvocation_Throws_OnOrchestrationFunctionWithoutContex Assert.ThrowsAny(() => durableController.InitializeBindings(inputData)); } - // TODO: change the name of the method being tested to InitializeBindings [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() }; From 96f6ee7af621c8bd4eea065e6e95aad8f1878161 Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Mon, 28 Feb 2022 14:42:04 -0800 Subject: [PATCH 20/43] Turned repeated variables in unit tests into static members --- test/Unit/Durable/DurableControllerTests.cs | 50 +++++++++------------ 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index e6da6111..0e7cff64 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -23,6 +23,9 @@ 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 InitializeBindings_SetsDurableClient_ForDurableClientFunction() @@ -51,22 +54,20 @@ public void InitializeBindings_SetsDurableClient_ForDurableClientFunction() public void InitializeBindings_SetsOrchestrationContext_ForOrchestrationFunction() { var durableController = CreateDurableController(DurableFunctionType.OrchestrationFunction); - const string contextParameterName = "ParameterName"; - var orchestrationContext = new OrchestrationContext { InstanceId = Guid.NewGuid().ToString() }; - var orchestrationBindingInfo = new OrchestrationBindingInfo(contextParameterName, orchestrationContext); var inputData = new[] { - CreateParameterBinding("ParameterName", orchestrationContext) + CreateParameterBinding("ParameterName", _orchestrationContext) }; Action externalInvoker; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)).Returns(orchestrationBindingInfo); + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)) + .Returns(_orchestrationBindingInfo); 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 externalInvoker), Times.Once); } @@ -85,12 +86,10 @@ public void InitializeBindings_Throws_OnOrchestrationFunctionWithoutContextParam 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.InitializeBindings(inputData); @@ -114,21 +113,18 @@ 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 orchestrationBindingInfo = new OrchestrationBindingInfo(contextParameterName, orchestrationContext); var inputData = new[] { - CreateParameterBinding(contextParameterName, orchestrationContext) + CreateParameterBinding(_contextParameterName, _orchestrationContext) }; Action externalInvoker; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)).Returns(orchestrationBindingInfo); + _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)) + .Returns(_orchestrationBindingInfo); durableController.InitializeBindings(inputData); - Assert.True(durableController.TryGetInputBindingParameterValue(contextParameterName, out var value)); - Assert.Equal(orchestrationContext.InstanceId, ((OrchestrationContext)value).InstanceId); + Assert.True(durableController.TryGetInputBindingParameterValue(_contextParameterName, out var value)); + Assert.Equal(_orchestrationContext.InstanceId, ((OrchestrationContext)value).InstanceId); } [Theory] @@ -137,34 +133,28 @@ 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) }; Action externalInvoker; _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext(It.IsAny(), out externalInvoker)); 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 orchestrationBindingInfo = new OrchestrationBindingInfo(contextParameterName, orchestrationContext); - + 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 externalInvoker)) + .Returns(_orchestrationBindingInfo); durableController.InitializeBindings(inputData); var expectedResult = new Hashtable(); @@ -178,8 +168,8 @@ public void TryInvokeOrchestrationFunction_InvokesOrchestrationFunction() _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); } From 1cb30b86fa76bb24b682e6d6d965f8dd19b2fbe8 Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Tue, 1 Mar 2022 15:50:05 -0800 Subject: [PATCH 21/43] Revert durableController name to durableFunctionsUtils --- src/PowerShell/PowerShellManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index c5ad60d3..29c0e794 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -205,16 +205,16 @@ 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.InitializeBindings(inputData); + durableFunctionsUtils.InitializeBindings(inputData); AddEntryPointInvocationCommand(functionInfo); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.FunctionCodeReady); - var orchestrationParamName = durableController.GetOrchestrationParameterName(); + var orchestrationParamName = durableFunctionsUtils.GetOrchestrationParameterName(); SetInputBindingParameterValues(functionInfo, inputData, orchestrationParamName, triggerMetadata, traceContext, retryContext); stopwatch.OnCheckpoint(FunctionInvocationPerformanceStopwatch.Checkpoint.InputBindingValuesReady); @@ -225,7 +225,7 @@ public Hashtable InvokeFunction( { if(functionInfo.DurableFunctionInfo.IsOrchestrationFunction) { - return durableController.InvokeOrchestrationFunction(); + return durableFunctionsUtils.InvokeOrchestrationFunction(); } else { @@ -255,7 +255,7 @@ public Hashtable InvokeFunction( finally { // TODO: determine if external SDK also needs this call - durableController.AfterFunctionInvocation(); + durableFunctionsUtils.AfterFunctionInvocation(); outputBindings.Clear(); ResetRunspace(); } From b8c6a6afad88cfd74d1f4358cd229d195f53fd7f Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Tue, 8 Mar 2022 11:28:27 -0800 Subject: [PATCH 22/43] Fixed issue with building the worker --- src/DurableSDK/PowerShellServices.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index f7317de1..8baf4739 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -45,9 +45,9 @@ public PowerShellServices(PowerShell pwsh) .InvokeAndClearCommands(); if (availableModules.Count() > 0) { - // throw new Exception(errorRecords); - // InvalidOperationException - throw e; + // 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; } From baae3cc1f3d013c5d751756e8c982f041c5b980c Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Tue, 8 Mar 2022 13:58:14 -0800 Subject: [PATCH 23/43] Fix E2E test --- .../Microsoft.Azure.Functions.PowerShellWorker.psm1 | 11 +++++++++-- .../DurableEndToEndTests.cs | 4 +++- .../DurableClientOrchContextProperties/run.ps1 | 1 - .../DurableOrchestratorAccessContextProps/run.ps1 | 5 ++--- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 index 1d4d00ad..2f82bb9b 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 @@ -98,7 +98,11 @@ function Start-DurableOrchestration { [Parameter( ValueFromPipelineByPropertyName=$true)] - [object] $DurableClient + [object] $DurableClient, + + [Parameter( + ValueFromPipelineByPropertyName=$true)] + [string] $InstanceId ) $ErrorActionPreference = 'Stop' @@ -107,7 +111,10 @@ function Start-DurableOrchestration { $DurableClient = GetDurableClientFromModulePrivateData } - $InstanceId = (New-Guid).Guid + # TODO: Port this change to the External SDK + if (-not $InstanceId) { + $InstanceId = (New-Guid).Guid + } $Uri = if ($DurableClient.rpcBaseUrl) { diff --git a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs index c79a9228..da7a1134 100644 --- a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs +++ b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs @@ -187,7 +187,9 @@ public async Task OrchestratationContextHasAllExpectedProperties() { var statusResponseBody = await GetResponseBodyAsync(statusResponse); Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus); - Assert.Equal("myInstanceId", statusResponseBody.output[0].ToString()); + Assert.Equal("true", statusResponseBody.output[0].ToString()); + Assert.Equal("Hello myInstanceId", statusResponseBody.output[1].ToString()); + Assert.Equal("false", statusResponseBody.output[2].ToString()); return; } diff --git a/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 index 468e7e88..228d57f6 100644 --- a/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 +++ b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 @@ -2,7 +2,6 @@ using namespace System.Net param($Request, $TriggerMetadata) -$FunctionName = $Request.Params.FunctionName $InstanceId = Start-DurableOrchestration -FunctionName "DurableOrchestratorAccessContextProps" -InstanceId "myInstanceId" Write-Host "Started orchestration with ID = '$InstanceId'" diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 index f8eaa7fe..031a5492 100644 --- a/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 +++ b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 @@ -2,8 +2,7 @@ param($Context) $output = @() -$Context.InstanceId -$Context.IsReplaying +$output += $Context.IsReplaying $output += Invoke-DurableActivity -FunctionName 'DurableActivity' -Input $Context.InstanceId -$Context.IsReplaying +$output += $Context.IsReplaying $output From 6feb7fed948f8bdccd4d6574991fb1d2bca322cc Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Tue, 8 Mar 2022 14:10:43 -0800 Subject: [PATCH 24/43] Fixed unit test setup --- test/Unit/Durable/OrchestrationInvokerTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Unit/Durable/OrchestrationInvokerTests.cs b/test/Unit/Durable/OrchestrationInvokerTests.cs index 167af701..9754269b 100644 --- a/test/Unit/Durable/OrchestrationInvokerTests.cs +++ b/test/Unit/Durable/OrchestrationInvokerTests.cs @@ -34,6 +34,7 @@ public void InvocationRunsToCompletionIfNotStopped() { var invocationAsyncResult = DurableTestUtilities.CreateInvocationResult(completed: true); DurableTestUtilities.ExpectBeginInvoke(_mockPowerShellServices, invocationAsyncResult); + _mockPowerShellServices.Setup(_ => _.UseExternalDurableSDK()).Returns(false); _orchestrationInvoker.Invoke(_orchestrationBindingInfo, _mockPowerShellServices.Object); @@ -42,6 +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.VerifyNoOtherCalls(); } From 94d83ae267697858c255129ddfbc57a1503d03d5 Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Tue, 8 Mar 2022 14:12:35 -0800 Subject: [PATCH 25/43] Fixed another unit test setup --- test/Unit/Durable/OrchestrationInvokerTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Unit/Durable/OrchestrationInvokerTests.cs b/test/Unit/Durable/OrchestrationInvokerTests.cs index 9754269b..2a2db694 100644 --- a/test/Unit/Durable/OrchestrationInvokerTests.cs +++ b/test/Unit/Durable/OrchestrationInvokerTests.cs @@ -51,12 +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 d11a5616a9b9c93bd18bcfdce67a9a80d946b8b9 Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Tue, 8 Mar 2022 14:23:58 -0800 Subject: [PATCH 26/43] Remove string representation of booleans --- .../DurableEndToEndTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs index da7a1134..39417633 100644 --- a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs +++ b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs @@ -187,9 +187,9 @@ public async Task OrchestratationContextHasAllExpectedProperties() { var statusResponseBody = await GetResponseBodyAsync(statusResponse); Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus); - Assert.Equal("true", statusResponseBody.output[0].ToString()); + Assert.True(statusResponseBody.output[0]); Assert.Equal("Hello myInstanceId", statusResponseBody.output[1].ToString()); - Assert.Equal("false", statusResponseBody.output[2].ToString()); + Assert.False(statusResponseBody.output[2]); return; } From 0e0c077c991f1d2ef93056e6919721fbf3c153dc Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 9 Mar 2022 07:28:05 -0800 Subject: [PATCH 27/43] patch e2e test --- .../DurableEndToEndTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs index 39417633..8a6dec22 100644 --- a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs +++ b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs @@ -187,9 +187,9 @@ public async Task OrchestratationContextHasAllExpectedProperties() { var statusResponseBody = await GetResponseBodyAsync(statusResponse); Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus); - Assert.True(statusResponseBody.output[0]); + Assert.Equal("True", statusResponseBody.output[0].ToString()); Assert.Equal("Hello myInstanceId", statusResponseBody.output[1].ToString()); - Assert.False(statusResponseBody.output[2]); + Assert.Equal("False", statusResponseBody.output[2].toString()); return; } From 1c4a7ae3468f28cb4f068865c63d726c31ac956d Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 9 Mar 2022 07:45:34 -0800 Subject: [PATCH 28/43] remove typo in toString --- .../DurableEndToEndTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs index 8a6dec22..124ccef3 100644 --- a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs +++ b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs @@ -189,7 +189,7 @@ public async Task OrchestratationContextHasAllExpectedProperties() Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus); Assert.Equal("True", statusResponseBody.output[0].ToString()); Assert.Equal("Hello myInstanceId", statusResponseBody.output[1].ToString()); - Assert.Equal("False", statusResponseBody.output[2].toString()); + Assert.Equal("False", statusResponseBody.output[2].ToString()); return; } From 3bd961c61f03af126c18bc4ba42309c64fb5299b Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 9 Mar 2022 08:17:21 -0800 Subject: [PATCH 29/43] Return results from Start-DurableExternalEventListener (#685) (#753) Co-authored-by: Greg Roll --- src/Durable/Commands/GetDurableTaskResult.cs | 36 +++++++++++++++++++ src/Durable/DurableTaskHandler.cs | 17 +++++++++ src/Durable/Tasks/ActivityInvocationTask.cs | 6 ++-- src/Durable/Tasks/DurableTask.cs | 4 +-- src/Durable/Tasks/DurableTimerTask.cs | 4 +-- src/Durable/Tasks/ExternalEventTask.cs | 6 ++-- ...soft.Azure.Functions.PowerShellWorker.psd1 | 1 + ...soft.Azure.Functions.PowerShellWorker.psm1 | 16 +++++++-- .../Durable/ActivityInvocationTaskTests.cs | 4 +-- 9 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 src/Durable/Commands/GetDurableTaskResult.cs diff --git a/src/Durable/Commands/GetDurableTaskResult.cs b/src/Durable/Commands/GetDurableTaskResult.cs new file mode 100644 index 00000000..74f5493b --- /dev/null +++ b/src/Durable/Commands/GetDurableTaskResult.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +#pragma warning disable 1591 // Missing XML comment for publicly visible type or member 'member' + +namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Commands +{ + using System.Collections; + using System.Management.Automation; + using Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks; + + [Cmdlet("Get", "DurableTaskResult")] + public class GetDurableTaskResultCommand : PSCmdlet + { + [Parameter(Mandatory = true)] + [ValidateNotNull] + public DurableTask[] Task { get; set; } + + private readonly DurableTaskHandler _durableTaskHandler = new DurableTaskHandler(); + + protected override void EndProcessing() + { + var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; + var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey]; + + _durableTaskHandler.GetTaskResult(Task, context, WriteObject); + } + + protected override void StopProcessing() + { + _durableTaskHandler.Stop(); + } + } +} diff --git a/src/Durable/DurableTaskHandler.cs b/src/Durable/DurableTaskHandler.cs index 3981b686..818a5b99 100644 --- a/src/Durable/DurableTaskHandler.cs +++ b/src/Durable/DurableTaskHandler.cs @@ -164,6 +164,7 @@ public void WaitAny( if (scheduledHistoryEvent != null) { scheduledHistoryEvent.IsProcessed = true; + scheduledHistoryEvent.IsPlayed = true; } if (completedHistoryEvent != null) @@ -179,6 +180,7 @@ public void WaitAny( } completedHistoryEvent.IsProcessed = true; + completedHistoryEvent.IsPlayed = true; } } @@ -195,6 +197,21 @@ public void WaitAny( } } + public void GetTaskResult( + IReadOnlyCollection tasksToQueryResultFor, + OrchestrationContext context, + Action output) + { + foreach (var task in tasksToQueryResultFor) { + var scheduledHistoryEvent = task.GetScheduledHistoryEvent(context, true); + var processedHistoryEvent = task.GetCompletedHistoryEvent(context, scheduledHistoryEvent, true); + if (processedHistoryEvent != null) + { + output(GetEventResult(processedHistoryEvent)); + } + } + } + public void Stop() { _waitForStop.Set(); diff --git a/src/Durable/Tasks/ActivityInvocationTask.cs b/src/Durable/Tasks/ActivityInvocationTask.cs index 87bf61c2..a0ba49b4 100644 --- a/src/Durable/Tasks/ActivityInvocationTask.cs +++ b/src/Durable/Tasks/ActivityInvocationTask.cs @@ -37,15 +37,15 @@ internal ActivityInvocationTask(string functionName, object functionInput) { } - internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context) + internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed) { return context.History.FirstOrDefault( e => e.EventType == HistoryEventType.TaskScheduled && e.Name == FunctionName && - !e.IsProcessed); + e.IsProcessed == processed); } - internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent) + internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent, bool processed) { return scheduledHistoryEvent == null ? null diff --git a/src/Durable/Tasks/DurableTask.cs b/src/Durable/Tasks/DurableTask.cs index af71f36d..228e769b 100644 --- a/src/Durable/Tasks/DurableTask.cs +++ b/src/Durable/Tasks/DurableTask.cs @@ -11,9 +11,9 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks public abstract class DurableTask { - internal abstract HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context); + internal abstract HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed = false); - internal abstract HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent); + internal abstract HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent, bool processed = false); internal abstract OrchestrationAction CreateOrchestrationAction(); } diff --git a/src/Durable/Tasks/DurableTimerTask.cs b/src/Durable/Tasks/DurableTimerTask.cs index 86a75abe..4bb357ef 100644 --- a/src/Durable/Tasks/DurableTimerTask.cs +++ b/src/Durable/Tasks/DurableTimerTask.cs @@ -28,7 +28,7 @@ internal DurableTimerTask( Action = new CreateDurableTimerAction(FireAt); } - internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context) + internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed) { return context.History.FirstOrDefault( e => e.EventType == HistoryEventType.TimerCreated && @@ -36,7 +36,7 @@ internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext con !e.IsProcessed); } - internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent) + internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent, bool processed) { return scheduledHistoryEvent == null ? null diff --git a/src/Durable/Tasks/ExternalEventTask.cs b/src/Durable/Tasks/ExternalEventTask.cs index 1bd1fc58..5d12ad66 100644 --- a/src/Durable/Tasks/ExternalEventTask.cs +++ b/src/Durable/Tasks/ExternalEventTask.cs @@ -21,17 +21,17 @@ public ExternalEventTask(string externalEventName) } // There is no corresponding history event for an expected external event - internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context) + internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed) { return null; } - internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent taskScheduled) + internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent taskScheduled, bool processed) { return context.History.FirstOrDefault( e => e.EventType == HistoryEventType.EventRaised && e.Name == ExternalEventName && - !e.IsProcessed); + e.IsPlayed == processed); } internal override OrchestrationAction CreateOrchestrationAction() diff --git a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psd1 b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psd1 index 7ae51c2b..7bed18b9 100644 --- a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psd1 +++ b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psd1 @@ -59,6 +59,7 @@ FunctionsToExport = @( # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( 'Get-OutputBinding', + 'Get-DurableTaskResult' 'Invoke-DurableActivity', 'Push-OutputBinding', 'Set-DurableCustomStatus', diff --git a/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 b/src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1 index 1d4d00ad..0fc66666 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 @@ -235,6 +235,8 @@ function New-DurableOrchestrationCheckStatusResponse { The TaskHubName of the orchestration instance that will handle the external event. .PARAMETER ConnectionName The name of the connection string associated with TaskHubName +.PARAMETER AppCode + The Azure Functions system key #> function Send-DurableExternalEvent { [CmdletBinding()] @@ -263,12 +265,16 @@ function Send-DurableExternalEvent { [Parameter( ValueFromPipelineByPropertyName=$true)] - [string] $ConnectionName + [string] $ConnectionName, + + [Parameter( + ValueFromPipelineByPropertyName=$true)] + [string] $AppCode ) $DurableClient = GetDurableClientFromModulePrivateData - $RequestUrl = GetRaiseEventUrl -DurableClient $DurableClient -InstanceId $InstanceId -EventName $EventName -TaskHubName $TaskHubName -ConnectionName $ConnectionName + $RequestUrl = GetRaiseEventUrl -DurableClient $DurableClient -InstanceId $InstanceId -EventName $EventName -TaskHubName $TaskHubName -ConnectionName $ConnectionName -AppCode $AppCode $Body = $EventData | ConvertTo-Json -Compress @@ -280,7 +286,8 @@ function GetRaiseEventUrl( [string] $InstanceId, [string] $EventName, [string] $TaskHubName, - [string] $ConnectionName) { + [string] $ConnectionName, + [string] $AppCode) { $RequestUrl = $DurableClient.BaseUrl + "/instances/$InstanceId/raiseEvent/$EventName" @@ -291,6 +298,9 @@ function GetRaiseEventUrl( if ($null -eq $ConnectionName) { $query += "connection=$ConnectionName" } + if ($null -eq $AppCode) { + $query += "code=$AppCode" + } if ($query.Count -gt 0) { $RequestUrl += "?" + [string]::Join("&", $query) } diff --git a/test/Unit/Durable/ActivityInvocationTaskTests.cs b/test/Unit/Durable/ActivityInvocationTaskTests.cs index c96cc755..530711ca 100644 --- a/test/Unit/Durable/ActivityInvocationTaskTests.cs +++ b/test/Unit/Durable/ActivityInvocationTaskTests.cs @@ -225,8 +225,8 @@ public void GetCompletedHistoryEvent_ReturnsTaskCompletedOrTaskFailed(bool succe var orchestrationContext = new OrchestrationContext { History = history }; var task = new ActivityInvocationTask(FunctionName, FunctionInput); - var scheduledEvent = task.GetScheduledHistoryEvent(orchestrationContext); - var completedEvent = task.GetCompletedHistoryEvent(orchestrationContext, scheduledEvent); + var scheduledEvent = task.GetScheduledHistoryEvent(orchestrationContext, false); + var completedEvent = task.GetCompletedHistoryEvent(orchestrationContext, scheduledEvent, false); Assert.Equal(scheduledEvent.EventId, completedEvent.TaskScheduledId); } From 932e124928b24088cc610b6953bc8be5c7c90bcf Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 9 Mar 2022 09:03:04 -0800 Subject: [PATCH 30/43] add e2e test for GetTaskResult --- .../DurableEndToEndTests.cs | 52 +++++++++++++++++++ .../function.json | 9 ++++ .../DurableOrchestratorGetTaskResult/run.ps1 | 8 +++ 3 files changed, 69 insertions(+) create mode 100644 test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/function.json create mode 100644 test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 diff --git a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs index 124ccef3..c82cc1a3 100644 --- a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs +++ b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs @@ -201,6 +201,58 @@ public async Task OrchestratationContextHasAllExpectedProperties() } } + [Fact] + public async Task OrchestratationCanAlwaysObtainTaskResult() + { + var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClient", queryString: "DurableOrchestratorGetTaskResult"); + Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode); + + var initialResponseBody = await initialResponse.Content.ReadAsStringAsync(); + dynamic initialResponseBodyObject = JsonConvert.DeserializeObject(initialResponseBody); + var statusQueryGetUri = (string)initialResponseBodyObject.statusQueryGetUri; + + var startTime = DateTime.UtcNow; + + using (var httpClient = new HttpClient()) + { + while (true) + { + var statusResponse = await httpClient.GetAsync(statusQueryGetUri); + switch (statusResponse.StatusCode) + { + case HttpStatusCode.Accepted: + { + var statusResponseBody = await GetResponseBodyAsync(statusResponse); + var runtimeStatus = (string)statusResponseBody.runtimeStatus; + Assert.True( + runtimeStatus == "Running" || runtimeStatus == "Pending", + $"Unexpected runtime status: {runtimeStatus}"); + + if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout) + { + Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}"); + } + + await Task.Delay(TimeSpan.FromSeconds(2)); + break; + } + + case HttpStatusCode.OK: + { + var statusResponseBody = await GetResponseBodyAsync(statusResponse); + Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus); + Assert.Equal("Hello world", statusResponseBody.output[0].ToString()); + return; + } + + default: + Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}"); + break; + } + } + } + } + [Fact] public async Task ActivityCanHaveQueueBinding() { diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/function.json b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/function.json new file mode 100644 index 00000000..336f5a18 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/function.json @@ -0,0 +1,9 @@ +{ + "bindings": [ + { + "name": "Context", + "type": "orchestrationTrigger", + "direction": "in" + } + ] +} \ No newline at end of file diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 new file mode 100644 index 00000000..ad48627d --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 @@ -0,0 +1,8 @@ +param($Context) + +$output = @() + +$task = Invoke-DurableActivity -FunctionName 'DurableActivity' -Input "mundo" -NoWait +$firstTask = Wait-DurableTask -Task @($task) -Any +$output += Get-DurableTaskResult $firstTask +$output From 3d1a7b4c001d8df1a99bf4324c4c0875485c03e1 Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 9 Mar 2022 09:32:42 -0800 Subject: [PATCH 31/43] parametrize test --- .../DurableEndToEndTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs index c82cc1a3..fc7b084c 100644 --- a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs +++ b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs @@ -204,7 +204,7 @@ public async Task OrchestratationContextHasAllExpectedProperties() [Fact] public async Task OrchestratationCanAlwaysObtainTaskResult() { - var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClient", queryString: "DurableOrchestratorGetTaskResult"); + var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClient", queryString: "?FunctionName=DurableOrchestratorGetTaskResult"); Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode); var initialResponseBody = await initialResponse.Content.ReadAsStringAsync(); From adec8fafb5dfab8f365b6213c9e6fe7d5d7e8675 Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 9 Mar 2022 09:44:09 -0800 Subject: [PATCH 32/43] patch new e2e test --- .../DurableEndToEndTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs index fc7b084c..067e9fba 100644 --- a/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs +++ b/test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs @@ -241,7 +241,7 @@ public async Task OrchestratationCanAlwaysObtainTaskResult() { var statusResponseBody = await GetResponseBodyAsync(statusResponse); Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus); - Assert.Equal("Hello world", statusResponseBody.output[0].ToString()); + Assert.Equal("Hello world", statusResponseBody.output.ToString()); return; } From 8d5d9bfa7ae060847986cd4ef5084f32e2b5fb77 Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 9 Mar 2022 10:55:47 -0800 Subject: [PATCH 33/43] patch external contrib --- .../TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 index ad48627d..9e5fb97c 100644 --- a/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 +++ b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 @@ -4,5 +4,5 @@ $output = @() $task = Invoke-DurableActivity -FunctionName 'DurableActivity' -Input "mundo" -NoWait $firstTask = Wait-DurableTask -Task @($task) -Any -$output += Get-DurableTaskResult $firstTask +$output += Get-DurableTaskResult -Task @($firstTask) $output From 835165316b44f3f14385f2dad8c5330d95e773ba Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 9 Mar 2022 12:09:41 -0800 Subject: [PATCH 34/43] fix typo in test --- .../TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 index 9e5fb97c..23380916 100644 --- a/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 +++ b/test/E2E/TestFunctionApp/DurableOrchestratorGetTaskResult/run.ps1 @@ -2,7 +2,7 @@ param($Context) $output = @() -$task = Invoke-DurableActivity -FunctionName 'DurableActivity' -Input "mundo" -NoWait +$task = Invoke-DurableActivity -FunctionName 'DurableActivity' -Input "world" -NoWait $firstTask = Wait-DurableTask -Task @($task) -Any $output += Get-DurableTaskResult -Task @($firstTask) $output From 7232e58bce790bee6aa4860d1ae8b1e7b1ddff23 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 14 Mar 2022 10:30:07 -0700 Subject: [PATCH 35/43] comment changes --- src/DurableSDK/IPowerShellServices.cs | 4 ++-- src/DurableSDK/OrchestrationContext.cs | 2 ++ src/DurableSDK/OrchestrationInvoker.cs | 3 +-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/DurableSDK/IPowerShellServices.cs b/src/DurableSDK/IPowerShellServices.cs index ebe24fab..a13d998c 100644 --- a/src/DurableSDK/IPowerShellServices.cs +++ b/src/DurableSDK/IPowerShellServices.cs @@ -21,9 +21,9 @@ internal interface IPowerShellServices void ClearOrchestrationContext(); - public void TracePipelineObject(); - public void AddParameter(string name, object value); + void TracePipelineObject(); + void AddParameter(string name, object value); IAsyncResult BeginInvoke(PSDataCollection output); diff --git a/src/DurableSDK/OrchestrationContext.cs b/src/DurableSDK/OrchestrationContext.cs index 6b051036..b4b76f8c 100644 --- a/src/DurableSDK/OrchestrationContext.cs +++ b/src/DurableSDK/OrchestrationContext.cs @@ -38,6 +38,8 @@ public class OrchestrationContext internal object ExternalResult; internal bool ExternalIsError; + // 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; diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index d01184a6..8f2bcbb6 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -49,7 +49,7 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe // Marks the first OrchestratorStarted event as processed orchestrationStart.IsProcessed = true; - + // Finish initializing the Function invocation pwsh.AddParameter(orchestrationBindingInfo.ParameterName, context); pwsh.TracePipelineObject(); @@ -62,7 +62,6 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe { // The orchestration function should be stopped and restarted pwsh.StopInvoke(); - // return (Hashtable)orchestrationBindingInfo.Context.OrchestrationActionCollector.output; return CreateOrchestrationResult(isDone: false, actions, output: null, context.CustomStatus); } else From d9b3bafa9186b8b072afb5d10c30ddf0d073303b Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Wed, 16 Mar 2022 10:07:48 -0700 Subject: [PATCH 36/43] Adds IExternalInvoker (#776) * Define a contract for the external invoker * Remove extraneous comments and variables --- src/DurableSDK/ExternalInvoker.cs | 27 ++++ src/DurableSDK/IExternalInvoker.cs | 14 ++ src/DurableSDK/IOrchestrationInvoker.cs | 4 +- src/DurableSDK/IPowerShellServices.cs | 2 +- src/DurableSDK/OrchestrationContext.cs | 9 +- src/DurableSDK/OrchestrationInvoker.cs | 123 ++++++++++-------- src/DurableSDK/PowerShellServices.cs | 37 ++++-- src/DurableWorker/DurableController.cs | 10 +- ...soft.Azure.Functions.PowerShellWorker.psm1 | 2 +- test/Unit/Durable/DurableControllerTests.cs | 35 +++-- 10 files changed, 168 insertions(+), 95 deletions(-) create mode 100644 src/DurableSDK/ExternalInvoker.cs create mode 100644 src/DurableSDK/IExternalInvoker.cs 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(); From edf3011b71c2fd6ff6864db478753b4639aefe4a Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 16 Mar 2022 10:18:05 -0700 Subject: [PATCH 37/43] rename hasOrchestrationContext to hasInitializedDurableFunction --- src/DurableSDK/PowerShellServices.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 64355b25..2a403c1a 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -20,7 +20,7 @@ internal class PowerShellServices : IPowerShellServices 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) @@ -81,8 +81,7 @@ public void SetDurableClient(object durableClient) _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("DurableClient", durableClient) .InvokeAndClearCommands(); - // TODO: is _hasSetOrchestrationContext properly named? - _hasSetOrchestrationContext = true; + _hasInitializedDurableFunction = true; } public OrchestrationBindingInfo SetOrchestrationContext( @@ -117,7 +116,7 @@ public OrchestrationBindingInfo SetOrchestrationContext( .AddParameter("OrchestrationContext", orchestrationBindingInfo.Context) .InvokeAndClearCommands(); } - _hasSetOrchestrationContext = true; + _hasInitializedDurableFunction = true; return orchestrationBindingInfo; } @@ -129,7 +128,7 @@ public void AddParameter(string name, object value) public void ClearOrchestrationContext() { - if (_hasSetOrchestrationContext) + if (_hasInitializedDurableFunction) { _pwsh.AddCommand(SetFunctionInvocationContextCommand) .AddParameter("Clear", true) From aca4c0b75e71b057c6698f6d4789f3a21af29a09 Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 16 Mar 2022 10:23:46 -0700 Subject: [PATCH 38/43] remove outdated TODO comment --- src/PowerShell/PowerShellManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index 29c0e794..c604da81 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -254,7 +254,6 @@ public Hashtable InvokeFunction( } finally { - // TODO: determine if external SDK also needs this call durableFunctionsUtils.AfterFunctionInvocation(); outputBindings.Clear(); ResetRunspace(); From e5ca2baa3358e53ea88b10b30487caf8b7bd9834 Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 16 Mar 2022 10:24:05 -0700 Subject: [PATCH 39/43] remove now unused function - CreateOrchestrationBindingInfo --- src/DurableWorker/DurableController.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/DurableWorker/DurableController.cs b/src/DurableWorker/DurableController.cs index 427fd9be..3d756e15 100644 --- a/src/DurableWorker/DurableController.cs +++ b/src/DurableWorker/DurableController.cs @@ -112,21 +112,5 @@ 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]; - - // TODO: make this de-serialization constructor depend on the external SDK - return new OrchestrationBindingInfo( - context.Name, - JsonConvert.DeserializeObject(context.Data.String)); - } } } From f69b1462380eee1d3b343feaa337a8792589daa2 Mon Sep 17 00:00:00 2001 From: Michael Peng Date: Thu, 17 Mar 2022 10:22:34 -0700 Subject: [PATCH 40/43] Allow worker to read results directly from the external SDK (#777) --- src/DurableSDK/ExternalInvoker.cs | 9 ++++---- src/DurableSDK/IExternalInvoker.cs | 4 +++- src/DurableSDK/OrchestrationContext.cs | 12 ---------- src/DurableSDK/OrchestrationInvoker.cs | 25 ++++++++------------- src/DurableSDK/PowerShellServices.cs | 3 +-- src/PowerShell/PowerShellManager.cs | 4 ++-- test/Unit/Durable/DurableControllerTests.cs | 1 - 7 files changed, 19 insertions(+), 39 deletions(-) diff --git a/src/DurableSDK/ExternalInvoker.cs b/src/DurableSDK/ExternalInvoker.cs index f0a73b86..fa8a31c6 100644 --- a/src/DurableSDK/ExternalInvoker.cs +++ b/src/DurableSDK/ExternalInvoker.cs @@ -6,22 +6,21 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable { using System; + using System.Collections; using System.Management.Automation; internal class ExternalInvoker : IExternalInvoker { private readonly Func _externalSDKInvokerFunction; - private readonly IPowerShellServices _powerShellServices; - public ExternalInvoker(Func invokerFunction, IPowerShellServices powerShellServices) + public ExternalInvoker(Func invokerFunction) { _externalSDKInvokerFunction = invokerFunction; - _powerShellServices = powerShellServices; } - public void Invoke() + public Hashtable Invoke(IPowerShellServices powerShellServices) { - _externalSDKInvokerFunction.Invoke(_powerShellServices.GetPowerShell()); + return (Hashtable)_externalSDKInvokerFunction.Invoke(powerShellServices.GetPowerShell()); } } } diff --git a/src/DurableSDK/IExternalInvoker.cs b/src/DurableSDK/IExternalInvoker.cs index 16d17e23..3a703f3d 100644 --- a/src/DurableSDK/IExternalInvoker.cs +++ b/src/DurableSDK/IExternalInvoker.cs @@ -5,10 +5,12 @@ 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 - void Invoke(); + Hashtable Invoke(IPowerShellServices powerShellServices); } } diff --git a/src/DurableSDK/OrchestrationContext.cs b/src/DurableSDK/OrchestrationContext.cs index e7edec60..27f082db 100644 --- a/src/DurableSDK/OrchestrationContext.cs +++ b/src/DurableSDK/OrchestrationContext.cs @@ -35,18 +35,6 @@ public class OrchestrationContext internal OrchestrationActionCollector OrchestrationActionCollector { get; } = new OrchestrationActionCollector(); - 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.ExternalSDKResult = result; - this.ExternalSDKIsError = isError; - } - internal object CustomStatus { get; set; } } } diff --git a/src/DurableSDK/OrchestrationInvoker.cs b/src/DurableSDK/OrchestrationInvoker.cs index 4706c2c3..b71116b4 100644 --- a/src/DurableSDK/OrchestrationInvoker.cs +++ b/src/DurableSDK/OrchestrationInvoker.cs @@ -16,6 +16,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable internal class OrchestrationInvoker : IOrchestrationInvoker { private IExternalInvoker _externalInvoker; + internal static string isOrchestrationFailureKey = "IsOrchestrationFailure"; public Hashtable Invoke( OrchestrationBindingInfo orchestrationBindingInfo, @@ -25,32 +26,24 @@ public Hashtable Invoke( { if (powerShellServices.UseExternalDurableSDK()) { - return InvokeExternalDurableSDK(orchestrationBindingInfo, powerShellServices); + return InvokeExternalDurableSDK(powerShellServices); } return InvokeInternalDurableSDK(orchestrationBindingInfo, powerShellServices); } + catch (Exception ex) + { + ex.Data.Add(isOrchestrationFailureKey, true); + throw; + } finally { powerShellServices.ClearStreamsAndCommands(); } } - public Hashtable InvokeExternalDurableSDK( - OrchestrationBindingInfo orchestrationBindingInfo, - IPowerShellServices powerShellServices) + public Hashtable InvokeExternalDurableSDK(IPowerShellServices powerShellServices) { - - _externalInvoker.Invoke(); - var result = orchestrationBindingInfo.Context.ExternalSDKResult; - var isError = orchestrationBindingInfo.Context.ExternalSDKIsError; - if (isError) - { - throw (Exception)result; - } - else - { - return (Hashtable)result; - } + return _externalInvoker.Invoke(powerShellServices); } public Hashtable InvokeInternalDurableSDK( diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 2a403c1a..549d4417 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -99,11 +99,10 @@ public OrchestrationBindingInfo SetOrchestrationContext( // 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) orchestrationBindingInfo.Context.SetExternalResult) .InvokeAndClearCommands>(); if (output.Count() == 1) { - externalInvoker = new ExternalInvoker(output[0], this); + externalInvoker = new ExternalInvoker(output[0]); } else { diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index c604da81..0344c05d 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -243,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)); } diff --git a/test/Unit/Durable/DurableControllerTests.cs b/test/Unit/Durable/DurableControllerTests.cs index e16f04d7..b57cb767 100644 --- a/test/Unit/Durable/DurableControllerTests.cs +++ b/test/Unit/Durable/DurableControllerTests.cs @@ -119,7 +119,6 @@ public void TryGetInputBindingParameterValue_RetrievesOrchestrationContextParame { CreateParameterBinding(_contextParameterName, _orchestrationContext) }; - _mockPowerShellServices.Setup(_ => _.SetOrchestrationContext( It.IsAny(), out It.Ref.IsAny)) From 4387799f8ab3f3a8227ce15bd007f9c8dbc18d5c Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 17 Mar 2022 10:26:04 -0700 Subject: [PATCH 41/43] comment out external SDK path --- src/DurableSDK/PowerShellServices.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index 549d4417..d7433850 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -25,10 +25,13 @@ internal class PowerShellServices : IPowerShellServices 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) @@ -53,7 +56,8 @@ public PowerShellServices(PowerShell pwsh) throw new InvalidOperationException("The external Durable SDK was detected, but unable to be imported.", e); } _useExternalDurableSDK = false; - } + }*/ + _useExternalDurableSDK = false; if (_useExternalDurableSDK) { From a9a0a6eb21a49a08a97f9e77a2ffcf2d4a5ad41b Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 12 Jul 2022 10:36:14 -0700 Subject: [PATCH 42/43] change serialization --- src/DurableSDK/PowerShellServices.cs | 6 +++--- src/Utility/TypeExtensions.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DurableSDK/PowerShellServices.cs b/src/DurableSDK/PowerShellServices.cs index d7433850..fda8d80d 100644 --- a/src/DurableSDK/PowerShellServices.cs +++ b/src/DurableSDK/PowerShellServices.cs @@ -25,7 +25,7 @@ internal class PowerShellServices : IPowerShellServices public PowerShellServices(PowerShell pwsh) { - /* This logic will be commented out until the external SDK is published on the PS Gallery + //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 @@ -56,8 +56,8 @@ public PowerShellServices(PowerShell pwsh) throw new InvalidOperationException("The external Durable SDK was detected, but unable to be imported.", e); } _useExternalDurableSDK = false; - }*/ - _useExternalDurableSDK = false; + } + //_useExternalDurableSDK = false; if (_useExternalDurableSDK) { 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); From 5f862e6ceef431a1c7a2f9fe28c61dc2f96797ce Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 12 Jul 2022 14:44:52 -0700 Subject: [PATCH 43/43] restore orchestrationInvoker and powershellServices --- src/DurableSDK/OrchestrationInvoker.cs | 115 ++++++++++++++++-------- src/DurableSDK/PowerShellServices.cs | 118 ++++++++++++++++++++++--- 2 files changed, 186 insertions(+), 47 deletions(-) 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/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);