Skip to content

Commit 537d99d

Browse files
Add ApplicationFailure.Builder (#2495)
* Add ApplicationFailure.Builder
1 parent f6d4d46 commit 537d99d

File tree

7 files changed

+212
-90
lines changed

7 files changed

+212
-90
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,4 @@ public enum ApplicationErrorCategory {
2929
UNSPECIFIED,
3030
/** Expected application error with little/no severity. */
3131
BENIGN,
32-
;
3332
}

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

Lines changed: 131 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
* <li>nonRetryable is set to false
5252
* <li>details are set to null
5353
* <li>stack trace is copied from the original exception
54-
* <li>category is set to ApplicationErrorCategory.APPLICATION_ERROR_CATEGORY_UNSPECIFIED
54+
* <li>category is set to {@link ApplicationErrorCategory#UNSPECIFIED}
5555
* </ul>
5656
*/
5757
public final class ApplicationFailure extends TemporalFailure {
@@ -61,6 +61,16 @@ public final class ApplicationFailure extends TemporalFailure {
6161
private Duration nextRetryDelay;
6262
private final ApplicationErrorCategory category;
6363

64+
/** Creates a new builder for {@link ApplicationFailure}. */
65+
public static ApplicationFailure.Builder newBuilder() {
66+
return new ApplicationFailure.Builder();
67+
}
68+
69+
/** Creates a new builder for {@link ApplicationFailure} initialized with the provided failure. */
70+
public static ApplicationFailure.Builder newBuilder(ApplicationFailure options) {
71+
return new ApplicationFailure.Builder(options);
72+
}
73+
6474
/**
6575
* New ApplicationFailure with {@link #isNonRetryable()} flag set to false.
6676
*
@@ -178,45 +188,7 @@ public static ApplicationFailure newNonRetryableFailureWithCause(
178188
ApplicationErrorCategory.UNSPECIFIED);
179189
}
180190

181-
/**
182-
* New ApplicationFailure with a specified category and {@link #isNonRetryable()} flag set to
183-
* false.
184-
*
185-
* <p>Note that this exception still may not be retried by the service if its type is included in
186-
* the doNotRetry property of the correspondent retry policy.
187-
*
188-
* @param message optional error message
189-
* @param type error type
190-
* @param category the category of the application failure.
191-
* @param cause failure cause. Each element of the cause chain will be converted to
192-
* ApplicationFailure for network transmission across network if it doesn't extend {@link
193-
* TemporalFailure}
194-
* @param details optional details about the failure. They are serialized using the same approach
195-
* as arguments and results.
196-
*/
197-
public static ApplicationFailure newFailureWithCategory(
198-
String message,
199-
String type,
200-
ApplicationErrorCategory category,
201-
@Nullable Throwable cause,
202-
Object... details) {
203-
return new ApplicationFailure(
204-
message, type, false, new EncodedValues(details), cause, null, category);
205-
}
206-
207-
static ApplicationFailure newFromValues(
208-
String message,
209-
String type,
210-
boolean nonRetryable,
211-
Values details,
212-
Throwable cause,
213-
Duration nextRetryDelay,
214-
ApplicationErrorCategory category) {
215-
return new ApplicationFailure(
216-
message, type, nonRetryable, details, cause, nextRetryDelay, category);
217-
}
218-
219-
ApplicationFailure(
191+
private ApplicationFailure(
220192
String message,
221193
String type,
222194
boolean nonRetryable,
@@ -262,7 +234,7 @@ public void setNextRetryDelay(Duration nextRetryDelay) {
262234
this.nextRetryDelay = nextRetryDelay;
263235
}
264236

265-
public ApplicationErrorCategory getApplicationErrorCategory() {
237+
public ApplicationErrorCategory getCategory() {
266238
return category;
267239
}
268240

@@ -274,4 +246,122 @@ private static String getMessage(String message, String type, boolean nonRetryab
274246
+ ", nonRetryable="
275247
+ nonRetryable;
276248
}
249+
250+
public static final class Builder {
251+
private String message;
252+
private String type;
253+
private Values details;
254+
private boolean nonRetryable;
255+
private Throwable cause;
256+
private Duration nextRetryDelay;
257+
private ApplicationErrorCategory category;
258+
259+
private Builder() {}
260+
261+
private Builder(ApplicationFailure options) {
262+
if (options == null) {
263+
return;
264+
}
265+
this.message = options.getOriginalMessage();
266+
this.type = options.type;
267+
this.details = options.details;
268+
this.nonRetryable = options.nonRetryable;
269+
this.nextRetryDelay = options.nextRetryDelay;
270+
this.category = options.category;
271+
}
272+
273+
/**
274+
* Sets the error type of this failure. This is used by {@link
275+
* io.temporal.common.RetryOptions.Builder#setDoNotRetry(String...)} to determine if the
276+
* exception is non retryable.
277+
*/
278+
public Builder setType(String type) {
279+
this.type = type;
280+
return this;
281+
}
282+
283+
/**
284+
* Set the optional error message.
285+
*
286+
* <p>Default is "".
287+
*/
288+
public Builder setMessage(String message) {
289+
this.message = message;
290+
return this;
291+
}
292+
293+
/**
294+
* Set the optional details of the failure.
295+
*
296+
* <p>Details are serialized using the same approach as arguments and results.
297+
*/
298+
public Builder setDetails(Object... details) {
299+
this.details = new EncodedValues(details);
300+
return this;
301+
}
302+
303+
/**
304+
* Set the optional details of the failure.
305+
*
306+
* <p>Details are serialized using the same approach as arguments and results.
307+
*/
308+
public Builder setDetails(Values details) {
309+
this.details = details;
310+
return this;
311+
}
312+
313+
/**
314+
* Set the non retryable flag on the failure.
315+
*
316+
* <p>It means that this exception is not going to be retried even if it is not included into
317+
* retry policy doNotRetry list.
318+
*
319+
* <p>Default is false.
320+
*/
321+
public Builder setNonRetryable(boolean nonRetryable) {
322+
this.nonRetryable = nonRetryable;
323+
return this;
324+
}
325+
326+
/**
327+
* Set the optional cause of the failure. Each element of the cause chain will be converted to
328+
* {@link ApplicationFailure} for network transmission across network if it doesn't extend
329+
* {@link TemporalFailure}.
330+
*/
331+
public Builder setCause(Throwable cause) {
332+
this.cause = cause;
333+
return this;
334+
}
335+
336+
/**
337+
* Set the optional delay before the next retry attempt. Overrides the normal retry delay.
338+
*
339+
* <p>Default is null.
340+
*/
341+
public Builder setNextRetryDelay(Duration nextRetryDelay) {
342+
this.nextRetryDelay = nextRetryDelay;
343+
return this;
344+
}
345+
346+
/**
347+
* Set the optional category of the failure.
348+
*
349+
* <p>Default is {@link ApplicationErrorCategory#UNSPECIFIED}.
350+
*/
351+
public Builder setCategory(ApplicationErrorCategory category) {
352+
this.category = category;
353+
return this;
354+
}
355+
356+
public ApplicationFailure build() {
357+
return new ApplicationFailure(
358+
message,
359+
type,
360+
nonRetryable,
361+
details == null ? new EncodedValues(null) : details,
362+
cause,
363+
nextRetryDelay,
364+
category);
365+
}
366+
}
277367
}

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

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,18 @@ private RuntimeException failureToExceptionImpl(Failure failure, DataConverter d
9999
ApplicationFailureInfo info = failure.getApplicationFailureInfo();
100100
Optional<Payloads> details =
101101
info.hasDetails() ? Optional.of(info.getDetails()) : Optional.empty();
102-
return ApplicationFailure.newFromValues(
103-
failure.getMessage(),
104-
info.getType(),
105-
info.getNonRetryable(),
106-
new EncodedValues(details, dataConverter),
107-
cause,
108-
info.hasNextRetryDelay()
109-
? ProtobufTimeUtils.toJavaDuration(info.getNextRetryDelay())
110-
: null,
111-
FailureUtils.categoryFromProto(info.getCategory()));
102+
return ApplicationFailure.newBuilder()
103+
.setMessage(failure.getMessage())
104+
.setType(info.getType())
105+
.setNonRetryable(info.getNonRetryable())
106+
.setDetails(new EncodedValues(details, dataConverter))
107+
.setCause(cause)
108+
.setNextRetryDelay(
109+
info.hasNextRetryDelay()
110+
? ProtobufTimeUtils.toJavaDuration(info.getNextRetryDelay())
111+
: null)
112+
.setCategory(FailureUtils.categoryFromProto(info.getCategory()))
113+
.build();
112114
}
113115
case TIMEOUT_FAILURE_INFO:
114116
{
@@ -148,14 +150,12 @@ private RuntimeException failureToExceptionImpl(Failure failure, DataConverter d
148150
info.hasLastHeartbeatDetails()
149151
? Optional.of(info.getLastHeartbeatDetails())
150152
: Optional.empty();
151-
return ApplicationFailure.newFromValues(
152-
failure.getMessage(),
153-
"ResetWorkflow",
154-
false,
155-
new EncodedValues(details, dataConverter),
156-
cause,
157-
null,
158-
ApplicationErrorCategory.UNSPECIFIED);
153+
return ApplicationFailure.newBuilder()
154+
.setMessage(failure.getMessage())
155+
.setType("ResetWorkflow")
156+
.setDetails(new EncodedValues(details, dataConverter))
157+
.setCause(cause)
158+
.build();
159159
}
160160
case ACTIVITY_FAILURE_INFO:
161161
{
@@ -211,14 +211,12 @@ private RuntimeException failureToExceptionImpl(Failure failure, DataConverter d
211211
case FAILUREINFO_NOT_SET:
212212
default:
213213
// All unknown types are considered to be retryable ApplicationError.
214-
return ApplicationFailure.newFromValues(
215-
failure.getMessage(),
216-
"",
217-
false,
218-
new EncodedValues(Optional.empty(), dataConverter),
219-
cause,
220-
null,
221-
ApplicationErrorCategory.UNSPECIFIED);
214+
return ApplicationFailure.newBuilder()
215+
.setMessage(failure.getMessage())
216+
.setType("")
217+
.setDetails(new EncodedValues(Optional.empty(), dataConverter))
218+
.setCause(cause)
219+
.build();
222220
}
223221
}
224222

@@ -265,7 +263,7 @@ private Failure exceptionToFailure(Throwable throwable) {
265263
ApplicationFailureInfo.newBuilder()
266264
.setType(ae.getType())
267265
.setNonRetryable(ae.isNonRetryable())
268-
.setCategory(FailureUtils.categoryToProto(ae.getApplicationErrorCategory()));
266+
.setCategory(FailureUtils.categoryToProto(ae.getCategory()));
269267
Optional<Payloads> details = ((EncodedValues) ae.getDetails()).toPayloads();
270268
if (details.isPresent()) {
271269
info.setDetails(details.get());

temporal-sdk/src/main/java/io/temporal/internal/common/FailureUtils.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ private FailureUtils() {}
3030

3131
public static boolean isBenignApplicationFailure(@Nullable Throwable t) {
3232
if (t instanceof ApplicationFailure
33-
&& ((ApplicationFailure) t).getApplicationErrorCategory()
34-
== ApplicationErrorCategory.BENIGN) {
33+
&& ((ApplicationFailure) t).getCategory() == ApplicationErrorCategory.BENIGN) {
3534
return true;
3635
}
3736
return false;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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.failure;
22+
23+
import org.junit.Assert;
24+
import org.junit.Test;
25+
26+
public class ApplicationFailureTest {
27+
28+
@Test
29+
public void applicationFailureCopy() {
30+
ApplicationFailure originalAppFailure =
31+
ApplicationFailure.newBuilder().setType("TestType").setMessage("test message").build();
32+
ApplicationFailure newAppFailure =
33+
ApplicationFailure.newBuilder(originalAppFailure).setNonRetryable(true).build();
34+
Assert.assertEquals(originalAppFailure.getType(), newAppFailure.getType());
35+
Assert.assertEquals(
36+
originalAppFailure.getOriginalMessage(), newAppFailure.getOriginalMessage());
37+
Assert.assertNotEquals(originalAppFailure.isNonRetryable(), newAppFailure.isNonRetryable());
38+
}
39+
}

temporal-sdk/src/test/java/io/temporal/internal/worker/ActivityFailedMetricsTests.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,11 @@ public void execute(boolean isBenign) {
111111
if (!isBenign) {
112112
throw ApplicationFailure.newFailure("Non-benign activity failure", "NonBenignType");
113113
} else {
114-
throw ApplicationFailure.newFailureWithCategory(
115-
"Benign activity failure", "BenignType", ApplicationErrorCategory.BENIGN, null);
114+
throw ApplicationFailure.newBuilder()
115+
.setMessage("Benign activity failure")
116+
.setType("BenignType")
117+
.setCategory(ApplicationErrorCategory.BENIGN)
118+
.build();
116119
}
117120
}
118121
}
@@ -198,8 +201,7 @@ public void activityFailureMetricBenignApplicationError() {
198201
nonBenignErr.getCause().getCause() instanceof ApplicationFailure);
199202
ApplicationFailure af = (ApplicationFailure) nonBenignErr.getCause().getCause();
200203
assertFalse(
201-
"Failure should not be benign",
202-
af.getApplicationErrorCategory() == ApplicationErrorCategory.BENIGN);
204+
"Failure should not be benign", af.getCategory() == ApplicationErrorCategory.BENIGN);
203205
assertEquals("Non-benign activity failure", af.getOriginalMessage());
204206

205207
reporter.assertCounter(
@@ -227,9 +229,7 @@ public void activityFailureMetricBenignApplicationError() {
227229
"Inner cause should be ApplicationFailure",
228230
benignErr.getCause().getCause() instanceof ApplicationFailure);
229231
ApplicationFailure af2 = (ApplicationFailure) benignErr.getCause().getCause();
230-
assertTrue(
231-
"Failure should be benign",
232-
af2.getApplicationErrorCategory() == ApplicationErrorCategory.BENIGN);
232+
assertTrue("Failure should be benign", af2.getCategory() == ApplicationErrorCategory.BENIGN);
233233
assertEquals("Benign activity failure", af2.getOriginalMessage());
234234

235235
// Expect metrics to remain unchanged for benign failure
@@ -271,8 +271,7 @@ public void localActivityFailureMetricBenignApplicationError() {
271271
nonBenignErr.getCause().getCause() instanceof ApplicationFailure);
272272
ApplicationFailure af = (ApplicationFailure) nonBenignErr.getCause().getCause();
273273
assertFalse(
274-
"Failure should not be benign",
275-
af.getApplicationErrorCategory() == ApplicationErrorCategory.BENIGN);
274+
"Failure should not be benign", af.getCategory() == ApplicationErrorCategory.BENIGN);
276275
assertEquals("Non-benign activity failure", af.getOriginalMessage());
277276

278277
// Expect metrics to be incremented for non-benign failure
@@ -300,9 +299,7 @@ public void localActivityFailureMetricBenignApplicationError() {
300299
"Inner cause should be ApplicationFailure",
301300
benignErr.getCause().getCause() instanceof ApplicationFailure);
302301
ApplicationFailure af2 = (ApplicationFailure) benignErr.getCause().getCause();
303-
assertTrue(
304-
"Failure should be benign",
305-
af2.getApplicationErrorCategory() == ApplicationErrorCategory.BENIGN);
302+
assertTrue("Failure should be benign", af2.getCategory() == ApplicationErrorCategory.BENIGN);
306303
assertEquals("Benign activity failure", af2.getOriginalMessage());
307304

308305
// Expect metrics to remain unchanged for benign failure

0 commit comments

Comments
 (0)