Skip to content

Commit 42765bc

Browse files
Copilotbachuv
andauthored
Add Java-standard exception mappings to StatusRuntimeExceptionHelper
New gRPC status code → Java exception mappings: - INVALID_ARGUMENT → IllegalArgumentException - FAILED_PRECONDITION → IllegalStateException - NOT_FOUND → NoSuchElementException - UNIMPLEMENTED → UnsupportedOperationException All mapped exceptions include the Status.Code in the message, preserve the original StatusRuntimeException as cause, and handle null descriptions. Both toRuntimeException and toException support all new mappings. Tests added for each new mapping in both helper paths. Agent-Logs-Url: https://github.com/microsoft/durabletask-java/sessions/ea332218-fd02-4338-8805-e40f0009eb55 Co-authored-by: bachuv <15795068+bachuv@users.noreply.github.com>
1 parent a2d7c00 commit 42765bc

File tree

2 files changed

+178
-29
lines changed

2 files changed

+178
-29
lines changed

client/src/main/java/com/microsoft/durabletask/StatusRuntimeExceptionHelper.java

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,50 @@
55
import io.grpc.Status;
66
import io.grpc.StatusRuntimeException;
77

8+
import java.util.NoSuchElementException;
89
import java.util.concurrent.CancellationException;
910
import java.util.concurrent.TimeoutException;
1011

