Skip to content

Commit d184e72

Browse files
Add Get-TaskResult CmdLet for DF (#786)
Co-authored-by: Greg Roll <[email protected]>
1 parent 6dd32c3 commit d184e72

File tree

13 files changed

+263
-16
lines changed

13 files changed

+263
-16
lines changed

release_notes.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
* Bug fix: Activity Functions can now use output bindings (https://github.com/Azure/azure-functions-powershell-worker/issues/646)
1+
* Bug fix: [Context.InstanceId can now be accessed](https://github.com/Azure/azure-functions-powershell-worker/issues/727)
2+
* Bug fix: [Data in External Events is now read and returned to orchestrator](https://github.com/Azure/azure-functions-powershell-worker/issues/68)
3+
* New feature (external contribution): [Get-TaskResult can now be used to obtain the result of an already-completed Durable Functions Task](https://github.com/Azure/azure-functions-powershell-worker/pull/786)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
#pragma warning disable 1591 // Missing XML comment for publicly visible type or member 'member'
7+
8+
namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Commands
9+
{
10+
using System.Collections;
11+
using System.Management.Automation;
12+
using Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks;
13+
14+
[Cmdlet("Get", "DurableTaskResult")]
15+
public class GetDurableTaskResultCommand : PSCmdlet
16+
{
17+
[Parameter(Mandatory = true)]
18+
[ValidateNotNull]
19+
public DurableTask[] Task { get; set; }
20+
21+
private readonly DurableTaskHandler _durableTaskHandler = new DurableTaskHandler();
22+
23+
protected override void EndProcessing()
24+
{
25+
var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData;
26+
var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey];
27+
28+
_durableTaskHandler.GetTaskResult(Task, context, WriteObject);
29+
}
30+
31+
protected override void StopProcessing()
32+
{
33+
_durableTaskHandler.Stop();
34+
}
35+
}
36+
}

src/Durable/DurableTaskHandler.cs

+15
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,21 @@ public void WaitAny(
195195
}
196196
}
197197

198+
public void GetTaskResult(
199+
IReadOnlyCollection<DurableTask> tasksToQueryResultFor,
200+
OrchestrationContext context,
201+
Action<object> output)
202+
{
203+
foreach (var task in tasksToQueryResultFor) {
204+
var scheduledHistoryEvent = task.GetScheduledHistoryEvent(context, true);
205+
var processedHistoryEvent = task.GetCompletedHistoryEvent(context, scheduledHistoryEvent, true);
206+
if (processedHistoryEvent != null)
207+
{
208+
output(GetEventResult(processedHistoryEvent));
209+
}
210+
}
211+
}
212+
198213
public void Stop()
199214
{
200215
_waitForStop.Set();

src/Durable/Tasks/ActivityInvocationTask.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ internal ActivityInvocationTask(string functionName, object functionInput)
3737
{
3838
}
3939

40-
internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context)
40+
internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed)
4141
{
4242
return context.History.FirstOrDefault(
4343
e => e.EventType == HistoryEventType.TaskScheduled &&
4444
e.Name == FunctionName &&
45-
!e.IsProcessed);
45+
e.IsProcessed == processed);
4646
}
4747

48-
internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent)
48+
internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent, bool processed)
4949
{
5050
return scheduledHistoryEvent == null
5151
? null

src/Durable/Tasks/DurableTask.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Tasks
1111

1212
public abstract class DurableTask
1313
{
14-
internal abstract HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context);
14+
internal abstract HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed = false);
1515

16-
internal abstract HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent);
16+
internal abstract HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent, bool processed = false);
1717

1818
internal abstract OrchestrationAction CreateOrchestrationAction();
1919
}

src/Durable/Tasks/DurableTimerTask.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ internal DurableTimerTask(
2828
Action = new CreateDurableTimerAction(FireAt);
2929
}
3030

31-
internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context)
31+
internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed)
3232
{
3333
return context.History.FirstOrDefault(
3434
e => e.EventType == HistoryEventType.TimerCreated &&
3535
e.FireAt.Equals(FireAt) &&
3636
!e.IsProcessed);
3737
}
3838

39-
internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent)
39+
internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent scheduledHistoryEvent, bool processed)
4040
{
4141
return scheduledHistoryEvent == null
4242
? null

src/Durable/Tasks/ExternalEventTask.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@ public ExternalEventTask(string externalEventName)
2121
}
2222

2323
// There is no corresponding history event for an expected external event
24-
internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context)
24+
internal override HistoryEvent GetScheduledHistoryEvent(OrchestrationContext context, bool processed)
2525
{
2626
return null;
2727
}
2828

29-
internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent taskScheduled)
29+
internal override HistoryEvent GetCompletedHistoryEvent(OrchestrationContext context, HistoryEvent taskScheduled, bool processed)
3030
{
3131
return context.History.FirstOrDefault(
3232
e => e.EventType == HistoryEventType.EventRaised &&
3333
e.Name == ExternalEventName &&
34-
!e.IsProcessed);
34+
e.IsPlayed == processed);
3535
}
3636

