Skip to content

Commit bb4a5bd

Browse files
Copilotbachuv
andauthored
Wrap gRPC StatusRuntimeException across all DurableTaskGrpcClient methods
Add StatusRuntimeExceptionHelper to translate gRPC StatusRuntimeException into SDK-level exceptions: - CANCELLED -> CancellationException - DEADLINE_EXCEEDED -> TimeoutException (for checked exception variant) - Other codes -> RuntimeException with descriptive message Wrap all previously-unwrapped gRPC calls in DurableTaskGrpcClient: - scheduleNewOrchestrationInstance, raiseEvent, getInstanceMetadata - terminate, queryInstances, createTaskHub, deleteTaskHub - purgeInstance, suspendInstance, resumeInstance Update partially-wrapped methods to use helper as fallback: - waitForInstanceStart, waitForInstanceCompletion, purgeInstances, rewindInstance Add comprehensive unit tests for StatusRuntimeExceptionHelper. Agent-Logs-Url: https://github.com/microsoft/durabletask-java/sessions/6c71e4a3-6ba6-4f7c-b5fb-038b059128d0 Co-authored-by: bachuv <15795068+bachuv@users.noreply.github.com>
1 parent 20baafb commit bb4a5bd

3 files changed

Lines changed: 287 additions & 16 deletions

File tree

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

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ public String scheduleNewOrchestrationInstance(
158158
CreateInstanceRequest request = builder.build();
159159
CreateInstanceResponse response = this.sidecarClient.startInstance(request);
160160
return response.getInstanceId();
161+
} catch (StatusRuntimeException e) {
162+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "scheduleNewOrchestrationInstance");
161163
} finally {
162164
createScope.close();
163165
createSpan.end();
@@ -181,7 +183,11 @@ public void raiseEvent(String instanceId, String eventName, Object eventPayload)
181183
}
182184

183185
RaiseEventRequest request = builder.build();
184-
this.sidecarClient.raiseEvent(request);
186+
try {
187+
this.sidecarClient.raiseEvent(request);
188+
} catch (StatusRuntimeException e) {
189+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "raiseEvent");
190+
}
185191
}
186192

187193
@Override
@@ -190,8 +196,12 @@ public OrchestrationMetadata getInstanceMetadata(String instanceId, boolean getI
190196
.setInstanceId(instanceId)
191197
.setGetInputsAndOutputs(getInputsAndOutputs)
192198
.build();
193-
GetInstanceResponse response = this.sidecarClient.getInstance(request);
194-
return new OrchestrationMetadata(response, this.dataConverter, request.getGetInputsAndOutputs());
199+
try {
200+
GetInstanceResponse response = this.sidecarClient.getInstance(request);
201+
return new OrchestrationMetadata(response, this.dataConverter, request.getGetInputsAndOutputs());
202+
} catch (StatusRuntimeException e) {
203+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "getInstanceMetadata");
204+
}
195205
}
196206

197207
@Override
@@ -216,7 +226,11 @@ public OrchestrationMetadata waitForInstanceStart(String instanceId, Duration ti
216226
if (e.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED) {
217227
throw new TimeoutException("Start orchestration timeout reached.");
218228
}
219-
throw e;
229+
Exception translated = StatusRuntimeExceptionHelper.toException(e, "waitForInstanceStart");
230+
if (translated instanceof RuntimeException) {
231+
throw (RuntimeException) translated;
232+
}
233+
throw new RuntimeException(translated);
220234
}
221235
return new OrchestrationMetadata(response, this.dataConverter, request.getGetInputsAndOutputs());
222236
}
@@ -243,7 +257,11 @@ public OrchestrationMetadata waitForInstanceCompletion(String instanceId, Durati
243257
if (e.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED) {
244258
throw new TimeoutException("Orchestration instance completion timeout reached.");
245259
}
246-
throw e;
260+
Exception translated = StatusRuntimeExceptionHelper.toException(e, "waitForInstanceCompletion");
261+
if (translated instanceof RuntimeException) {
262+
throw (RuntimeException) translated;
263+
}
264+
throw new RuntimeException(translated);
247265
}
248266
return new OrchestrationMetadata(response, this.dataConverter, request.getGetInputsAndOutputs());
249267
}
@@ -260,7 +278,11 @@ public void terminate(String instanceId, @Nullable Object output) {
260278
if (serializeOutput != null){
261279
builder.setOutput(StringValue.of(serializeOutput));
262280
}
263-
this.sidecarClient.terminateInstance(builder.build());
281+
try {
282+
this.sidecarClient.terminateInstance(builder.build());
283+
} catch (StatusRuntimeException e) {
284+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "terminate");
285+
}
264286
}
265287

