diff --git a/.gitignore b/.gitignore
index c0e90cb4..b807c462 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,8 +63,10 @@ PowerShell.sln.DotSettings.user
StyleCop.Cache
examples/PSCoreApp/Modules
-src/Modules
-!src/Modules/Microsoft.Azure.Functions.PowerShellWorker
+src/Modules/Microsoft.PowerShell.*
+src/Modules/PackageManagement
+src/Modules/PowerShellGet
+src/Modules/ThreadJob
# protobuf
protobuf/*
diff --git a/examples/durable/DurableApp/FlakyActivity/function.json b/examples/durable/DurableApp/FlakyActivity/function.json
new file mode 100644
index 00000000..806ce0f8
--- /dev/null
+++ b/examples/durable/DurableApp/FlakyActivity/function.json
@@ -0,0 +1,9 @@
+{
+ "bindings": [
+ {
+ "name": "name",
+ "type": "activityTrigger",
+ "direction": "in"
+ }
+ ]
+}
diff --git a/examples/durable/DurableApp/FlakyActivity/run.ps1 b/examples/durable/DurableApp/FlakyActivity/run.ps1
new file mode 100644
index 00000000..d5f86deb
--- /dev/null
+++ b/examples/durable/DurableApp/FlakyActivity/run.ps1
@@ -0,0 +1,9 @@
+param($name)
+
+# Intentional intermittent error
+$random = Get-Random -Minimum 0.0 -Maximum 1.0
+if ($random -gt 0.2) {
+ throw 'Nope, no luck this time...'
+}
+
+"Hello $name"
diff --git a/examples/durable/DurableApp/FunctionChainingWithRetriesOrchestrator/function.json b/examples/durable/DurableApp/FunctionChainingWithRetriesOrchestrator/function.json
new file mode 100644
index 00000000..0c950e30
--- /dev/null
+++ b/examples/durable/DurableApp/FunctionChainingWithRetriesOrchestrator/function.json
@@ -0,0 +1,9 @@
+{
+ "bindings": [
+ {
+ "name": "Context",
+ "type": "orchestrationTrigger",
+ "direction": "in"
+ }
+ ]
+}
diff --git a/examples/durable/DurableApp/FunctionChainingWithRetriesOrchestrator/run.ps1 b/examples/durable/DurableApp/FunctionChainingWithRetriesOrchestrator/run.ps1
new file mode 100644
index 00000000..ec1eef1c
--- /dev/null
+++ b/examples/durable/DurableApp/FunctionChainingWithRetriesOrchestrator/run.ps1
@@ -0,0 +1,17 @@
+using namespace System.Net
+
+param($Context)
+
+$ErrorActionPreference = 'Stop'
+
+$output = @()
+
+$retryOptions = New-DurableRetryOptions `
+ -FirstRetryInterval (New-Timespan -Seconds 1) `
+ -MaxNumberOfAttempts 7
+
+$output += Invoke-ActivityFunction -FunctionName 'FlakyActivity' -Input 'Tokyo' -RetryOptions $retryOptions
+$output += Invoke-ActivityFunction -FunctionName 'FlakyActivity' -Input 'Seattle' -RetryOptions $retryOptions
+$output += Invoke-ActivityFunction -FunctionName 'FlakyActivity' -Input 'London' -RetryOptions $retryOptions
+
+$output
diff --git a/examples/durable/DurableApp/FunctionChainingWithRetriesStart/function.json b/examples/durable/DurableApp/FunctionChainingWithRetriesStart/function.json
new file mode 100644
index 00000000..54e2a634
--- /dev/null
+++ b/examples/durable/DurableApp/FunctionChainingWithRetriesStart/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/FunctionChainingWithRetriesStart/run.ps1 b/examples/durable/DurableApp/FunctionChainingWithRetriesStart/run.ps1
new file mode 100644
index 00000000..5e5154fa
--- /dev/null
+++ b/examples/durable/DurableApp/FunctionChainingWithRetriesStart/run.ps1
@@ -0,0 +1,13 @@
+using namespace System.Net
+
+param($Request, $TriggerMetadata)
+
+Write-Host 'FunctionChainingWithRetriesStart started'
+
+$InstanceId = Start-NewOrchestration -FunctionName 'FunctionChainingWithRetriesOrchestrator' -InputObject 'Hello'
+Write-Host "Started orchestration with ID = '$InstanceId'"
+
+$Response = New-OrchestrationCheckStatusResponse -Request $Request -InstanceId $InstanceId
+Push-OutputBinding -Name Response -Value $Response
+
+Write-Host 'FunctionChainingWithRetriesStart completed'
diff --git a/src/Durable/Actions/CallActivityWithRetryAction.cs b/src/Durable/Actions/CallActivityWithRetryAction.cs
new file mode 100644
index 00000000..a65168f4
--- /dev/null
+++ b/src/Durable/Actions/CallActivityWithRetryAction.cs
@@ -0,0 +1,71 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Azure.Functions.PowerShellWorker.Durable.Actions
+{
+ ///
+ /// An orchestration action that represents calling an activity function with retry.
+ ///
+ internal class CallActivityWithRetryAction : OrchestrationAction
+ {
+ ///
+ /// The activity function name.
+ ///
+ public readonly string FunctionName;
+
+ ///
+ /// The input to the activity function.
+ ///
+ public readonly object Input;
+
+ ///
+ /// Retry options.
+ ///
+ public readonly Dictionary RetryOptions;
+
+ public CallActivityWithRetryAction(string functionName, object input, RetryOptions retryOptions)
+ : base(ActionType.CallActivityWithRetry)
+ {
+ FunctionName = functionName;
+ Input = input;
+ RetryOptions = ToDictionary(retryOptions);
+ }
+
+ private static Dictionary ToDictionary(RetryOptions retryOptions)
+ {
+ var result = new Dictionary()
+ {
+ { "firstRetryIntervalInMilliseconds", ToIntMilliseconds(retryOptions.FirstRetryInterval) },
+ { "maxNumberOfAttempts", retryOptions.MaxNumberOfAttempts }
+ };
+
+ AddOptionalValue(result, "backoffCoefficient", retryOptions.BackoffCoefficient, x => x);
+ AddOptionalValue(result, "maxRetryIntervalInMilliseconds", retryOptions.MaxRetryInterval, ToIntMilliseconds);
+ AddOptionalValue(result, "retryTimeoutInMilliseconds", retryOptions.RetryTimeout, ToIntMilliseconds);
+
+ return result;
+ }
+
+ private static void AddOptionalValue(
+ Dictionary dictionary,
+ string name,
+ T? nullable,
+ Func transformValue) where T : struct
+ {
+ if (nullable.HasValue)
+ {
+ dictionary.Add(name, transformValue(nullable.Value));
+ }
+ }
+
+ private static object ToIntMilliseconds(TimeSpan timespan)
+ {
+ return (int)timespan.TotalMilliseconds;
+ }
+ }
+}
diff --git a/src/Durable/Commands/InvokeActivityFunctionCommand.cs b/src/Durable/Commands/InvokeActivityFunctionCommand.cs
index ac0384d2..7dda9e35 100644
--- a/src/Durable/Commands/InvokeActivityFunctionCommand.cs
+++ b/src/Durable/Commands/InvokeActivityFunctionCommand.cs
@@ -33,6 +33,10 @@ public class InvokeActivityFunctionCommand : PSCmdlet
[Parameter]
public SwitchParameter NoWait { get; set; }
+ [Parameter]
+ [ValidateNotNull]
+ public RetryOptions RetryOptions { get; set; }
+
private readonly DurableTaskHandler _durableTaskHandler = new DurableTaskHandler();
protected override void EndProcessing()
@@ -41,11 +45,14 @@ protected override void EndProcessing()
var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey];
var loadedFunctions = FunctionLoader.GetLoadedFunctions();
- var task = new ActivityInvocationTask(FunctionName, Input);
+ var task = new ActivityInvocationTask(FunctionName, Input, RetryOptions);
ActivityInvocationTask.ValidateTask(task, loadedFunctions);
_durableTaskHandler.StopAndInitiateDurableTaskOrReplay(
- task, context, NoWait.IsPresent, WriteObject, failureReason => DurableActivityErrorHandler.Handle(this, failureReason));
+ task, context, NoWait.IsPresent,
+ output: WriteObject,
+ onFailure: failureReason => DurableActivityErrorHandler.Handle(this, failureReason),
+ retryOptions: RetryOptions);
}
protected override void StopProcessing()
diff --git a/src/Durable/DurableTaskHandler.cs b/src/Durable/DurableTaskHandler.cs
index c47e043b..3981b686 100644
--- a/src/Durable/DurableTaskHandler.cs
+++ b/src/Durable/DurableTaskHandler.cs
@@ -20,7 +20,8 @@ public void StopAndInitiateDurableTaskOrReplay(
OrchestrationContext context,
bool noWait,
Action