Skip to content

Commit 873444f

Browse files
Add TerminalException.metadata field (#596)
1 parent 26e8003 commit 873444f

File tree

6 files changed

+105
-8
lines changed

6 files changed

+105
-8
lines changed

sdk-common/src/main/java/dev/restate/sdk/common/TerminalException.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
99
package dev.restate.sdk.common;
1010

11+
import java.util.Map;
12+
import java.util.Objects;
13+
1114
/** When thrown in a Restate service method, it will complete the invocation with an error. */
1215
public class TerminalException extends RuntimeException {
1316

@@ -17,6 +20,7 @@ public class TerminalException extends RuntimeException {
1720
public static final int INTERNAL_SERVER_ERROR_CODE = 500;
1821

1922
private final int code;
23+
private final Map<String, String> metadata;
2024

2125
public TerminalException() {
2226
this(INTERNAL_SERVER_ERROR_CODE);
@@ -34,16 +38,37 @@ public TerminalException(int code) {
3438
* @param message error message
3539
*/
3640
public TerminalException(int code, String message) {
37-
super(message);
38-
this.code = code;
41+
this(code, message, null);
3942
}
4043

4144
/**
4245
* Like {@link #TerminalException(int, String)}, with code {@link #INTERNAL_SERVER_ERROR_CODE}.
4346
*/
4447
public TerminalException(String message) {
48+
this(INTERNAL_SERVER_ERROR_CODE, message, null);
49+
}
50+
51+
/**
52+
* Create a new {@link TerminalException}.
53+
*
54+
* @param code HTTP response status code
55+
* @param message error message
56+
* @param metadata error metadata (supported only from Restate > 1.6)
57+
*/
58+
public TerminalException(int code, String message, Map<String, String> metadata) {
4559
super(message);
46-
this.code = INTERNAL_SERVER_ERROR_CODE;
60+
this.code = code;
61+
this.metadata = Objects.requireNonNullElse(metadata, Map.of());
62+
}
63+
64+
/**
65+
* Create a new {@link TerminalException}.
66+
*
67+
* @param message error message
68+
* @param metadata error metadata (supported only from Restate > 1.6)
69+
*/
70+
public TerminalException(String message, Map<String, String> metadata) {
71+
this(INTERNAL_SERVER_ERROR_CODE, message, metadata);
4772
}
4873

4974
/**
@@ -52,4 +77,11 @@ public TerminalException(String message) {
5277
public int getCode() {
5378
return code;
5479
}
80+
81+
/**
82+
* @return the error metadata
83+
*/
84+
public Map<String, String> getMetadata() {
85+
return metadata;
86+
}
5587
}

sdk-core/src/main/java/dev/restate/sdk/core/ProtocolException.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import com.google.protobuf.MessageLite;
1212
import dev.restate.sdk.common.TerminalException;
13+
import dev.restate.sdk.core.generated.protocol.Protocol;
1314
import dev.restate.sdk.core.statemachine.NotificationId;
1415

1516
public class ProtocolException extends RuntimeException {
@@ -20,6 +21,7 @@ public class ProtocolException extends RuntimeException {
2021
public static final int INTERNAL_CODE = 500;
2122
public static final int JOURNAL_MISMATCH_CODE = 570;
2223
static final int PROTOCOL_VIOLATION_CODE = 571;
24+
static final int UNSUPPORTED_FEATURE = 573;
2325

2426
private final int code;
2527

@@ -129,4 +131,19 @@ public static ProtocolException idempotencyKeyIsEmpty() {
129131
public static ProtocolException unauthorized(Throwable e) {
130132
return new ProtocolException("Unauthorized", UNAUTHORIZED_CODE, e);
131133
}
134+
135+
public static ProtocolException unsupportedFeature(
136+
String featureName,
137+
Protocol.ServiceProtocolVersion requiredVersion,
138+
Protocol.ServiceProtocolVersion negotiatedVersion) {
139+
return new ProtocolException(
140+
"Current service protocol version does not support "
141+
+ featureName
142+
+ ". "
143+
+ "Negotiated version: "
144+
+ negotiatedVersion.getNumber()
145+
+ ", minimum required: "
146+
+ requiredVersion.getNumber(),
147+
UNSUPPORTED_FEATURE);
148+
}
132149
}

sdk-core/src/main/java/dev/restate/sdk/core/statemachine/StateMachineImpl.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ public void completeAwakeable(String awakeableId, Slice value) {
411411
@Override
412412
public void completeAwakeable(String awakeableId, TerminalException exception) {
413413
LOG.debug("Executing 'Complete awakeable {} with failure'", awakeableId);
414+
verifyErrorMetadataFeatureSupport(exception);
414415
completeAwakeable(awakeableId, builder -> builder.setFailure(toProtocolFailure(exception)));
415416
}
416417

@@ -455,6 +456,7 @@ public void completeSignal(
455456
"Executing 'Complete signal {} to invocation {} with failure'",
456457
signalName,
457458
targetInvocationId);
459+
verifyErrorMetadataFeatureSupport(exception);
458460
this.completeSignal(
459461
targetInvocationId,
460462
signalName,
@@ -520,6 +522,7 @@ public int promiseComplete(String key, Slice value) {
520522
@Override
521523
public int promiseComplete(String key, TerminalException exception) {
522524
LOG.debug("Executing 'Complete promise {} with failure'", key);
525+
verifyErrorMetadataFeatureSupport(exception);
523526
return this.promiseComplete(
524527
key, builder -> builder.setCompletionFailure(toProtocolFailure(exception)));
525528
}
@@ -567,6 +570,9 @@ public void proposeRunCompletion(
567570
Duration attemptDuration,
568571
@Nullable RetryPolicy retryPolicy) {
569572
LOG.debug("Executing 'Run completed with failure'");
573+
if (exception instanceof TerminalException) {
574+
verifyErrorMetadataFeatureSupport((TerminalException) exception);
575+
}
570576
try {
571577
this.stateContext
572578
.getCurrentState()
@@ -639,6 +645,7 @@ public void writeOutput(Slice value) {
639645
@Override
640646
public void writeOutput(TerminalException exception) {
641647
LOG.debug("Executing 'Write invocation output with failure'");
648+
verifyErrorMetadataFeatureSupport(exception);
642649
this.stateContext
643650
.getCurrentState()
644651
.processNonCompletableCommand(
@@ -666,4 +673,15 @@ private void cancelInputSubscription() {
666673
this.inputSubscription = null;
667674
}
668675
}
676+
677+
private void verifyErrorMetadataFeatureSupport(TerminalException exception) {
678+
if (!exception.getMetadata().isEmpty()
679+
&& stateContext.getNegotiatedProtocolVersion().getNumber()
680+
< Protocol.ServiceProtocolVersion.V6.getNumber()) {
681+
throw ProtocolException.unsupportedFeature(
682+
"terminal error metadata",
683+
Protocol.ServiceProtocolVersion.V6,
684+
stateContext.getNegotiatedProtocolVersion());
685+
}
686+
}
669687
}

sdk-core/src/main/java/dev/restate/sdk/core/statemachine/Util.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,48 @@
1616
import dev.restate.sdk.core.generated.protocol.Protocol;
1717
import java.nio.ByteBuffer;
1818
import java.time.Duration;
19+
import java.util.Map;
1920
import java.util.Objects;
21+
import java.util.stream.Collectors;
2022

2123
public class Util {
2224

23-
static Protocol.Failure toProtocolFailure(int code, String message) {
25+
static Protocol.Failure toProtocolFailure(
26+
int code, String message, Map<String, String> metadata) {
2427
Protocol.Failure.Builder builder = Protocol.Failure.newBuilder().setCode(code);
2528
if (message != null) {
2629
builder.setMessage(message);
2730
}
31+
if (metadata != null) {
32+
for (Map.Entry<String, String> entry : metadata.entrySet()) {
33+
builder.addMetadata(
34+
Protocol.FailureMetadata.newBuilder()
35+
.setKey(entry.getKey())
36+
.setValue(entry.getValue()));
37+
}
38+
}
2839
return builder.build();
2940
}
3041

3142
static Protocol.Failure toProtocolFailure(Throwable throwable) {
3243
if (throwable instanceof TerminalException) {
33-
return toProtocolFailure(((TerminalException) throwable).getCode(), throwable.getMessage());
44+
return toProtocolFailure(
45+
((TerminalException) throwable).getCode(),
46+
throwable.getMessage(),
47+
((TerminalException) throwable).getMetadata());
3448
}
35-
return toProtocolFailure(TerminalException.INTERNAL_SERVER_ERROR_CODE, throwable.toString());
49+
return toProtocolFailure(
50+
TerminalException.INTERNAL_SERVER_ERROR_CODE, throwable.toString(), Map.of());
3651
}
3752

3853
static TerminalException toRestateException(Protocol.Failure failure) {
39-
return new TerminalException(failure.getCode(), failure.getMessage());
54+
return new TerminalException(
55+
failure.getCode(),
56+
failure.getMessage(),
57+
failure.getMetadataList().stream()
58+
.collect(
59+
Collectors.toMap(
60+
Protocol.FailureMetadata::getKey, Protocol.FailureMetadata::getValue)));
4061
}
4162

4263
/** NOTE! This method rewinds the buffer!!! */

sdk-core/src/main/service-protocol/dev/restate/service/protocol.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ enum ServiceProtocolVersion {
3434
V5 = 5;
3535
// Added:
3636
// * StartMessage.random_seed
37+
// * Failure.metadata
3738
V6 = 6;
3839
}
3940

@@ -633,6 +634,14 @@ message Failure {
633634
uint32 code = 1;
634635
// Contains a concise error message, e.g. Throwable#getMessage() in Java.
635636
string message = 2;
637+
638+
// Error metadata
639+
repeated FailureMetadata metadata = 3;
640+
}
641+
642+
message FailureMetadata {
643+
string key = 1;
644+
string value = 2;
636645
}
637646

638647
message Header {

sdk-core/src/test/java/dev/restate/sdk/core/statemachine/ProtoUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ public static Protocol.SendSignalCommandMessage sendCancelSignal(String targetIn
456456
}
457457

458458
public static Protocol.Failure failure(int code, String message) {
459-
return Util.toProtocolFailure(code, message);
459+
return Util.toProtocolFailure(code, message, Map.of());
460460
}
461461

462462
public static Protocol.Failure failure(Throwable throwable) {

0 commit comments

Comments
 (0)