|
9 | 9 | import org.junit.jupiter.params.provider.ValueSource; |
10 | 10 |
|
11 | 11 | import java.time.Duration; |
| 12 | +import java.util.HashMap; |
| 13 | +import java.util.Map; |
12 | 14 | import java.util.concurrent.TimeoutException; |
13 | 15 | import java.util.concurrent.atomic.AtomicBoolean; |
14 | 16 | import java.util.concurrent.atomic.AtomicInteger; |
@@ -309,4 +311,260 @@ private FailureDetails retryOnFailuresCoreTest( |
309 | 311 | return details; |
310 | 312 | } |
311 | 313 | } |
| 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 | + } |
312 | 570 | } |
0 commit comments