Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"bindings": [
{
"name": "Context",
"type": "orchestrationTrigger",
"direction": "in"
}
]
}
22 changes: 22 additions & 0 deletions examples/durable/DurableApp/CustomStatusOrchestrator/run.ps1
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions examples/durable/DurableApp/CustomStatusStart/function.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
13 changes: 13 additions & 0 deletions examples/durable/DurableApp/CustomStatusStart/run.ps1
Original file line number Diff line number Diff line change
@@ -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'
29 changes: 29 additions & 0 deletions src/Durable/Commands/SetDurableCustomStatusCommand.cs
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to assign a Position for this parameter? I took a look at the other Durable cmdlets and this is the only one with a Positional parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to be very careful with adding positional parameters in general, but in this case I believe this is justified. The cmdlet takes just one parameter, and the purpose of this parameter is very obvious, so I want to allow users omit the parameter name: Set-DurableCustomStatus "my status".

I think we can carefully allow some positional parameters for some other durable-related cmdlets, but we can do this later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good @AnatoliB, thank you.

ValueFromPipeline = true)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would the use case be here for ValueFromPipeline? Something like:

# 1)
'Processing Tokyo' | Set-DurableCustomStatus

# 2)
 @{ ProgressMessage = 'Processing Seattle'; Stage = 2 }  | Set-DurableCustomStatus

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly.

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;
}
}
}
4 changes: 3 additions & 1 deletion src/Durable/OrchestrationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
14 changes: 10 additions & 4 deletions src/Durable/OrchestrationFailureException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,23 @@ public OrchestrationFailureException()
{
}

public OrchestrationFailureException(List<List<OrchestrationAction>> actions, Exception innerException)
: base(FormatOrchestrationFailureMessage(actions, innerException), innerException)
public OrchestrationFailureException(
List<List<OrchestrationAction>> actions,
object customStatus,
Exception innerException)
: base(FormatOrchestrationFailureMessage(actions, customStatus, innerException), innerException)
{
}

private static string FormatOrchestrationFailureMessage(List<List<OrchestrationAction>> actions, Exception exception)
private static string FormatOrchestrationFailureMessage(
List<List<OrchestrationAction>> 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;
}
Expand Down
11 changes: 6 additions & 5 deletions src/Durable/OrchestrationInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
}
}
}
Expand All @@ -68,9 +68,10 @@ public Hashtable Invoke(OrchestrationBindingInfo orchestrationBindingInfo, IPowe
private static Hashtable CreateOrchestrationResult(
bool isDone,
List<List<OrchestrationAction>> 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 } };
}
}
Expand Down
13 changes: 12 additions & 1 deletion src/Durable/OrchestrationMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable
/// </summary>
internal class OrchestrationMessage
{
public OrchestrationMessage(bool isDone, List<List<OrchestrationAction>> actions, object output, string error = null)
public OrchestrationMessage(
bool isDone,
List<List<OrchestrationAction>> actions,
object output,
object customStatus,
string error = null)
{
IsDone = isDone;
Actions = actions;
Output = output;
Error = error;
CustomStatus = customStatus;
}

/// <summary>
Expand All @@ -41,5 +47,10 @@ public OrchestrationMessage(bool isDone, List<List<OrchestrationAction>> actions
/// The orchestration error message.
/// </summary>
public readonly string Error;

/// <summary>
/// Custom orchestration status.
/// </summary>
public readonly object CustomStatus;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ CmdletsToExport = @(
'Get-OutputBinding',
'Invoke-ActivityFunction',
'Push-OutputBinding',
'Set-DurableCustomStatus',
'Set-FunctionInvocationContext',
'Start-DurableExternalEventListener',
'Start-DurableTimer',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(10));

using (var httpClient = new HttpClient())
{
while (true)
Expand All @@ -65,6 +68,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;
}
Expand All @@ -77,6 +82,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;
}

Expand Down
4 changes: 4 additions & 0 deletions test/E2E/TestFunctionApp/DurableOrchestrator/run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
9 changes: 6 additions & 3 deletions test/Unit/Durable/OrchestrationFailureExceptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class OrchestrationFailureExceptionTests
[Fact]
public void MessageContainsInnerExceptionMessage()
{
var e = new OrchestrationFailureException(new List<List<OrchestrationAction>>(), _innerException);
var e = new OrchestrationFailureException(new List<List<OrchestrationAction>>(), customStatus: null, _innerException);

var labelPos = e.Message.IndexOf(OrchestrationFailureException.OutOfProcDataLabel);
Assert.Equal(_innerException.Message, e.Message.Substring(0, labelPos));
Expand All @@ -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;
Expand All @@ -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<dynamic>)((IEnumerable<dynamic>)orchestrationMessage.Actions).Single();
for (var i = 0; i < actions.Single().Count(); i++)
{
Expand Down