diff --git a/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerTest.java b/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerTest.java index 67ce5dceda..6cafa9324f 100644 --- a/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerTest.java +++ b/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerTest.java @@ -19,7 +19,10 @@ package io.github.resilience4j.circuitbreaker; import io.github.resilience4j.test.HelloWorldService; -import io.vavr.*; +import io.vavr.CheckedConsumer; +import io.vavr.CheckedFunction0; +import io.vavr.CheckedFunction1; +import io.vavr.CheckedRunnable; import io.vavr.control.Try; import org.junit.Before; import org.junit.Test; @@ -30,14 +33,16 @@ import java.io.IOException; import java.net.SocketTimeoutException; import java.time.Duration; -import java.util.concurrent.*; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import static io.vavr.API.*; -import static io.vavr.API.$; -import static io.vavr.Predicates.*; +import static io.vavr.Predicates.instanceOf; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/resilience4j-core/src/main/java/io/github/resilience4j/core/CallableUtils.java b/resilience4j-core/src/main/java/io/github/resilience4j/core/CallableUtils.java new file mode 100644 index 0000000000..b039d5e27d --- /dev/null +++ b/resilience4j-core/src/main/java/io/github/resilience4j/core/CallableUtils.java @@ -0,0 +1,81 @@ +package io.github.resilience4j.core; + +import java.util.concurrent.Callable; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class CallableUtils { + + private CallableUtils(){} + + /** + * Returns a composed function that first applies the Callable and then applies + * the resultHandler. + * + * @param return type of callable + * @param return type of handler + * @param resultHandler the function applied after callable + * @return a function composed of supplier and resultHandler + */ + public static Callable andThen(Callable callable, Function resultHandler){ + return () -> resultHandler.apply(callable.call()); + } + + /** + * Returns a composed function that first applies the Callable and then applies + * {@linkplain BiFunction} {@code after} to the result. + * + * @param return type of callable + * @param return type of handler + * @param handler the function applied after callable + * @return a function composed of supplier and handler + */ + public static Callable andThen(Callable callable, BiFunction handler){ + return () -> { + try{ + T result = callable.call(); + return handler.apply(result, null); + }catch (Exception exception){ + return handler.apply(null, exception); + } + }; + } + + /** + * Returns a composed function that first applies the Callable and then applies + * either the resultHandler or exceptionHandler. + * + * @param return type of callable + * @param return type of resultHandler and exceptionHandler + * @param resultHandler the function applied after callable was successful + * @param exceptionHandler the function applied after callable has failed + * @return a function composed of supplier and handler + */ + public static Callable andThen(Callable callable, Function resultHandler, Function exceptionHandler){ + return () -> { + try{ + T result = callable.call(); + return resultHandler.apply(result); + }catch (Exception exception){ + return exceptionHandler.apply(exception); + } + }; + } + + /** + * Returns a composed function that first executes the Callable and optionally recovers from an exception. + * + * @param return type of after + * @param exceptionHandler the exception handler + * @return a function composed of callable and exceptionHandler + */ + public static Callable recover(Callable callable, Function exceptionHandler){ + return () -> { + try{ + return callable.call(); + }catch (Exception exception){ + return exceptionHandler.apply(exception); + } + }; + } +} diff --git a/resilience4j-core/src/main/java/io/github/resilience4j/core/EventProcessor.java b/resilience4j-core/src/main/java/io/github/resilience4j/core/EventProcessor.java index 6ae62205bb..315478da2d 100644 --- a/resilience4j-core/src/main/java/io/github/resilience4j/core/EventProcessor.java +++ b/resilience4j-core/src/main/java/io/github/resilience4j/core/EventProcessor.java @@ -25,8 +25,8 @@ public class EventProcessor implements EventPublisher { - protected volatile boolean consumerRegistered; - @Nullable private volatile EventConsumer onEventConsumer; + private boolean consumerRegistered; + @Nullable private EventConsumer onEventConsumer; private ConcurrentMap, EventConsumer> eventConsumers = new ConcurrentHashMap<>(); public boolean hasConsumers(){ @@ -34,7 +34,7 @@ public boolean hasConsumers(){ } @SuppressWarnings("unchecked") - public void registerConsumer(Class eventType, EventConsumer eventConsumer){ + public synchronized void registerConsumer(Class eventType, EventConsumer eventConsumer){ consumerRegistered = true; eventConsumers.put(eventType, (EventConsumer) eventConsumer); } @@ -58,7 +58,7 @@ public boolean processEvent(E event) { } @Override - public void onEvent(@Nullable EventConsumer onEventConsumer) { + public synchronized void onEvent(@Nullable EventConsumer onEventConsumer) { consumerRegistered = true; this.onEventConsumer = onEventConsumer; } diff --git a/resilience4j-core/src/main/java/io/github/resilience4j/core/SupplierUtils.java b/resilience4j-core/src/main/java/io/github/resilience4j/core/SupplierUtils.java new file mode 100644 index 0000000000..7d6b3a4fa8 --- /dev/null +++ b/resilience4j-core/src/main/java/io/github/resilience4j/core/SupplierUtils.java @@ -0,0 +1,79 @@ +package io.github.resilience4j.core; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +public class SupplierUtils { + + private SupplierUtils(){} + + /** + * Returns a composed function that first applies the Supplier and then applies + * the resultHandler. + * + * @param return type of callable + * @param return type of handler + * @param resultHandler the function applied after supplier + * @return a function composed of supplier and resultHandler + */ + public static Supplier andThen(Supplier supplier, Function resultHandler){ + return () -> resultHandler.apply(supplier.get()); + } + + /** + * Returns a composed function that first applies the Supplier and then applies + * {@linkplain BiFunction} {@code after} to the result. + * + * @param return type of after + * @param handler the function applied after supplier + * @return a function composed of supplier and handler + */ + public static Supplier andThen(Supplier supplier, BiFunction handler){ + return () -> { + try{ + T result = supplier.get(); + return handler.apply(result, null); + }catch (Exception exception){ + return handler.apply(null, exception); + } + }; + } + + /** + * Returns a composed function that first executes the Supplier and optionally recovers from an exception. + * + * @param return type of after + * @param exceptionHandler the exception handler + * @return a function composed of supplier and exceptionHandler + */ + public static Supplier recover(Supplier supplier, Function exceptionHandler){ + return () -> { + try{ + return supplier.get(); + }catch (Exception exception){ + return exceptionHandler.apply(exception); + } + }; + } + + /** + * Returns a composed function that first applies the Supplier and then applies + * either the resultHandler or exceptionHandler. + * + * @param return type of after + * @param resultHandler the function applied after Supplier was successful + * @param exceptionHandler the function applied after Supplier has failed + * @return a function composed of supplier and handler + */ + public static Supplier andThen(Supplier supplier, Function resultHandler, Function exceptionHandler){ + return () -> { + try{ + T result = supplier.get(); + return resultHandler.apply(result); + }catch (Exception exception){ + return exceptionHandler.apply(exception); + } + }; + } +} diff --git a/resilience4j-core/src/test/java/io/github/resilience4j/core/CallableUtilsTest.java b/resilience4j-core/src/test/java/io/github/resilience4j/core/CallableUtilsTest.java new file mode 100644 index 0000000000..11e8dc3bf5 --- /dev/null +++ b/resilience4j-core/src/test/java/io/github/resilience4j/core/CallableUtilsTest.java @@ -0,0 +1,85 @@ +package io.github.resilience4j.core; + +import org.junit.Test; + +import javax.xml.ws.WebServiceException; +import java.io.IOException; +import java.util.concurrent.Callable; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CallableUtilsTest { + + @Test + public void shouldChainCallableAndResultHandler() throws Exception { + + Callable supplier = () -> "BLA"; + //When + Callable callableWithRecovery = CallableUtils.andThen(supplier, result -> "Bla"); + + String result = callableWithRecovery.call(); + + //Then + assertThat(result).isEqualTo("Bla"); + } + + + @Test + public void shouldChainCallableAndRecoverFromException() throws Exception { + + Callable callable = () -> { + throw new IOException("BAM!"); + }; + //When + Callable callableWithRecovery = CallableUtils.andThen(callable, (result, ex) -> "Bla"); + + String result = callableWithRecovery.call(); + + //Then + assertThat(result).isEqualTo("Bla"); + } + + @Test + public void shouldChainCallableAndRecoverWithErrorHandler() throws Exception { + + Callable callable = () -> { + throw new IOException("BAM!"); + }; + //When + Callable callableWithRecovery = CallableUtils.andThen(callable, (result) -> result, ex -> "Bla"); + + String result = callableWithRecovery.call(); + + //Then + assertThat(result).isEqualTo("Bla"); + } + + @Test + public void shouldRecoverCallableFromException() throws Exception { + + Callable callable = () -> { + throw new IOException("BAM!"); + }; + //When + Callable callableWithRecovery = CallableUtils.recover(callable, (ex) -> "Bla"); + + String result = callableWithRecovery.call(); + + //Then + assertThat(result).isEqualTo("Bla"); + } + + @Test(expected = WebServiceException.class) + public void shouldRethrowException() throws Exception { + + Callable callable = () -> { + throw new IOException("BAM!"); + }; + //When + Callable callableWithRecovery = CallableUtils.recover(callable, (ex) -> { + throw new WebServiceException(); + }); + + callableWithRecovery.call(); + } +} diff --git a/resilience4j-core/src/test/java/io/github/resilience4j/core/SupplierUtilsTest.java b/resilience4j-core/src/test/java/io/github/resilience4j/core/SupplierUtilsTest.java new file mode 100644 index 0000000000..d68e106f45 --- /dev/null +++ b/resilience4j-core/src/test/java/io/github/resilience4j/core/SupplierUtilsTest.java @@ -0,0 +1,84 @@ +package io.github.resilience4j.core; + +import org.junit.Test; + +import javax.xml.ws.WebServiceException; +import java.util.function.Supplier; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SupplierUtilsTest { + + @Test + public void shouldChainSupplierAndResultHandler() { + + Supplier supplier = () -> "BLA"; + //When + Supplier supplierWithRecovery = SupplierUtils.andThen(supplier, result -> "Bla"); + + String result = supplierWithRecovery.get(); + + //Then + assertThat(result).isEqualTo("Bla"); + } + + @Test + public void shouldChainSupplierAndRecoverWithHandler() { + + Supplier supplier = () -> { + throw new RuntimeException("BAM!"); + }; + //When + Supplier supplierWithRecovery = SupplierUtils.andThen(supplier, (result, ex) -> "Bla"); + + String result = supplierWithRecovery.get(); + + //Then + assertThat(result).isEqualTo("Bla"); + } + + @Test + public void shouldChainSupplierAndRecoverWithErrorHandler() { + + Supplier supplier = () -> { + throw new RuntimeException("BAM!"); + }; + //When + Supplier supplierWithRecovery = SupplierUtils.andThen(supplier, (result) -> result, ex -> "Bla"); + + String result = supplierWithRecovery.get(); + + //Then + assertThat(result).isEqualTo("Bla"); + } + + + @Test + public void shouldRecoverSupplierFromException() { + + Supplier supplier = () -> { + throw new RuntimeException("BAM!"); + }; + //When + Supplier supplierWithRecovery = SupplierUtils.recover(supplier, (ex) -> "Bla"); + + String result = supplierWithRecovery.get(); + + //Then + assertThat(result).isEqualTo("Bla"); + } + + @Test(expected = WebServiceException.class) + public void shouldRethrowException() { + + Supplier supplier = () -> { + throw new RuntimeException("BAM!"); + }; + //When + Supplier supplierWithRecovery = SupplierUtils.recover(supplier, (ex) -> { + throw new WebServiceException(); + }); + + supplierWithRecovery.get(); + } +} diff --git a/resilience4j-documentation/src/docs/asciidoc/core_guides/circuitbreaker.adoc b/resilience4j-documentation/src/docs/asciidoc/core_guides/circuitbreaker.adoc index cb18470162..777bb737b4 100644 --- a/resilience4j-documentation/src/docs/asciidoc/core_guides/circuitbreaker.adoc +++ b/resilience4j-documentation/src/docs/asciidoc/core_guides/circuitbreaker.adoc @@ -18,6 +18,8 @@ As an alternative you can provide your own custom global `CircuitBreakerConfig`. * the size of the ring buffer when the CircuitBreaker is closed * a custom CircuitBreakerEventListener which handles CircuitBreaker events * a custom Predicate which evaluates if an exception should be recorded as a failure and thus increase the failure rate +* a list of exceptions which should increase failure count +* a list of exceptions which should be ignored and not increase failure count [source,java,indent=0] ---- @@ -27,6 +29,8 @@ CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() .waitDurationInOpenState(Duration.ofMillis(1000)) .ringBufferSizeInHalfOpenState(2) .ringBufferSizeInClosedState(2) + .recordExceptions(IOException.class, TimeoutException.class) + .ignoreExceptions(BusinessException.class, OtherBusinessException.class) .build(); // Create a CircuitBreakerRegistry with a custom global configuration @@ -110,9 +114,39 @@ The CircuitBreaker supports resetting, returning to its original state while los circuitBreaker.reset(); ---- -===== Recover from an exception +===== Recover from an exception before call -If you want to recover from any exception, you can chain the method `Try.recover()`. The recovery method is only invoked, if `Try.of()` returns a `Failure` Monad. +If you want to recover from an exception before the CircuitBreaker records it as a failure, you can do the following: + +[source,java,indent=0] +---- +Supplier supplier = () -> { + throw new RuntimeException("BAM!"); + }; +Supplier supplierWithRecovery = SupplierUtils.recover(supplier, (exception) -> "Hello Recovery"); +String result = circuitBreaker.executeSupplier(supplierWithRecovery); +assertThat(result).isEqualTo("Hello Recovery"); +---- + +`SupplierUtils` and `CallableUtils` contain other methods like `andThen` which can take can be used to chain functions: + +[source,java,indent=0] +---- +Supplier supplierWithResultAndExceptionHandler = SupplierUtils.andThen(supplier, (result, exception) -> "Hello Recovery"); + +Supplier supplier = () -> httpClient.doRemoteCall(); +Supplier supplierWithResultHandling = SupplierUtils.andThen(supplier, result -> { + if (result.getStatusCode() == 400) { + throw new ClientException(); + } else if (result.getStatusCode() == 500) { + throw new ServerException(); + }); +HttpResponse httpResponse = circuitBreaker.executeSupplier(supplierWithResultHandling); +---- + +===== Recover from an exception after call + +If you want to recover from an exception after the CircuitBreaker recorded it as a failure, you can chain the method `Try.recover()`. The recovery method is only invoked, if `Try.of()` returns a `Failure` Monad. [source,java,indent=0] ---- @@ -120,7 +154,8 @@ include::../../../../../resilience4j-circuitbreaker/src/test/java/io/github/resi ---- ===== Customize the exception handler -The default exception handler counts all type of exceptions as failures and triggers the CircuitBreaker. If you want to use a custom exception handler, you have to implement the functional interface `Predicate` which has a method `test`. The Predicate must return true if the exception should count as a failure, otherwise it must return false. + +The default exception predicate counts all type of exceptions as failures and triggers the CircuitBreaker. If you want to use a custom exception predicate, you have to implement the functional interface `Predicate` which has a method `test`. The Predicate must return true if the exception should count as a failure, otherwise it must return false. The following example shows how to ignore an `IOException`, but all other exception types still count as failures. [source,java,indent=0] @@ -130,7 +165,6 @@ include::../../../../../resilience4j-circuitbreaker/src/test/java/io/github/resi ===== Consume emitted CircuitBreakerEvents - A `CircuitBreakerEvent` can be a state transition, a circuit breaker reset, a successful call, a recorded error or an ignored error. All events contains additional information like event creation time and processing duration of the call. If you want to consume events, you have to register an event consumer. [source,java] diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/LabelNames.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/LabelNames.java index fdee518910..9676a17285 100644 --- a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/LabelNames.java +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/LabelNames.java @@ -21,6 +21,9 @@ /** Common constants for metric binder implementations based on tags. */ public final class LabelNames { + + private LabelNames() {} + public static final List NAME = Collections.singletonList("name"); public static final List NAME_AND_KIND = Arrays.asList("name", "kind"); }