Skip to content

Commit 483ab47

Browse files
davidmrdavidGreg Roll
andauthored
Add Get-TaskResult CmdLet for DF (#786)
Co-authored-by: Greg Roll <[email protected]>
1 parent 54b90b9 commit 483ab47

File tree

13 files changed

+148
-16
lines changed

13 files changed

+148
-16
lines changed

release_notes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
* 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)
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)
Lines changed: 36 additions & 0 deletions
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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,21 @@ public void WaitAny(
206206
}
207207
}
208208

209+
public void GetTaskResult(
210+
IReadOnlyCollection<DurableTask> tasksToQueryResultFor,
211+
OrchestrationContext context,
212+
Action<object> output)
213+
{
214+
foreach (var task in tasksToQueryResultFor) {
215+
var scheduledHistoryEvent = task.GetScheduledHistoryEvent(context, true);
216+
var processedHistoryEvent = task.GetCompletedHistoryEvent(context, scheduledHistoryEvent, true);
217+
if (processedHistoryEvent != null)
218+
{
219+
output(GetEventResult(processedHistoryEvent));
220+
}
221+
}
222+
}
223+
209224
public void Stop()
210225
{
211226
_waitForStop.Set();

src/Durable/Tasks/ActivityInvocationTask.cs

Lines changed: 3 additions & 3 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ function New-DurableOrchestrationCheckStatusResponse {
243243
The TaskHubName of the orchestration instance that will handle the external event.
244244
.PARAMETER ConnectionName
245245
The name of the connection string associated with TaskHubName
246+
.PARAMETER AppCode
247+
The Azure Functions system key
246248
#>
247249
function Send-DurableExternalEvent {
248250
[CmdletBinding()]
@@ -271,12 +273,16 @@ function Send-DurableExternalEvent {
271273

272274
[Parameter(
273275
ValueFromPipelineByPropertyName=$true)]
274-
[string] $ConnectionName
276+
[string] $ConnectionName,
277+
278+
[Parameter(
279+
ValueFromPipelineByPropertyName=$true)]
280+
[string] $AppCode
275281
)
276282

277283
$DurableClient = GetDurableClientFromModulePrivateData
278284

279-
$RequestUrl = GetRaiseEventUrl -DurableClient $DurableClient -InstanceId $InstanceId -EventName $EventName -TaskHubName $TaskHubName -ConnectionName $ConnectionName
285+
$RequestUrl = GetRaiseEventUrl -DurableClient $DurableClient -InstanceId $InstanceId -EventName $EventName -TaskHubName $TaskHubName -ConnectionName $ConnectionName -AppCode $AppCode
280286

281287
$Body = $EventData | ConvertTo-Json -Compress
282288

@@ -288,7 +294,8 @@ function GetRaiseEventUrl(
288294
[string] $InstanceId,
289295
[string] $EventName,
290296
[string] $TaskHubName,
291-
[string] $ConnectionName) {
297+
[string] $ConnectionName,
298+
[string] $AppCode) {
292299

293300
$RequestUrl = $DurableClient.BaseUrl + "/instances/$InstanceId/raiseEvent/$EventName"
294301

@@ -299,6 +306,9 @@ function GetRaiseEventUrl(
299306
if ($null -eq $ConnectionName) {
300307
$query += "connection=$ConnectionName"
301308
}
309+
if ($null -eq $AppCode) {
310+
$query += "code=$AppCode"
311+
}
302312
if ($query.Count -gt 0) {
303313
$RequestUrl += "?" + [string]::Join("&", $query)
304314
}

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,58 @@ public async Task ExternalEventReturnsData()
262262
}
263263
}
264264

265+
[Fact]
266+
public async Task OrchestratationCanAlwaysObtainTaskResult()
267+
{
268+
var initialResponse = await Utilities.GetHttpTriggerResponse("DurableClient", queryString: "?FunctionName=DurableOrchestratorGetTaskResult");
269+
Assert.Equal(HttpStatusCode.Accepted, initialResponse.StatusCode);
270+
271+
var initialResponseBody = await initialResponse.Content.ReadAsStringAsync();
272+
dynamic initialResponseBodyObject = JsonConvert.DeserializeObject(initialResponseBody);
273+
var statusQueryGetUri = (string)initialResponseBodyObject.statusQueryGetUri;
274+
275+
var startTime = DateTime.UtcNow;
276+
277+
using (var httpClient = new HttpClient())
278+
{
279+
while (true)
280+
{
281+
var statusResponse = await httpClient.GetAsync(statusQueryGetUri);
282+
switch (statusResponse.StatusCode)
283+
{
284+
case HttpStatusCode.Accepted:
285+
{
286+
var statusResponseBody = await GetResponseBodyAsync(statusResponse);
287+
var runtimeStatus = (string)statusResponseBody.runtimeStatus;
288+
Assert.True(
289+
runtimeStatus == "Running" || runtimeStatus == "Pending",
290+
$"Unexpected runtime status: {runtimeStatus}");
291+
292+
if (DateTime.UtcNow > startTime + _orchestrationCompletionTimeout)
293+
{
294+
Assert.True(false, $"The orchestration has not completed after {_orchestrationCompletionTimeout}");
295+
}
296+
297+
await Task.Delay(TimeSpan.FromSeconds(2));
298+
break;
299+
}
300+
301+
case HttpStatusCode.OK:
302+
{
303+
var statusResponseBody = await GetResponseBodyAsync(statusResponse);
304+
Assert.Equal("Completed", (string)statusResponseBody.runtimeStatus);
305+
Assert.Equal("Hello world", statusResponseBody.output.ToString());
306+
return;
307+
}
308+
309+
default:
310+
Assert.True(false, $"Unexpected orchestration status code: {statusResponse.StatusCode}");
311+
break;
312+
}
313+
}
314+
}
315+
}
316+
265317
[Fact]
266318
public async Task ActivityCanHaveQueueBinding()
267319
{
Lines changed: 9 additions & 0 deletions
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+
}
Lines changed: 8 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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)