diff --git a/libraries-5/pom.xml b/libraries-5/pom.xml index 74326251d089..3475cca545bb 100644 --- a/libraries-5/pom.xml +++ b/libraries-5/pom.xml @@ -132,6 +132,16 @@ lanterna ${lanterna.version} + + io.github.resilience4j + resilience4j-retry + ${resilience4j.version} + + + io.github.resilience4j + resilience4j-circuitbreaker + ${resilience4j.version} + org.springframework.boot spring-boot-starter-test @@ -149,6 +159,7 @@ 1.29.2 7.28.1 0.14.1 + 2.1.0 diff --git a/libraries-5/src/test/java/com/baeldung/resilience4j/CircuitBreakerVsRetryUnitTest.java b/libraries-5/src/test/java/com/baeldung/resilience4j/CircuitBreakerVsRetryUnitTest.java new file mode 100644 index 000000000000..d1eb609f5d5d --- /dev/null +++ b/libraries-5/src/test/java/com/baeldung/resilience4j/CircuitBreakerVsRetryUnitTest.java @@ -0,0 +1,114 @@ +package com.baeldung.resilience4j; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import io.github.resilience4j.core.IntervalFunction; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; + +public class CircuitBreakerVsRetryUnitTest { + + interface PaymentService { + + String processPayment(int i); + } + + private PaymentService paymentService; + + @Before + public void setUp() { + paymentService = mock(PaymentService.class); + } + + @Test + public void whenRetryWithExponentialBackoffIsUsed_thenItRetriesAndSucceeds() { + IntervalFunction intervalFn = IntervalFunction.ofExponentialBackoff(1000, 2); + RetryConfig retryConfig = RetryConfig.custom() + .maxAttempts(5) + .intervalFunction(intervalFn) + .build(); + + Retry retry = Retry.of("paymentRetry", retryConfig); + + when(paymentService.processPayment(1)).thenThrow(new RuntimeException("First Failure")) + .thenThrow(new RuntimeException("Second Failure")) + .thenReturn("Success"); + + Callable decoratedCallable = Retry.decorateCallable(retry, () -> paymentService.processPayment(1)); + + try { + String result = decoratedCallable.call(); + assertEquals("Success", result); + } catch (Exception ignored) { + + } + + verify(paymentService, times(3)).processPayment(1); + } + + @Test + public void whenCircuitBreakerTransitionsThroughStates_thenBehaviorIsVerified() { + CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() + .failureRateThreshold(50) + .slidingWindowSize(5) + .permittedNumberOfCallsInHalfOpenState(3) + .build(); + + CircuitBreaker circuitBreaker = CircuitBreaker.of("paymentCircuitBreaker", circuitBreakerConfig); + + AtomicInteger callCount = new AtomicInteger(0); + + when(paymentService.processPayment(anyInt())).thenAnswer(invocationOnMock -> { + callCount.incrementAndGet(); + throw new RuntimeException("Service Failure"); + }); + + Callable decoratedCallable = CircuitBreaker.decorateCallable(circuitBreaker, () -> paymentService.processPayment(1)); + + for (int i = 0; i < 10; i++) { + try { + decoratedCallable.call(); + } catch (Exception ignored) { + + } + } + + assertEquals(5, callCount.get()); + assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); + + callCount.set(0); + circuitBreaker.transitionToHalfOpenState(); + + assertEquals(CircuitBreaker.State.HALF_OPEN, circuitBreaker.getState()); + reset(paymentService); + when(paymentService.processPayment(anyInt())).thenAnswer(invocationOnMock -> { + callCount.incrementAndGet(); + return "Success"; + }); + + for (int i = 0; i < 3; i++) { + try { + decoratedCallable.call(); + } catch (Exception ignored) { + + } + } + + assertEquals(3, callCount.get()); + assertEquals(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); + } +}