-
Notifications
You must be signed in to change notification settings - Fork 16
Fix orchestration stuck using anyOf/allOf with retry policy #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -970,6 +970,7 @@ private class RetriableTask<V> extends CompletableTask<V> { | |
| private final TaskOrchestrationContext context; | ||
| private final Instant firstAttempt; | ||
| private final TaskFactory<V> taskFactory; | ||
| private Task<V> currentTask; | ||
|
|
||
| private int attemptNumber; | ||
| private FailureDetails lastFailure; | ||
|
|
@@ -988,25 +989,40 @@ private RetriableTask( | |
| TaskFactory<V> taskFactory, | ||
| @Nullable RetryPolicy retryPolicy, | ||
| @Nullable RetryHandler retryHandler) { | ||
| super(new CompletableFuture<>()); | ||
| this.context = context; | ||
| this.taskFactory = taskFactory; | ||
| this.policy = retryPolicy; | ||
| this.handler = retryHandler; | ||
| this.firstAttempt = context.getCurrentInstant(); | ||
| this.totalRetryTime = Duration.ZERO; | ||
| this.taskFactory = taskFactory; | ||
| // make sure the taskFactory#create method is called when create RetriableTask, | ||
| // so when use with anyOf/allOf the pendingActions is not empty when return to the sidecar. | ||
| this.currentTask = taskFactory.create(); | ||
| // to make sure the RetriableTask future is same as currentTask, | ||
| // so they complete at the same time | ||
| setFuture(this.currentTask.future); | ||
| } | ||
|
|
||
| private void setFuture(CompletableFuture<V> future) { | ||
| this.future = future; | ||
| } | ||
|
|
||
| @Override | ||
| public V await() { | ||
| Instant startTime = this.context.getCurrentInstant(); | ||
| while (true) { | ||
| Task<V> currentTask = this.taskFactory.create(); | ||
| // For first try, currentTask is not null, this will be skipped. | ||
| if (this.currentTask == null) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maintainability nit: Can you add a comment here explaining when |
||
| this.currentTask = this.taskFactory.create(); | ||
| // to make sure the RetriableTask future is same as currentTask, | ||
| // so they complete at the same time | ||
| setFuture(this.currentTask.future); | ||
| } | ||
|
|
||
| this.attemptNumber++; | ||
|
|
||
| try { | ||
| return currentTask.await(); | ||
| return this.currentTask.await(); | ||
| } catch (TaskFailedException ex) { | ||
| this.lastFailure = ex.getErrorDetails(); | ||
| if (!this.shouldRetry()) { | ||
|
|
@@ -1026,6 +1042,8 @@ public V await() { | |
| } | ||
|
|
||
| this.totalRetryTime = Duration.between(startTime, this.context.getCurrentInstant()); | ||
| //empty currentTask. | ||
| this.currentTask = null; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than setting // Generate a new task/future pair for the next attempt
this.currentTask = this.taskFactory.create();
setFuture(this.currentTask.future); |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package com.functions; | ||
|
|
||
| import com.microsoft.azure.functions.annotation.*; | ||
| import com.microsoft.azure.functions.*; | ||
|
|
||
| import java.time.Duration; | ||
| import java.util.*; | ||
|
|
||
| import com.microsoft.durabletask.*; | ||
| import com.microsoft.durabletask.azurefunctions.DurableActivityTrigger; | ||
| import com.microsoft.durabletask.azurefunctions.DurableClientContext; | ||
| import com.microsoft.durabletask.azurefunctions.DurableClientInput; | ||
| import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; | ||
|
|
||
| public class ParallelFunctions { | ||
| @FunctionName("StartParallelOrchestration") | ||
| public HttpResponseMessage startParallelOrchestration( | ||
| @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request, | ||
| @DurableClientInput(name = "durableContext") DurableClientContext durableContext, | ||
| final ExecutionContext context) { | ||
| context.getLogger().info("Java HTTP trigger processed a request."); | ||
|
|
||
| DurableTaskClient client = durableContext.getClient(); | ||
| String instanceId = client.scheduleNewOrchestrationInstance("Parallel"); | ||
| context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); | ||
| return durableContext.createCheckStatusResponse(request, instanceId); | ||
| } | ||
|
|
||
| @FunctionName("Parallel") | ||
| public List<String> parallelOrchestrator( | ||
| @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { | ||
| RetryPolicy retryPolicy = new RetryPolicy(2, Duration.ofSeconds(5)); | ||
| TaskOptions taskOptions = new TaskOptions(retryPolicy); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test covers the base case where activities succeed, but shouldn't we also add a test for cases where at least one function call fails and is retried? Otherwise I feel like we're not really validating the correctness of this PR. Also, would it make sense to have this test coverage in the main client library?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You mean adding similar test cases for integration tests that run on durable sidecar right? |
||
| List<Task<String>> tasks = new ArrayList<>(); | ||
| tasks.add(ctx.callActivity("Append", "Input1", taskOptions, String.class)); | ||
| tasks.add(ctx.callActivity("Append", "Input2", taskOptions, String.class)); | ||
| tasks.add(ctx.callActivity("Append", "Input3", taskOptions, String.class)); | ||
| return ctx.allOf(tasks).await(); | ||
| } | ||
|
|
||
| @FunctionName("Append") | ||
| public String append( | ||
| @DurableActivityTrigger(name = "name") String name, | ||
| final ExecutionContext context) { | ||
| context.getLogger().info("Append: " + name); | ||
| return name + "-test"; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code style consistency nit: