From e1819ac4d81681475845ab8310ddad611911d07d Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Fri, 29 Jan 2021 17:23:53 -0800 Subject: [PATCH 1/3] Implement orchestration custom status --- .../CustomStatusOrchestrator/function.json | 9 ++++++ .../CustomStatusOrchestrator/run.ps1 | 22 ++++++++++++++ .../CustomStatusStart/function.json | 24 +++++++++++++++ .../DurableApp/CustomStatusStart/run.ps1 | 13 +++++++++ .../Commands/SetDurableCustomStatusCommand.cs | 29 +++++++++++++++++++ src/Durable/OrchestrationContext.cs | 4 ++- src/Durable/OrchestrationFailureException.cs | 14 ++++++--- src/Durable/OrchestrationInvoker.cs | 11 +++---- src/Durable/OrchestrationMessage.cs | 13 ++++++++- ...soft.Azure.Functions.PowerShellWorker.psd1 | 1 + .../DurableEndToEndTests.cs | 3 ++ .../DurableOrchestrator/run.ps1 | 4 +++ .../OrchestrationFailureExceptionTests.cs | 9 ++++-- 13 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 examples/durable/DurableApp/CustomStatusOrchestrator/function.json create mode 100644 examples/durable/DurableApp/CustomStatusOrchestrator/run.ps1 create mode 100644 examples/durable/DurableApp/CustomStatusStart/function.json create mode 100644 examples/durable/DurableApp/CustomStatusStart/run.ps1 create mode 100644 src/Durable/Commands/SetDurableCustomStatusCommand.cs diff --git a/examples/durable/DurableApp/CustomStatusOrchestrator/function.json b/examples/durable/DurableApp/CustomStatusOrchestrator/function.json new file mode 100644 index 00000000..0c950e30 --- /dev/null +++ b/examples/durable/DurableApp/CustomStatusOrchestrator/function.json @@ -0,0 +1,9 @@ +{ + "bindings": [ + { + "name": "Context", + "type": "orchestrationTrigger", + "direction": "in" + } + ] +} diff --git a/examples/durable/DurableApp/CustomStatusOrchestrator/run.ps1 b/examples/durable/DurableApp/CustomStatusOrchestrator/run.ps1 new file mode 100644 index 00000000..7fdd3843 --- /dev/null +++ b/examples/durable/DurableApp/CustomStatusOrchestrator/run.ps1 @@ -0,0 +1,22 @@ +using namespace System.Net + +param($Context) + +Write-Host 'CustomStatusOrchestrator: started.' + +$output = @() + +Set-DurableCustomStatus -CustomStatus 'Processing Tokyo' +$output += Invoke-ActivityFunction -FunctionName 'SayHello' -Input 'Tokyo' + +Set-DurableCustomStatus -CustomStatus @{ ProgressMessage = 'Processing Seattle'; Stage = 2 } +$output += Invoke-ActivityFunction -FunctionName 'SayHello' -Input 'Seattle' + +Set-DurableCustomStatus -CustomStatus @('Processing London', 'Last stage') +$output += Invoke-ActivityFunction -FunctionName 'SayHello' -Input 'London' + +Set-DurableCustomStatus 'Processing completed' + +Write-Host 'CustomStatusOrchestrator: finished.' + +$output diff --git a/examples/durable/DurableApp/CustomStatusStart/function.json b/examples/durable/DurableApp/CustomStatusStart/function.json new file mode 100644 index 00000000..54e2a634 --- /dev/null +++ b/examples/durable/DurableApp/CustomStatusStart/function.json @@ -0,0 +1,24 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + }, + { + "name": "starter", + "type": "durableClient", + "direction": "in" + } + ] +} diff --git a/examples/durable/DurableApp/CustomStatusStart/run.ps1 b/examples/durable/DurableApp/CustomStatusStart/run.ps1 new file mode 100644 index 00000000..49c6fefb --- /dev/null +++ b/examples/durable/DurableApp/CustomStatusStart/run.ps1 @@ -0,0 +1,13 @@ +using namespace System.Net + +param($Request, $TriggerMetadata) + +Write-Host 'CustomStatusStart started' + +$InstanceId = Start-NewOrchestration -FunctionName 'CustomStatusOrchestrator' -InputObject 'Hello' +Write-Host "Started orchestration with ID = '$InstanceId'" + +$Response = New-OrchestrationCheckStatusResponse -Request $Request -InstanceId $InstanceId +Push-OutputBinding -Name Response -Value $Response + +Write-Host 'CustomStatusStart completed' diff --git a/src/Durable/Commands/SetDurableCustomStatusCommand.cs b/src/Durable/Commands/SetDurableCustomStatusCommand.cs new file mode 100644 index 00000000..83de20c2 --- /dev/null +++ b/src/Durable/Commands/SetDurableCustomStatusCommand.cs @@ -0,0 +1,29 @@ +// +// 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; + + [Cmdlet("Set", "DurableCustomStatus")] + public class SetDurableCustomStatusCommand : PSCmdlet + { + [Parameter( + Mandatory = true, + Position = 0, + ValueFromPipeline = true)] + public object CustomStatus { get; set; } + + protected override void EndProcessing() + { + var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; + var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey]; + context.CustomStatus = CustomStatus; + } + } +} diff --git a/src/Durable/OrchestrationContext.cs b/src/Durable/OrchestrationContext.cs index 4b99a3d7..0d11acee 100644 --- a/src/Durable/OrchestrationContext.cs +++ b/src/Durable/OrchestrationContext.cs @@ -31,8 +31,10 @@ public class OrchestrationContext [DataMember] internal HistoryEvent[] History { get; set; } - public DateTime CurrentUtcDateTime {get; internal set; } + public DateTime CurrentUtcDateTime { get; internal set; } internal OrchestrationActionCollector OrchestrationActionCollector { get; } = new OrchestrationActionCollector(); + + internal object CustomStatus { get; set; } } } diff --git a/src/Durable/OrchestrationFailureException.cs b/src/Durable/OrchestrationFailureException.cs index 66203531..6cbe20d2 100644 --- a/src/Durable/OrchestrationFailureException.cs +++ b/src/Durable/OrchestrationFailureException.cs @@ -24,17 +24,23 @@ public OrchestrationFailureException() { } - public OrchestrationFailureException(List> actions, Exception innerException) - : base(FormatOrchestrationFailureMessage(actions, innerException), innerException) + public OrchestrationFailureException( + List> actions, + object customStatus, + Exception innerException) + : base(FormatOrchestrationFailureMessage(actions, customStatus, innerException), innerException) { } - private static string FormatOrchestrationFailureMessage(List> actions, Exception exception) + private static string FormatOrchestrationFailureMessage( + List> actions, + object customStatus, + Exception exception) { // For more details on why this message looks like this, see: // - https://github.com/Azure/azure-functions-durable-js/pull/145 // - https://github.com/Azure/azure-functions-durable-extension/pull/1171 - var orchestrationMessage = new OrchestrationMessage(isDone: false, actions, output: null, exception.Message); + var orchestrationMessage = new OrchestrationMessage(isDone: false, actions, output: null, customStatus, exception.Message); var message = $"{exception.Message}{OutOfProcDataLabel}{JsonConvert.SerializeObject(orchestrationMessage)}"; return message; } diff --git a/src/Durable/OrchestrationInvoker.cs b/src/Durable/OrchestrationInvoker.cs index 1d3721bb..fef557ba 100644 --- a/src/Durable/OrchestrationInvoker.cs +++ b/src/Durable/OrchestrationInvoker.cs @@ -40,7 +40,7 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe { // The orchestration function should be stopped and restarted pwsh.StopInvoke(); - return CreateOrchestrationResult(isDone: false, actions, output: null); + return CreateOrchestrationResult(isDone: false, actions, output: null, context.CustomStatus); } else { @@ -49,13 +49,13 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe // The orchestration function completed pwsh.EndInvoke(asyncResult); var result = FunctionReturnValueBuilder.CreateReturnValueFromFunctionOutput(outputBuffer); - return CreateOrchestrationResult(isDone: true, actions, output: result); + 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, e); + throw new OrchestrationFailureException(actions, context.CustomStatus, e); } } } @@ -68,9 +68,10 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe private static Hashtable CreateOrchestrationResult( bool isDone, List> actions, - object output) + object output, + object customStatus) { - var orchestrationMessage = new OrchestrationMessage(isDone, actions, output); + var orchestrationMessage = new OrchestrationMessage(isDone, actions, output, customStatus); return new Hashtable { { AzFunctionInfo.DollarReturn, orchestrationMessage } }; } } diff --git a/src/Durable/OrchestrationMessage.cs b/src/Durable/OrchestrationMessage.cs index 09315e98..37293ee9 100644 --- a/src/Durable/OrchestrationMessage.cs +++ b/src/Durable/OrchestrationMessage.cs @@ -14,12 +14,18 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable /// internal class OrchestrationMessage { - public OrchestrationMessage(bool isDone, List> actions, object output, string error = null) + public OrchestrationMessage( + bool isDone, + List> actions, + object output, + object customStatus, + string error = null) { IsDone = isDone; Actions = actions; Output = output; Error = error; + CustomStatus = customStatus; } /// @@ -41,5 +47,10 @@ public OrchestrationMessage(bool isDone, List> actions /// The orchestration error message. /// public readonly string Error; + + /// + /// Custom orchestration status. + /// + public readonly object CustomStatus; } } 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 7b24724b..ee6bcd24 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 @@ CmdletsToExport = @( 'Get-OutputBinding', 'Invoke-ActivityFunction', 'Push-OutputBinding', + 'Set-DurableCustomStatus', 'Set-FunctionInvocationContext', 'Start-DurableExternalEventListener', 'Start-DurableTimer', 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 2fd4e132..3890a825 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 @@ -65,6 +65,8 @@ public async Task DurableClientFollowsAsyncPattern() Assert.True(false, $"The orchestration has not completed after {orchestrationCompletionTimeout}"); } + Assert.Equal("Custom status: started", (string)statusResponseBody.customStatus); + await Task.Delay(TimeSpan.FromSeconds(2)); break; } @@ -77,6 +79,7 @@ public async Task DurableClientFollowsAsyncPattern() Assert.Equal("Hello Seattle", statusResponseBody.output[1].ToString()); Assert.Equal("Hello London", statusResponseBody.output[2].ToString()); Assert.Equal("Hello Toronto", statusResponseBody.output[3].ToString()); + Assert.Equal("Custom status: finished", (string)statusResponseBody.customStatus); return; } diff --git a/test/E2E/TestFunctionApp/DurableOrchestrator/run.ps1 b/test/E2E/TestFunctionApp/DurableOrchestrator/run.ps1 index 11a7d588..8122ad9d 100644 --- a/test/E2E/TestFunctionApp/DurableOrchestrator/run.ps1 +++ b/test/E2E/TestFunctionApp/DurableOrchestrator/run.ps1 @@ -6,6 +6,8 @@ $ErrorActionPreference = 'Stop' Write-Host "DurableOrchestrator: started. Input: $($Context.Input)" +Set-DurableCustomStatus -CustomStatus 'Custom status: started' + # Function chaining $output = @() $output += Invoke-ActivityFunction -FunctionName "DurableActivity" -Input "Tokyo" @@ -21,6 +23,8 @@ $retryOptions = New-DurableRetryOptions -FirstRetryInterval (New-Timespan -Secon $inputData = @{ Name = 'Toronto'; StartTime = $Context.CurrentUtcDateTime } $output += Invoke-ActivityFunction -FunctionName "DurableActivityFlaky" -Input $inputData -RetryOptions $retryOptions +Set-DurableCustomStatus -CustomStatus 'Custom status: finished' + Write-Host "DurableOrchestrator: finished." return $output diff --git a/test/Unit/Durable/OrchestrationFailureExceptionTests.cs b/test/Unit/Durable/OrchestrationFailureExceptionTests.cs index af0fa95c..8b3a555d 100644 --- a/test/Unit/Durable/OrchestrationFailureExceptionTests.cs +++ b/test/Unit/Durable/OrchestrationFailureExceptionTests.cs @@ -20,7 +20,7 @@ public class OrchestrationFailureExceptionTests [Fact] public void MessageContainsInnerExceptionMessage() { - var e = new OrchestrationFailureException(new List>(), _innerException); + var e = new OrchestrationFailureException(new List>(), customStatus: null, _innerException); var labelPos = e.Message.IndexOf(OrchestrationFailureException.OutOfProcDataLabel); Assert.Equal(_innerException.Message, e.Message.Substring(0, labelPos)); @@ -35,8 +35,10 @@ public void MessageContainsSerializedOrchestrationMessage() new CallActivityAction("activity2", "input2") } }; - - var e = new OrchestrationFailureException(actions, _innerException); + + const string CustomStatus = "my custom status"; + + var e = new OrchestrationFailureException(actions, customStatus: CustomStatus, _innerException); var labelPos = e.Message.IndexOf(OrchestrationFailureException.OutOfProcDataLabel); var startPos = labelPos + OrchestrationFailureException.OutOfProcDataLabel.Length; @@ -45,6 +47,7 @@ public void MessageContainsSerializedOrchestrationMessage() Assert.False((bool)orchestrationMessage.IsDone); Assert.Null(orchestrationMessage.Output.Value); Assert.Equal(_innerException.Message, (string)orchestrationMessage.Error); + Assert.Equal(CustomStatus, (string)orchestrationMessage.CustomStatus); var deserializedActions = (IEnumerable)((IEnumerable)orchestrationMessage.Actions).Single(); for (var i = 0; i < actions.Single().Count(); i++) { From ef1aa3777ce789f10594f96616f84a8a11c41aa4 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Tue, 23 Feb 2021 20:52:46 -0800 Subject: [PATCH 2/3] Make DurableClientFollowsAsyncPattern E2E wait for custom status --- .../DurableEndToEndTests.cs | 3 +++ 1 file changed, 3 insertions(+) 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 3890a825..86e94877 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 @@ -45,6 +45,9 @@ public async Task DurableClientFollowsAsyncPattern() var orchestrationCompletionTimeout = TimeSpan.FromSeconds(90); var startTime = DateTime.UtcNow; + // Allow the orchestration to proceed until the first custom status is set + await Task.Delay(TimeSpan.FromSeconds(5)); + using (var httpClient = new HttpClient()) { while (true) From 010e0601f2b7c053b7934f2d8f86d7ea3311ec69 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Tue, 23 Feb 2021 21:05:13 -0800 Subject: [PATCH 3/3] Wait for 10 sec --- .../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 86e94877..a1fb3c07 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 @@ -46,7 +46,7 @@ public async Task DurableClientFollowsAsyncPattern() var startTime = DateTime.UtcNow; // Allow the orchestration to proceed until the first custom status is set - await Task.Delay(TimeSpan.FromSeconds(5)); + await Task.Delay(TimeSpan.FromSeconds(10)); using (var httpClient = new HttpClient()) {