Skip to content

Commit e8fc02a

Browse files
committed
udpate
1 parent 17f1ec2 commit e8fc02a

File tree

3 files changed

+324
-341
lines changed

3 files changed

+324
-341
lines changed

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

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import org.junit.jupiter.params.provider.ValueSource;
1010

1111
import java.time.Duration;
12+
import java.util.HashMap;
13+
import java.util.Map;
1214
import java.util.concurrent.TimeoutException;
1315
import java.util.concurrent.atomic.AtomicBoolean;
1416
import java.util.concurrent.atomic.AtomicInteger;
@@ -309,4 +311,260 @@ private FailureDetails retryOnFailuresCoreTest(
309311
return details;
310312
}
311313
}
314+
315+
/**
316+
* Tests that inner exception details are preserved without a provider, and no properties are included.
317+
*/
318+
@Test
319+
void innerExceptionDetailsArePreserved() throws TimeoutException {
320+
final String orchestratorName = "Parent";
321+
final String subOrchestratorName = "Sub";
322+
final String activityName = "ThrowException";
323+
324+
DurableTaskGrpcWorker worker = this.createWorkerBuilder()
325+
.addOrchestrator(orchestratorName, ctx -> {
326+
ctx.callSubOrchestrator(subOrchestratorName, "", String.class).await();
327+
})
328+
.addOrchestrator(subOrchestratorName, ctx -> {
329+
ctx.callActivity(activityName).await();
330+
})
331+
.addActivity(activityName, ctx -> {
332+
throw new RuntimeException("first",
333+
new IllegalArgumentException("second",
334+
new IllegalStateException("third")));
335+
})
336+
.buildAndStart();
337+
338+
DurableTaskClient client = this.createClientBuilder().build();
339+
try (worker; client) {
340+
String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, "");
341+
OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true);
342+
assertNotNull(instance);
343+
assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus());
344+
345+
// Top-level: parent orchestration failed with TaskFailedException wrapping the sub-orchestration
346+
FailureDetails topLevel = instance.getFailureDetails();
347+
assertNotNull(topLevel);
348+
assertEquals("com.microsoft.durabletask.TaskFailedException", topLevel.getErrorType());
349+
assertTrue(topLevel.getErrorMessage().contains(subOrchestratorName));
350+
351+
// Level 1: sub-orchestration failed with TaskFailedException wrapping the activity
352+
assertNotNull(topLevel.getInnerFailure());
353+
FailureDetails subOrchFailure = topLevel.getInnerFailure();
354+
assertEquals("com.microsoft.durabletask.TaskFailedException", subOrchFailure.getErrorType());
355+
assertTrue(subOrchFailure.getErrorMessage().contains(activityName));
356+
357+
// Level 2: actual exception from the activity - RuntimeException("first")
358+
assertNotNull(subOrchFailure.getInnerFailure());
359+
FailureDetails activityFailure = subOrchFailure.getInnerFailure();
360+
assertEquals("java.lang.RuntimeException", activityFailure.getErrorType());
361+
assertEquals("first", activityFailure.getErrorMessage());
362+
363+
// Level 3: inner cause - IllegalArgumentException("second")
364+
assertNotNull(activityFailure.getInnerFailure());
365+
FailureDetails innerCause1 = activityFailure.getInnerFailure();
366+
assertEquals("java.lang.IllegalArgumentException", innerCause1.getErrorType());
367+
assertEquals("second", innerCause1.getErrorMessage());
368+
369+
// Level 4: innermost cause - IllegalStateException("third")
370+
assertNotNull(innerCause1.getInnerFailure());
371+
FailureDetails innerCause2 = innerCause1.getInnerFailure();
372+
assertEquals("java.lang.IllegalStateException", innerCause2.getErrorType());
373+
assertEquals("third", innerCause2.getErrorMessage());
374+
assertNull(innerCause2.getInnerFailure());
375+
376+
// No provider registered, so no properties at any level
377+
assertNull(topLevel.getProperties());
378+
assertNull(subOrchFailure.getProperties());
379+
assertNull(activityFailure.getProperties());
380+
assertNull(innerCause1.getProperties());
381+
assertNull(innerCause2.getProperties());
382+
}
383+
}
384+
385+
/**
386+
* Tests that a registered {@link ExceptionPropertiesProvider} extracts custom properties
387+
* from an activity exception into {@link FailureDetails#getProperties()}.
388+
*/
389+
@Test
390+
void customExceptionPropertiesInFailureDetails() throws TimeoutException {
391+
final String orchestratorName = "OrchestrationWithCustomException";
392+
final String activityName = "BusinessActivity";
393+
394+
ExceptionPropertiesProvider provider = exception -> {
395+
if (exception instanceof IllegalArgumentException) {
396+
Map<String, Object> props = new HashMap<>();
397+
props.put("paramName", exception.getMessage());
398+
return props;
399+
}
400+
if (exception instanceof BusinessValidationException) {
401+
BusinessValidationException bve = (BusinessValidationException) exception;
402+
Map<String, Object> props = new HashMap<>();
403+
props.put("errorCode", bve.errorCode);
404+
props.put("retryCount", bve.retryCount);
405+
props.put("isCritical", bve.isCritical);
406+
return props;
407+
}
408+
return null;
409+
};
410+
411+
DurableTaskGrpcWorker worker = this.createWorkerBuilder()
412+
.exceptionPropertiesProvider(provider)
413+
.addOrchestrator(orchestratorName, ctx -> {
414+
ctx.callActivity(activityName).await();
415+
})
416+
.addActivity(activityName, ctx -> {
417+
throw new BusinessValidationException(
418+
"Business logic validation failed",
419+
"VALIDATION_FAILED",
420+
3,
421+
true);
422+
})
423+
.buildAndStart();
424+
425+
DurableTaskClient client = this.createClientBuilder().build();
426+
try (worker; client) {
427+
String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, "");
428+
OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true);
429+
assertNotNull(instance);
430+
assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus());
431+
432+
FailureDetails topLevel = instance.getFailureDetails();
433+
assertNotNull(topLevel);
434+
assertEquals("com.microsoft.durabletask.TaskFailedException", topLevel.getErrorType());
435+
436+
// The activity failure is in the inner failure
437+
assertNotNull(topLevel.getInnerFailure());
438+
FailureDetails innerFailure = topLevel.getInnerFailure();
439+
assertTrue(innerFailure.getErrorType().contains("BusinessValidationException"));
440+
assertEquals("Business logic validation failed", innerFailure.getErrorMessage());
441+
442+
// Verify custom properties are included
443+
assertNotNull(innerFailure.getProperties());
444+
assertEquals(3, innerFailure.getProperties().size());
445+
assertEquals("VALIDATION_FAILED", innerFailure.getProperties().get("errorCode"));
446+
assertEquals(3.0, innerFailure.getProperties().get("retryCount"));
447+
assertEquals(true, innerFailure.getProperties().get("isCritical"));
448+
}
449+
}
450+
451+
/**
452+
* Tests that properties from a directly-thrown orchestration exception are on the top-level failure.
453+
*/
454+
@Test
455+
void orchestrationDirectExceptionWithProperties() throws TimeoutException {
456+
final String orchestratorName = "OrchestrationWithDirectException";
457+
final String paramName = "testParameter";
458+
459+
ExceptionPropertiesProvider provider = exception -> {
460+
if (exception instanceof IllegalArgumentException) {
461+
Map<String, Object> props = new HashMap<>();
462+
props.put("paramName", exception.getMessage());
463+
return props;
464+
}
465+
return null;
466+
};
467+
468+
DurableTaskGrpcWorker worker = this.createWorkerBuilder()
469+
.exceptionPropertiesProvider(provider)
470+
.addOrchestrator(orchestratorName, ctx -> {
471+
throw new IllegalArgumentException(paramName);
472+
})
473+
.buildAndStart();
474+
475+
DurableTaskClient client = this.createClientBuilder().build();
476+
try (worker; client) {
477+
String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, "");
478+
OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true);
479+
assertNotNull(instance);
480+
assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus());
481+
482+
FailureDetails details = instance.getFailureDetails();
483+
assertNotNull(details);
484+
assertEquals("java.lang.IllegalArgumentException", details.getErrorType());
485+
assertTrue(details.getErrorMessage().contains(paramName));
486+
487+
// Verify custom properties from provider
488+
assertNotNull(details.getProperties());
489+
assertEquals(1, details.getProperties().size());
490+
assertEquals(paramName, details.getProperties().get("paramName"));
491+
}
492+
}
493+
494+
/**
495+
* Tests that custom properties survive through a parent -> sub-orchestration -> activity chain.
496+
*/
497+
@Test
498+
void nestedOrchestrationExceptionPropertiesPreserved() throws TimeoutException {
499+
final String parentOrchName = "ParentOrch";
500+
final String subOrchName = "SubOrch";
501+
final String activityName = "ActivityWithProps";
502+
final String errorCode = "ERR_123";
503+
504+
ExceptionPropertiesProvider provider = exception -> {
505+
if (exception instanceof BusinessValidationException) {
506+
BusinessValidationException bve = (BusinessValidationException) exception;
507+
Map<String, Object> props = new HashMap<>();
508+
props.put("errorCode", bve.errorCode);
509+
props.put("retryCount", bve.retryCount);
510+
props.put("isCritical", bve.isCritical);
511+
return props;
512+
}
513+
return null;
514+
};
515+
516+
DurableTaskGrpcWorker worker = this.createWorkerBuilder()
517+
.exceptionPropertiesProvider(provider)
518+
.addOrchestrator(parentOrchName, ctx -> {
519+
ctx.callSubOrchestrator(subOrchName, "", String.class).await();
520+
})
521+
.addOrchestrator(subOrchName, ctx -> {
522+
ctx.callActivity(activityName).await();
523+
})
524+
.addActivity(activityName, ctx -> {
525+
throw new BusinessValidationException("nested error", errorCode, 5, false);
526+
})
527+
.buildAndStart();
528+
529+
DurableTaskClient client = this.createClientBuilder().build();
530+
try (worker; client) {
531+
String instanceId = client.scheduleNewOrchestrationInstance(parentOrchName, "");
532+
OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true);
533+
assertNotNull(instance);
534+
assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus());
535+
536+
// Parent -> TaskFailedException wrapping sub-orch
537+
FailureDetails topLevel = instance.getFailureDetails();
538+
assertNotNull(topLevel);
539+
assertTrue(topLevel.isCausedBy(TaskFailedException.class));
540+
541+
// Sub-orch -> TaskFailedException wrapping activity
542+
assertNotNull(topLevel.getInnerFailure());
543+
assertTrue(topLevel.getInnerFailure().isCausedBy(TaskFailedException.class));
544+
545+
// Activity -> BusinessValidationException with properties
546+
assertNotNull(topLevel.getInnerFailure().getInnerFailure());
547+
FailureDetails activityFailure = topLevel.getInnerFailure().getInnerFailure();
548+
assertTrue(activityFailure.getErrorType().contains("BusinessValidationException"));
549+
550+
// Verify properties survived the full chain
551+
assertNotNull(activityFailure.getProperties());
552+
assertEquals(errorCode, activityFailure.getProperties().get("errorCode"));
553+
assertEquals(5.0, activityFailure.getProperties().get("retryCount"));
554+
assertEquals(false, activityFailure.getProperties().get("isCritical"));
555+
}
556+
}
557+
558+
static class BusinessValidationException extends RuntimeException {
559+
final String errorCode;
560+
final int retryCount;
561+
final boolean isCritical;
562+
563+
BusinessValidationException(String message, String errorCode, int retryCount, boolean isCritical) {
564+
super(message);
565+
this.errorCode = errorCode;
566+
this.retryCount = retryCount;
567+
this.isCritical = isCritical;
568+
}
569+
}
312570
}

0 commit comments

Comments
 (0)