Skip to content

Commit 0d7ae22

Browse files
Add support for nextRetryDelay (#2081)
Add support for nextRetryDelay
1 parent 0d847a6 commit 0d7ae22

File tree

8 files changed

+222
-30
lines changed

8 files changed

+222
-30
lines changed

temporal-sdk/src/main/java/io/temporal/failure/ApplicationFailure.java

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.temporal.common.converter.DataConverter;
2525
import io.temporal.common.converter.EncodedValues;
2626
import io.temporal.common.converter.Values;
27+
import java.time.Duration;
2728
import java.util.Objects;
2829
import javax.annotation.Nullable;
2930

@@ -56,6 +57,7 @@ public final class ApplicationFailure extends TemporalFailure {
5657
private final String type;
5758
private final Values details;
5859
private boolean nonRetryable;
60+
private Duration nextRetryDelay;
5961

6062
/**
6163
* New ApplicationFailure with {@link #isNonRetryable()} flag set to false.
@@ -90,7 +92,33 @@ public static ApplicationFailure newFailure(String message, String type, Object.
9092
*/
9193
public static ApplicationFailure newFailureWithCause(
9294
String message, String type, @Nullable Throwable cause, Object... details) {
93-
return new ApplicationFailure(message, type, false, new EncodedValues(details), cause);
95+
return new ApplicationFailure(message, type, false, new EncodedValues(details), cause, null);
96+
}
97+
98+
/**
99+
* New ApplicationFailure with {@link #isNonRetryable()} flag set to false.
100+
*
101+
* <p>Note that this exception still may not be retried by the service if its type is included in
102+
* the doNotRetry property of the correspondent retry policy.
103+
*
104+
* @param message optional error message
105+
* @param type optional error type that is used by {@link
106+
* io.temporal.common.RetryOptions.Builder#setDoNotRetry(String...)}.
107+
* @param details optional details about the failure. They are serialized using the same approach
108+
* as arguments and results.
109+
* @param cause failure cause. Each element of the cause chain will be converted to
110+
* ApplicationFailure for network transmission across network if it doesn't extend {@link
111+
* TemporalFailure}
112+
* @param nextRetryDelay delay before the next retry attempt.
113+
*/
114+
public static ApplicationFailure newFailureWithCauseAndDelay(
115+
String message,
116+
String type,
117+
@Nullable Throwable cause,
118+
Duration nextRetryDelay,
119+
Object... details) {
120+
return new ApplicationFailure(
121+
message, type, false, new EncodedValues(details), cause, nextRetryDelay);
94122
}
95123

96124
/**
@@ -125,20 +153,31 @@ public static ApplicationFailure newNonRetryableFailure(
125153
*/
126154
public static ApplicationFailure newNonRetryableFailureWithCause(
127155
String message, String type, @Nullable Throwable cause, Object... details) {
128-
return new ApplicationFailure(message, type, true, new EncodedValues(details), cause);
156+
return new ApplicationFailure(message, type, true, new EncodedValues(details), cause, null);
129157
}
130158

131159
static ApplicationFailure newFromValues(
132-
String message, String type, boolean nonRetryable, Values details, Throwable cause) {
133-
return new ApplicationFailure(message, type, nonRetryable, details, cause);
160+
String message,
161+
String type,
162+
boolean nonRetryable,
163+
Values details,
164+
Throwable cause,
165+
Duration nextRetryDelay) {
166+
return new ApplicationFailure(message, type, nonRetryable, details, cause, nextRetryDelay);
134167
}
135168

136169
ApplicationFailure(
137-
String message, String type, boolean nonRetryable, Values details, Throwable cause) {
170+
String message,
171+
String type,
172+
boolean nonRetryable,
173+
Values details,
174+
Throwable cause,
175+
Duration nextRetryDelay) {
138176
super(getMessage(message, Objects.requireNonNull(type), nonRetryable), message, cause);
139177
this.type = type;
140178
this.details = details;
141179
this.nonRetryable = nonRetryable;
180+
this.nextRetryDelay = nextRetryDelay;
142181
}
143182

144183
public String getType() {
@@ -149,6 +188,11 @@ public Values getDetails() {
149188
return details;
150189
}
151190

191+
@Nullable
192+
public Duration getNextRetryDelay() {
193+
return nextRetryDelay;
194+
}
195+
152196
public void setNonRetryable(boolean nonRetryable) {
153197
this.nonRetryable = nonRetryable;
154198
}
@@ -162,6 +206,10 @@ public void setDataConverter(DataConverter converter) {
162206
((EncodedValues) details).setDataConverter(converter);
163207
}
164208

209+
public void setNextRetryDelay(Duration nextRetryDelay) {
210+
this.nextRetryDelay = nextRetryDelay;
211+
}
212+
165213
private static String getMessage(String message, String type, boolean nonRetryable) {
166214
return (Strings.isNullOrEmpty(message) ? "" : "message='" + message + "', ")
167215
+ "type='"

temporal-sdk/src/main/java/io/temporal/failure/DefaultFailureConverter.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import io.temporal.common.converter.EncodedValues;
4141
import io.temporal.common.converter.FailureConverter;
4242
import io.temporal.internal.activity.ActivityTaskHandlerImpl;
43+
import io.temporal.internal.common.ProtobufTimeUtils;
4344
import io.temporal.internal.sync.POJOWorkflowImplementationFactory;
4445
import io.temporal.serviceclient.CheckedExceptionWrapper;
4546
import java.io.PrintWriter;
@@ -106,7 +107,10 @@ private TemporalFailure failureToExceptionImpl(Failure failure, DataConverter da
106107
info.getType(),
107108
info.getNonRetryable(),
108109
new EncodedValues(details, dataConverter),
109-
cause);
110+
cause,
111+
info.hasNextRetryDelay()
112+
? ProtobufTimeUtils.toJavaDuration(info.getNextRetryDelay())
113+
: null);
110114
}
111115
case TIMEOUT_FAILURE_INFO:
112116
{
@@ -151,7 +155,8 @@ private TemporalFailure failureToExceptionImpl(Failure failure, DataConverter da
151155
"ResetWorkflow",
152156
false,
153157
new EncodedValues(details, dataConverter),
154-
cause);
158+
cause,
159+
null);
155160
}
156161
case ACTIVITY_FAILURE_INFO:
157162
{
@@ -231,6 +236,9 @@ private Failure exceptionToFailure(Throwable throwable) {
231236
if (details.isPresent()) {
232237
info.setDetails(details.get());
233238
}
239+
if (ae.getNextRetryDelay() != null) {
240+
info.setNextRetryDelay(ProtobufTimeUtils.toProtoDuration(ae.getNextRetryDelay()));
241+
}
234242
failure.setApplicationFailureInfo(info);
235243
} else if (throwable instanceof TimeoutFailure) {
236244
TimeoutFailure te = (TimeoutFailure) throwable;

temporal-sdk/src/main/java/io/temporal/internal/worker/LocalActivityWorker.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import io.temporal.workflow.Functions;
5151
import java.time.Duration;
5252
import java.util.Objects;
53+
import java.util.Optional;
5354
import java.util.concurrent.*;
5455
import javax.annotation.Nonnull;
5556
import javax.annotation.Nullable;
@@ -160,8 +161,9 @@ private RetryDecision shouldRetry(
160161
return new RetryDecision(RetryState.RETRY_STATE_MAXIMUM_ATTEMPTS_REACHED, null);
161162
}
162163

164+
Optional<Duration> nextRetryDelay = getNextRetryDelay(attemptThrowable);
163165
long sleepMillis = retryOptions.calculateSleepTime(currentAttempt);
164-
Duration sleep = Duration.ofMillis(sleepMillis);
166+
Duration sleep = nextRetryDelay.orElse(Duration.ofMillis(sleepMillis));
165167
if (RetryOptionsUtils.isDeadlineReached(
166168
executionContext.getScheduleToCloseDeadline(), sleepMillis)) {
167169
return new RetryDecision(RetryState.RETRY_STATE_TIMEOUT, null);
@@ -807,6 +809,13 @@ private static boolean isNonRetryableApplicationFailure(@Nullable Throwable exec
807809
&& ((ApplicationFailure) executionThrowable).isNonRetryable();
808810
}
809811

812+
private static Optional<Duration> getNextRetryDelay(@Nullable Throwable executionThrowable) {
813+
if (executionThrowable instanceof ApplicationFailure) {
814+
return Optional.ofNullable(((ApplicationFailure) executionThrowable).getNextRetryDelay());
815+
}
816+
return Optional.empty();
817+
}
818+
810819
private static class RetryDecision {
811820
private final @Nullable RetryState retryState;
812821
private final @Nullable Duration nextAttemptBackoff;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved.
3+
*
4+
* Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this material except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package io.temporal.activity;
22+
23+
import static org.junit.Assert.*;
24+
import static org.junit.Assume.assumeFalse;
25+
26+
import io.temporal.failure.ApplicationFailure;
27+
import io.temporal.testing.internal.SDKTestOptions;
28+
import io.temporal.testing.internal.SDKTestWorkflowRule;
29+
import io.temporal.workflow.Workflow;
30+
import io.temporal.workflow.WorkflowInterface;
31+
import io.temporal.workflow.WorkflowMethod;
32+
import io.temporal.workflow.shared.TestActivities;
33+
import java.time.Duration;
34+
import org.junit.Assert;
35+
import org.junit.Rule;
36+
import org.junit.Test;
37+
38+
public class ActivityNextRetryDelayTest {
39+
40+
@Rule
41+
public SDKTestWorkflowRule testWorkflowRule =
42+
SDKTestWorkflowRule.newBuilder()
43+
.setWorkflowTypes(TestWorkflowImpl.class)
44+
.setActivityImplementations(new NextRetryDelayActivityImpl())
45+
.build();
46+
47+
@Test
48+
public void activityNextRetryDelay() {
49+
assumeFalse(
50+
"Real Server doesn't support next retry delay yet", SDKTestWorkflowRule.useExternalService);
51+
TestWorkflowReturnDuration workflow =
52+
testWorkflowRule.newWorkflowStub(TestWorkflowReturnDuration.class);
53+
Duration result = workflow.execute(false);
54+
Assert.assertTrue(result.toMillis() > 5000 && result.toMillis() < 7000);
55+
}
56+
57+
@Test
58+
public void localActivityNextRetryDelay() {
59+
TestWorkflowReturnDuration workflow =
60+
testWorkflowRule.newWorkflowStub(TestWorkflowReturnDuration.class);
61+
Duration result = workflow.execute(true);
62+
Assert.assertTrue(result.toMillis() > 5000 && result.toMillis() < 7000);
63+
}
64+
65+
@WorkflowInterface
66+
public interface TestWorkflowReturnDuration {
67+
@WorkflowMethod
68+
Duration execute(boolean useLocalActivity);
69+
}
70+
71+
public static class TestWorkflowImpl implements TestWorkflowReturnDuration {
72+
73+
private final TestActivities.NoArgsActivity activities =
74+
Workflow.newActivityStub(
75+
TestActivities.NoArgsActivity.class,
76+
SDKTestOptions.newActivityOptions20sScheduleToClose());
77+
78+
private final TestActivities.NoArgsActivity localActivities =
79+
Workflow.newLocalActivityStub(
80+
TestActivities.NoArgsActivity.class,
81+
SDKTestOptions.newLocalActivityOptions20sScheduleToClose());
82+
83+
@Override
84+
public Duration execute(boolean useLocalActivity) {
85+
long t1 = Workflow.currentTimeMillis();
86+
if (useLocalActivity) {
87+
localActivities.execute();
88+
} else {
89+
activities.execute();
90+
}
91+
long t2 = Workflow.currentTimeMillis();
92+
return Duration.ofMillis(t2 - t1);
93+
}
94+
}
95+
96+
public static class NextRetryDelayActivityImpl implements TestActivities.NoArgsActivity {
97+
@Override
98+
public void execute() {
99+
int attempt = Activity.getExecutionContext().getInfo().getAttempt();
100+
if (attempt < 4) {
101+
throw ApplicationFailure.newFailureWithCauseAndDelay(
102+
"test retry delay failure " + attempt,
103+
"test failure",
104+
null,
105+
Duration.ofSeconds(attempt));
106+
}
107+
}
108+
}
109+
}

temporal-sdk/src/test/java/io/temporal/client/schedules/ScheduleTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ public void triggerScheduleNoPolicy() {
251251
handle.delete();
252252
}
253253

254-
@Test
254+
@Test(timeout = 30000)
255255
public void backfillSchedules() {
256256
// assumeTrue("skipping for test server", SDKTestWorkflowRule.useExternalService);
257257
Instant now = Instant.now();

temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1703,15 +1703,22 @@ private static RetryState attemptActivityRetry(
17031703
throw new IllegalStateException("RetryPolicy is always present");
17041704
}
17051705
Optional<ApplicationFailureInfo> info = failure.map(Failure::getApplicationFailureInfo);
1706+
Optional<java.time.Duration> nextRetryDelay = Optional.empty();
1707+
17061708
if (info.isPresent()) {
17071709
if (info.get().getNonRetryable()) {
17081710
return RetryState.RETRY_STATE_NON_RETRYABLE_FAILURE;
17091711
}
1712+
if (info.get().hasNextRetryDelay()) {
1713+
nextRetryDelay =
1714+
Optional.ofNullable(ProtobufTimeUtils.toJavaDuration(info.get().getNextRetryDelay()));
1715+
}
17101716
}
1717+
17111718
TestServiceRetryState nextAttempt = data.retryState.getNextAttempt(failure);
17121719
TestServiceRetryState.BackoffInterval backoffInterval =
17131720
data.retryState.getBackoffIntervalInSeconds(
1714-
info.map(ApplicationFailureInfo::getType), data.store.currentTime());
1721+
info.map(ApplicationFailureInfo::getType), data.store.currentTime(), nextRetryDelay);
17151722
if (backoffInterval.getRetryState() == RetryState.RETRY_STATE_IN_PROGRESS) {
17161723
data.nextBackoffInterval = ProtobufTimeUtils.toProtoDuration(backoffInterval.getInterval());
17171724
PollActivityTaskQueueResponse.Builder task = data.activityTask.getTask();

0 commit comments

Comments
 (0)