From 004591dd95bd18a1d873bfbb39541c16e3899f69 Mon Sep 17 00:00:00 2001 From: David Justo Date: Tue, 24 May 2022 16:27:58 -0700 Subject: [PATCH] Port "Fix various DF bugs" to v4.x/ps7.0 (#796) * Fix various DF bugs (#783) * remove release_notes.md --- release_notes.md | 1 - src/Durable/DurableTaskHandler.cs | 13 +- src/Durable/OrchestrationContext.cs | 4 +- ...soft.Azure.Functions.PowerShellWorker.psm1 | 12 +- .../DurableEndToEndTests.cs | 115 ++++++++++++++++++ .../function.json | 24 ++++ .../run.ps1 | 9 ++ .../function.json | 9 ++ .../run.ps1 | 8 ++ .../function.json | 9 ++ .../DurableOrchestratorRaiseEvent/run.ps1 | 7 ++ 11 files changed, 205 insertions(+), 6 deletions(-) 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/release_notes.md b/release_notes.md index c636147d..e69de29b 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1 +0,0 @@ -* Bug fix: Activity Functions can now use output bindings (https://github.com/Azure/azure-functions-powershell-worker/issues/646) \ No newline at end of file diff --git a/src/Durable/DurableTaskHandler.cs b/src/Durable/DurableTaskHandler.cs index 3981b686..a49c1148 100644 --- a/src/Durable/DurableTaskHandler.cs +++ b/src/Durable/DurableTaskHandler.cs @@ -47,6 +47,7 @@ public void StopAndInitiateDurableTaskOrReplay( } completedHistoryEvent.IsProcessed = true; + context.IsReplaying = completedHistoryEvent.IsPlayed; switch (completedHistoryEvent.EventType) { @@ -57,7 +58,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) { @@ -126,6 +133,7 @@ public void WaitAll( var allTasksCompleted = completedEvents.Count == tasksToWaitFor.Count; if (allTasksCompleted) { + context.IsReplaying = completedEvents.Count == 0 ? false : completedEvents[0].IsPlayed; CurrentUtcDateTimeUpdater.UpdateCurrentUtcDateTime(context); foreach (var completedHistoryEvent in completedEvents) @@ -164,6 +172,7 @@ public void WaitAny( if (scheduledHistoryEvent != null) { scheduledHistoryEvent.IsProcessed = true; + scheduledHistoryEvent.IsPlayed = true; } if (completedHistoryEvent != null) @@ -179,12 +188,14 @@ public void WaitAny( } completedHistoryEvent.IsProcessed = true; + completedHistoryEvent.IsPlayed = true; } } 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/Durable/OrchestrationContext.cs b/src/Durable/OrchestrationContext.cs index 0d11acee..27f082db 100644 --- a/src/Durable/OrchestrationContext.cs +++ b/src/Durable/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; } 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..2fa1ee02 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 @@ -80,6 +80,8 @@ function Get-DurableStatus { The input value that will be passed to the orchestration Azure Function. .PARAMETER DurableClient The orchestration client object. +.PARAMETER InstanceId + The InstanceId for the new orchestration. #> function Start-DurableOrchestration { [CmdletBinding()] @@ -98,7 +100,11 @@ function Start-DurableOrchestration { [Parameter( ValueFromPipelineByPropertyName=$true)] - [object] $DurableClient + [object] $DurableClient, + + [Parameter( + ValueFromPipelineByPropertyName=$true)] + [string] $InstanceId ) $ErrorActionPreference = 'Stop' @@ -107,7 +113,9 @@ function Start-DurableOrchestration { $DurableClient = GetDurableClientFromModulePrivateData } - $InstanceId = (New-Guid).Guid + 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 ddc20c5e..181d294e 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 @@ -12,6 +12,7 @@ namespace Azure.Functions.PowerShell.Tests.E2E using System.Net.Http; using Newtonsoft.Json; + using System.Text; [Collection(Constants.FunctionAppCollectionName)] public class DurableEndToEndTests @@ -147,6 +148,120 @@ 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("True", statusResponseBody.output[0].ToString()); + Assert.Equal("Hello myInstanceId", statusResponseBody.output[1].ToString()); + Assert.Equal("False", statusResponseBody.output[2].ToString()); + return; + } + + default: + Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}"); + break; + } + } + } + } + + [Fact] + public async Task ExternalEventReturnsData() + { + var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClient", queryString: "?FunctionName=DurableOrchestratorRaiseEvent"); + Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode); + + var initialResponseBody = await initialResponse.Content.ReadAsStringAsync(); + dynamic initialResponseBodyObject = JsonConvert.DeserializeObject(initialResponseBody); + var statusQueryGetUri = (string)initialResponseBodyObject.statusQueryGetUri; + var raiseEventUri = (string)initialResponseBodyObject.sendEventPostUri; + + raiseEventUri = raiseEventUri.Replace("{eventName}", "TESTEVENTNAME"); + + var startTime = DateTime.UtcNow; + + using (var httpClient = new HttpClient()) + { + while (true) + { + // Send external event payload + var json = JsonConvert.SerializeObject("helloWorld!"); + var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); + await httpClient.PostAsync(raiseEventUri, httpContent); + + 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("helloWorld!", statusResponseBody.output.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..228d57f6 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableClientOrchContextProperties/run.ps1 @@ -0,0 +1,9 @@ +using namespace System.Net + +param($Request, $TriggerMetadata) + +$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..031a5492 --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorAccessContextProps/run.ps1 @@ -0,0 +1,8 @@ +param($Context) + +$output = @() + +$output += $Context.IsReplaying +$output += Invoke-DurableActivity -FunctionName 'DurableActivity' -Input $Context.InstanceId +$output += $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..f7b9360b --- /dev/null +++ b/test/E2E/TestFunctionApp/DurableOrchestratorRaiseEvent/run.ps1 @@ -0,0 +1,7 @@ +param($Context) + +$output = @() + +$output += Start-DurableExternalEventListener -EventName "TESTEVENTNAME" + +$output