266288
@Override
@@ -274,8 +296,12 @@ public OrchestrationStatusQueryResult queryInstances(OrchestrationStatusQuery qu
274296
instanceQueryBuilder.setMaxInstanceCount(query.getMaxInstanceCount());
275297
query.getRuntimeStatusList().forEach(runtimeStatus -> Optional.ofNullable(runtimeStatus).ifPresent(status -> instanceQueryBuilder.addRuntimeStatus(OrchestrationRuntimeStatus.toProtobuf(status))));
276298
query.getTaskHubNames().forEach(taskHubName -> Optional.ofNullable(taskHubName).ifPresent(name -> instanceQueryBuilder.addTaskHubNames(StringValue.of(name))));
277-
QueryInstancesResponse queryInstancesResponse = this.sidecarClient.queryInstances(QueryInstancesRequest.newBuilder().setQuery(instanceQueryBuilder).build());
278-
return toQueryResult(queryInstancesResponse, query.isFetchInputsAndOutputs());
299+
try {
300+
QueryInstancesResponse queryInstancesResponse = this.sidecarClient.queryInstances(QueryInstancesRequest.newBuilder().setQuery(instanceQueryBuilder).build());
301+
return toQueryResult(queryInstancesResponse, query.isFetchInputsAndOutputs());
302+
} catch (StatusRuntimeException e) {
303+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "queryInstances");
304+
}
279305
}
280306

281307
private OrchestrationStatusQueryResult toQueryResult(QueryInstancesResponse queryInstancesResponse, boolean fetchInputsAndOutputs){
@@ -288,12 +314,20 @@ private OrchestrationStatusQueryResult toQueryResult(QueryInstancesResponse quer
288314

289315
@Override
290316
public void createTaskHub(boolean recreateIfExists) {
291-
this.sidecarClient.createTaskHub(CreateTaskHubRequest.newBuilder().setRecreateIfExists(recreateIfExists).build());
317+
try {
318+
this.sidecarClient.createTaskHub(CreateTaskHubRequest.newBuilder().setRecreateIfExists(recreateIfExists).build());
319+
} catch (StatusRuntimeException e) {
320+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "createTaskHub");
321+
}
292322
}
293323

294324
@Override
295325
public void deleteTaskHub() {
296-
this.sidecarClient.deleteTaskHub(DeleteTaskHubRequest.newBuilder().build());
326+
try {
327+
this.sidecarClient.deleteTaskHub(DeleteTaskHubRequest.newBuilder().build());
328+
} catch (StatusRuntimeException e) {
329+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "deleteTaskHub");
330+
}
297331
}
298332

299333
@Override
@@ -302,8 +336,12 @@ public PurgeResult purgeInstance(String instanceId) {
302336
.setInstanceId(instanceId)
303337
.build();
304338

305-
PurgeInstancesResponse response = this.sidecarClient.purgeInstances(request);
306-
return toPurgeResult(response);
339+
try {
340+
PurgeInstancesResponse response = this.sidecarClient.purgeInstances(request);
341+
return toPurgeResult(response);
342+
} catch (StatusRuntimeException e) {
343+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "purgeInstance");
344+
}
307345
}
308346

309347
@Override
@@ -331,7 +369,11 @@ public PurgeResult purgeInstances(PurgeInstanceCriteria purgeInstanceCriteria) t
331369
String timeOutException = String.format("Purge instances timeout duration of %s reached.", timeout);
332370
throw new TimeoutException(timeOutException);
333371
}
334-
throw e;
372+
Exception translated = StatusRuntimeExceptionHelper.toException(e, "purgeInstances");
373+
if (translated instanceof RuntimeException) {
374+
throw (RuntimeException) translated;
375+
}
376+
throw new RuntimeException(translated);
335377
}
336378
}
337379

@@ -342,7 +384,11 @@ public void suspendInstance(String instanceId, @Nullable String reason) {
342384
if (reason != null) {
343385
suspendRequestBuilder.setReason(StringValue.of(reason));
344386
}
345-
this.sidecarClient.suspendInstance(suspendRequestBuilder.build());
387+
try {
388+
this.sidecarClient.suspendInstance(suspendRequestBuilder.build());
389+
} catch (StatusRuntimeException e) {
390+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "suspendInstance");
391+
}
346392
}
347393

348394
@Override
@@ -352,7 +398,11 @@ public void resumeInstance(String instanceId, @Nullable String reason) {
352398
if (reason != null) {
353399
resumeRequestBuilder.setReason(StringValue.of(reason));
354400
}
355-
this.sidecarClient.resumeInstance(resumeRequestBuilder.build());
401+
try {
402+
this.sidecarClient.resumeInstance(resumeRequestBuilder.build());
403+
} catch (StatusRuntimeException e) {
404+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "resumeInstance");
405+
}
356406
}
357407