1112
/**
1213
* Utility class to translate gRPC {@link StatusRuntimeException} into SDK-level exceptions.
1314
* This ensures callers do not need to depend on gRPC types directly.
15+
*
16+
* <p>Status code mappings:
17+
* <ul>
18+
* <li>{@code CANCELLED} → {@link CancellationException}</li>
19+
* <li>{@code DEADLINE_EXCEEDED} → {@link TimeoutException} (via {@link #toException})</li>
20+
* <li>{@code INVALID_ARGUMENT} → {@link IllegalArgumentException}</li>
21+
* <li>{@code FAILED_PRECONDITION} → {@link IllegalStateException}</li>
22+
* <li>{@code NOT_FOUND} → {@link NoSuchElementException}</li>
23+
* <li>{@code UNIMPLEMENTED} → {@link UnsupportedOperationException}</li>
24+
* <li>All other codes → {@link RuntimeException}</li>
25+
* </ul>
1426
*/
1527
final class StatusRuntimeExceptionHelper {
1628

1729
/**
18-
* Translates a {@link StatusRuntimeException} into an appropriate SDK-level exception.
30+
* Translates a {@link StatusRuntimeException} into an appropriate SDK-level unchecked exception.
1931
*
2032
* @param e the gRPC exception to translate
2133
* @param operationName the name of the operation that failed, used in exception messages
2234
* @return a translated RuntimeException (never returns null)
2335
*/
2436
static RuntimeException toRuntimeException(StatusRuntimeException e, String operationName) {
2537
Status.Code code = e.getStatus().getCode();
38+
String message = formatMessage(operationName, code, getDescriptionOrDefault(e));
2639
switch (code) {
2740
case CANCELLED:
2841
return createCancellationException(e, operationName);
42+
case INVALID_ARGUMENT:
43+
return new IllegalArgumentException(message, e);
44+
case FAILED_PRECONDITION:
45+
return new IllegalStateException(message, e);
46+
case NOT_FOUND:
47+
return createNoSuchElementException(e, message);
48+
case UNIMPLEMENTED:
49+
return new UnsupportedOperationException(message, e);
2950
default:
30-
return createRuntimeException(e, operationName, code);
51+
return new RuntimeException(message, e);
3152
}
3253
}
3354

@@ -45,14 +66,22 @@ static RuntimeException toRuntimeException(StatusRuntimeException e, String oper
4566
*/
4667
static Exception toException(StatusRuntimeException e, String operationName) {
4768
Status.Code code = e.getStatus().getCode();
69+
String message = formatMessage(operationName, code, getDescriptionOrDefault(e));
4870
switch (code) {
4971
case DEADLINE_EXCEEDED:
50-
return new TimeoutException(
51-
formatMessage(operationName, code, getDescriptionOrDefault(e)));
72+
return new TimeoutException(message);
5273
case CANCELLED:
5374
return createCancellationException(e, operationName);
75+
case INVALID_ARGUMENT:
76+
return new IllegalArgumentException(message, e);
77+
case FAILED_PRECONDITION:
78+
return new IllegalStateException(message, e);
79+
case NOT_FOUND:
80+
return createNoSuchElementException(e, message);
81+
case UNIMPLEMENTED:
82+
return new UnsupportedOperationException(message, e);
5483
default:
55-
return createRuntimeException(e, operationName, code);
84+
return new RuntimeException(message, e);
5685
}
5786
}
5887

@@ -64,10 +93,11 @@ private static CancellationException createCancellationException(
6493
return ce;
6594
}
6695

67-
private static RuntimeException createRuntimeException(
68-
StatusRuntimeException e, String operationName, Status.Code code) {
69-
return new RuntimeException(
70-
formatMessage(operationName, code, getDescriptionOrDefault(e)), e);
96+
private static NoSuchElementException createNoSuchElementException(
97+
StatusRuntimeException e, String message) {
98+
NoSuchElementException ne = new NoSuchElementException(message);
99+
ne.initCause(e);
100+
return ne;
71101
}
72102

73103
private static String formatMessage(String operationName, Status.Code code, String description) {

client/src/test/java/com/microsoft/durabletask/StatusRuntimeExceptionHelperTest.java

Lines changed: 139 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.grpc.StatusRuntimeException;
88
import org.junit.jupiter.api.Test;
99

10+
import java.util.NoSuchElementException;
1011
import java.util.concurrent.CancellationException;
1112
import java.util.concurrent.TimeoutException;
1213

@@ -17,7 +18,7 @@
1718
*/
1819
public class StatusRuntimeExceptionHelperTest {
1920

20-
// Tests for toRuntimeException
21+
// ── toRuntimeException tests ──
2122

2223
@Test
2324
void toRuntimeException_cancelledStatus_returnsCancellationException() {
@@ -45,33 +46,62 @@ void toRuntimeException_cancelledStatusWithDescription_returnsCancellationExcept
4546
}
4647

4748
@Test
48-
void toRuntimeException_unavailableStatus_returnsRuntimeException() {
49+
void toRuntimeException_invalidArgumentStatus_returnsIllegalArgumentException() {
4950
StatusRuntimeException grpcException = new StatusRuntimeException(
50-
Status.UNAVAILABLE.withDescription("Connection refused"));
51+
Status.INVALID_ARGUMENT.withDescription("instanceId is required"));
5152

5253
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
53-
grpcException, "terminate");
54+
grpcException, "scheduleNewOrchestrationInstance");
5455

55-
assertInstanceOf(RuntimeException.class, result);
56-
assertNotEquals(CancellationException.class, result.getClass());
57-
assertTrue(result.getMessage().contains("terminate"));
58-
assertTrue(result.getMessage().contains("UNAVAILABLE"));
59-
assertTrue(result.getMessage().contains("Connection refused"));
56+
assertInstanceOf(IllegalArgumentException.class, result);
57+
assertTrue(result.getMessage().contains("scheduleNewOrchestrationInstance"));
58+
assertTrue(result.getMessage().contains("INVALID_ARGUMENT"));
59+
assertTrue(result.getMessage().contains("instanceId is required"));
6060
assertSame(grpcException, result.getCause());
6161
}
6262

6363
@Test
64-
void toRuntimeException_internalStatus_returnsRuntimeException() {
64+
void toRuntimeException_failedPreconditionStatus_returnsIllegalStateException() {
6565
StatusRuntimeException grpcException = new StatusRuntimeException(
66-
Status.INTERNAL.withDescription("Internal server error"));
66+
Status.FAILED_PRECONDITION.withDescription("instance already running"));
6767

6868
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
6969
grpcException, "suspendInstance");
7070

71-
assertInstanceOf(RuntimeException.class, result);
71+
assertInstanceOf(IllegalStateException.class, result);
7272
assertTrue(result.getMessage().contains("suspendInstance"));
73-
assertTrue(result.getMessage().contains("INTERNAL"));
74-
assertTrue(result.getMessage().contains("Internal server error"));
73+
assertTrue(result.getMessage().contains("FAILED_PRECONDITION"));
74+
assertTrue(result.getMessage().contains("instance already running"));
75+
assertSame(grpcException, result.getCause());
76+
}
77+
78+
@Test
79+
void toRuntimeException_notFoundStatus_returnsNoSuchElementException() {
80+
StatusRuntimeException grpcException = new StatusRuntimeException(
81+
Status.NOT_FOUND.withDescription("instance not found"));
82+
83+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
84+
grpcException, "getInstanceMetadata");
85+
86+
assertInstanceOf(NoSuchElementException.class, result);
87+
assertTrue(result.getMessage().contains("getInstanceMetadata"));
88+
assertTrue(result.getMessage().contains("NOT_FOUND"));
89+
assertTrue(result.getMessage().contains("instance not found"));
90+
assertSame(grpcException, result.getCause());
91+
}
92+
93+
@Test
94+
void toRuntimeException_unimplementedStatus_returnsUnsupportedOperationException() {
95+
StatusRuntimeException grpcException = new StatusRuntimeException(
96+
Status.UNIMPLEMENTED.withDescription("method not supported"));
97+
98+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
99+
grpcException, "rewindInstance");
100+
101+
assertInstanceOf(UnsupportedOperationException.class, result);
102+
assertTrue(result.getMessage().contains("rewindInstance"));
103+
assertTrue(result.getMessage().contains("UNIMPLEMENTED"));
104+
assertTrue(result.getMessage().contains("method not supported"));
75105
assertSame(grpcException, result.getCause());
76106
}
77107

