diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/Bulkhead.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/Bulkhead.java index f1be5170c7..ef53708699 100644 --- a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/Bulkhead.java +++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/Bulkhead.java @@ -35,6 +35,8 @@ import java.util.function.Supplier; /** + * A Bulkhead instance is thread-safe can be used to decorate multiple requests. + * * A {@link Bulkhead} represent an entity limiting the amount of parallel operations. It does not assume nor does it mandate usage * of any particular concurrency and/or io model. These details are left for the client to manage. This bulkhead, depending on the * underlying concurrency/io model can be used to shed load, and, where it makes sense, limit resource use (i.e. limit amount of diff --git a/resilience4j-cache/src/main/java/io/github/resilience4j/cache/Cache.java b/resilience4j-cache/src/main/java/io/github/resilience4j/cache/Cache.java index 70ed4ebdb9..4051141a6c 100644 --- a/resilience4j-cache/src/main/java/io/github/resilience4j/cache/Cache.java +++ b/resilience4j-cache/src/main/java/io/github/resilience4j/cache/Cache.java @@ -19,7 +19,7 @@ package io.github.resilience4j.cache; import io.github.resilience4j.cache.event.CacheEvent; -import io.github.resilience4j.cache.internal.CacheContext; +import io.github.resilience4j.cache.internal.CacheImpl; import io.reactivex.Flowable; import io.vavr.CheckedFunction0; import io.vavr.CheckedFunction1; @@ -72,7 +72,7 @@ public interface Cache { */ static Cache of(javax.cache.Cache cache){ Objects.requireNonNull(cache, "Cache must not be null"); - return new CacheContext<>(cache); + return new CacheImpl<>(cache); } /** diff --git a/resilience4j-cache/src/main/java/io/github/resilience4j/cache/internal/CacheContext.java b/resilience4j-cache/src/main/java/io/github/resilience4j/cache/internal/CacheImpl.java similarity index 97% rename from resilience4j-cache/src/main/java/io/github/resilience4j/cache/internal/CacheContext.java rename to resilience4j-cache/src/main/java/io/github/resilience4j/cache/internal/CacheImpl.java index 5775b561ff..b505443f8d 100644 --- a/resilience4j-cache/src/main/java/io/github/resilience4j/cache/internal/CacheContext.java +++ b/resilience4j-cache/src/main/java/io/github/resilience4j/cache/internal/CacheImpl.java @@ -35,15 +35,15 @@ import java.util.concurrent.atomic.LongAdder; import java.util.function.Supplier; -public class CacheContext implements Cache { +public class CacheImpl implements Cache { - private static final Logger LOG = LoggerFactory.getLogger(CacheContext.class); + private static final Logger LOG = LoggerFactory.getLogger(CacheImpl.class); private final javax.cache.Cache cache; private final FlowableProcessor eventPublisher; private final CacheMetrics metrics; - public CacheContext(javax.cache.Cache cache) { + public CacheImpl(javax.cache.Cache cache) { this.cache = cache; PublishProcessor publisher = PublishProcessor.create(); this.eventPublisher = publisher.toSerialized(); diff --git a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreaker.java b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreaker.java index d8cd94ee0a..8902407847 100644 --- a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreaker.java +++ b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreaker.java @@ -35,6 +35,8 @@ import java.util.function.Supplier; /** + * A CircuitBreaker instance is thread-safe can be used to decorate multiple requests. + * * A {@link CircuitBreaker} manages the state of a backend system. * The CircuitBreaker is implemented via a finite state machine with three states: CLOSED, OPEN and HALF_OPEN. * The CircuitBreaker does not know anything about the backend’s state by itself, but uses the information provided by the decorators via diff --git a/resilience4j-documentation/src/docs/asciidoc/core_guides/retry.adoc b/resilience4j-documentation/src/docs/asciidoc/core_guides/retry.adoc index 9aa9412f02..a4bdedc580 100644 --- a/resilience4j-documentation/src/docs/asciidoc/core_guides/retry.adoc +++ b/resilience4j-documentation/src/docs/asciidoc/core_guides/retry.adoc @@ -12,19 +12,19 @@ RetryConfig config = RetryConfig.custom() .maxAttempts(3) .waitDuration(Duration.ofMillis(500)) .build(); -Retry retryContext = Retry.of("id", config); +Retry retry = Retry.of("id", config); ---- -In order to create a custom `Retry` context, you can use the Retry context builder. You can configure the maximum number of retry attempts and the wait duration between successive attempts. Furthermore, you can configure a custom Predicate which evaluates if an exception should trigger a retry. +In order to create a custom-configured `Retry`, you can use the RetryConfig builder. You can configure the maximum number of retry attempts and the wait duration between successive attempts. Furthermore, you can configure a custom Predicate which evaluates if an exception should trigger a retry. [source,java] ---- RetryConfig config = RetryConfig.custom() .maxAttempts(2) .waitDurationInOpenState(Duration.ofMillis(100)) - .retryOnException(throwable -> Match.of(throwable) - .whenType(WebServiceException.class).then(false) - .otherwise(true).get()) + .retryOnException(throwable -> API.Match(throwable).of( + API.Case($(Predicates.instanceOf(WebServiceException.class)), true), + API.Case($(), false))) .build(); ---- @@ -39,9 +39,9 @@ HelloWorldService helloWorldService = mock(HelloWorldService.class); given(helloWorldService.sayHelloWorld()).willThrow(new WebServiceException("BAM!")); // Create a Retry with default configuration -Retry retryContext = Retry.ofDefaults("id"); +Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService -CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retryContext, helloWorldService::sayHelloWorld); +CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retry, helloWorldService::sayHelloWorld); // When I invoke the function Try result = Try.of(retryableSupplier).recover((throwable) -> "Hello world from recovery function"); @@ -69,9 +69,9 @@ The RetryContext emits a stream of RetryEvents to any Observer/Consumer who subs [source,java] ---- -Retry retryContext = Retry.ofDefaults("id"); +Retry retry = Retry.ofDefaults("id"); CircularEventConsumer circularEventConsumer = new CircularEventConsumer<>(10); -retryContext.getEventStream() +retry.getEventStream() .subscribe(circularEventConsumer); List bufferedEvents = circularEventConsumer.getBufferedEvents(); diff --git a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/CircuitBreakerMetrics.java b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/CircuitBreakerMetrics.java index e0640af323..e4f35b8e57 100644 --- a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/CircuitBreakerMetrics.java +++ b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/CircuitBreakerMetrics.java @@ -18,9 +18,6 @@ */ package io.github.resilience4j.metrics; -import static com.codahale.metrics.MetricRegistry.name; -import static java.util.Objects.requireNonNull; - import com.codahale.metrics.Gauge; import com.codahale.metrics.Metric; import com.codahale.metrics.MetricRegistry; @@ -31,12 +28,20 @@ import java.util.Map; +import static com.codahale.metrics.MetricRegistry.name; +import static java.util.Objects.requireNonNull; + /** * An adapter which exports {@link CircuitBreaker.Metrics} as Dropwizard Metrics Gauges. */ public class CircuitBreakerMetrics implements MetricSet { private static final String DEFAULT_PREFIX = "resilience4j.circuitbreaker"; + public static final String SUCCESSFUL = "successful"; + public static final String FAILED = "failed"; + public static final String NOT_PERMITTED = "not_permitted"; + public static final String BUFFERED = "buffered"; + public static final String BUFFERED_MAX = "buffered_max"; private final MetricRegistry metricRegistry = new MetricRegistry(); private CircuitBreakerMetrics(Iterable circuitBreakers) { @@ -50,15 +55,15 @@ private CircuitBreakerMetrics(String prefix, Iterable circuitBre String name = circuitBreaker.getName(); CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics(); - metricRegistry.register(name(prefix, name, "successful"), + metricRegistry.register(name(prefix, name, SUCCESSFUL), (Gauge) metrics::getNumberOfSuccessfulCalls); - metricRegistry.register(name(prefix, name, "failed"), + metricRegistry.register(name(prefix, name, FAILED), (Gauge) metrics::getNumberOfFailedCalls); - metricRegistry.register(name(prefix, name, "not_permitted"), + metricRegistry.register(name(prefix, name, NOT_PERMITTED), (Gauge) metrics::getNumberOfNotPermittedCalls); - metricRegistry.register(name(prefix, name, "buffered"), + metricRegistry.register(name(prefix, name, BUFFERED), (Gauge) metrics::getNumberOfBufferedCalls); - metricRegistry.register(name(prefix, name, "buffered_max"), + metricRegistry.register(name(prefix, name, BUFFERED_MAX), (Gauge) metrics::getMaxNumberOfBufferedCalls); } ); diff --git a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/RetryMetrics.java b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/RetryMetrics.java index 61de893e1e..9cb9a98291 100644 --- a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/RetryMetrics.java +++ b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/RetryMetrics.java @@ -15,6 +15,10 @@ */ public class RetryMetrics implements MetricSet { + public static final String SUCCESSFUL_CALLS_WITHOUT_RETRY = "successful_calls_without_retry"; + public static final String SUCCESSFUL_CALLS_WITH_RETRY = "successful_calls_with_retry"; + public static final String FAILED_CALLS_WITHOUT_RETRY = "failed_calls_without_retry"; + public static final String FAILED_CALLS_WITH_RETRY = "failed_calls_with_retry"; private final MetricRegistry metricRegistry = new MetricRegistry(); private static final String DEFAULT_PREFIX = "resilience4j.retry"; @@ -29,9 +33,14 @@ private RetryMetrics(String prefix, Iterable retries){ String name = retry.getName(); Retry.Metrics metrics = retry.getMetrics(); - metricRegistry.register(name(prefix, name, "retry_max_ratio"), - new RetryRatio(metrics.getNumAttempts(), metrics.getMaxAttempts())); - + metricRegistry.register(name(prefix, name, SUCCESSFUL_CALLS_WITHOUT_RETRY), + (Gauge) metrics::getNumberOfSuccessfulCallsWithoutRetryAttempt); + metricRegistry.register(name(prefix, name, SUCCESSFUL_CALLS_WITH_RETRY), + (Gauge) metrics::getNumberOfSuccessfulCallsWithRetryAttempt); + metricRegistry.register(name(prefix, name, FAILED_CALLS_WITHOUT_RETRY), + (Gauge) metrics::getNumberOfFailedCallsWithoutRetryAttempt); + metricRegistry.register(name(prefix, name, FAILED_CALLS_WITH_RETRY), + (Gauge) metrics::getNumberOfFailedCallsWithRetryAttempt); }); } diff --git a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/Timer.java b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/Timer.java index 52a82541a2..b3fc0a5469 100644 --- a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/Timer.java +++ b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/Timer.java @@ -2,7 +2,7 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Snapshot; -import io.github.resilience4j.metrics.internal.TimerContext; +import io.github.resilience4j.metrics.internal.TimerImpl; import io.vavr.CheckedFunction0; import io.vavr.CheckedFunction1; import io.vavr.CheckedRunnable; @@ -12,25 +12,14 @@ import java.util.function.Function; import java.util.function.Supplier; -import static com.codahale.metrics.Timer.Context; - public interface Timer { /** - * Starts the Timer - */ - Context time(); - - /** - * Stops the Timer and records a failed call. - * This method must be invoked when a call failed. - */ - void onError(Context context); - - /** - * Stops the Timer and records a successful call. + * Creates a Timer context and starts the timer + * + * @return the Timer context */ - void onSuccess(Context context); + Timer.Context context(); /** * Returns the name of this Timer. @@ -61,7 +50,7 @@ public interface Timer { * @return a Bulkhead instance */ static Timer ofMetricRegistry(String name, MetricRegistry metricRegistry) { - return new TimerContext(name, metricRegistry); + return new TimerImpl(name, metricRegistry); } /** @@ -71,7 +60,7 @@ static Timer ofMetricRegistry(String name, MetricRegistry metricRegistry) { * @return a Bulkhead instance */ static Timer of(String name) { - return new TimerContext(name, new MetricRegistry()); + return new TimerImpl(name, new MetricRegistry()); } @@ -126,13 +115,13 @@ default CompletionStage executeCompletionStageSupplier(Supplier CheckedFunction0 decorateCheckedSupplier(Timer timer, CheckedFunction0 supplier){ return () -> { - final Context context = timer.time(); + final Timer.Context context = timer.context(); try { T returnValue = supplier.apply(); - timer.onSuccess(context); + context.onSuccess(); return returnValue; }catch (Throwable e){ - timer.onError(context); + context.onError(); throw e; } }; @@ -147,12 +136,12 @@ static CheckedFunction0 decorateCheckedSupplier(Timer timer, CheckedFunct */ static CheckedRunnable decorateCheckedRunnable(Timer timer, CheckedRunnable runnable){ return () -> { - final Context context = timer.time(); + final Timer.Context context = timer.context(); try { runnable.run(); - timer.onSuccess(context); + context.onSuccess(); }catch (Throwable e){ - timer.onError(context); + context.onError(); throw e; } }; @@ -167,13 +156,13 @@ static CheckedRunnable decorateCheckedRunnable(Timer timer, CheckedRunnable runn */ static Supplier decorateSupplier(Timer timer, Supplier supplier){ return () -> { - final Context context = timer.time(); + final Timer.Context context = timer.context(); try { T returnValue = supplier.get(); - timer.onSuccess(context); + context.onSuccess(); return returnValue; }catch (Throwable e){ - timer.onError(context); + context.onError(); throw e; } }; @@ -188,13 +177,13 @@ static Supplier decorateSupplier(Timer timer, Supplier supplier){ */ static Callable decorateCallable(Timer timer, Callable callable){ return () -> { - final Context context = timer.time(); + final Timer.Context context = timer.context(); try { T returnValue = callable.call(); - timer.onSuccess(context); + context.onSuccess(); return returnValue; }catch (Throwable e){ - timer.onError(context); + context.onError(); throw e; } }; @@ -210,12 +199,12 @@ static Callable decorateCallable(Timer timer, Callable callable){ */ static Runnable decorateRunnable(Timer timer, Runnable runnable){ return () -> { - final Context context = timer.time(); + final Timer.Context context = timer.context(); try { runnable.run(); - timer.onSuccess(context); + context.onSuccess(); }catch (Throwable e){ - timer.onError(context); + context.onError(); throw e; } }; @@ -231,13 +220,13 @@ static Runnable decorateRunnable(Timer timer, Runnable runnable){ */ static Function decorateFunction(Timer timer, Function function){ return (T t) -> { - final Context context = timer.time(); + final Timer.Context context = timer.context(); try { R returnValue = function.apply(t); - timer.onSuccess(context); + context.onSuccess(); return returnValue; }catch (Throwable e){ - timer.onError(context); + context.onError(); throw e; } }; @@ -252,13 +241,13 @@ static Function decorateFunction(Timer timer, Function functi */ static CheckedFunction1 decorateCheckedFunction(Timer timer, CheckedFunction1 function){ return (T t) -> { - final Context context = timer.time(); + final Timer.Context context = timer.context(); try { R returnValue = function.apply(t); - timer.onSuccess(context); + context.onSuccess(); return returnValue; }catch (Throwable e){ - timer.onError(context); + context.onError(); throw e; } }; @@ -272,26 +261,41 @@ static CheckedFunction1 decorateCheckedFunction(Timer timer, Checke */ static Supplier> decorateCompletionStageSupplier(Timer timer, Supplier> stageSupplier) { return () -> { - final Context context = timer.time(); + final Timer.Context context = timer.context(); try { final CompletionStage stage = stageSupplier.get(); stage.whenComplete((result, throwable) -> { if (throwable != null) { - timer.onError(context); + context.onError(); } else { - timer.onSuccess(context); + context.onSuccess(); } }); return stage; } catch (Throwable throwable) { - timer.onError(context); + context.onError(); throw throwable; } }; } + + interface Context { + + /** + * Stops the Timer and records a failed call. + * This method must be invoked when a call failed. + */ + void onError(); + + /** + * Stops the Timer and records a successful call. + */ + void onSuccess(); + } + interface Metrics { /** diff --git a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/internal/TimerContext.java b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/internal/TimerImpl.java similarity index 74% rename from resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/internal/TimerContext.java rename to resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/internal/TimerImpl.java index 8bf4be0211..184be82627 100644 --- a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/internal/TimerContext.java +++ b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/internal/TimerImpl.java @@ -5,10 +5,12 @@ import io.github.resilience4j.metrics.Timer; import static com.codahale.metrics.MetricRegistry.name; -import static com.codahale.metrics.Timer.*; -public class TimerContext implements Timer{ +public class TimerImpl implements Timer{ + public static final String SUCCESSFUL = "successful"; + public static final String TOTAL = "total"; + public static final String FAILED = "failed"; private final String timerName; private final MetricRegistry metricRegistry; private com.codahale.metrics.Timer successfulCallsTimer; @@ -16,30 +18,20 @@ public class TimerContext implements Timer{ private com.codahale.metrics.Counter failedCallsCounter; private final TimerMetrics metrics; - public TimerContext(String timerName, MetricRegistry metricRegistry){ + public TimerImpl(String timerName, MetricRegistry metricRegistry){ this.timerName = timerName; this.metricRegistry = metricRegistry; - this.successfulCallsTimer = metricRegistry.timer(name(timerName, "successful")); - this.totalCallsCounter = metricRegistry.counter(name(timerName, "total")); - this.failedCallsCounter = metricRegistry.counter(name(timerName, "failed")); + this.successfulCallsTimer = metricRegistry.timer(name(timerName, SUCCESSFUL)); + this.totalCallsCounter = metricRegistry.counter(name(timerName, TOTAL)); + this.failedCallsCounter = metricRegistry.counter(name(timerName, FAILED)); this.metrics = new TimerMetrics(); } @Override - public Context time() { + public Timer.Context context() { totalCallsCounter.inc(); - return successfulCallsTimer.time(); - } - - @Override - public void onError(Context context) { - failedCallsCounter.inc(); - } - - @Override - public void onSuccess(Context context) { - context.stop(); + return new ContextImpl(); } @Override @@ -57,6 +49,25 @@ public Metrics getMetrics() { return metrics; } + public final class ContextImpl implements Timer.Context { + com.codahale.metrics.Timer.Context timerContext; + + private ContextImpl() { + timerContext = successfulCallsTimer.time(); + } + + @Override + public void onError() { + failedCallsCounter.inc(); + } + + @Override + public void onSuccess() { + timerContext.stop(); + } + } + + private final class TimerMetrics implements Metrics { private TimerMetrics() { } diff --git a/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/RetryMetricsTest.java b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/RetryMetricsTest.java index 96e72ff565..d046304f41 100644 --- a/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/RetryMetricsTest.java +++ b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/RetryMetricsTest.java @@ -1,17 +1,16 @@ package io.github.resilience4j.metrics; import com.codahale.metrics.MetricRegistry; -import io.github.resilience4j.circuitbreaker.CircuitBreaker; -import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.RetryRegistry; import io.github.resilience4j.test.HelloWorldService; -import io.vavr.CheckedFunction0; import io.vavr.control.Try; import org.junit.Before; import org.junit.Test; import org.mockito.BDDMockito; +import javax.xml.ws.WebServiceException; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -28,34 +27,57 @@ public void setUp(){ } @Test - public void shouldRegisterMetrics() throws Throwable { + public void shouldRegisterMetricsWithoutRetry() throws Throwable { //Given RetryRegistry retryRegistry = RetryRegistry.ofDefaults(); Retry retry = retryRegistry.retry("testName"); - CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults(); - CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("testName"); metricRegistry.registerAll(RetryMetrics.ofRetryRegistry(retryRegistry)); // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorld()).willReturn("Hello world"); // Setup circuitbreaker with retry - CheckedFunction0 decoratedSupplier = CircuitBreaker - .decorateCheckedSupplier(circuitBreaker, helloWorldService::returnHelloWorld); - decoratedSupplier = Retry - .decorateCheckedSupplier(retry, decoratedSupplier); - - - //When - String value = Try.of(decoratedSupplier) - .recover(throwable -> "Hello from Recovery").get(); + String value = retry.executeSupplier(helloWorldService::returnHelloWorld); //Then assertThat(value).isEqualTo("Hello world"); // Then the helloWorldService should be invoked 1 time BDDMockito.then(helloWorldService).should(times(1)).returnHelloWorld(); - assertThat(metricRegistry.getMetrics()).hasSize(1); - assertThat(metricRegistry.getGauges().get("resilience4j.retry.testName.retry_max_ratio").getValue()).isEqualTo(0.0); + assertThat(metricRegistry.getMetrics()).hasSize(4); + assertThat(metricRegistry.getGauges().get("resilience4j.retry.testName." + RetryMetrics.SUCCESSFUL_CALLS_WITH_RETRY).getValue()).isEqualTo(0L); + assertThat(metricRegistry.getGauges().get("resilience4j.retry.testName." + RetryMetrics.SUCCESSFUL_CALLS_WITHOUT_RETRY).getValue()).isEqualTo(1L); + assertThat(metricRegistry.getGauges().get("resilience4j.retry.testName." + RetryMetrics.FAILED_CALLS_WITH_RETRY).getValue()).isEqualTo(0L); + assertThat(metricRegistry.getGauges().get("resilience4j.retry.testName." + RetryMetrics.FAILED_CALLS_WITHOUT_RETRY).getValue()).isEqualTo(0L); + } + + @Test + public void shouldRegisterMetricsWithRetry() throws Throwable { + //Given + RetryRegistry retryRegistry = RetryRegistry.ofDefaults(); + Retry retry = retryRegistry.retry("testName"); + metricRegistry.registerAll(RetryMetrics.ofRetryRegistry(retryRegistry)); + + // Given the HelloWorldService returns Hello world + BDDMockito.given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")) + .willReturn("Hello world") + .willThrow(new WebServiceException("BAM!")) + .willThrow(new WebServiceException("BAM!")) + .willThrow(new WebServiceException("BAM!")); + + // Setup circuitbreaker with retry + String value1 = retry.executeSupplier(helloWorldService::returnHelloWorld); + Try.ofSupplier(Retry.decorateSupplier(retry, helloWorldService::returnHelloWorld)); + + //Then + assertThat(value1).isEqualTo("Hello world"); + // Then the helloWorldService should be invoked 1 time + BDDMockito.then(helloWorldService).should(times(5)).returnHelloWorld(); + assertThat(metricRegistry.getMetrics()).hasSize(4); + assertThat(metricRegistry.getGauges().get("resilience4j.retry.testName." + RetryMetrics.SUCCESSFUL_CALLS_WITH_RETRY).getValue()).isEqualTo(1L); + assertThat(metricRegistry.getGauges().get("resilience4j.retry.testName." + RetryMetrics.SUCCESSFUL_CALLS_WITHOUT_RETRY).getValue()).isEqualTo(0L); + assertThat(metricRegistry.getGauges().get("resilience4j.retry.testName." + RetryMetrics.FAILED_CALLS_WITH_RETRY).getValue()).isEqualTo(1L); + assertThat(metricRegistry.getGauges().get("resilience4j.retry.testName." + RetryMetrics.FAILED_CALLS_WITHOUT_RETRY).getValue()).isEqualTo(0L); } @Test @@ -63,28 +85,21 @@ public void shouldUseCustomPrefix() throws Throwable { //Given RetryRegistry retryRegistry = RetryRegistry.ofDefaults(); Retry retry = retryRegistry.retry("testName"); - CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults(); - CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("testName"); metricRegistry.registerAll(RetryMetrics.ofRetryRegistry("testPrefix",retryRegistry)); // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorld()).willReturn("Hello world"); - // Setup circuitbreaker with retry - CheckedFunction0 decoratedSupplier = CircuitBreaker - .decorateCheckedSupplier(circuitBreaker, helloWorldService::returnHelloWorld); - decoratedSupplier = Retry - .decorateCheckedSupplier(retry, decoratedSupplier); - - //When - String value = Try.of(decoratedSupplier) - .recover(throwable -> "Hello from Recovery").get(); + String value = retry.executeSupplier(helloWorldService::returnHelloWorld); //Then assertThat(value).isEqualTo("Hello world"); // Then the helloWorldService should be invoked 1 time BDDMockito.then(helloWorldService).should(times(1)).returnHelloWorld(); - assertThat(metricRegistry.getMetrics()).hasSize(1); - assertThat(metricRegistry.getGauges().get("testPrefix.testName.retry_max_ratio").getValue()).isEqualTo(0.0); + assertThat(metricRegistry.getMetrics()).hasSize(4); + assertThat(metricRegistry.getGauges().get("testPrefix.testName." + RetryMetrics.SUCCESSFUL_CALLS_WITH_RETRY).getValue()).isEqualTo(0L); + assertThat(metricRegistry.getGauges().get("testPrefix.testName." + RetryMetrics.SUCCESSFUL_CALLS_WITHOUT_RETRY).getValue()).isEqualTo(1L); + assertThat(metricRegistry.getGauges().get("testPrefix.testName." + RetryMetrics.FAILED_CALLS_WITH_RETRY).getValue()).isEqualTo(0L); + assertThat(metricRegistry.getGauges().get("testPrefix.testName." + RetryMetrics.FAILED_CALLS_WITHOUT_RETRY).getValue()).isEqualTo(0L); } } diff --git a/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/TimerTest.java b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/TimerTest.java index bfe68438d7..8b01d633e6 100644 --- a/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/TimerTest.java +++ b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/TimerTest.java @@ -63,7 +63,7 @@ public void shouldDecorateCheckedSupplier() throws Throwable { // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorldWithException()).willReturn("Hello world"); - // And measure the time with Metrics + // And measure the call with a Timer CheckedFunction0 timedSupplier = Timer.decorateCheckedSupplier(timer, helloWorldService::returnHelloWorldWithException); String value = timedSupplier.apply(); @@ -85,7 +85,7 @@ public void shouldDecorateCallable() throws Throwable { // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorldWithException()).willReturn("Hello world"); - // And measure the time with Metrics + // And measure the call with a Timer Callable timedSupplier = Timer.decorateCallable(timer, helloWorldService::returnHelloWorldWithException); String value = timedSupplier.call(); @@ -104,7 +104,7 @@ public void shouldExecuteCallable() throws Throwable { // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorldWithException()).willReturn("Hello world"); - // And measure the time with Metrics + // And measure the call with a Timer String value = timer.executeCallable(helloWorldService::returnHelloWorldWithException); assertThat(timer.getMetrics().getNumberOfTotalCalls()).isEqualTo(1); @@ -119,7 +119,7 @@ public void shouldExecuteCallable() throws Throwable { @Test public void shouldDecorateRunnable() throws Throwable { - // And measure the time with Metrics + // And measure the call with a Timer Runnable timedRunnable = Timer.decorateRunnable(timer, helloWorldService::sayHelloWorld); timedRunnable.run(); @@ -134,7 +134,7 @@ public void shouldDecorateRunnable() throws Throwable { @Test public void shouldExecuteRunnable() throws Throwable { - // And measure the time with Metrics + // And measure the call with a Timer timer.executeRunnable(helloWorldService::sayHelloWorld); assertThat(timer.getMetrics().getNumberOfTotalCalls()).isEqualTo(1); @@ -149,7 +149,7 @@ public void shouldExecuteRunnable() throws Throwable { public void shouldExecuteCompletionStageSupplier() throws Throwable { // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorld()).willReturn("Hello world"); - // And measure the time with Metrics + // And measure the call with a Timer Supplier> completionStageSupplier = () -> CompletableFuture.supplyAsync(helloWorldService::returnHelloWorld); @@ -186,7 +186,7 @@ public void shouldExecuteCompletionStageAndReturnWithExceptionAtSyncStage() thro public void shouldExecuteCompletionStageAndReturnWithExceptionAtASyncStage() throws Throwable { // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorld()).willThrow(new WebServiceException("BAM!")); - // And measure the time with Metrics + // And measure the call with a Timer Supplier> completionStageSupplier = () -> CompletableFuture.supplyAsync(helloWorldService::returnHelloWorld); @@ -206,7 +206,7 @@ public void shouldExecuteCompletionStageAndReturnWithExceptionAtASyncStage() thr @Test public void shouldDecorateCheckedRunnableAndReturnWithSuccess() throws Throwable { - // And measure the time with Metrics + // And measure the call with a Timer CheckedRunnable timedRunnable = Timer.decorateCheckedRunnable(timer, helloWorldService::sayHelloWorldWithException); timedRunnable.run(); @@ -220,10 +220,9 @@ public void shouldDecorateCheckedRunnableAndReturnWithSuccess() throws Throwable @Test public void shouldDecorateSupplierAndReturnWithException() throws Throwable { - // And measure the time with Metrics BDDMockito.given(helloWorldService.returnHelloWorld()).willThrow(new RuntimeException("BAM!")); - // And measure the time with Metrics + // And measure the call with a Timer Supplier supplier = Timer.decorateSupplier(timer, helloWorldService::returnHelloWorld); Try result = Try.of(supplier::get); @@ -244,7 +243,7 @@ public void shouldDecorateSupplier() throws Throwable { // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorld()).willReturn("Hello world"); - // And measure the time with Metrics + // And measure the call with a Timer Supplier timedSupplier = Timer.decorateSupplier(timer, helloWorldService::returnHelloWorld); Stream.range(0,2).forEach((i) -> timedSupplier.get()); @@ -261,7 +260,7 @@ public void shouldExecuteSupplier() throws Throwable { // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorld()).willReturn("Hello world").willThrow(new IllegalArgumentException("BAM!")); - // And measure the time with Metrics + // And measure the call with a Timer Stream.range(0,2).forEach((i) -> { try{ timer.executeSupplier(helloWorldService::returnHelloWorld); diff --git a/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/RateLimiter.java b/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/RateLimiter.java index de19e398cf..3d1588d214 100644 --- a/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/RateLimiter.java +++ b/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/RateLimiter.java @@ -34,6 +34,8 @@ import java.util.function.Supplier; /** + * A RateLimiter instance is thread-safe can be used to decorate multiple requests. + * * A RateLimiter distributes permits at a configurable rate. {@link #getPermission} blocks if necessary * until a permit is available, and then takes it. Once acquired, permits need not be released. */ diff --git a/resilience4j-ratpack/src/main/java/io/github/resilience4j/ratpack/retry/RetryMethodInterceptor.java b/resilience4j-ratpack/src/main/java/io/github/resilience4j/ratpack/retry/RetryMethodInterceptor.java index 1fc99764d4..3b0ff7ce47 100644 --- a/resilience4j-ratpack/src/main/java/io/github/resilience4j/ratpack/retry/RetryMethodInterceptor.java +++ b/resilience4j-ratpack/src/main/java/io/github/resilience4j/ratpack/retry/RetryMethodInterceptor.java @@ -42,14 +42,14 @@ public class RetryMethodInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { Retry annotation = invocation.getMethod().getAnnotation(Retry.class); + io.github.resilience4j.retry.Retry retry = registry.retry(annotation.name()); + if (retry == null) { + return invocation.proceed(); + } RecoveryFunction recoveryFunction = annotation.recovery().newInstance(); if (registry == null) { registry = RetryRegistry.ofDefaults(); } - io.github.resilience4j.retry.Retry retry = registry.newRetry(annotation.name()); - if (retry == null) { - return invocation.proceed(); - } Class returnType = invocation.getMethod().getReturnType(); if (Promise.class.isAssignableFrom(returnType)) { Promise result = (Promise) proceed(invocation, retry, recoveryFunction); @@ -82,20 +82,20 @@ public Object invoke(MethodInvocation invocation) throws Throwable { } else if (CompletionStage.class.isAssignableFrom(returnType)) { CompletionStage stage = (CompletionStage) proceed(invocation, retry, recoveryFunction); - return executeCompletionStage(invocation, stage, retry, recoveryFunction); + return executeCompletionStage(invocation, stage, retry.context(), recoveryFunction); } return proceed(invocation, retry, recoveryFunction); } @SuppressWarnings("unchecked") - private CompletionStage executeCompletionStage(MethodInvocation invocation, CompletionStage stage, io.github.resilience4j.retry.Retry retry, RecoveryFunction recoveryFunction) { + private CompletionStage executeCompletionStage(MethodInvocation invocation, CompletionStage stage, io.github.resilience4j.retry.Retry.Context context, RecoveryFunction recoveryFunction) { final CompletableFuture promise = new CompletableFuture(); stage.whenComplete((v, t) -> { if (t != null) { try { - retry.onError((Exception) t); + context.onError((Exception) t); CompletionStage next = (CompletionStage) invocation.proceed(); - CompletableFuture temp = executeCompletionStage(invocation, next, retry, recoveryFunction).toCompletableFuture(); + CompletableFuture temp = executeCompletionStage(invocation, next, context, recoveryFunction).toCompletableFuture(); promise.complete(temp.join()); } catch (Throwable t2) { try { @@ -106,6 +106,7 @@ private CompletionStage executeCompletionStage(MethodInvocation invocation, C } } } else { + context.onSuccess(); promise.complete(v); } }); @@ -113,20 +114,23 @@ private CompletionStage executeCompletionStage(MethodInvocation invocation, C } private Object proceed(MethodInvocation invocation, io.github.resilience4j.retry.Retry retry, RecoveryFunction recoveryFunction) throws Throwable { + io.github.resilience4j.retry.Retry.Context context = retry.context(); try { - return invocation.proceed(); + Object result = invocation.proceed(); + context.onSuccess(); + return result; } catch (Exception e) { // exception thrown, we know a direct value was attempted to be returned Object result; - retry.onError(e); + context.onError(e); while (true) { try { result = invocation.proceed(); - retry.onSuccess(); + context.onSuccess(); return result; } catch (Exception e1) { try { - retry.onError(e1); + context.onError(e1); } catch (Throwable t) { return recoveryFunction.apply(t); } diff --git a/resilience4j-ratpack/src/main/java/io/github/resilience4j/ratpack/retry/RetryTransformer.java b/resilience4j-ratpack/src/main/java/io/github/resilience4j/ratpack/retry/RetryTransformer.java index 8c9c3dbf6a..81abf8677e 100644 --- a/resilience4j-ratpack/src/main/java/io/github/resilience4j/ratpack/retry/RetryTransformer.java +++ b/resilience4j-ratpack/src/main/java/io/github/resilience4j/ratpack/retry/RetryTransformer.java @@ -55,18 +55,19 @@ public RetryTransformer recover(Function recoverer) { @Override public Upstream apply(Upstream upstream) throws Exception { return down -> { + Retry.Context context = retry.context(); Downstream downstream = new Downstream() { @Override public void success(T value) { - retry.onSuccess(); + context.onSuccess(); down.success(value); } @Override public void error(Throwable throwable) { try { - retry.onError((Exception) throwable); + context.onError((Exception) throwable); upstream.connect(this); } catch (Throwable t) { if (recoverer != null) { diff --git a/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/Resilience4jModuleSpec.groovy b/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/Resilience4jModuleSpec.groovy index 395a697ea8..ec257c62f7 100644 --- a/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/Resilience4jModuleSpec.groovy +++ b/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/Resilience4jModuleSpec.groovy @@ -92,8 +92,18 @@ class Resilience4jModuleSpec extends Specification { timer.count == 3 and: - registry.gauges.size() == 8 - registry.gauges.keySet() == ['resilience4j.circuitbreaker.test.buffered', 'resilience4j.circuitbreaker.test.buffered_max', 'resilience4j.circuitbreaker.test.failed', 'resilience4j.circuitbreaker.test.not_permitted', 'resilience4j.circuitbreaker.test.successful', 'resilience4j.ratelimiter.test.available_permissions', 'resilience4j.ratelimiter.test.number_of_waiting_threads', 'resilience4j.retry.test.retry_max_ratio'].toSet() + registry.gauges.size() == 11 + registry.gauges.keySet() == ['resilience4j.circuitbreaker.test.buffered', + 'resilience4j.circuitbreaker.test.buffered_max', + 'resilience4j.circuitbreaker.test.failed', + 'resilience4j.circuitbreaker.test.not_permitted', + 'resilience4j.circuitbreaker.test.successful', + 'resilience4j.ratelimiter.test.available_permissions', + 'resilience4j.ratelimiter.test.number_of_waiting_threads', + 'resilience4j.retry.test.successful_calls_without_retry', + 'resilience4j.retry.test.successful_calls_with_retry', + 'resilience4j.retry.test.failed_calls_without_retry', + 'resilience4j.retry.test.failed_calls_with_retry'].toSet() } def "test prometheus"() { @@ -140,7 +150,9 @@ class Resilience4jModuleSpec extends Specification { def families = collectorRegistry.metricFamilySamples().collect { it.name }.sort() then: - families == ['resilience4j_circuitbreaker_calls', 'resilience4j_circuitbreaker_states', 'resilience4j_ratelimiter'].sort() + families == ['resilience4j_circuitbreaker_calls', + 'resilience4j_circuitbreaker_states', + 'resilience4j_ratelimiter'].sort() } static class Something { diff --git a/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/retry/RetrySpec.groovy b/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/retry/RetrySpec.groovy index 163cfa59a2..63707bc8ba 100644 --- a/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/retry/RetrySpec.groovy +++ b/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/retry/RetrySpec.groovy @@ -16,9 +16,8 @@ package io.github.resilience4j.ratpack.retry -import io.github.resilience4j.ratpack.recovery.RecoveryFunction import io.github.resilience4j.ratpack.Resilience4jModule -import io.github.resilience4j.ratpack.retry.Retry +import io.github.resilience4j.ratpack.recovery.RecoveryFunction import io.github.resilience4j.retry.RetryConfig import io.github.resilience4j.retry.RetryRegistry import io.reactivex.Flowable @@ -76,7 +75,7 @@ class RetrySpec extends Specification { actual.body.text == 'retry promise' } - def "test circuit break a method via annotation with fallback"() { + def "test retry a method via annotation with fallback"() { given: RetryRegistry registry = RetryRegistry.of(buildConfig()) app = ratpack { diff --git a/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/retry/RetryTransformerSpec.groovy b/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/retry/RetryTransformerSpec.groovy index 48f694cd24..54e07ce9ee 100644 --- a/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/retry/RetryTransformerSpec.groovy +++ b/resilience4j-ratpack/src/test/groovy/io/github/resilience4j/ratpack/retry/RetryTransformerSpec.groovy @@ -16,7 +16,6 @@ package io.github.resilience4j.ratpack.retry -import io.github.resilience4j.ratpack.retry.RetryTransformer import io.github.resilience4j.retry.Retry import io.github.resilience4j.retry.RetryConfig import ratpack.exec.Blocking @@ -68,6 +67,35 @@ class RetryTransformerSpec extends Specification { times.get() == 3 } + + def "same retry transformer instance can be used to transform multiple promises"() { + given: + Retry retry = buildRetry() + RetryTransformer transformer = RetryTransformer.of(retry) + Exception e1 = new Exception("puke1") + Exception e2 = new Exception("puke2") + AtomicInteger times = new AtomicInteger(0) + + when: + def r1 = ExecHarness.yieldSingle { + Blocking. get { times.getAndIncrement(); throw e1 } + .transform(transformer) + } + def r2 = ExecHarness.yieldSingle { + Blocking. get { times.getAndIncrement(); throw e2 } + .transform(transformer) + } + + then: + r1.value == null + r1.error + r1.throwable == e1 + r2.value == null + r2.error + r2.throwable == e2 + times.get() == 6 + } + def "can retry promise 3 times then recover"() { given: Retry retry = buildRetry() diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/AsyncRetry.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/AsyncRetry.java index 8a9e78e0e7..9dc5d90650 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/AsyncRetry.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/AsyncRetry.java @@ -1,7 +1,7 @@ package io.github.resilience4j.retry; import io.github.resilience4j.retry.event.RetryEvent; -import io.github.resilience4j.retry.internal.AsyncRetryContext; +import io.github.resilience4j.retry.internal.AsyncRetryImpl; import io.reactivex.Flowable; import java.util.concurrent.CompletableFuture; @@ -10,6 +10,9 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +/** + * A AsyncRetry instance is thread-safe can be used to decorate multiple requests. + */ public interface AsyncRetry { /** @@ -20,23 +23,25 @@ public interface AsyncRetry { String getName(); /** - * Records a successful call. + * Returns a reactive stream of RetryEvents. + * + * @return a reactive stream of RetryEvents */ - void onSuccess(); + Flowable getEventStream(); /** - * Records an failed call. - * @param throwable the exception to handle - * @return delay in milliseconds until the next try + * Creates a retry Context. + * + * @return the retry Context */ - long onError(Throwable throwable); + AsyncRetry.Context context(); /** - * Returns a reactive stream of RetryEvents. + * Returns the RetryConfig of this Retry. * - * @return a reactive stream of RetryEvents + * @return the RetryConfig of this Retry */ - Flowable getEventStream(); + RetryConfig getRetryConfig(); /** * Creates a Retry with a custom Retry configuration. @@ -47,7 +52,7 @@ public interface AsyncRetry { * @return a Retry with a custom Retry configuration. */ static AsyncRetry of(String id, RetryConfig retryConfig){ - return new AsyncRetryContext(id, retryConfig); + return new AsyncRetryImpl(id, retryConfig); } /** @@ -75,21 +80,21 @@ static AsyncRetry ofDefaults(String id){ /** * Decorates CompletionStageSupplier with Retry * - * @param retryContext retry context + * @param retry the retry context * @param scheduler execution service to use to schedule retries * @param supplier completion stage supplier * @param type of completion stage result * @return decorated supplier */ static Supplier> decorateCompletionStage( - AsyncRetry retryContext, + AsyncRetry retry, ScheduledExecutorService scheduler, Supplier> supplier ) { return () -> { final CompletableFuture promise = new CompletableFuture<>(); - final Runnable block = new AsyncRetryBlock<>(scheduler, retryContext, supplier, promise); + final Runnable block = new AsyncRetryBlock<>(scheduler, retry.context(), supplier, promise); block.run(); return promise; @@ -104,31 +109,61 @@ static Supplier> decorateCompletionStage( Metrics getMetrics(); interface Metrics { + + /** + * Returns the number of successful calls without a retry attempt. + * + * @return the number of successful calls without a retry attempt + */ + long getNumberOfSuccessfulCallsWithoutRetryAttempt(); + /** - * Returns how many attempts this have been made by this retry. + * Returns the number of failed calls without a retry attempt. * - * @return how many retries have been attempted, but failed. + * @return the number of failed calls without a retry attempt */ - int getNumAttempts(); + long getNumberOfFailedCallsWithoutRetryAttempt(); /** - * Returns how many retry attempts are allowed before failure. + * Returns the number of successful calls after a retry attempt. * - * @return how many retries are allowed before failure. + * @return the number of successful calls after a retry attempt + */ + long getNumberOfSuccessfulCallsWithRetryAttempt(); + + /** + * Returns the number of failed calls after all retry attempts. + * + * @return the number of failed calls after all retry attempts + */ + long getNumberOfFailedCallsWithRetryAttempt(); + } + + interface Context { + + /** + * Records a successful call. + */ + void onSuccess(); + + /** + * Records an failed call. + * @param throwable the exception to handle + * @return delay in milliseconds until the next try */ - int getMaxAttempts(); + long onError(Throwable throwable); } } class AsyncRetryBlock implements Runnable { private final ScheduledExecutorService scheduler; - private final AsyncRetry retryContext; + private final AsyncRetry.Context retryContext; private final Supplier> supplier; private final CompletableFuture promise; AsyncRetryBlock( ScheduledExecutorService scheduler, - AsyncRetry retryContext, + AsyncRetry.Context retryContext, Supplier> supplier, CompletableFuture promise ) { diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/IntervalFunction.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/IntervalFunction.java index 57515aa8a6..71ce7d6814 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/IntervalFunction.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/IntervalFunction.java @@ -19,22 +19,26 @@ static IntervalFunction ofDefaults() { return of(DEFAULT_INITIAL_INTERVAL); } - static IntervalFunction of(long intervalMillis, Function backoffFn) { + static IntervalFunction of(long intervalMillis, Function backoffFunction) { checkInterval(intervalMillis); - requireNonNull(backoffFn); + requireNonNull(backoffFunction); return (attempt) -> { checkAttempt(attempt); - return Stream.iterate(intervalMillis, backoffFn).get(attempt - 1); + return Stream.iterate(intervalMillis, backoffFunction).get(attempt - 1); }; } - static IntervalFunction of(Duration interval, Function backoffFn) { - return of(interval.toMillis(), backoffFn); + static IntervalFunction of(Duration interval, Function backoffFunction) { + return of(interval.toMillis(), backoffFunction); } static IntervalFunction of(long intervalMillis) { - return of(intervalMillis, x -> x); + checkInterval(intervalMillis); + return (attempt) -> { + checkAttempt(attempt); + return intervalMillis; + }; } static IntervalFunction of(Duration interval) { @@ -155,27 +159,27 @@ static double randomize(final double current, final double randomizationFactor) return (min + (Math.random() * (max - min + 1))); } - static void checkInterval(long v) { - if (v < 10) { - throw new IllegalArgumentException("Illegal argument interval: " + v + " millis"); + static void checkInterval(long interval) { + if (interval < 10) { + throw new IllegalArgumentException("Illegal argument interval: " + interval + " millis"); } } - static void checkMultiplier(double v) { - if (v < 1.0) { - throw new IllegalArgumentException("Illegal argument multiplier: " + v); + static void checkMultiplier(double multiplier) { + if (multiplier < 1.0) { + throw new IllegalArgumentException("Illegal argument multiplier: " + multiplier); } } - static void checkRandomizationFactor(double v) { - if (v < 0.0 || v >= 1.0) { - throw new IllegalArgumentException("Illegal argument randomizationFactor: " + v); + static void checkRandomizationFactor(double randomizationFactor) { + if (randomizationFactor < 0.0 || randomizationFactor >= 1.0) { + throw new IllegalArgumentException("Illegal argument randomizationFactor: " + randomizationFactor); } } - static void checkAttempt(long v) { - if (v < 1) { - throw new IllegalArgumentException("Illegal argument attempt: " + v); + static void checkAttempt(long attempt) { + if (attempt < 1) { + throw new IllegalArgumentException("Illegal argument attempt: " + attempt); } } } \ No newline at end of file diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/Retry.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/Retry.java index 8cbff9f7af..bce9eea329 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/Retry.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/Retry.java @@ -19,7 +19,7 @@ package io.github.resilience4j.retry; import io.github.resilience4j.retry.event.RetryEvent; -import io.github.resilience4j.retry.internal.RetryContext; +import io.github.resilience4j.retry.internal.RetryImpl; import io.reactivex.Flowable; import io.vavr.CheckedFunction0; import io.vavr.CheckedFunction1; @@ -29,6 +29,10 @@ import java.util.function.Function; import java.util.function.Supplier; +/** + * A Retry instance is thread-safe can be used to decorate multiple requests. + * A Retry. + */ public interface Retry { /** @@ -39,24 +43,18 @@ public interface Retry { String getName(); /** - * Records a successful call. - */ - void onSuccess(); - - /** - * Handles a checked exception + * Creates a retry Context. * - * @param exception the exception to handle - * @throws Throwable the exception + * @return the retry Context */ - void onError(Exception exception) throws Throwable; + Retry.Context context(); /** - * Handles a runtime exception + * Returns the RetryConfig of this Retry. * - * @param runtimeException the exception to handle + * @return the RetryConfig of this Retry */ - void onRuntimeError(RuntimeException runtimeException); + RetryConfig getRetryConfig(); /** * Returns a reactive stream of RetryEvents. @@ -73,8 +71,8 @@ public interface Retry { * * @return a Retry with a custom Retry configuration. */ - static RetryContext of(String name, RetryConfig retryConfig){ - return new RetryContext(name, retryConfig); + static Retry of(String name, RetryConfig retryConfig){ + return new RetryImpl(name, retryConfig); } /** @@ -85,8 +83,8 @@ static RetryContext of(String name, RetryConfig retryConfig){ * * @return a Retry with a custom Retry configuration. */ - static RetryContext of(String name, Supplier retryConfigSupplier){ - return new RetryContext(name, retryConfigSupplier.get()); + static Retry of(String name, Supplier retryConfigSupplier){ + return new RetryImpl(name, retryConfigSupplier.get()); } /** @@ -96,7 +94,7 @@ static RetryContext of(String name, Supplier retryConfigSupplier){ * @return a Retry with default configuration */ static Retry ofDefaults(String name){ - return new RetryContext(name, RetryConfig.ofDefaults()); + return new RetryImpl(name, RetryConfig.ofDefaults()); } /** @@ -135,20 +133,21 @@ default void executeRunnable(Runnable runnable){ /** * Creates a retryable supplier. * - * @param retryContext the retry context + * @param retry the retry context * @param supplier the original function * @param the type of results supplied by this supplier * * @return a retryable function */ - static CheckedFunction0 decorateCheckedSupplier(Retry retryContext, CheckedFunction0 supplier){ + static CheckedFunction0 decorateCheckedSupplier(Retry retry, CheckedFunction0 supplier){ return () -> { + Retry.Context context = retry.context(); do try { T result = supplier.apply(); - retryContext.onSuccess(); + context.onSuccess(); return result; } catch (Exception exception) { - retryContext.onError(exception); + context.onError(exception); } while (true); }; } @@ -156,19 +155,20 @@ static CheckedFunction0 decorateCheckedSupplier(Retry retryContext, Check /** * Creates a retryable runnable. * - * @param retryContext the retry context + * @param retry the retry context * @param runnable the original runnable * * @return a retryable runnable */ - static CheckedRunnable decorateCheckedRunnable(Retry retryContext, CheckedRunnable runnable){ + static CheckedRunnable decorateCheckedRunnable(Retry retry, CheckedRunnable runnable){ return () -> { + Retry.Context context = retry.context(); do try { runnable.run(); - retryContext.onSuccess(); + context.onSuccess(); break; } catch (Exception exception) { - retryContext.onError(exception); + context.onError(exception); } while (true); }; } @@ -176,21 +176,22 @@ static CheckedRunnable decorateCheckedRunnable(Retry retryContext, CheckedRunnab /** * Creates a retryable function. * - * @param retryContext the retry context + * @param retry the retry context * @param function the original function * @param the type of the input to the function * @param the result type of the function * * @return a retryable function */ - static CheckedFunction1 decorateCheckedFunction(Retry retryContext, CheckedFunction1 function){ + static CheckedFunction1 decorateCheckedFunction(Retry retry, CheckedFunction1 function){ return (T t) -> { + Retry.Context context = retry.context(); do try { R result = function.apply(t); - retryContext.onSuccess(); + context.onSuccess(); return result; } catch (Exception exception) { - retryContext.onError(exception); + context.onError(exception); } while (true); }; } @@ -198,20 +199,21 @@ static CheckedFunction1 decorateCheckedFunction(Retry retryContext, /** * Creates a retryable supplier. * - * @param retryContext the retry context + * @param retry the retry context * @param supplier the original function * @param the type of results supplied by this supplier * * @return a retryable function */ - static Supplier decorateSupplier(Retry retryContext, Supplier supplier){ + static Supplier decorateSupplier(Retry retry, Supplier supplier){ return () -> { + Retry.Context context = retry.context(); do try { T result = supplier.get(); - retryContext.onSuccess(); + context.onSuccess(); return result; } catch (RuntimeException runtimeException) { - retryContext.onRuntimeError(runtimeException); + context.onRuntimeError(runtimeException); } while (true); }; } @@ -219,20 +221,21 @@ static Supplier decorateSupplier(Retry retryContext, Supplier supplier /** * Creates a retryable callable. * - * @param retryContext the retry context + * @param retry the retry context * @param supplier the original function * @param the type of results supplied by this supplier * * @return a retryable function */ - static Callable decorateCallable(Retry retryContext, Callable supplier){ + static Callable decorateCallable(Retry retry, Callable supplier){ return () -> { + Retry.Context context = retry.context(); do try { T result = supplier.call(); - retryContext.onSuccess(); + context.onSuccess(); return result; } catch (RuntimeException runtimeException) { - retryContext.onRuntimeError(runtimeException); + context.onRuntimeError(runtimeException); } while (true); }; } @@ -240,19 +243,20 @@ static Callable decorateCallable(Retry retryContext, Callable supplier /** * Creates a retryable runnable. * - * @param retryContext the retry context + * @param retry the retry context * @param runnable the original runnable * * @return a retryable runnable */ - static Runnable decorateRunnable(Retry retryContext, Runnable runnable){ + static Runnable decorateRunnable(Retry retry, Runnable runnable){ return () -> { + Retry.Context context = retry.context(); do try { runnable.run(); - retryContext.onSuccess(); + context.onSuccess(); break; } catch (RuntimeException runtimeException) { - retryContext.onRuntimeError(runtimeException); + context.onRuntimeError(runtimeException); } while (true); }; } @@ -260,21 +264,22 @@ static Runnable decorateRunnable(Retry retryContext, Runnable runnable){ /** * Creates a retryable function. * - * @param retryContext the retry context + * @param retry the retry context * @param function the original function * @param the type of the input to the function * @param the result type of the function * * @return a retryable function */ - static Function decorateFunction(Retry retryContext, Function function){ + static Function decorateFunction(Retry retry, Function function){ return (T t) -> { + Retry.Context context = retry.context(); do try { R result = function.apply(t); - retryContext.onSuccess(); + context.onSuccess(); return result; } catch (RuntimeException runtimeException) { - retryContext.onRuntimeError(runtimeException); + context.onRuntimeError(runtimeException); } while (true); }; } @@ -287,18 +292,56 @@ static Function decorateFunction(Retry retryContext, Function Metrics getMetrics(); interface Metrics { + + /** + * Returns the number of successful calls without a retry attempt. + * + * @return the number of successful calls without a retry attempt + */ + long getNumberOfSuccessfulCallsWithoutRetryAttempt(); + + /** + * Returns the number of failed calls without a retry attempt. + * + * @return the number of failed calls without a retry attempt + */ + long getNumberOfFailedCallsWithoutRetryAttempt(); + + /** + * Returns the number of successful calls after a retry attempt. + * + * @return the number of successful calls after a retry attempt + */ + long getNumberOfSuccessfulCallsWithRetryAttempt(); + + /** + * Returns the number of failed calls after all retry attempts. + * + * @return the number of failed calls after all retry attempts + */ + long getNumberOfFailedCallsWithRetryAttempt(); + } + + interface Context { + + /** + * Records a successful call. + */ + void onSuccess(); + /** - * Returns how many attempts this have been made by this retry. + * Handles a checked exception * - * @return how many retries have been attempted, but failed. + * @param exception the exception to handle + * @throws Throwable the exception */ - int getNumAttempts(); + void onError(Exception exception) throws Throwable; /** - * Returns how many retry attempts are allowed before failure. + * Handles a runtime exception * - * @return how many retries are allowed before failure. + * @param runtimeException the exception to handle */ - int getMaxAttempts(); + void onRuntimeError(RuntimeException runtimeException); } } diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/RetryConfig.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/RetryConfig.java index 54e12347cd..892fc544d1 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/RetryConfig.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/RetryConfig.java @@ -30,13 +30,16 @@ public class RetryConfig { private int maxAttempts = DEFAULT_MAX_ATTEMPTS; - private IntervalFunction intervalFunction = (x) -> DEFAULT_WAIT_DURATION; + private IntervalFunction intervalFunction = (numOfAttempts) -> DEFAULT_WAIT_DURATION; // The default exception predicate retries all exceptions. private Predicate exceptionPredicate = (exception) -> true; private RetryConfig(){ } + /** + * @return the maximum allowed retries to make. + */ public int getMaxAttempts() { return maxAttempts; } diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/RetryRegistry.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/RetryRegistry.java index 76c47e02b3..eca24bf9a5 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/RetryRegistry.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/RetryRegistry.java @@ -41,17 +41,6 @@ public interface RetryRegistry { */ Retry retry(String name); - /** - * Returns a new {@link Retry} with the default Retry configuration. - * - * This should be used when a retry is used in concurrent code blocks, and each retry - * should be isolated to its own scope. - * - * @param name the name of the Retry - * @return The {@link Retry} - */ - Retry newRetry(String name); - /** * Returns a managed {@link Retry} or creates a new one with a custom Retry configuration. * @@ -61,18 +50,6 @@ public interface RetryRegistry { */ Retry retry(String name, RetryConfig retryConfig); - /** - * Returns a new {@link Retry} with a custom Retry configuration. - * - * This should be used when a retry is used in concurrent code blocks, and each retry - * should be isolated to its own scope. - * - * @param name the name of the Retry - * @param retryConfig a custom Retry configuration - * @return The {@link Retry} - */ - Retry newRetry(String name, RetryConfig retryConfig); - /** * Returns a managed {@link Retry} or creates a new one with a custom Retry configuration. * @@ -82,18 +59,6 @@ public interface RetryRegistry { */ Retry retry(String name, Supplier retryConfigSupplier); - /** - * Returns a new {@link Retry} with a custom Retry configuration. - * - * This should be used when a retry is used in concurrent code blocks, and each retry - * should be isolated to its own scope. - * - * @param name the name of the Retry - * @param retryConfigSupplier a supplier of a custom Retry configuration - * @return The {@link Retry} - */ - Retry newRetry(String name, Supplier retryConfigSupplier); - /** * Creates a RetryRegistry with a custom Retry configuration. diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/AbstractRetryEvent.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/AbstractRetryEvent.java index 32b2c343d9..55976f4ff4 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/AbstractRetryEvent.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/AbstractRetryEvent.java @@ -45,7 +45,7 @@ public ZonedDateTime getCreationTime() { } @Override - public int getNumberOfAttempts() { + public int getNumberOfRetryAttempts() { return numberOfAttempts; } diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryEvent.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryEvent.java index b4e386bf86..d6df88cf23 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryEvent.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryEvent.java @@ -33,11 +33,11 @@ public interface RetryEvent { String getName(); /** - * Returns the number of attempts. + * Returns the number of retry attempts. * - * @return the the number of attempts + * @return the the number of retry attempts */ - int getNumberOfAttempts(); + int getNumberOfRetryAttempts(); /** * Returns the type of the Retry event. @@ -66,7 +66,9 @@ public interface RetryEvent { enum Type { /** A RetryEvent which informs that a call has been retried, but still failed */ ERROR, - /** A RetryEvent which informs that a call has been retried and a retry was successful */ - SUCCESS + /** A RetryEvent which informs that a call has been successful */ + SUCCESS, + /** A RetryEvent which informs that an error has been ignored */ + IGNORED_ERROR } } diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnErrorEvent.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnErrorEvent.java index b8f203a014..7ddc1ae571 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnErrorEvent.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnErrorEvent.java @@ -36,7 +36,7 @@ public String toString() { return String.format("%s: Retry '%s' recorded a failed retry attempt. Number of retry attempts: '%d', Last exception was: '%s'.", getCreationTime(), getName(), - getNumberOfAttempts(), + getNumberOfRetryAttempts(), getLastThrowable().toString()); } } diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnIgnoredErrorEvent.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnIgnoredErrorEvent.java new file mode 100644 index 0000000000..6d4aab6728 --- /dev/null +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnIgnoredErrorEvent.java @@ -0,0 +1,41 @@ +/* + * + * Copyright 2016 Robert Winkler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +package io.github.resilience4j.retry.event; + +/** + * A RetryEvent which informs that an error has been ignored + */ +public class RetryOnIgnoredErrorEvent extends AbstractRetryEvent { + + public RetryOnIgnoredErrorEvent(String name, Throwable lastThrowable) { + super(name, 0, lastThrowable); + } + @Override + public Type getEventType() { + return Type.IGNORED_ERROR; + } + + @Override + public String toString() { + return String.format("%s: Retry '%s' recorded an error which has been ignored: '%s'.", + getCreationTime(), + getName(), + getLastThrowable().toString()); + } +} diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnSuccessEvent.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnSuccessEvent.java index e15600518a..7c23da9212 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnSuccessEvent.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/event/RetryOnSuccessEvent.java @@ -37,7 +37,7 @@ public String toString() { return String.format("%s: Retry '%s' recorded a successful retry attempt. Number of retry attempts: '%d', Last exception was: '%s'.", getCreationTime(), getName(), - getNumberOfAttempts(), + getNumberOfRetryAttempts(), getLastThrowable().toString()); } } diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/AsyncRetryContext.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/AsyncRetryContext.java deleted file mode 100644 index 899ac6fb30..0000000000 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/AsyncRetryContext.java +++ /dev/null @@ -1,110 +0,0 @@ -package io.github.resilience4j.retry.internal; - -import io.github.resilience4j.retry.AsyncRetry; -import io.github.resilience4j.retry.Retry; -import io.github.resilience4j.retry.RetryConfig; -import io.github.resilience4j.retry.event.RetryEvent; -import io.github.resilience4j.retry.event.RetryOnErrorEvent; -import io.github.resilience4j.retry.event.RetryOnSuccessEvent; -import io.reactivex.Flowable; -import io.reactivex.processors.FlowableProcessor; -import io.reactivex.processors.PublishProcessor; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; - -public class AsyncRetryContext implements AsyncRetry { - - private final String name; - private final int maxAttempts; - private final Function intervalFunction; - private final Metrics metrics; - private final FlowableProcessor eventPublisher; - private final Predicate exceptionPredicate; - - private final AtomicInteger numOfAttempts = new AtomicInteger(0); - - public AsyncRetryContext(String name, RetryConfig config) { - this.name = name; - this.maxAttempts = config.getMaxAttempts(); - this.intervalFunction = config.getIntervalFunction(); - this.exceptionPredicate = config.getExceptionPredicate(); - - PublishProcessor publisher = PublishProcessor.create(); - this.eventPublisher = publisher.toSerialized(); - this.metrics = this.new AsyncRetryContextMetrics(); - } - - @Override - public String getName() { - return name; - } - - @Override - public void onSuccess() { - int currentNumOfAttempts = numOfAttempts.get(); - publishRetryEvent(() -> new RetryOnSuccessEvent(name, currentNumOfAttempts, null)); - } - - @Override - public long onError(Throwable throwable) { - if (!exceptionPredicate.test(throwable)) { - return -1; - } - - int attempt = numOfAttempts.addAndGet(1); - - if (attempt > maxAttempts) { - return -1; - } - - publishRetryEvent(() -> new RetryOnErrorEvent(name, attempt, throwable)); - return intervalFunction.apply(attempt); - } - - @Override - public Flowable getEventStream() { - return eventPublisher; - } - - - private void publishRetryEvent(Supplier event) { - if(eventPublisher.hasSubscribers()) { - eventPublisher.onNext(event.get()); - } - } - - /** - * {@inheritDoc} - */ - @Override - public Metrics getMetrics() { - return this.metrics; - } - - /** - * {@inheritDoc} - */ - public final class AsyncRetryContextMetrics implements Metrics { - private AsyncRetryContextMetrics() { - } - - /** - * @return current number of retry attempts made. - */ - @Override - public int getNumAttempts() { - return numOfAttempts.get(); - } - - /** - * @return the maximum allowed retries to make. - */ - @Override - public int getMaxAttempts() { - return maxAttempts; - } - } -} diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/AsyncRetryImpl.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/AsyncRetryImpl.java new file mode 100644 index 0000000000..e59566607e --- /dev/null +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/AsyncRetryImpl.java @@ -0,0 +1,141 @@ +package io.github.resilience4j.retry.internal; + +import io.github.resilience4j.retry.AsyncRetry; +import io.github.resilience4j.retry.RetryConfig; +import io.github.resilience4j.retry.event.RetryEvent; +import io.github.resilience4j.retry.event.RetryOnErrorEvent; +import io.github.resilience4j.retry.event.RetryOnIgnoredErrorEvent; +import io.github.resilience4j.retry.event.RetryOnSuccessEvent; +import io.reactivex.Flowable; +import io.reactivex.processors.FlowableProcessor; +import io.reactivex.processors.PublishProcessor; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class AsyncRetryImpl implements AsyncRetry { + + private final String name; + private final int maxAttempts; + private final Function intervalFunction; + private final Metrics metrics; + private final FlowableProcessor eventPublisher; + private final Predicate exceptionPredicate; + private final RetryConfig config; + + private LongAdder succeededAfterRetryCounter; + private LongAdder failedAfterRetryCounter; + private LongAdder succeededWithoutRetryCounter; + private LongAdder failedWithoutRetryCounter; + + public AsyncRetryImpl(String name, RetryConfig config) { + this.config = config; + this.name = name; + this.maxAttempts = config.getMaxAttempts(); + this.intervalFunction = config.getIntervalFunction(); + this.exceptionPredicate = config.getExceptionPredicate(); + + PublishProcessor publisher = PublishProcessor.create(); + this.eventPublisher = publisher.toSerialized(); + this.metrics = this.new AsyncRetryMetrics(); + succeededAfterRetryCounter = new LongAdder(); + failedAfterRetryCounter = new LongAdder(); + succeededWithoutRetryCounter = new LongAdder(); + failedWithoutRetryCounter = new LongAdder(); + } + + public final class ContextImpl implements AsyncRetry.Context { + + private final AtomicInteger numOfAttempts = new AtomicInteger(0); + private final AtomicReference lastException = new AtomicReference<>(); + + @Override + public void onSuccess() { + int currentNumOfAttempts = numOfAttempts.get(); + if(currentNumOfAttempts > 0) { + publishRetryEvent(() -> new RetryOnSuccessEvent(name, currentNumOfAttempts, lastException.get())); + } + } + + @Override + public long onError(Throwable throwable) { + if (!exceptionPredicate.test(throwable)) { + failedWithoutRetryCounter.increment(); + publishRetryEvent(() -> new RetryOnIgnoredErrorEvent(getName(), throwable)); + return -1; + } + lastException.set(throwable); + int attempt = numOfAttempts.incrementAndGet(); + + if (attempt > maxAttempts) { + failedAfterRetryCounter.increment(); + publishRetryEvent(() -> new RetryOnErrorEvent(name, attempt, throwable)); + return -1; + } + + return intervalFunction.apply(attempt); + } + } + + @Override + public String getName() { + return name; + } + + @Override + public Flowable getEventStream() { + return eventPublisher; + } + + @Override + public Context context() { + return new ContextImpl(); + } + + @Override + public RetryConfig getRetryConfig() { + return config; + } + + + private void publishRetryEvent(Supplier event) { + if(eventPublisher.hasSubscribers()) { + eventPublisher.onNext(event.get()); + } + } + + @Override + public Metrics getMetrics() { + return this.metrics; + } + + + public final class AsyncRetryMetrics implements AsyncRetry.Metrics { + private AsyncRetryMetrics() { + } + + @Override + public long getNumberOfSuccessfulCallsWithoutRetryAttempt() { + return succeededWithoutRetryCounter.longValue(); + } + + @Override + public long getNumberOfFailedCallsWithoutRetryAttempt() { + return failedWithoutRetryCounter.longValue(); + } + + @Override + public long getNumberOfSuccessfulCallsWithRetryAttempt() { + return succeededAfterRetryCounter.longValue(); + } + + @Override + public long getNumberOfFailedCallsWithRetryAttempt() { + return failedAfterRetryCounter.longValue(); + } + } +} diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/InMemoryRetryRegistry.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/InMemoryRetryRegistry.java index d814a669e6..f2e678b58c 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/InMemoryRetryRegistry.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/InMemoryRetryRegistry.java @@ -71,11 +71,6 @@ public Retry retry(String name) { defaultRetryConfig)); } - @Override - public Retry newRetry(String name) { - return Retry.of(name, defaultRetryConfig); - } - /** * {@inheritDoc} */ @@ -85,19 +80,9 @@ public Retry retry(String name, RetryConfig customRetryConfig) { customRetryConfig)); } - @Override - public Retry newRetry(String name, RetryConfig retryConfig) { - return Retry.of(name, retryConfig); - } - @Override public Retry retry(String name, Supplier retryConfigSupplier) { return retries.computeIfAbsent(Objects.requireNonNull(name, "Name must not be null"), (k) -> Retry.of(name, retryConfigSupplier.get())); } - - @Override - public Retry newRetry(String name, Supplier retryConfigSupplier) { - return Retry.of(name, retryConfigSupplier.get()); - } } diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/RetryContext.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/RetryContext.java deleted file mode 100644 index 802f4c2e50..0000000000 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/RetryContext.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * - * Copyright 2016 Robert Winkler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * - */ -package io.github.resilience4j.retry.internal; - -import io.github.resilience4j.retry.Retry; -import io.github.resilience4j.retry.RetryConfig; -import io.github.resilience4j.retry.event.RetryEvent; -import io.github.resilience4j.retry.event.RetryOnErrorEvent; -import io.github.resilience4j.retry.event.RetryOnSuccessEvent; -import io.reactivex.Flowable; -import io.reactivex.processors.FlowableProcessor; -import io.reactivex.processors.PublishProcessor; -import io.vavr.CheckedConsumer; -import io.vavr.control.Option; -import io.vavr.control.Try; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; - -public class RetryContext implements Retry { - - private final AtomicInteger numOfAttempts = new AtomicInteger(0); - private AtomicReference lastException = new AtomicReference<>(); - private AtomicReference lastRuntimeException = new AtomicReference<>(); - - private final Metrics metrics; - private final FlowableProcessor eventPublisher; - - private String name; - private int maxAttempts; - private Function intervalFunction; - private Predicate exceptionPredicate; - /*package*/ static CheckedConsumer sleepFunction = Thread::sleep; - - public RetryContext(String name, RetryConfig config){ - this.name = name; - this.maxAttempts = config.getMaxAttempts(); - this.intervalFunction = config.getIntervalFunction(); - this.exceptionPredicate = config.getExceptionPredicate(); - PublishProcessor publisher = PublishProcessor.create(); - this.metrics = this.new RetryContextMetrics(); - this.eventPublisher = publisher.toSerialized(); - } - - /** - * {@inheritDoc} - */ - @Override - public String getName() { - return name; - } - - /** - * {@inheritDoc} - */ - @Override - public void onSuccess() { - int currentNumOfAttempts = numOfAttempts.get(); - if(currentNumOfAttempts > 0){ - Throwable throwable = Option.of(lastException.get()).getOrElse(lastRuntimeException.get()); - publishRetryEvent(() -> new RetryOnSuccessEvent(getName(), currentNumOfAttempts, throwable)); - } - } - - private void throwOrSleepAfterException() throws Exception { - int currentNumOfAttempts = numOfAttempts.incrementAndGet(); - if(currentNumOfAttempts >= maxAttempts){ - Exception throwable = lastException.get(); - publishRetryEvent(() -> new RetryOnErrorEvent(getName(), currentNumOfAttempts, throwable)); - throw throwable; - }else{ - waitIntervalAfterFailure(); - } - } - - private void throwOrSleepAfterRuntimeException(){ - int currentNumOfAttempts = numOfAttempts.incrementAndGet(); - if(currentNumOfAttempts >= maxAttempts){ - RuntimeException throwable = lastRuntimeException.get(); - publishRetryEvent(() -> new RetryOnErrorEvent(getName(), currentNumOfAttempts, throwable)); - throw throwable; - }else{ - waitIntervalAfterFailure(); - } - } - - private void waitIntervalAfterFailure() { - // wait interval until the next attempt should start - long interval = intervalFunction.apply(numOfAttempts.get()); - Try.run(() -> sleepFunction.accept(interval)) - .getOrElseThrow(ex -> lastRuntimeException.get()); - } - - /** - * {@inheritDoc} - */ - @Override - public void onError(Exception exception) throws Throwable{ - if(exceptionPredicate.test(exception)){ - lastException.set(exception); - throwOrSleepAfterException(); - }else{ - throw exception; - } - } - - /** - * {@inheritDoc} - */ - @Override - public void onRuntimeError(RuntimeException runtimeException){ - if(exceptionPredicate.test(runtimeException)){ - lastRuntimeException.set(runtimeException); - throwOrSleepAfterRuntimeException(); - }else{ - throw runtimeException; - } - } - - private void publishRetryEvent(Supplier event) { - if(eventPublisher.hasSubscribers()) { - eventPublisher.onNext(event.get()); - } - } - - /** - * {@inheritDoc} - */ - @Override - public Flowable getEventStream() { - return eventPublisher; - } - - /** - * {@inheritDoc} - */ - @Override - public Metrics getMetrics() { - return this.metrics; - } - - /** - * {@inheritDoc} - */ - public final class RetryContextMetrics implements Metrics { - private RetryContextMetrics() { - } - - /** - * @return current number of retry attempts made. - */ - @Override - public int getNumAttempts() { - return numOfAttempts.get(); - } - - /** - * @return the maximum allowed retries to make. - */ - @Override - public int getMaxAttempts() { - return maxAttempts; - } - } -} diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/RetryImpl.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/RetryImpl.java new file mode 100644 index 0000000000..bd9c7b1390 --- /dev/null +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/internal/RetryImpl.java @@ -0,0 +1,212 @@ +/* + * + * Copyright 2016 Robert Winkler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +package io.github.resilience4j.retry.internal; + +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import io.github.resilience4j.retry.event.RetryEvent; +import io.github.resilience4j.retry.event.RetryOnErrorEvent; +import io.github.resilience4j.retry.event.RetryOnIgnoredErrorEvent; +import io.github.resilience4j.retry.event.RetryOnSuccessEvent; +import io.reactivex.Flowable; +import io.reactivex.processors.FlowableProcessor; +import io.reactivex.processors.PublishProcessor; +import io.vavr.CheckedConsumer; +import io.vavr.control.Option; +import io.vavr.control.Try; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class RetryImpl implements Retry { + + + private final Metrics metrics; + private final FlowableProcessor eventPublisher; + + private String name; + private RetryConfig config; + private int maxAttempts; + private Function intervalFunction; + private Predicate exceptionPredicate; + private LongAdder succeededAfterRetryCounter; + private LongAdder failedAfterRetryCounter; + private LongAdder succeededWithoutRetryCounter; + private LongAdder failedWithoutRetryCounter; + /*package*/ static CheckedConsumer sleepFunction = Thread::sleep; + + public RetryImpl(String name, RetryConfig config){ + this.name = name; + this.config = config; + this.maxAttempts = config.getMaxAttempts(); + this.intervalFunction = config.getIntervalFunction(); + this.exceptionPredicate = config.getExceptionPredicate(); + PublishProcessor publisher = PublishProcessor.create(); + this.metrics = this.new RetryMetrics(); + this.eventPublisher = publisher.toSerialized(); + succeededAfterRetryCounter = new LongAdder(); + failedAfterRetryCounter = new LongAdder(); + succeededWithoutRetryCounter = new LongAdder(); + failedWithoutRetryCounter = new LongAdder(); + } + + public final class ContextImpl implements Retry.Context { + + private final AtomicInteger numOfAttempts = new AtomicInteger(0); + private final AtomicReference lastException = new AtomicReference<>(); + private final AtomicReference lastRuntimeException = new AtomicReference<>(); + + private ContextImpl() { + } + + public void onSuccess() { + int currentNumOfAttempts = numOfAttempts.get(); + if(currentNumOfAttempts > 0){ + succeededAfterRetryCounter.increment(); + Throwable throwable = Option.of(lastException.get()).getOrElse(lastRuntimeException.get()); + publishRetryEvent(() -> new RetryOnSuccessEvent(getName(), currentNumOfAttempts, throwable)); + }else{ + succeededWithoutRetryCounter.increment(); + } + } + + public void onError(Exception exception) throws Throwable{ + if(exceptionPredicate.test(exception)){ + lastException.set(exception); + throwOrSleepAfterException(); + }else{ + failedWithoutRetryCounter.increment(); + publishRetryEvent(() -> new RetryOnIgnoredErrorEvent(getName(), exception)); + throw exception; + } + } + + public void onRuntimeError(RuntimeException runtimeException){ + if(exceptionPredicate.test(runtimeException)){ + lastRuntimeException.set(runtimeException); + throwOrSleepAfterRuntimeException(); + }else{ + failedWithoutRetryCounter.increment(); + throw runtimeException; + } + } + + private void throwOrSleepAfterException() throws Exception { + int currentNumOfAttempts = numOfAttempts.incrementAndGet(); + if(currentNumOfAttempts >= maxAttempts){ + failedAfterRetryCounter.increment(); + Exception throwable = lastException.get(); + publishRetryEvent(() -> new RetryOnErrorEvent(getName(), currentNumOfAttempts, throwable)); + throw throwable; + }else{ + waitIntervalAfterFailure(); + } + } + + private void throwOrSleepAfterRuntimeException(){ + int currentNumOfAttempts = numOfAttempts.incrementAndGet(); + if(currentNumOfAttempts >= maxAttempts){ + failedAfterRetryCounter.increment(); + RuntimeException throwable = lastRuntimeException.get(); + publishRetryEvent(() -> new RetryOnErrorEvent(getName(), currentNumOfAttempts, throwable)); + throw throwable; + }else{ + waitIntervalAfterFailure(); + } + } + + private void waitIntervalAfterFailure() { + // wait interval until the next attempt should start + long interval = intervalFunction.apply(numOfAttempts.get()); + Try.run(() -> sleepFunction.accept(interval)) + .getOrElseThrow(ex -> lastRuntimeException.get()); + } + + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + @Override + public Context context() { + return new ContextImpl(); + } + + @Override + public RetryConfig getRetryConfig() { + return config; + } + + + private void publishRetryEvent(Supplier event) { + if(eventPublisher.hasSubscribers()) { + eventPublisher.onNext(event.get()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Flowable getEventStream() { + return eventPublisher; + } + + /** + * {@inheritDoc} + */ + @Override + public Metrics getMetrics() { + return this.metrics; + } + + public final class RetryMetrics implements Metrics { + private RetryMetrics() { + } + + @Override + public long getNumberOfSuccessfulCallsWithoutRetryAttempt() { + return succeededWithoutRetryCounter.longValue(); + } + + @Override + public long getNumberOfFailedCallsWithoutRetryAttempt() { + return failedWithoutRetryCounter.longValue(); + } + + @Override + public long getNumberOfSuccessfulCallsWithRetryAttempt() { + return succeededAfterRetryCounter.longValue(); + } + + @Override + public long getNumberOfFailedCallsWithRetryAttempt() { + return failedAfterRetryCounter.longValue(); + } + } +} diff --git a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/transformer/RetryTransformer.java b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/transformer/RetryTransformer.java index 50191e4a4c..e5974747d8 100644 --- a/resilience4j-retry/src/main/java/io/github/resilience4j/retry/transformer/RetryTransformer.java +++ b/resilience4j-retry/src/main/java/io/github/resilience4j/retry/transformer/RetryTransformer.java @@ -58,34 +58,34 @@ public static RetryTransformer of(Retry retry) { @Override public Publisher apply(Flowable upstream) { - return Flowable.fromPublisher(d -> { + return Flowable.fromPublisher(downstream -> { SubscriptionArbiter sa = new SubscriptionArbiter(); - d.onSubscribe(sa); - RetrySubscriber repeatSubscriber = new RetrySubscriber<>(d, retry.getMetrics().getMaxAttempts(), sa, upstream, retry); + downstream.onSubscribe(sa); + RetrySubscriber repeatSubscriber = new RetrySubscriber<>(downstream, retry.getRetryConfig().getMaxAttempts(), sa, upstream, retry); upstream.subscribe(repeatSubscriber); }); } @Override public ObservableSource apply(Observable upstream) { - return Flowable.fromPublisher(d -> { + return Observable.fromPublisher(downstream -> { Flowable flowable = upstream.toFlowable(BackpressureStrategy.BUFFER); SubscriptionArbiter sa = new SubscriptionArbiter(); - d.onSubscribe(sa); - RetrySubscriber retrySubscriber = new RetrySubscriber<>(d, retry.getMetrics().getMaxAttempts(), sa, flowable, retry); + downstream.onSubscribe(sa); + RetrySubscriber retrySubscriber = new RetrySubscriber<>(downstream, retry.getRetryConfig().getMaxAttempts(), sa, flowable, retry); flowable.subscribe(retrySubscriber); - }).toObservable(); + }); } @Override public SingleSource apply(Single upstream) { - return Flowable.fromPublisher(d -> { + return Single.fromPublisher(downstream -> { Flowable flowable = upstream.toFlowable(); SubscriptionArbiter sa = new SubscriptionArbiter(); - d.onSubscribe(sa); - RetrySubscriber retrySubscriber = new RetrySubscriber<>(d, retry.getMetrics().getMaxAttempts(), sa, flowable, retry); + downstream.onSubscribe(sa); + RetrySubscriber retrySubscriber = new RetrySubscriber<>(downstream, retry.getRetryConfig().getMaxAttempts(), sa, flowable, retry); flowable.subscribe(retrySubscriber); - }).singleOrError(); + }); } static final class RetrySubscriber extends AtomicInteger implements Subscriber { @@ -93,7 +93,7 @@ static final class RetrySubscriber extends AtomicInteger implements Subscribe private final Subscriber actual; private final SubscriptionArbiter sa; private final Publisher source; - private final Retry retry; + private final Retry.Context context; private long remaining; RetrySubscriber(Subscriber actual, long count, SubscriptionArbiter sa, Publisher source, @@ -101,7 +101,7 @@ static final class RetrySubscriber extends AtomicInteger implements Subscribe this.actual = actual; this.sa = sa; this.source = source; - this.retry = retry; + this.context = retry.context(); this.remaining = count; } @@ -118,7 +118,7 @@ public void onNext(T t) { if (LOG.isDebugEnabled()) { LOG.info("onNext"); } - retry.onSuccess(); + context.onSuccess(); actual.onNext(t); sa.produced(1L); } @@ -135,7 +135,7 @@ public void onError(Throwable t) { actual.onError(t); } else { try { - retry.onError((Exception) t); + context.onError((Exception) t); subscribeNext(); } catch (Throwable t2) { actual.onError(t2); diff --git a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/event/RetryEventTest.java b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/event/RetryEventTest.java index d8805e676a..3340ada224 100644 --- a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/event/RetryEventTest.java +++ b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/event/RetryEventTest.java @@ -30,23 +30,33 @@ public class RetryEventTest { @Test public void testRetryOnErrorEvent() { RetryOnErrorEvent retryOnErrorEvent = new RetryOnErrorEvent("test", 2, - new IOException()); + new IOException("Bla")); Assertions.assertThat(retryOnErrorEvent.getName()).isEqualTo("test"); - Assertions.assertThat(retryOnErrorEvent.getNumberOfAttempts()).isEqualTo(2); + Assertions.assertThat(retryOnErrorEvent.getNumberOfRetryAttempts()).isEqualTo(2); Assertions.assertThat(retryOnErrorEvent.getEventType()).isEqualTo(Type.ERROR); Assertions.assertThat(retryOnErrorEvent.getLastThrowable()).isInstanceOf(IOException.class); - Assertions.assertThat(retryOnErrorEvent.toString()).contains("Retry 'test' recorded a failed retry attempt. Number of retry attempts: '2', Last exception was: 'java.io.IOException'."); + Assertions.assertThat(retryOnErrorEvent.toString()).contains("Retry 'test' recorded a failed retry attempt. Number of retry attempts: '2', Last exception was: 'java.io.IOException: Bla'."); } @Test public void testRetryOnSuccessEvent() { RetryOnSuccessEvent retryOnSuccessEvent = new RetryOnSuccessEvent("test", 2, - new IOException()); + new IOException("Bla")); Assertions.assertThat(retryOnSuccessEvent.getName()).isEqualTo("test"); - Assertions.assertThat(retryOnSuccessEvent.getNumberOfAttempts()).isEqualTo(2); + Assertions.assertThat(retryOnSuccessEvent.getNumberOfRetryAttempts()).isEqualTo(2); Assertions.assertThat(retryOnSuccessEvent.getEventType()).isEqualTo(Type.SUCCESS); Assertions.assertThat(retryOnSuccessEvent.getLastThrowable()).isInstanceOf(IOException.class); - Assertions.assertThat(retryOnSuccessEvent.toString()).contains("Retry 'test' recorded a successful retry attempt. Number of retry attempts: '2', Last exception was: 'java.io.IOException'."); + Assertions.assertThat(retryOnSuccessEvent.toString()).contains("Retry 'test' recorded a successful retry attempt. Number of retry attempts: '2', Last exception was: 'java.io.IOException: Bla'."); + } + + @Test + public void testRetryOnIgnoredErrorEvent() { + RetryOnIgnoredErrorEvent retryOnIgnoredErrorEvent = new RetryOnIgnoredErrorEvent("test",new IOException("Bla")); + Assertions.assertThat(retryOnIgnoredErrorEvent.getName()).isEqualTo("test"); + Assertions.assertThat(retryOnIgnoredErrorEvent.getNumberOfRetryAttempts()).isEqualTo(0); + Assertions.assertThat(retryOnIgnoredErrorEvent.getEventType()).isEqualTo(Type.IGNORED_ERROR); + Assertions.assertThat(retryOnIgnoredErrorEvent.getLastThrowable()).isInstanceOf(IOException.class); + Assertions.assertThat(retryOnIgnoredErrorEvent.toString()).contains("Retry 'test' recorded an error which has been ignored: 'java.io.IOException: Bla'."); } } diff --git a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/EventConsumerTest.java b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/EventConsumerTest.java index b1458c6a32..0378087bd7 100644 --- a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/EventConsumerTest.java +++ b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/EventConsumerTest.java @@ -32,6 +32,7 @@ import org.mockito.Mockito; import javax.xml.ws.WebServiceException; +import java.io.IOException; public class EventConsumerTest { @@ -42,7 +43,7 @@ public class EventConsumerTest { @Before public void setUp(){ helloWorldService = Mockito.mock(HelloWorldService.class); - RetryContext.sleepFunction = sleep -> sleptTime += sleep; + RetryImpl.sleepFunction = sleep -> sleptTime += sleep; } @Test @@ -51,12 +52,12 @@ public void shouldReturnAfterThreeAttempts() { BDDMockito.willThrow(new WebServiceException("BAM!")).given(helloWorldService).sayHelloWorld(); // Create a Retry with default configuration - RetryContext retryContext = (RetryContext) Retry.ofDefaults("id"); - TestSubscriber testSubscriber = retryContext.getEventStream() + Retry retry = Retry.ofDefaults("id"); + TestSubscriber testSubscriber = retry.getEventStream() .map(RetryEvent::getEventType) .test(); // Decorate the invocation of the HelloWorldService - CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retryContext, helloWorldService::sayHelloWorld); + CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retry, helloWorldService::sayHelloWorld); // When Try result = Try.run(retryableRunnable); @@ -69,7 +70,8 @@ public void shouldReturnAfterThreeAttempts() { Assertions.assertThat(result.failed().get()).isInstanceOf(WebServiceException.class); Assertions.assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION*2); - testSubscriber.assertValueCount(1).assertValues(RetryEvent.Type.ERROR); + testSubscriber.assertValueCount(1) + .assertValues(RetryEvent.Type.ERROR); } @Test @@ -78,12 +80,12 @@ public void shouldReturnAfterTwoAttempts() { BDDMockito.willThrow(new WebServiceException("BAM!")).willNothing().given(helloWorldService).sayHelloWorld(); // Create a Retry with default configuration - RetryContext retryContext = (RetryContext) Retry.ofDefaults("id"); - TestSubscriber testSubscriber = retryContext.getEventStream() + Retry retry = Retry.ofDefaults("id"); + TestSubscriber testSubscriber = retry.getEventStream() .map(RetryEvent::getEventType) .test(); // Decorate the invocation of the HelloWorldService - CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retryContext, helloWorldService::sayHelloWorld); + CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retry, helloWorldService::sayHelloWorld); // When Try result = Try.run(retryableRunnable); @@ -96,4 +98,32 @@ public void shouldReturnAfterTwoAttempts() { testSubscriber.assertValueCount(1).assertValues(RetryEvent.Type.SUCCESS); } + + @Test + public void shouldIgnoreError() { + // Given the HelloWorldService throws an exception + BDDMockito.willThrow(new WebServiceException("BAM!")).willNothing().given(helloWorldService).sayHelloWorld(); + + // Create a Retry with default configuration + RetryConfig config = RetryConfig.custom() + .retryOnException(t -> t instanceof IOException) + .maxAttempts(3).build(); + Retry retry = Retry.of("id", config); + TestSubscriber testSubscriber = retry.getEventStream() + .map(RetryEvent::getEventType) + .test(); + // Decorate the invocation of the HelloWorldService + CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retry, helloWorldService::sayHelloWorld); + + // When + Try result = Try.run(retryableRunnable); + + // Then the helloWorldService should be invoked 2 times + BDDMockito.then(helloWorldService).should(Mockito.times(1)).sayHelloWorld(); + // and the result should be a sucess + Assertions.assertThat(result.isFailure()).isTrue(); + Assertions.assertThat(sleptTime).isEqualTo(0); + + testSubscriber.assertValueCount(1).assertValues(RetryEvent.Type.IGNORED_ERROR); + } } diff --git a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/RunnableRetryTest.java b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/RunnableRetryTest.java index b6868705ec..46a139ac0c 100644 --- a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/RunnableRetryTest.java +++ b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/RunnableRetryTest.java @@ -44,7 +44,7 @@ public class RunnableRetryTest { @Before public void setUp(){ helloWorldService = Mockito.mock(HelloWorldService.class); - RetryContext.sleepFunction = sleep -> sleptTime += sleep; + RetryImpl.sleepFunction = sleep -> sleptTime += sleep; } @Test @@ -68,9 +68,9 @@ public void testDecorateRunnable() { BDDMockito.willThrow(new WebServiceException("BAM!")).given(helloWorldService).sayHelloWorld(); // Create a Retry with default configuration - RetryContext retryContext = (RetryContext) Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - Runnable runnable = Retry.decorateRunnable(retryContext, helloWorldService::sayHelloWorld); + Runnable runnable = Retry.decorateRunnable(retry, helloWorldService::sayHelloWorld); // When Try result = Try.run(runnable::run); @@ -102,9 +102,9 @@ public void shouldReturnAfterThreeAttempts() { BDDMockito.willThrow(new WebServiceException("BAM!")).given(helloWorldService).sayHelloWorld(); // Create a Retry with default configuration - RetryContext retryContext = (RetryContext) Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retryContext, helloWorldService::sayHelloWorld); + CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retry, helloWorldService::sayHelloWorld); // When Try result = Try.run(retryableRunnable); @@ -125,9 +125,9 @@ public void shouldReturnAfterOneAttempt() { // Create a Retry with default configuration RetryConfig config = RetryConfig.custom().maxAttempts(1).build(); - Retry retryContext = Retry.of("id", config); + Retry retry = Retry.of("id", config); // Decorate the invocation of the HelloWorldService - CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retryContext, helloWorldService::sayHelloWorld); + CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retry, helloWorldService::sayHelloWorld); // When Try result = Try.run(retryableRunnable); @@ -152,10 +152,10 @@ public void shouldReturnAfterOneAttemptAndIgnoreException() { Case($(Predicates.instanceOf(WebServiceException.class)), false), Case($(), true))) .build(); - Retry retryContext = Retry.of("id", config); + Retry retry = Retry.of("id", config); // Decorate the invocation of the HelloWorldService - CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retryContext, helloWorldService::sayHelloWorld); + CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retry, helloWorldService::sayHelloWorld); // When Try result = Try.run(retryableRunnable); @@ -180,9 +180,9 @@ public void shouldTakeIntoAccountBackoffFunction() { .intervalFunction(IntervalFunction.of(Duration.ofMillis(500), x -> x * x)) .build(); - Retry retryContext = Retry.of("id", config); + Retry retry = Retry.of("id", config); // Decorate the invocation of the HelloWorldService - CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retryContext, helloWorldService::sayHelloWorld); + CheckedRunnable retryableRunnable = Retry.decorateCheckedRunnable(retry, helloWorldService::sayHelloWorld); // When Try result = Try.run(retryableRunnable); diff --git a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/SupplierRetryTest.java b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/SupplierRetryTest.java index 34e4f105b0..54424bcc52 100644 --- a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/SupplierRetryTest.java +++ b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/internal/SupplierRetryTest.java @@ -26,7 +26,6 @@ import io.vavr.CheckedFunction0; import io.vavr.Predicates; import io.vavr.control.Try; -import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.mockito.BDDMockito; @@ -37,6 +36,7 @@ import java.util.function.Supplier; import static io.vavr.API.$; +import static org.assertj.core.api.Assertions.assertThat; public class SupplierRetryTest { @@ -46,7 +46,7 @@ public class SupplierRetryTest { @Before public void setUp(){ helloWorldService = Mockito.mock(HelloWorldService.class); - RetryContext.sleepFunction = sleep -> sleptTime += sleep; + RetryImpl.sleepFunction = sleep -> sleptTime += sleep; } @Test @@ -54,15 +54,16 @@ public void shouldNotRetry() { // Given the HelloWorldService returns Hello world BDDMockito.given(helloWorldService.returnHelloWorld()).willReturn("Hello world"); // Create a Retry with default configuration - Retry retryContext = Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - Supplier supplier = Retry.decorateSupplier(retryContext, helloWorldService::returnHelloWorld); + Supplier supplier = Retry.decorateSupplier(retry, helloWorldService::returnHelloWorld); // When String result = supplier.get(); // Then the helloWorldService should be invoked 1 time BDDMockito.then(helloWorldService).should(Mockito.times(1)).returnHelloWorld(); - Assertions.assertThat(sleptTime).isEqualTo(0); + assertThat(result).isEqualTo("Hello world"); + assertThat(sleptTime).isEqualTo(0); } @Test @@ -71,17 +72,43 @@ public void testDecorateSupplier() { BDDMockito.given(helloWorldService.returnHelloWorld()).willThrow(new WebServiceException("BAM!")).willReturn("Hello world"); // Create a Retry with default configuration - Retry retryContext = Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - Supplier supplier = Retry.decorateSupplier(retryContext, helloWorldService::returnHelloWorld); + Supplier supplier = Retry.decorateSupplier(retry, helloWorldService::returnHelloWorld); // When String result = supplier.get(); // Then the helloWorldService should be invoked 2 times BDDMockito.then(helloWorldService).should(Mockito.times(2)).returnHelloWorld(); - Assertions.assertThat(result).isEqualTo("Hello world"); - Assertions.assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); + assertThat(result).isEqualTo("Hello world"); + assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); + } + + @Test + public void testDecorateSupplierAndInvokeTwice() { + // Given the HelloWorldService throws an exception + BDDMockito.given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")) + .willReturn("Hello world") + .willThrow(new WebServiceException("BAM!")) + .willReturn("Hello world"); + + // Create a Retry with default configuration + Retry retry = Retry.ofDefaults("id"); + // Decorate the invocation of the HelloWorldService + Supplier supplier = Retry.decorateSupplier(retry, helloWorldService::returnHelloWorld); + + // When + String result = supplier.get(); + String result2 = supplier.get(); + + // Then the helloWorldService should be invoked 2 times + BDDMockito.then(helloWorldService).should(Mockito.times(4)).returnHelloWorld(); + assertThat(result).isEqualTo("Hello world"); + assertThat(result2).isEqualTo("Hello world"); + assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION*2); + assertThat(retry.getMetrics().getNumberOfSuccessfulCallsWithRetryAttempt()).isEqualTo(2); } @Test @@ -90,17 +117,17 @@ public void testDecorateCallable() throws Exception { BDDMockito.given(helloWorldService.returnHelloWorldWithException()).willThrow(new WebServiceException("BAM!")).willReturn("Hello world"); // Create a Retry with default configuration - Retry retryContext = Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - Callable callable = Retry.decorateCallable(retryContext, helloWorldService::returnHelloWorldWithException); + Callable callable = Retry.decorateCallable(retry, helloWorldService::returnHelloWorldWithException); // When String result = callable.call(); // Then the helloWorldService should be invoked 2 times BDDMockito.then(helloWorldService).should(Mockito.times(2)).returnHelloWorldWithException(); - Assertions.assertThat(result).isEqualTo("Hello world"); - Assertions.assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); + assertThat(result).isEqualTo("Hello world"); + assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); } @Test @@ -109,15 +136,15 @@ public void testExecuteCallable() throws Exception { BDDMockito.given(helloWorldService.returnHelloWorldWithException()).willThrow(new WebServiceException("BAM!")).willReturn("Hello world"); // Create a Retry with default configuration - Retry retryContext = Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - String result = retryContext.executeCallable(helloWorldService::returnHelloWorldWithException); + String result = retry.executeCallable(helloWorldService::returnHelloWorldWithException); // Then the helloWorldService should be invoked 2 times BDDMockito.then(helloWorldService).should(Mockito.times(2)).returnHelloWorldWithException(); - Assertions.assertThat(result).isEqualTo("Hello world"); - Assertions.assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); + assertThat(result).isEqualTo("Hello world"); + assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); } @@ -127,15 +154,15 @@ public void testExecuteSupplier() { BDDMockito.given(helloWorldService.returnHelloWorld()).willThrow(new WebServiceException("BAM!")).willReturn("Hello world"); // Create a Retry with default configuration - Retry retryContext = Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - String result = retryContext.executeSupplier(helloWorldService::returnHelloWorld); + String result = retry.executeSupplier(helloWorldService::returnHelloWorld); // Then the helloWorldService should be invoked 2 times BDDMockito.then(helloWorldService).should(Mockito.times(2)).returnHelloWorld(); - Assertions.assertThat(result).isEqualTo("Hello world"); - Assertions.assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); + assertThat(result).isEqualTo("Hello world"); + assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); } @Test @@ -144,17 +171,17 @@ public void shouldReturnSuccessfullyAfterSecondAttempt() { BDDMockito.given(helloWorldService.returnHelloWorld()).willThrow(new WebServiceException("BAM!")).willReturn("Hello world"); // Create a Retry with default configuration - Retry retryContext = Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retryContext, helloWorldService::returnHelloWorld); + CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retry, helloWorldService::returnHelloWorld); // When Try result = Try.of(retryableSupplier); // Then the helloWorldService should be invoked 2 times BDDMockito.then(helloWorldService).should(Mockito.times(2)).returnHelloWorld(); - Assertions.assertThat(result.get()).isEqualTo("Hello world"); - Assertions.assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); + assertThat(result.get()).isEqualTo("Hello world"); + assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION); } @Test @@ -163,9 +190,9 @@ public void shouldReturnAfterThreeAttempts() { BDDMockito.given(helloWorldService.returnHelloWorld()).willThrow(new WebServiceException("BAM!")); // Create a Retry with default configuration - Retry retryContext = Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retryContext, helloWorldService::returnHelloWorld); + CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retry, helloWorldService::returnHelloWorld); // When Try result = Try.of(retryableSupplier); @@ -173,10 +200,10 @@ public void shouldReturnAfterThreeAttempts() { // Then the helloWorldService should be invoked 3 times BDDMockito.then(helloWorldService).should(Mockito.times(3)).returnHelloWorld(); // and the result should be a failure - Assertions.assertThat(result.isFailure()).isTrue(); + assertThat(result.isFailure()).isTrue(); // and the returned exception should be of type RuntimeException - Assertions.assertThat(result.failed().get()).isInstanceOf(WebServiceException.class); - Assertions.assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION*2); + assertThat(result.failed().get()).isInstanceOf(WebServiceException.class); + assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION*2); } @Test @@ -186,9 +213,9 @@ public void shouldReturnAfterOneAttempt() { // Create a Retry with custom configuration RetryConfig config = RetryConfig.custom().maxAttempts(1).build(); - Retry retryContext = Retry.of("id", config); + Retry retry = Retry.of("id", config); // Decorate the invocation of the HelloWorldService - CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retryContext, helloWorldService::returnHelloWorld); + CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retry, helloWorldService::returnHelloWorld); // When Try result = Try.of(retryableSupplier); @@ -196,10 +223,10 @@ public void shouldReturnAfterOneAttempt() { // Then the helloWorldService should be invoked 1 time BDDMockito.then(helloWorldService).should(Mockito.times(1)).returnHelloWorld(); // and the result should be a failure - Assertions.assertThat(result.isFailure()).isTrue(); + assertThat(result.isFailure()).isTrue(); // and the returned exception should be of type RuntimeException - Assertions.assertThat(result.failed().get()).isInstanceOf(WebServiceException.class); - Assertions.assertThat(sleptTime).isEqualTo(0); + assertThat(result.failed().get()).isInstanceOf(WebServiceException.class); + assertThat(sleptTime).isEqualTo(0); } @Test @@ -213,9 +240,9 @@ public void shouldReturnAfterOneAttemptAndIgnoreException() { API.Case($(Predicates.instanceOf(WebServiceException.class)), false), API.Case($(), true))) .build(); - Retry retryContext = Retry.of("id", config); + Retry retry = Retry.of("id", config); // Decorate the invocation of the HelloWorldService - CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retryContext, helloWorldService::returnHelloWorld); + CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retry, helloWorldService::returnHelloWorld); // When Try result = Try.of(retryableSupplier); @@ -223,10 +250,10 @@ public void shouldReturnAfterOneAttemptAndIgnoreException() { // Then the helloWorldService should be invoked only once, because the exception should be rethrown immediately. BDDMockito.then(helloWorldService).should(Mockito.times(1)).returnHelloWorld(); // and the result should be a failure - Assertions.assertThat(result.isFailure()).isTrue(); + assertThat(result.isFailure()).isTrue(); // and the returned exception should be of type RuntimeException - Assertions.assertThat(result.failed().get()).isInstanceOf(WebServiceException.class); - Assertions.assertThat(sleptTime).isEqualTo(0); + assertThat(result.failed().get()).isInstanceOf(WebServiceException.class); + assertThat(sleptTime).isEqualTo(0); } @Test @@ -235,19 +262,20 @@ public void shouldReturnAfterThreeAttemptsAndRecover() { BDDMockito.given(helloWorldService.returnHelloWorld()).willThrow(new WebServiceException("BAM!")); // Create a Retry with default configuration - Retry retryContext = Retry.ofDefaults("id"); + Retry retry = Retry.ofDefaults("id"); // Decorate the invocation of the HelloWorldService - CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retryContext, helloWorldService::returnHelloWorld); + CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retry, helloWorldService::returnHelloWorld); // When Try result = Try.of(retryableSupplier).recover((throwable) -> "Hello world from recovery function"); + assertThat(retry.getMetrics().getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(1); // Then the helloWorldService should be invoked 3 times BDDMockito.then(helloWorldService).should(Mockito.times(3)).returnHelloWorld(); // and the returned exception should be of type RuntimeException - Assertions.assertThat(result.get()).isEqualTo("Hello world from recovery function"); - Assertions.assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION*2); + assertThat(result.get()).isEqualTo("Hello world from recovery function"); + assertThat(sleptTime).isEqualTo(RetryConfig.DEFAULT_WAIT_DURATION*2); } @Test @@ -257,16 +285,16 @@ public void shouldTakeIntoAccountBackoffFunction() { // Create a Retry with a backoff function doubling the interval RetryConfig config = RetryConfig.custom().intervalFunction(IntervalFunction.ofExponentialBackoff(500, 2.0)).build(); - Retry retryContext = Retry.of("id", config); + Retry retry = Retry.of("id", config); // Decorate the invocation of the HelloWorldService - CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retryContext, helloWorldService::returnHelloWorld); + CheckedFunction0 retryableSupplier = Retry.decorateCheckedSupplier(retry, helloWorldService::returnHelloWorld); // When Try result = Try.of(retryableSupplier); // Then the slept time should be according to the backoff function BDDMockito.then(helloWorldService).should(Mockito.times(3)).returnHelloWorld(); - Assertions.assertThat(sleptTime).isEqualTo( + assertThat(sleptTime).isEqualTo( RetryConfig.DEFAULT_WAIT_DURATION + RetryConfig.DEFAULT_WAIT_DURATION*2); } diff --git a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/transformer/RetryTransformerTest.java b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/transformer/RetryTransformerTest.java index 81a108fae7..cfd4ff8207 100644 --- a/resilience4j-retry/src/test/java/io/github/resilience4j/retry/transformer/RetryTransformerTest.java +++ b/resilience4j-retry/src/test/java/io/github/resilience4j/retry/transformer/RetryTransformerTest.java @@ -18,38 +18,66 @@ import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.RetryConfig; +import io.github.resilience4j.test.HelloWorldService; import io.reactivex.Flowable; import io.reactivex.Observable; import io.reactivex.Single; +import org.junit.Before; import org.junit.Test; +import org.mockito.BDDMockito; +import org.mockito.Mockito; +import javax.xml.ws.WebServiceException; import java.io.IOException; -import java.util.NoSuchElementException; -import java.util.concurrent.Callable; -import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; public class RetryTransformerTest { + private HelloWorldService helloWorldService; + + @Before + public void setUp(){ + helloWorldService = Mockito.mock(HelloWorldService.class); + } + @Test public void shouldReturnOnCompleteUsingSingle() { //Given RetryConfig config = RetryConfig.ofDefaults(); Retry retry = Retry.of("testName", config); + RetryTransformer retryTransformer = RetryTransformer.of(retry); - Single.just(1) - .compose(RetryTransformer.of(retry)) + given(helloWorldService.returnHelloWorld()) + .willReturn("Hello world") + .willThrow(new WebServiceException("BAM!")) + .willThrow(new WebServiceException("BAM!")) + .willReturn("Hello world"); + + //When + Single.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) .test() .assertValueCount(1) - .assertValues(1) + .assertValues("Hello world") + .assertComplete(); + + Single.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) + .test() + .assertValueCount(1) + .assertValues("Hello world") .assertComplete(); //Then + BDDMockito.then(helloWorldService).should(Mockito.times(4)).returnHelloWorld(); Retry.Metrics metrics = retry.getMetrics(); - assertThat(metrics.getNumAttempts()).isEqualTo(0); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); + assertThat(metrics.getNumberOfSuccessfulCallsWithoutRetryAttempt()).isEqualTo(1); + assertThat(metrics.getNumberOfSuccessfulCallsWithRetryAttempt()).isEqualTo(1); + assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(0); + assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(0); } @Test @@ -57,229 +85,177 @@ public void shouldReturnOnErrorUsingSingle() { //Given RetryConfig config = RetryConfig.ofDefaults(); Retry retry = Retry.of("testName", config); + RetryTransformer retryTransformer = RetryTransformer.of(retry); - Single.fromCallable(() -> {throw new IOException("BAM!");}) - .compose(RetryTransformer.of(retry)) + given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")); + + //When + Single.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) + .test() + .assertError(WebServiceException.class) + .assertNotComplete() + .assertSubscribed(); + + Single.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) .test() - .assertError(IOException.class) + .assertError(WebServiceException.class) .assertNotComplete() .assertSubscribed(); //Then + BDDMockito.then(helloWorldService).should(Mockito.times(6)).returnHelloWorld(); Retry.Metrics metrics = retry.getMetrics(); - assertThat(metrics.getNumAttempts()).isEqualTo(3); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); + assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(2); + assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(0); } @Test public void shouldNotRetryFromPredicateUsingSingle() { //Given - RetryConfig config = RetryConfig.custom().retryOnException(t -> t instanceof IOException).maxAttempts(3).build(); + RetryConfig config = RetryConfig.custom() + .retryOnException(t -> t instanceof IOException) + .maxAttempts(3).build(); Retry retry = Retry.of("testName", config); + given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")); - Single.fromCallable(() -> {throw new NoSuchElementException("BAM!");}) + //When + Single.fromCallable(helloWorldService::returnHelloWorld) .compose(RetryTransformer.of(retry)) .test() - .assertError(NoSuchElementException.class) + .assertError(WebServiceException.class) .assertNotComplete() .assertSubscribed(); //Then + BDDMockito.then(helloWorldService).should(Mockito.times(1)).returnHelloWorld(); Retry.Metrics metrics = retry.getMetrics(); - assertThat(metrics.getNumAttempts()).isEqualTo(0); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); + assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(1); + assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(0); } @Test - public void shouldReturnOnErrorAfterRetryFailureUsingSingle() { + public void shouldReturnOnCompleteUsingObservable() { //Given RetryConfig config = RetryConfig.ofDefaults(); Retry retry = Retry.of("testName", config); + RetryTransformer retryTransformer = RetryTransformer.of(retry); - for (int i = 0; i < 3; i++) { - try { - retry.onError(new IOException("BAM!")); - } catch (Throwable t) { - } - } + given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")); - Single.fromCallable(() -> {throw new IOException("BAM!");}) - .compose(RetryTransformer.of(retry)) + //When + Observable.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) .test() - .assertError(IOException.class) + .assertError(WebServiceException.class) .assertNotComplete() .assertSubscribed(); - //Then - Retry.Metrics metrics = retry.getMetrics(); - - assertThat(metrics.getNumAttempts()).isEqualTo(4); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); - } - - @Test - public void shouldReturnOnCompleteAfterRetryFailureUsingSingle() { - //Given - RetryConfig config = RetryConfig.ofDefaults(); - Retry retry = Retry.of("testName", config); - AtomicInteger count = new AtomicInteger(0); - - Callable c = () -> { - if (count.get() == 0) { - count.incrementAndGet(); - throw new IOException("BAM!"); - } else { - return count.get(); - } - }; - - Single.fromCallable(c) - .compose(RetryTransformer.of(retry)) + Observable.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) .test() - .assertValueCount(1) - .assertValues(1) - .assertComplete(); - + .assertError(WebServiceException.class) + .assertNotComplete() + .assertSubscribed(); //Then + BDDMockito.then(helloWorldService).should(Mockito.times(6)).returnHelloWorld(); Retry.Metrics metrics = retry.getMetrics(); - assertThat(metrics.getNumAttempts()).isEqualTo(1); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); + assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(2); + assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(0); } @Test - public void shouldReturnOnCompleteUsingObservable() { + public void shouldReturnOnErrorUsingObservable() { //Given RetryConfig config = RetryConfig.ofDefaults(); Retry retry = Retry.of("testName", config); + RetryTransformer retryTransformer = RetryTransformer.of(retry); - Observable.just(1) - .compose(RetryTransformer.of(retry)) - .test() - .assertValueCount(1) - .assertValues(1) - .assertComplete(); - - //Then - Retry.Metrics metrics = retry.getMetrics(); + given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")); - assertThat(metrics.getNumAttempts()).isEqualTo(0); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); - } - - @Test - public void shouldReturnOnErrorUsingObservable() { - //Given - RetryConfig config = RetryConfig.ofDefaults(); - Retry retry = Retry.of("testName", config); + //When + Observable.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) + .test() + .assertError(WebServiceException.class) + .assertNotComplete() + .assertSubscribed(); - Observable.fromCallable(() -> {throw new IOException("BAM!");}) - .compose(RetryTransformer.of(retry)) + Observable.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) .test() - .assertError(IOException.class) + .assertError(WebServiceException.class) .assertNotComplete() .assertSubscribed(); //Then + BDDMockito.then(helloWorldService).should(Mockito.times(6)).returnHelloWorld(); Retry.Metrics metrics = retry.getMetrics(); - assertThat(metrics.getNumAttempts()).isEqualTo(3); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); + assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(2); + assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(0); } @Test public void shouldNotRetryFromPredicateUsingObservable() { //Given - RetryConfig config = RetryConfig.custom().retryOnException(t -> t instanceof IOException).maxAttempts(3).build(); + RetryConfig config = RetryConfig.custom() + .retryOnException(t -> t instanceof IOException) + .maxAttempts(3).build(); Retry retry = Retry.of("testName", config); + given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")); - Observable.fromCallable(() -> {throw new NoSuchElementException("BAM!");}) + //When + Observable.fromCallable(helloWorldService::returnHelloWorld) .compose(RetryTransformer.of(retry)) .test() - .assertError(NoSuchElementException.class) + .assertError(WebServiceException.class) .assertNotComplete() .assertSubscribed(); //Then + BDDMockito.then(helloWorldService).should(Mockito.times(1)).returnHelloWorld(); Retry.Metrics metrics = retry.getMetrics(); - assertThat(metrics.getNumAttempts()).isEqualTo(0); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); + assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(1); + assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(0); } @Test - public void shouldReturnOnErrorAfterRetryFailureUsingObservable() { + public void shouldReturnOnCompleteUsingFlowable() { //Given RetryConfig config = RetryConfig.ofDefaults(); Retry retry = Retry.of("testName", config); + RetryTransformer retryTransformer = RetryTransformer.of(retry); - for (int i = 0; i < 3; i++) { - try { - retry.onError(new IOException("BAM!")); - } catch (Throwable t) { - } - } + given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")); - Observable.fromCallable(() -> {throw new IOException("BAM!");}) - .compose(RetryTransformer.of(retry)) + //When + Flowable.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) .test() - .assertError(IOException.class) + .assertError(WebServiceException.class) .assertNotComplete() .assertSubscribed(); - //Then - Retry.Metrics metrics = retry.getMetrics(); - - assertThat(metrics.getNumAttempts()).isEqualTo(4); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); - } - - @Test - public void shouldReturnOnCompleteAfterRetryFailureUsingObservable() { - //Given - RetryConfig config = RetryConfig.ofDefaults(); - Retry retry = Retry.of("testName", config); - AtomicInteger count = new AtomicInteger(0); - - Callable c = () -> { - if (count.get() == 0) { - count.incrementAndGet(); - throw new IOException("BAM!"); - } else { - return count.get(); - } - }; - - Observable.fromCallable(c) - .compose(RetryTransformer.of(retry)) + Flowable.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) .test() - .assertValueCount(1) - .assertValues(1) - .assertComplete(); - - //Then - Retry.Metrics metrics = retry.getMetrics(); - - assertThat(metrics.getNumAttempts()).isEqualTo(1); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); - } - - @Test - public void shouldReturnOnCompleteUsingFlowable() { - //Given - RetryConfig config = RetryConfig.ofDefaults(); - Retry retry = Retry.of("testName", config); - - Flowable.just(1) - .compose(RetryTransformer.of(retry)) - .test() - .assertValueCount(1) - .assertValues(1) - .assertComplete(); - + .assertError(WebServiceException.class) + .assertNotComplete() + .assertSubscribed(); //Then + BDDMockito.then(helloWorldService).should(Mockito.times(6)).returnHelloWorld(); Retry.Metrics metrics = retry.getMetrics(); - assertThat(metrics.getNumAttempts()).isEqualTo(0); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); + assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(2); + assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(0); } @Test @@ -287,95 +263,56 @@ public void shouldReturnOnErrorUsingFlowable() { //Given RetryConfig config = RetryConfig.ofDefaults(); Retry retry = Retry.of("testName", config); + RetryTransformer retryTransformer = RetryTransformer.of(retry); - Flowable.fromCallable(() -> {throw new IOException("BAM!");}) - .compose(RetryTransformer.of(retry)) + given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")); + + //When + Flowable.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) .test() - .assertError(IOException.class) + .assertError(WebServiceException.class) .assertNotComplete() .assertSubscribed(); - //Then - Retry.Metrics metrics = retry.getMetrics(); - - assertThat(metrics.getNumAttempts()).isEqualTo(3); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); - } - @Test - public void shouldNotRetryFromPredicateUsingFlowable() { - //Given - RetryConfig config = RetryConfig.custom().retryOnException(t -> t instanceof IOException).maxAttempts(3).build(); - Retry retry = Retry.of("testName", config); - - Flowable.fromCallable(() -> {throw new NoSuchElementException("BAM!");}) - .compose(RetryTransformer.of(retry)) + Flowable.fromCallable(helloWorldService::returnHelloWorld) + .compose(retryTransformer) .test() - .assertError(NoSuchElementException.class) + .assertError(WebServiceException.class) .assertNotComplete() .assertSubscribed(); //Then + BDDMockito.then(helloWorldService).should(Mockito.times(6)).returnHelloWorld(); Retry.Metrics metrics = retry.getMetrics(); - assertThat(metrics.getNumAttempts()).isEqualTo(0); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); + assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(2); + assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(0); } @Test - public void shouldReturnOnErrorAfterRetryFailureUsingFlowable() { + public void shouldNotRetryFromPredicateUsingFlowable() { //Given - RetryConfig config = RetryConfig.ofDefaults(); + RetryConfig config = RetryConfig.custom() + .retryOnException(t -> t instanceof IOException) + .maxAttempts(3).build(); Retry retry = Retry.of("testName", config); + given(helloWorldService.returnHelloWorld()) + .willThrow(new WebServiceException("BAM!")); - for (int i = 0; i < 3; i++) { - try { - retry.onError(new IOException("BAM!")); - } catch (Throwable t) { - } - } - - Flowable.fromCallable(() -> {throw new IOException("BAM!");}) + //When + Flowable.fromCallable(helloWorldService::returnHelloWorld) .compose(RetryTransformer.of(retry)) .test() - .assertError(IOException.class) + .assertError(WebServiceException.class) .assertNotComplete() .assertSubscribed(); - //Then + BDDMockito.then(helloWorldService).should(Mockito.times(1)).returnHelloWorld(); Retry.Metrics metrics = retry.getMetrics(); - assertThat(metrics.getNumAttempts()).isEqualTo(4); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); + assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(1); + assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(0); } - @Test - public void shouldReturnOnCompleteAfterRetryFailureUsingFlowable() { - //Given - RetryConfig config = RetryConfig.ofDefaults(); - Retry retry = Retry.of("testName", config); - AtomicInteger count = new AtomicInteger(0); - - Callable c = () -> { - if (count.get() == 0) { - count.incrementAndGet(); - throw new IOException("BAM!"); - } else { - return count.get(); - } - }; - - Flowable.fromCallable(c) - .compose(RetryTransformer.of(retry)) - .test() - .assertValueCount(1) - .assertValues(1) - .assertComplete(); - - //Then - Retry.Metrics metrics = retry.getMetrics(); - - assertThat(metrics.getNumAttempts()).isEqualTo(1); - assertThat(metrics.getMaxAttempts()).isEqualTo(config.getMaxAttempts()); - } - - }