3737
internal override OrchestrationAction CreateOrchestrationAction()

src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psd1

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ FunctionsToExport = @(
5959
# 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.
6060
CmdletsToExport = @(
6161
'Get-OutputBinding',
62+
'Get-DurableTaskResult'
6263
'Invoke-DurableActivity',
6364
'Push-OutputBinding',
6465
'Set-DurableCustomStatus',

src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1

+13-3
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ function New-DurableOrchestrationCheckStatusResponse {
235235
The TaskHubName of the orchestration instance that will handle the external event.
236236
.PARAMETER ConnectionName
237237
The name of the connection string associated with TaskHubName
238+
.PARAMETER AppCode
239+
The Azure Functions system key
238240
#>
239241
function Send-DurableExternalEvent {
240242
[CmdletBinding()]
@@ -263,12 +265,16 @@ function Send-DurableExternalEvent {
263265

264266
[Parameter(
265267
ValueFromPipelineByPropertyName=$true)]
266-
[string] $ConnectionName
268+
[string] $ConnectionName,
269+
270+
[Parameter(
271+
ValueFromPipelineByPropertyName=$true)]
272+
[string] $AppCode
267273
)
268274

269275
$DurableClient = GetDurableClientFromModulePrivateData
270276

271-
$RequestUrl = GetRaiseEventUrl -DurableClient $DurableClient -InstanceId $InstanceId -EventName $EventName -TaskHubName $TaskHubName -ConnectionName $ConnectionName
277+
$RequestUrl = GetRaiseEventUrl -DurableClient $DurableClient -InstanceId $InstanceId -EventName $EventName -TaskHubName $TaskHubName -ConnectionName $ConnectionName -AppCode $AppCode
272278

273279
$Body = $EventData | ConvertTo-Json -Compress
274280

@@ -280,7 +286,8 @@ function GetRaiseEventUrl(
280286
[string] $InstanceId,
281287
[string] $EventName,
282288
[string] $TaskHubName,
283-
[string] $ConnectionName) {
289+
[string] $ConnectionName,
290+
[string] $AppCode) {
284291

285292
$RequestUrl = $DurableClient.BaseUrl + "/instances/$InstanceId/raiseEvent/$EventName"
286293

@@ -291,6 +298,9 @@ function GetRaiseEventUrl(
291298
if ($null -eq $ConnectionName) {
292299
$query += "connection=$ConnectionName"
293300
}
301+
if ($null -eq $AppCode) {
302+
$query += "code=$AppCode"
303+
}
294304
if ($query.Count -gt 0) {
295305
$RequestUrl += "?" + [string]::Join("&", $query)
296306
}

test/E2E/Azure.Functions.PowerShellWorker.E2E/Azure.Functions.PowerShellWorker.E2E/DurableEndToEndTests.cs

+166
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,172 @@ public async Task LegacyDurableCommandNamesStillWork()
147147
}
148148
}
149149

150+
[Fact]
151+
public async Task OrchestratationContextHasAllExpectedProperties()
152+
{
153+
var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClientOrchContextProperties", queryString: string.Empty);
154+
Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode);
155+
156+
var initialResponseBody = await initialResponse.Content.ReadAsStringAsync();
157+
dynamic initialResponseBodyObject = JsonConvert.DeserializeObject(initialResponseBody);
158+
var statusQueryGetUri = (string)initialResponseBodyObject.statusQueryGetUri;
159+
160+
var startTime = DateTime.UtcNow;
161+
162+
using (var httpClient = new HttpClient())
163+
{
164+
while (true)
165+
{
166+
var statusResponse = await httpClient.GetAsync(statusQueryGetUri);
167+
switch (statusResponse.StatusCode)
168+
{
169+
case HttpStatusCode.Accepted:
170+
{
171+
var statusResponseBody = await GetResponseBodyAsync(statusResponse);
172+
var runtimeStatus = (string)statusResponseBody.runtimeStatus;
173+
Assert.True(
174+
runtimeStatus == "Running" || runtimeStatus == "Pending",
175+
$"Unexpected runtime status: {runtimeStatus}");
176+
177+
if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout)
178+
{
179+
Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}");
180+
}
181+
182+
await Task.Delay(TimeSpan.FromSeconds(2));
183+
break;
184+
}
185+
186+
case HttpStatusCode.OK:
187+
{
188+
var statusResponseBody = await GetResponseBodyAsync(statusResponse);
189+
Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus);
190+
Assert.Equal("True", statusResponseBody.output[0].ToString());
191+
Assert.Equal("Hello myInstanceId", statusResponseBody.output[1].ToString());
192+
Assert.Equal("False", statusResponseBody.output[2].ToString());
193+
return;
194+
}
195+
196+
default:
197+
Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}");
198+
break;
199+
}
200+
}
201+
}
202+
}
203+
204+
[Fact]
205+
public async Task ExternalEventReturnsData()
206+
{
207+
var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClient", queryString: "?FunctionName=DurableOrchestratorRaiseEvent");
208+
Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode);
209+
210+
var initialResponseBody = await initialResponse.Content.ReadAsStringAsync();
211+
dynamic initialResponseBodyObject = JsonConvert.DeserializeObject(initialResponseBody);
212+
var statusQueryGetUri = (string)initialResponseBodyObject.statusQueryGetUri;
213+
var raiseEventUri = (string)initialResponseBodyObject.sendEventPostUri;
214+
215+
raiseEventUri = raiseEventUri.Replace("{eventName}", "TESTEVENTNAME");
216+
217+
var startTime = DateTime.UtcNow;
218+
219+
using (var httpClient = new HttpClient())
220+
{
221+
while (true)
222+
{
223+
// Send external event payload
224+
var json = JsonConvert.SerializeObject("helloWorld!");
225+
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
226+
await httpClient.PostAsync(raiseEventUri, httpContent);
227+
228+
var statusResponse = await httpClient.GetAsync(statusQueryGetUri);
229+
switch (statusResponse.StatusCode)
230+
{
231+
case HttpStatusCode.Accepted:
232+
{
233+
var statusResponseBody = await GetResponseBodyAsync(statusResponse);
234+
var runtimeStatus = (string)statusResponseBody.runtimeStatus;
235+
Assert.True(
236+
runtimeStatus == "Running" || runtimeStatus == "Pending",
237+
$"Unexpected runtime status: {runtimeStatus}");
238+
239+
if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout)
240+
{
241+
Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}");
242+
}
243+
244+
await Task.Delay(TimeSpan.FromSeconds(2));
245+
break;
246+
}
247+
248+
case HttpStatusCode.OK:
249+
{
250+
var statusResponseBody = await GetResponseBodyAsync(statusResponse);
251+
Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus);
252+
Assert.Equal("helloWorld!", statusResponseBody.output.ToString());
253+
return;
254+
}
255+
256+
default:
257+
Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}");
258+
break;
259+
}
260+
}
261+
}
262+
}
263+
264+
[Fact]
265+
public async Task OrchestratationCanAlwaysObtainTaskResult()
266+
{
267+
var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClient", queryString: "?FunctionName=DurableOrchestratorGetTaskResult");
268+
Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode);
269+
270+
var initialResponseBody = await initialResponse.Content.ReadAsStringAsync();
271+
dynamic initialResponseBodyObject = JsonConvert.DeserializeObject(initialResponseBody);
272+
var statusQueryGetUri = (string)initialResponseBodyObject.statusQueryGetUri;
273+
274+
var startTime = DateTime.UtcNow;
275+
276+
using (var httpClient = new HttpClient())
277+
{
278+
while (true)
279+
{
280+
var statusResponse = await httpClient.GetAsync(statusQueryGetUri);
281+
switch (statusResponse.StatusCode)
282+
{
283+
case HttpStatusCode.Accepted:
284+
{
285+
var statusResponseBody = await GetResponseBodyAsync(statusResponse);
286+
var runtimeStatus = (string)statusResponseBody.runtimeStatus;
287+
Assert.True(
288+
runtimeStatus == "Running" || runtimeStatus == "Pending",
289+
$"Unexpected runtime status: {runtimeStatus}");
290+
291+
if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout)
292+
{
293+
Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}");
294+
}
295+
296+
await Task.Delay(TimeSpan.FromSeconds(2));
297+
break;
298+
}
299+
300+
case HttpStatusCode.OK:
301+
{
302+
var statusResponseBody = await GetResponseBodyAsync(statusResponse);
303+
Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus);
304+
Assert.Equal("Hello world", statusResponseBody.output.ToString());
305+
return;
306+
}
307+
308+
default:
309+
Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}");
310+
break;
311+
}
312+
}
313+
}
314+
}
315+
150316
[Fact]
151317
public async Task ActivityCanHaveQueueBinding()
152318
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"bindings": [
3+
{
4+
"name": "Context",
5+
"type": "orchestrationTrigger",
6+
"direction": "in"
7+
}
8+
]
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
param($Context)
2+
3+
$output = @()
4+
5+
$task = Invoke-DurableActivity -FunctionName 'DurableActivity' -Input "world" -NoWait
6+
$firstTask = Wait-DurableTask -Task @($task) -Any
7+
$output += Get-DurableTaskResult -Task @($firstTask)
8+
$output

test/Unit/Durable/ActivityInvocationTaskTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ public void GetCompletedHistoryEvent_ReturnsTaskCompletedOrTaskFailed(bool succe
225225
var orchestrationContext = new OrchestrationContext { History = history };
226226

227227
var task = new ActivityInvocationTask(FunctionName, FunctionInput);
228-
var scheduledEvent = task.GetScheduledHistoryEvent(orchestrationContext);
229-
var completedEvent = task.GetCompletedHistoryEvent(orchestrationContext, scheduledEvent);
228+
var scheduledEvent = task.GetScheduledHistoryEvent(orchestrationContext, false);
229+
var completedEvent = task.GetCompletedHistoryEvent(orchestrationContext, scheduledEvent, false);
230230

231231
Assert.Equal(scheduledEvent.EventId, completedEvent.TaskScheduledId);
232232
}

0 commit comments

Comments
 (0)