From 972beee4a6b144b3e8490306330afdaabbdb23dd Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Mon, 20 Oct 2025 16:59:01 +0200 Subject: [PATCH 1/5] add suppressed exception --- .../resilience4j/DefaultCircuitBreakerProvider.java | 9 +++++++-- .../resilience4j/CircuitBreakerTest.java | 12 +++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java b/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java index 7bac57f93..c50f37fdc 100644 --- a/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java +++ b/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java @@ -66,7 +66,7 @@ public CircuitBreaker getCircuitBreaker( @Nonnull final ResilienceConfiguration return circuitBreaker; } - @SuppressWarnings( "PMD.PreserveStackTrace" ) // The circuit breaker stack-trace doesn't contain any info + // @SuppressWarnings( "PMD.PreserveStackTrace" ) // The circuit breaker stack-trace doesn't contain any info @Nonnull @Override public Callable decorateCallable( @@ -85,7 +85,12 @@ public Callable decorateCallable( catch( CallNotPermittedException e ) { log.debug("Circuit breaker '{}' is open, call not permitted.", circuitBreaker.getName()); val lastException = lastExceptions.get(circuitBreaker.getName()); - throw new ResilienceRuntimeException(lastException != null ? lastException : e); + if( lastException == null ) { + throw new ResilienceRuntimeException(e); + } + val resilienceRuntimeException = new ResilienceRuntimeException(lastException); + resilienceRuntimeException.addSuppressed(e); + throw resilienceRuntimeException; } }; } diff --git a/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java b/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java index ec9bd496c..e0540d570 100644 --- a/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java +++ b/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -21,6 +22,7 @@ import com.sap.cloud.sdk.cloudplatform.resilience.ResilienceRuntimeException; import com.sap.cloud.sdk.cloudplatform.thread.exception.ThreadContextExecutionException; +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -76,13 +78,21 @@ void testCircuitBreakerOpens() assertThat(callResults).hasSize(attemptedInvocations); assertThat(callResults).startsWith(attempts.toArray(new Boolean[0])); - assertThatThrownBy(wrappedCallable::call) + Throwable thrown = catchThrowable(wrappedCallable::call); + + assertThat(thrown) .isExactlyInstanceOf(ResilienceRuntimeException.class) .hasCauseExactlyInstanceOf(ThreadContextExecutionException.class) .hasRootCauseExactlyInstanceOf(Exception.class) .hasMessage( "com.sap.cloud.sdk.cloudplatform.thread.exception.ThreadContextExecutionException: java.lang.Exception: Simulated failure, attempt nr: 3"); + assertThat(thrown.getSuppressed().length).isEqualTo(1); + Throwable suppressed = thrown.getSuppressed()[0]; + assertThat(suppressed) + .isExactlyInstanceOf(CallNotPermittedException.class) + .hasMessage("CircuitBreaker 'circuitbreaker.test.2' is OPEN and does not permit further calls"); + verify(callable, times(circuitBreakerConfiguration.closedBufferSize())).call(); } From 1d8d659fdb543b9ea25716f773202b66cfa8b2bd Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Mon, 20 Oct 2025 16:59:18 +0200 Subject: [PATCH 2/5] release notes --- release_notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release_notes.md b/release_notes.md index 3112514a9..e150016a6 100644 --- a/release_notes.md +++ b/release_notes.md @@ -16,7 +16,7 @@ ### 📈 Improvements -- +- When the circuit breaker opens, the resulting `ResilienceRuntimeException` will have the original `CallNotPermittedException` from the circuit breaker stored as a suppressed exception. ### 🐛 Fixed Issues From 6bef4f5c09c89ee8e50516d560d3cdde1c053e6b Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Tue, 21 Oct 2025 09:41:06 +0200 Subject: [PATCH 3/5] review --- .../resilience4j/DefaultCircuitBreakerProvider.java | 9 +++++---- .../cloudplatform/resilience4j/CircuitBreakerTest.java | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java b/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java index c50f37fdc..d6fcad040 100644 --- a/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java +++ b/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java @@ -66,7 +66,6 @@ public CircuitBreaker getCircuitBreaker( @Nonnull final ResilienceConfiguration return circuitBreaker; } - // @SuppressWarnings( "PMD.PreserveStackTrace" ) // The circuit breaker stack-trace doesn't contain any info @Nonnull @Override public Callable decorateCallable( @@ -83,12 +82,14 @@ public Callable decorateCallable( return CircuitBreaker.decorateCallable(circuitBreaker, callable).call(); } catch( CallNotPermittedException e ) { - log.debug("Circuit breaker '{}' is open, call not permitted.", circuitBreaker.getName()); + val message = + "CircuitBreaker '" + circuitBreaker.getName() + "' is OPEN and does not permit further calls"; + log.debug(message); val lastException = lastExceptions.get(circuitBreaker.getName()); if( lastException == null ) { - throw new ResilienceRuntimeException(e); + throw new ResilienceRuntimeException(message, e); } - val resilienceRuntimeException = new ResilienceRuntimeException(lastException); + val resilienceRuntimeException = new ResilienceRuntimeException(message, lastException); resilienceRuntimeException.addSuppressed(e); throw resilienceRuntimeException; } diff --git a/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java b/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java index e0540d570..e966489d7 100644 --- a/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java +++ b/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java @@ -84,8 +84,9 @@ void testCircuitBreakerOpens() .isExactlyInstanceOf(ResilienceRuntimeException.class) .hasCauseExactlyInstanceOf(ThreadContextExecutionException.class) .hasRootCauseExactlyInstanceOf(Exception.class) - .hasMessage( - "com.sap.cloud.sdk.cloudplatform.thread.exception.ThreadContextExecutionException: java.lang.Exception: Simulated failure, attempt nr: 3"); + .hasMessage("CircuitBreaker 'circuitbreaker.test.2' is OPEN and does not permit further calls"); + + assertThat(thrown.getCause()).hasMessage("java.lang.Exception: Simulated failure, attempt nr: 3"); assertThat(thrown.getSuppressed().length).isEqualTo(1); Throwable suppressed = thrown.getSuppressed()[0]; From 3b91ca2cf94979dfd23c22dba1cb08e5cd9754c1 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Wed, 22 Oct 2025 10:38:32 +0200 Subject: [PATCH 4/5] review again --- .../resilience4j/DefaultCircuitBreakerProvider.java | 2 +- .../sdk/cloudplatform/resilience4j/CircuitBreakerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java b/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java index d6fcad040..b7b8abb7f 100644 --- a/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java +++ b/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java @@ -89,7 +89,7 @@ public Callable decorateCallable( if( lastException == null ) { throw new ResilienceRuntimeException(message, e); } - val resilienceRuntimeException = new ResilienceRuntimeException(message, lastException); + val resilienceRuntimeException = new ResilienceRuntimeException(message + ". Triggered by " + lastException.getMessage(), lastException); resilienceRuntimeException.addSuppressed(e); throw resilienceRuntimeException; } diff --git a/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java b/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java index e966489d7..7606894e6 100644 --- a/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java +++ b/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java @@ -84,7 +84,7 @@ void testCircuitBreakerOpens() .isExactlyInstanceOf(ResilienceRuntimeException.class) .hasCauseExactlyInstanceOf(ThreadContextExecutionException.class) .hasRootCauseExactlyInstanceOf(Exception.class) - .hasMessage("CircuitBreaker 'circuitbreaker.test.2' is OPEN and does not permit further calls"); + .hasMessage("CircuitBreaker 'circuitbreaker.test.2' is OPEN and does not permit further calls. Triggered by java.lang.Exception: Simulated failure, attempt nr: 3"); assertThat(thrown.getCause()).hasMessage("java.lang.Exception: Simulated failure, attempt nr: 3"); From dc9df09e95d8113c59a504ca054ea5644b1115c4 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Wed, 22 Oct 2025 10:47:23 +0200 Subject: [PATCH 5/5] PMD --- .../resilience4j/DefaultCircuitBreakerProvider.java | 5 ++++- .../sdk/cloudplatform/resilience4j/CircuitBreakerTest.java | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java b/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java index b7b8abb7f..43a2c15d3 100644 --- a/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java +++ b/cloudplatform/resilience4j/src/main/java/com/sap/cloud/sdk/cloudplatform/resilience4j/DefaultCircuitBreakerProvider.java @@ -89,7 +89,10 @@ public Callable decorateCallable( if( lastException == null ) { throw new ResilienceRuntimeException(message, e); } - val resilienceRuntimeException = new ResilienceRuntimeException(message + ". Triggered by " + lastException.getMessage(), lastException); + val resilienceRuntimeException = + new ResilienceRuntimeException( + message + ". Triggered by " + lastException.getMessage(), + lastException); resilienceRuntimeException.addSuppressed(e); throw resilienceRuntimeException; } diff --git a/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java b/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java index 7606894e6..333b9fdba 100644 --- a/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java +++ b/cloudplatform/resilience4j/src/test/java/com/sap/cloud/sdk/cloudplatform/resilience4j/CircuitBreakerTest.java @@ -84,7 +84,8 @@ void testCircuitBreakerOpens() .isExactlyInstanceOf(ResilienceRuntimeException.class) .hasCauseExactlyInstanceOf(ThreadContextExecutionException.class) .hasRootCauseExactlyInstanceOf(Exception.class) - .hasMessage("CircuitBreaker 'circuitbreaker.test.2' is OPEN and does not permit further calls. Triggered by java.lang.Exception: Simulated failure, attempt nr: 3"); + .hasMessage( + "CircuitBreaker 'circuitbreaker.test.2' is OPEN and does not permit further calls. Triggered by java.lang.Exception: Simulated failure, attempt nr: 3"); assertThat(thrown.getCause()).hasMessage("java.lang.Exception: Simulated failure, attempt nr: 3");