@@ -87,6 +117,39 @@ void toRuntimeException_deadlineExceededStatus_returnsRuntimeException() {
87117
assertTrue(result.getMessage().contains("DEADLINE_EXCEEDED"));
88118
}
89119

120+
@Test
121+
void toRuntimeException_unavailableStatus_returnsRuntimeException() {
122+
StatusRuntimeException grpcException = new StatusRuntimeException(
123+
Status.UNAVAILABLE.withDescription("Connection refused"));
124+
125+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
126+
grpcException, "terminate");
127+
128+
assertInstanceOf(RuntimeException.class, result);
129+
assertFalse(result instanceof IllegalArgumentException);
130+
assertFalse(result instanceof IllegalStateException);
131+
assertFalse(result instanceof UnsupportedOperationException);
132+
assertTrue(result.getMessage().contains("terminate"));
133+
assertTrue(result.getMessage().contains("UNAVAILABLE"));
134+
assertTrue(result.getMessage().contains("Connection refused"));
135+
assertSame(grpcException, result.getCause());
136+
}
137+
138+
@Test
139+
void toRuntimeException_internalStatus_returnsRuntimeException() {
140+
StatusRuntimeException grpcException = new StatusRuntimeException(
141+
Status.INTERNAL.withDescription("Internal server error"));
142+
143+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
144+
grpcException, "suspendInstance");
145+
146+
assertInstanceOf(RuntimeException.class, result);
147+
assertTrue(result.getMessage().contains("suspendInstance"));
148+
assertTrue(result.getMessage().contains("INTERNAL"));
149+
assertTrue(result.getMessage().contains("Internal server error"));
150+
assertSame(grpcException, result.getCause());
151+
}
152+
90153
@Test
91154
void toRuntimeException_preservesOperationName() {
92155
StatusRuntimeException grpcException = new StatusRuntimeException(Status.UNKNOWN);
@@ -106,11 +169,11 @@ void toRuntimeException_nullDescription_usesDefaultFallback() {
106169

107170
assertTrue(result.getMessage().contains("(no description)"),
108171
"Expected '(no description)' fallback but got: " + result.getMessage());
109-
assertFalse(result.getMessage().contains("null"),
110-
"Message should not contain literal 'null': " + result.getMessage());
172+
assertFalse(result.getMessage().contains(": null"),
173+
"Message should not contain literal ': null': " + result.getMessage());
111174
}
112175

113-
// Tests for toException (checked exception variant)
176+
// ── toException tests ──
114177

115178
@Test
116179
void toException_deadlineExceededStatus_returnsTimeoutException() {
@@ -151,6 +214,62 @@ void toException_cancelledStatus_returnsCancellationException() {
151214
assertSame(grpcException, result.getCause());
152215
}
153216

217+
@Test
218+
void toException_invalidArgumentStatus_returnsIllegalArgumentException() {
219+
StatusRuntimeException grpcException = new StatusRuntimeException(
220+
Status.INVALID_ARGUMENT.withDescription("bad input"));
221+
222+
Exception result = StatusRuntimeExceptionHelper.toException(
223+
grpcException, "waitForInstanceStart");
224+
225+
assertInstanceOf(IllegalArgumentException.class, result);
226+
assertTrue(result.getMessage().contains("waitForInstanceStart"));
227+
assertTrue(result.getMessage().contains("INVALID_ARGUMENT"));
228+
assertSame(grpcException, result.getCause());
229+
}
230+
231+
@Test
232+
void toException_failedPreconditionStatus_returnsIllegalStateException() {
233+
StatusRuntimeException grpcException = new StatusRuntimeException(
234+
Status.FAILED_PRECONDITION.withDescription("not ready"));
235+
236+
Exception result = StatusRuntimeExceptionHelper.toException(
237+
grpcException, "waitForInstanceCompletion");
238+
239+
assertInstanceOf(IllegalStateException.class, result);
240+
assertTrue(result.getMessage().contains("waitForInstanceCompletion"));
241+
assertTrue(result.getMessage().contains("FAILED_PRECONDITION"));
242+
assertSame(grpcException, result.getCause());
243+
}
244+
245+
@Test
246+
void toException_notFoundStatus_returnsNoSuchElementException() {
247+
StatusRuntimeException grpcException = new StatusRuntimeException(
248+
Status.NOT_FOUND.withDescription("not found"));
249+
250+
Exception result = StatusRuntimeExceptionHelper.toException(
251+
grpcException, "purgeInstances");
252+
253+
assertInstanceOf(NoSuchElementException.class, result);
254+
assertTrue(result.getMessage().contains("purgeInstances"));
255+
assertTrue(result.getMessage().contains("NOT_FOUND"));
256+
assertSame(grpcException, result.getCause());
257+
}
258+
259+
@Test
260+
void toException_unimplementedStatus_returnsUnsupportedOperationException() {
261+
StatusRuntimeException grpcException = new StatusRuntimeException(
262+
Status.UNIMPLEMENTED.withDescription("not implemented"));
263+
264+
Exception result = StatusRuntimeExceptionHelper.toException(
265+
grpcException, "rewindInstance");
266+
267+
assertInstanceOf(UnsupportedOperationException.class, result);
268+
assertTrue(result.getMessage().contains("rewindInstance"));
269+
assertTrue(result.getMessage().contains("UNIMPLEMENTED"));
270+
assertSame(grpcException, result.getCause());
271+
}
272+
154273
@Test
155274
void toException_unavailableStatus_returnsRuntimeException() {
156275
StatusRuntimeException grpcException = new StatusRuntimeException(Status.UNAVAILABLE);
@@ -159,7 +278,7 @@ void toException_unavailableStatus_returnsRuntimeException() {
159278
grpcException, "waitForInstanceCompletion");
160279

161280
assertInstanceOf(RuntimeException.class, result);
162-
assertNotEquals(CancellationException.class, result.getClass());
281+
assertFalse(result instanceof CancellationException);
163282
assertTrue(result.getMessage().contains("waitForInstanceCompletion"));
164283
assertTrue(result.getMessage().contains("UNAVAILABLE"));
165284
}
@@ -186,7 +305,7 @@ void toException_nullDescription_usesDefaultFallback() {
186305
assertInstanceOf(TimeoutException.class, result);
187306
assertTrue(result.getMessage().contains("(no description)"),
188307
"Expected '(no description)' fallback but got: " + result.getMessage());
189-
assertFalse(result.getMessage().contains("null"),
190-
"Message should not contain literal 'null': " + result.getMessage());
308+
assertFalse(result.getMessage().contains(": null"),
309+
"Message should not contain literal ': null': " + result.getMessage());
191310
}
192311
}

0 commit comments

Comments
 (0)