358408
@Override
@@ -374,7 +424,7 @@ public void rewindInstance(String instanceId, @Nullable String reason) {
374424
throw new IllegalStateException(
375425
"Orchestration instance '" + instanceId + "' is not in a failed state and cannot be rewound.", e);
376426
}
377-
throw e;
427+
throw StatusRuntimeExceptionHelper.toRuntimeException(e, "rewindInstance");
378428
}
379429
}
380430

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.microsoft.durabletask;
4+
5+
import io.grpc.Status;
6+
import io.grpc.StatusRuntimeException;
7+
8+
import java.util.concurrent.CancellationException;
9+
import java.util.concurrent.TimeoutException;
10+
11+
/**
12+
* Utility class to translate gRPC {@link StatusRuntimeException} into SDK-level exceptions.
13+
* This ensures callers do not need to depend on gRPC types directly.
14+
*/
15+
final class StatusRuntimeExceptionHelper {
16+
17+
/**
18+
* Translates a {@link StatusRuntimeException} into an appropriate SDK-level exception.
19+
*
20+
* @param e the gRPC exception to translate
21+
* @param operationName the name of the operation that failed, used in exception messages
22+
* @return a translated RuntimeException (never returns null)
23+
*/
24+
static RuntimeException toRuntimeException(StatusRuntimeException e, String operationName) {
25+
Status.Code code = e.getStatus().getCode();
26+
switch (code) {
27+
case CANCELLED:
28+
CancellationException ce = new CancellationException(
29+
"The " + operationName + " operation was canceled.");
30+
ce.initCause(e);
31+
return ce;
32+
default:
33+
return new RuntimeException(
34+
"The " + operationName + " operation failed with a " + code + " gRPC status: "
35+
+ e.getStatus().getDescription(),
36+
e);
37+
}
38+
}
39+
40+
/**
41+
* Translates a {@link StatusRuntimeException} into an appropriate SDK-level checked exception
42+
* for operations that declare {@code throws TimeoutException}.
43+
*
44+
* @param e the gRPC exception to translate
45+
* @param operationName the name of the operation that failed, used in exception messages
46+
* @return a translated Exception (never returns null)
47+
*/
48+
static Exception toException(StatusRuntimeException e, String operationName) {
49+
Status.Code code = e.getStatus().getCode();
50+
switch (code) {
51+
case DEADLINE_EXCEEDED:
52+
return new TimeoutException(
53+
"The " + operationName + " operation timed out: " + e.getStatus().getDescription());
54+
case CANCELLED:
55+
CancellationException ce = new CancellationException(
56+
"The " + operationName + " operation was canceled.");
57+
ce.initCause(e);
58+
return ce;
59+
default:
60+
return new RuntimeException(
61+
"The " + operationName + " operation failed with a " + code + " gRPC status: "
62+
+ e.getStatus().getDescription(),
63+
e);
64+
}
65+
}
66+
67+
// Cannot be instantiated
68+
private StatusRuntimeExceptionHelper() {
69+
}
70+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.durabletask;
5+
6+
import io.grpc.Status;
7+
import io.grpc.StatusRuntimeException;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.util.concurrent.CancellationException;
11+
import java.util.concurrent.TimeoutException;
12+
13+
import static org.junit.jupiter.api.Assertions.*;
14+
15+
/**
16+
* Unit tests for {@link StatusRuntimeExceptionHelper}.
17+
*/
18+
public class StatusRuntimeExceptionHelperTest {
19+
20+
// Tests for toRuntimeException
21+
22+
@Test
23+
void toRuntimeException_cancelledStatus_returnsCancellationException() {
24+
StatusRuntimeException grpcException = new StatusRuntimeException(Status.CANCELLED);
25+
26+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
27+
grpcException, "testOperation");
28+
29+
assertInstanceOf(CancellationException.class, result);
30+
assertTrue(result.getMessage().contains("testOperation"));
31+
assertTrue(result.getMessage().contains("canceled"));
32+
assertSame(grpcException, result.getCause());
33+
}
34+
35+
@Test
36+
void toRuntimeException_cancelledStatusWithDescription_returnsCancellationException() {
37+
StatusRuntimeException grpcException = new StatusRuntimeException(
38+
Status.CANCELLED.withDescription("context cancelled"));
39+
40+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
41+
grpcException, "raiseEvent");
42+
43+
assertInstanceOf(CancellationException.class, result);
44+
assertTrue(result.getMessage().contains("raiseEvent"));
45+
}
46+
47+
@Test
48+
void toRuntimeException_unavailableStatus_returnsRuntimeException() {
49+
StatusRuntimeException grpcException = new StatusRuntimeException(
50+
Status.UNAVAILABLE.withDescription("Connection refused"));
51+
52+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
53+
grpcException, "terminate");
54+
55+
assertInstanceOf(RuntimeException.class, result);
56+
assertNotEquals(CancellationException.class, result.getClass());
57+
assertTrue(result.getMessage().contains("terminate"));
58+
assertTrue(result.getMessage().contains("UNAVAILABLE"));
59+
assertSame(grpcException, result.getCause());
60+
}
61+
62+
@Test
63+
void toRuntimeException_internalStatus_returnsRuntimeException() {
64+
StatusRuntimeException grpcException = new StatusRuntimeException(
65+
Status.INTERNAL.withDescription("Internal server error"));
66+
67+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
68+
grpcException, "suspendInstance");
69+
70+
assertInstanceOf(RuntimeException.class, result);
71+
assertTrue(result.getMessage().contains("suspendInstance"));
72+
assertTrue(result.getMessage().contains("INTERNAL"));
73+
assertTrue(result.getMessage().contains("Internal server error"));
74+
assertSame(grpcException, result.getCause());
75+
}
76+
77+
@Test
78+
void toRuntimeException_deadlineExceededStatus_returnsRuntimeException() {
79+
StatusRuntimeException grpcException = new StatusRuntimeException(Status.DEADLINE_EXCEEDED);
80+
81+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
82+
grpcException, "getInstanceMetadata");
83+
84+
assertInstanceOf(RuntimeException.class, result);
85+
assertTrue(result.getMessage().contains("getInstanceMetadata"));
86+
assertTrue(result.getMessage().contains("DEADLINE_EXCEEDED"));
87+
}
88+
89+
@Test
90+
void toRuntimeException_preservesOperationName() {
91+
StatusRuntimeException grpcException = new StatusRuntimeException(Status.UNKNOWN);
92+
93+
RuntimeException result = StatusRuntimeExceptionHelper.toRuntimeException(
94+
grpcException, "customOperationName");
95+
96+
assertTrue(result.getMessage().contains("customOperationName"));
97+
}
98+
99+
// Tests for toException (checked exception variant)
100+
101+
@Test
102+
void toException_deadlineExceededStatus_returnsTimeoutException() {
103+
StatusRuntimeException grpcException = new StatusRuntimeException(
104+
Status.DEADLINE_EXCEEDED.withDescription("deadline exceeded after 10s"));
105+
106+
Exception result = StatusRuntimeExceptionHelper.toException(
107+
grpcException, "waitForInstanceStart");
108+
109+
assertInstanceOf(TimeoutException.class, result);
110+
assertTrue(result.getMessage().contains("waitForInstanceStart"));
111+
assertTrue(result.getMessage().contains("timed out"));
112+
}
113+
114+
@Test
115+
void toException_cancelledStatus_returnsCancellationException() {
116+
StatusRuntimeException grpcException = new StatusRuntimeException(Status.CANCELLED);
117+
118+
Exception result = StatusRuntimeExceptionHelper.toException(
119+
grpcException, "purgeInstances");
120+
121+
assertInstanceOf(CancellationException.class, result);
122+
assertTrue(result.getMessage().contains("purgeInstances"));
123+
assertTrue(result.getMessage().contains("canceled"));
124+
assertSame(grpcException, result.getCause());
125+
}
126+
127+
@Test
128+
void toException_unavailableStatus_returnsRuntimeException() {
129+
StatusRuntimeException grpcException = new StatusRuntimeException(Status.UNAVAILABLE);
130+
131+
Exception result = StatusRuntimeExceptionHelper.toException(
132+
grpcException, "waitForInstanceCompletion");
133+
134+
assertInstanceOf(RuntimeException.class, result);
135+
assertNotEquals(CancellationException.class, result.getClass());
136+
assertTrue(result.getMessage().contains("waitForInstanceCompletion"));
137+
assertTrue(result.getMessage().contains("UNAVAILABLE"));
138+
}
139+
140+
@Test
141+
void toException_internalStatus_returnsRuntimeExceptionWithCause() {
142+
StatusRuntimeException grpcException = new StatusRuntimeException(
143+
Status.INTERNAL.withDescription("server error"));
144+
145+
Exception result = StatusRuntimeExceptionHelper.toException(
146+
grpcException, "purgeInstances");
147+
148+
assertInstanceOf(RuntimeException.class, result);
149+
assertSame(grpcException, result.getCause());
150+
}
151+
}

0 commit comments

Comments
 (0)