From fec09f41a9793b7b59fdbbc0757a8da61eb05e27 Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 08:16:51 +0100 Subject: [PATCH 01/10] MIgrated cpilsworth/retrofit-circuitbreaker to the resilience4j project --- libraries.gradle | 6 + resilience4j-retrofit/build.gradle | 6 + .../CircuitBreakerCallAdapterFactory.java | 62 ++++++++++ .../retrofit/RetrofitCircuitBreaker.java | 88 ++++++++++++++ .../retrofit/RetrofitCircuitBreakerTest.java | 107 ++++++++++++++++++ .../retrofit/RetrofitService.java | 11 ++ settings.gradle | 1 + 7 files changed, 281 insertions(+) create mode 100644 resilience4j-retrofit/build.gradle create mode 100644 resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java create mode 100644 resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java create mode 100644 resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java create mode 100644 resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitService.java diff --git a/libraries.gradle b/libraries.gradle index f3ff4ba771..e706380dbe 100644 --- a/libraries.gradle +++ b/libraries.gradle @@ -14,6 +14,7 @@ ext { metricsVersion = '3.1.2' vertxVersion = '3.4.1' springBootVersion = '1.4.3.RELEASE' + retrofitVersion = '2.1.0' libraries = [ // compile @@ -41,6 +42,11 @@ ext { spring_boot_web: "org.springframework.boot:spring-boot-starter-web:${springBootVersion}", spring_boot_test: "org.springframework.boot:spring-boot-starter-test:${springBootVersion}", + // retrofit addon + retrofit: "com.squareup.retrofit2:retrofit:${retrofitVersion}", + retrofit_test: "com.squareup.retrofit2:converter-scalars:${retrofitVersion}", + retrofit_wiremock: "com.github.tomakehurst:wiremock:1.58", + // circuitbreaker documentation metrics: "io.dropwizard.metrics:metrics-core:${metricsVersion}", metrics_healthcheck: "io.dropwizard.metrics:metrics-healthchecks:${metricsVersion}" diff --git a/resilience4j-retrofit/build.gradle b/resilience4j-retrofit/build.gradle new file mode 100644 index 0000000000..082b7fcce7 --- /dev/null +++ b/resilience4j-retrofit/build.gradle @@ -0,0 +1,6 @@ +dependencies { + compile ( libraries.retrofit ) + compile project(':resilience4j-circuitbreaker') + testCompile ( libraries.retrofit_test ) + testCompile ( libraries.retrofit_wiremock ) +} \ No newline at end of file diff --git a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java new file mode 100644 index 0000000000..a13058a29d --- /dev/null +++ b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java @@ -0,0 +1,62 @@ +package io.github.resilience4j.retrofit; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import retrofit2.Call; +import retrofit2.CallAdapter; +import retrofit2.Response; +import retrofit2.Retrofit; + +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.function.Predicate; + +/** + * Creates {@link CallAdapter} that decorates a {@link Call} to provide integration with a + * Javaslang {@link CircuitBreaker} using {@link RetrofitCircuitBreaker} + */ +public final class CircuitBreakerCallAdapterFactory extends CallAdapter.Factory { + + private final CircuitBreaker circuitBreaker; + private final Predicate successResponse; + private final Predicate PREDICATE_IS_SUCCESSFUL = Response::isSuccessful; + + public CircuitBreakerCallAdapterFactory(final CircuitBreaker circuitBreaker) { + this.circuitBreaker = circuitBreaker; + this.successResponse = PREDICATE_IS_SUCCESSFUL; + } + + public CircuitBreakerCallAdapterFactory(final CircuitBreaker circuitBreaker, Predicate successResponse) { + this.circuitBreaker = circuitBreaker; + this.successResponse = successResponse; + } + + @Override + public CallAdapter get(Type returnType, Annotation[] annotations, Retrofit retrofit) { + if (getRawType(returnType) != Call.class) { + return null; + } + + final Type responseType = getCallResponseType(returnType); + return new CallAdapter>() { + @Override + public Type responseType() { + return responseType; + } + + @Override + public Call adapt(Call call) { + return RetrofitCircuitBreaker.decorateCall(circuitBreaker, call, successResponse); + } + }; + } + + static Type getCallResponseType(Type returnType) { + if (!(returnType instanceof ParameterizedType)) { + throw new IllegalArgumentException( + "Call return type must be parameterized as Call or Call"); + } + return getParameterUpperBound(0, (ParameterizedType) returnType); + } + +} \ No newline at end of file diff --git a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java new file mode 100644 index 0000000000..6b061a1a9a --- /dev/null +++ b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java @@ -0,0 +1,88 @@ +package io.github.resilience4j.retrofit; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.utils.CircuitBreakerUtils; +import io.github.resilience4j.metrics.StopWatch; +import okhttp3.Request; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import java.io.IOException; +import java.util.function.Predicate; + +/** + * Decorates a Retrofit {@link Call} to inform a Javaslang {@link CircuitBreaker} when an exception is thrown. + * All exceptions are marked as errors or responses not matching the supplied predicate. For example: + *

+ * + * RetrofitCircuitBreaker.decorateCall(circuitBreaker, call, (r) -> r.isSuccessful()); + * + */ +public interface RetrofitCircuitBreaker { + + /** + * Decorate {@link Call}s allow {@link CircuitBreaker} functionality. + * + * @param circuitBreaker {@link CircuitBreaker} to apply + * @param call Call to decorate + * @param responseSuccess determines whether the response should be considered an expected response + * @param Response type of call + * @return Original Call decorated with CircuitBreaker + */ + static Call decorateCall(final CircuitBreaker circuitBreaker, final Call call, final Predicate responseSuccess) { + return new Call() { + @Override + public Response execute() throws IOException { + CircuitBreakerUtils.isCallPermitted(circuitBreaker); + final StopWatch stopWatch = StopWatch.start(circuitBreaker.getName()); + try { + final Response response = call.execute(); + + if (responseSuccess.test(response)) { + circuitBreaker.onSuccess(stopWatch.stop().getProcessingDuration()); + } else { + final Throwable throwable = new Throwable("Response error: HTTP " + response.code() + " - " + response.message()); + circuitBreaker.onError(stopWatch.stop().getProcessingDuration(), throwable); + } + + return response; + } catch (Throwable throwable) { + circuitBreaker.onError(stopWatch.stop().getProcessingDuration(), throwable); + throw throwable; + } + } + + @Override + public void enqueue(Callback callback) { + call.enqueue(callback); + } + + @Override + public boolean isExecuted() { + return call.isExecuted(); + } + + @Override + public void cancel() { + call.cancel(); + } + + @Override + public boolean isCanceled() { + return call.isCanceled(); + } + + @Override + public Call clone() { + return decorateCall(circuitBreaker, call.clone(), responseSuccess); + } + + @Override + public Request request() { + return call.request(); + } + }; + } + +} \ No newline at end of file diff --git a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java new file mode 100644 index 0000000000..c81f3c3ade --- /dev/null +++ b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java @@ -0,0 +1,107 @@ +package io.github.resilience4j.retrofit; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import okhttp3.OkHttpClient; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import retrofit2.Retrofit; +import retrofit2.converter.scalars.ScalarsConverterFactory; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.junit.Assert.assertEquals; + +/** + * Tests the integration of the Retrofit HTTP client and JavaSlang-circuitbreaker. + * Validates that connection timeouts will trip circuit breaking + */ +public class RetrofitCircuitBreakerTest { + + @Rule + public WireMockRule wireMockRule = new WireMockRule(); + + private RetrofitService service; + private final CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() + .ringBufferSizeInClosedState(3) + .waitDurationInOpenState(Duration.ofMillis(1000)) + .build(); + private final CircuitBreaker circuitBreaker = CircuitBreaker.of("test", circuitBreakerConfig); + + + @Before + public void setUp() { + final long TIMEOUT = 300; // ms + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS) + .readTimeout(TIMEOUT, TimeUnit.MILLISECONDS) + .writeTimeout(TIMEOUT, TimeUnit.MILLISECONDS) + .build(); + + this.service = new Retrofit.Builder() + .addCallAdapterFactory(new CircuitBreakerCallAdapterFactory(circuitBreaker)) + .addConverterFactory(ScalarsConverterFactory.create()) + .baseUrl("http://localhost:8080/") + .client(client) + .build() + .create(RetrofitService.class); + } + + @Test + public void decorateSuccessfulCall() throws Exception { + stubFor(get(urlPathEqualTo("/greeting")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/plain") + .withBody("hello world"))); + + service.greeting().execute(); + + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + @Test + public void decorateTimingOutCall() throws Exception { + stubFor(get(urlPathEqualTo("/greeting")) + .willReturn(aResponse() + .withFixedDelay(500) + .withStatus(200) + .withHeader("Content-Type", "text/plain") + .withBody("hello world"))); + + try { + service.greeting().execute(); + } catch (Throwable t) { + } + + final CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics(); + assertEquals(1, metrics.getNumberOfFailedCalls()); + + // Circuit breaker should still be closed, not hit open threshold + assertEquals(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); + + try { + service.greeting().execute(); + } catch (Throwable t) { + } + + try { + service.greeting().execute(); + } catch (Throwable t) { + } + + assertEquals(3, metrics.getNumberOfFailedCalls()); + // Circuit breaker should be OPEN, threshold met + assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); + } + +} \ No newline at end of file diff --git a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitService.java b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitService.java new file mode 100644 index 0000000000..5c88e1fb43 --- /dev/null +++ b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitService.java @@ -0,0 +1,11 @@ +package io.github.resilience4j.retrofit; + +import retrofit2.Call; +import retrofit2.http.GET; + +public interface RetrofitService { + + @GET("greeting") + Call greeting(); + +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 62d4039b54..0a57c8f21f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,4 +11,5 @@ include 'resilience4j-consumer' include 'resilience4j-test' include 'resilience4j-vertx' include 'resilience4j-spring-boot' +include 'resilience4j-retrofit' From df797b0ab2543735f87a147082a77364264d42b0 Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 09:38:59 +0100 Subject: [PATCH 02/10] Changed the style of the example to avoid javadoc errors --- .../io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java index 6b061a1a9a..209de5d2e3 100644 --- a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java +++ b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java @@ -16,7 +16,7 @@ * All exceptions are marked as errors or responses not matching the supplied predicate. For example: *

* - * RetrofitCircuitBreaker.decorateCall(circuitBreaker, call, (r) -> r.isSuccessful()); + * RetrofitCircuitBreaker.decorateCall(circuitBreaker, call, Response::isSuccessful); * */ public interface RetrofitCircuitBreaker { From 3179224d4b11b48f7d3072bea23c20948425aa85 Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 10:25:54 +0100 Subject: [PATCH 03/10] Test that decorated calls are passed through to underlying object and improve test coverage --- .../retrofit/RetrofitCircuitBreakerTest.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java index c81f3c3ade..0b026234ab 100644 --- a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java +++ b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java @@ -7,6 +7,9 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.Mockito; +import retrofit2.Call; +import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.scalars.ScalarsConverterFactory; @@ -20,6 +23,8 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; /** * Tests the integration of the Retrofit HTTP client and JavaSlang-circuitbreaker. @@ -104,4 +109,31 @@ public void decorateTimingOutCall() throws Exception { assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); } + @Test + public void passThroughCallsToDecoratedObject() { + final Call call = mock(StringCall.class); + final Call decorated = RetrofitCircuitBreaker.decorateCall(circuitBreaker, call, Response::isSuccessful); + + decorated.cancel(); + Mockito.verify(call).cancel(); + + decorated.enqueue(null); + Mockito.verify(call).enqueue(any()); + + decorated.isExecuted(); + Mockito.verify(call).isExecuted(); + + decorated.isCanceled(); + Mockito.verify(call).isCanceled(); + + decorated.clone(); + Mockito.verify(call).clone(); + + decorated.request(); + Mockito.verify(call).request(); + } + + private interface StringCall extends Call { + } + } \ No newline at end of file From a6241795e34e837acb75e89f0349bec84fca08af Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 10:26:43 +0100 Subject: [PATCH 04/10] Simplify constructors and improve test coverage --- .../retrofit/CircuitBreakerCallAdapterFactory.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java index a13058a29d..343889373c 100644 --- a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java +++ b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java @@ -19,11 +19,9 @@ public final class CircuitBreakerCallAdapterFactory extends CallAdapter.Factory private final CircuitBreaker circuitBreaker; private final Predicate successResponse; - private final Predicate PREDICATE_IS_SUCCESSFUL = Response::isSuccessful; public CircuitBreakerCallAdapterFactory(final CircuitBreaker circuitBreaker) { - this.circuitBreaker = circuitBreaker; - this.successResponse = PREDICATE_IS_SUCCESSFUL; + this(circuitBreaker, Response::isSuccessful); } public CircuitBreakerCallAdapterFactory(final CircuitBreaker circuitBreaker, Predicate successResponse) { From 746a45863238ded7eba9c971d6684a3f7aa20d54 Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 10:33:12 +0100 Subject: [PATCH 05/10] Moved the readme into the project converted to asciidoc --- resilience4j-retrofit/README.adoc | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 resilience4j-retrofit/README.adoc diff --git a/resilience4j-retrofit/README.adoc b/resilience4j-retrofit/README.adoc new file mode 100644 index 0000000000..f0e2a665d5 --- /dev/null +++ b/resilience4j-retrofit/README.adoc @@ -0,0 +1,39 @@ += Retrofit Circuitbreaker + +https://square.github.io/retrofit/[Retrofit] client circuit breaking +using +https://github.com/robwin/javaslang-circuitbreaker[Javaslang-circuitbreaker] + +[source,java] +---- +// Create a CircuitBreaker +private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName"); + +// Create a retrofit instance with CircuitBreaker call adapter +Retrofit retrofit = new Retrofit.Builder() + .addCallAdapterFactory(new CircuitBreakerCallAdapterFactory(circuitBreaker)) + .baseUrl("http://localhost:8080/") + .build(); + +// Get an instance of your service with circuit breaking built in. +RetrofitService service = retrofit.create(RetrofitService.class); +---- + +By default, all exceptions and responses where +`!Response.isSuccessful()` will be recorded as an error in the +CircuitBreaker. + +Customising what is considered a _successful_ response is possible like +so: + +[source,java] +---- +Retrofit retrofit = new Retrofit.Builder() + .addCallAdapterFactory(new CircuitBreakerCallAdapterFactory(circuitBreaker, (r) -> r.code() < 500)); + .baseUrl("http://localhost:8080/") + .build(); +---- + +For circuit breaking triggered by timeout the thresholds can be set on +the OkHttpClient + From 2321f5f905d84e938b238dbae382f2a3e3b87151 Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 10:59:49 +0100 Subject: [PATCH 06/10] Updated readme title and remove old project link --- resilience4j-retrofit/README.adoc | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/resilience4j-retrofit/README.adoc b/resilience4j-retrofit/README.adoc index f0e2a665d5..13c197e3f7 100644 --- a/resilience4j-retrofit/README.adoc +++ b/resilience4j-retrofit/README.adoc @@ -1,8 +1,6 @@ -= Retrofit Circuitbreaker += resilience4j-retrofit https://square.github.io/retrofit/[Retrofit] client circuit breaking -using -https://github.com/robwin/javaslang-circuitbreaker[Javaslang-circuitbreaker] [source,java] ---- @@ -34,6 +32,5 @@ Retrofit retrofit = new Retrofit.Builder() .build(); ---- -For circuit breaking triggered by timeout the thresholds can be set on -the OkHttpClient +For circuit breaking triggered by timeout the thresholds can be set on the OkHttpClient From 7487f515f666efff3f5598becdbb31eb2d426e47 Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 11:50:29 +0100 Subject: [PATCH 07/10] Converted to use factory methods over constructors for creation * Updated javadoc * Updated class name * moved docs to documentation project (not currently included in index) --- .../src/docs/asciidoc/retrofit.adoc | 10 +++--- ...ry.java => CircuitBreakerCallAdapter.java} | 31 +++++++++++++++---- .../retrofit/RetrofitCircuitBreakerTest.java | 12 +++---- 3 files changed, 34 insertions(+), 19 deletions(-) rename resilience4j-retrofit/README.adoc => resilience4j-documentation/src/docs/asciidoc/retrofit.adoc (75%) rename resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/{CircuitBreakerCallAdapterFactory.java => CircuitBreakerCallAdapter.java} (51%) diff --git a/resilience4j-retrofit/README.adoc b/resilience4j-documentation/src/docs/asciidoc/retrofit.adoc similarity index 75% rename from resilience4j-retrofit/README.adoc rename to resilience4j-documentation/src/docs/asciidoc/retrofit.adoc index 13c197e3f7..f1d3812eca 100644 --- a/resilience4j-retrofit/README.adoc +++ b/resilience4j-documentation/src/docs/asciidoc/retrofit.adoc @@ -9,7 +9,7 @@ private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testNam // Create a retrofit instance with CircuitBreaker call adapter Retrofit retrofit = new Retrofit.Builder() - .addCallAdapterFactory(new CircuitBreakerCallAdapterFactory(circuitBreaker)) + .addCallAdapterFactory(CircuitBreakerCallAdapter.of(circuitBreaker)) .baseUrl("http://localhost:8080/") .build(); @@ -18,16 +18,14 @@ RetrofitService service = retrofit.create(RetrofitService.class); ---- By default, all exceptions and responses where -`!Response.isSuccessful()` will be recorded as an error in the -CircuitBreaker. +`!Response.isSuccessful()` will be recorded as an error in the CircuitBreaker. -Customising what is considered a _successful_ response is possible like -so: +Customising what is considered a _successful_ response is possible like so: [source,java] ---- Retrofit retrofit = new Retrofit.Builder() - .addCallAdapterFactory(new CircuitBreakerCallAdapterFactory(circuitBreaker, (r) -> r.code() < 500)); + .addCallAdapterFactory(CircuitBreakerCallAdapter.of(circuitBreaker, (r) -> r.code() < 500)); .baseUrl("http://localhost:8080/") .build(); ---- diff --git a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java similarity index 51% rename from resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java rename to resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java index 343889373c..cbe109dd60 100644 --- a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapterFactory.java +++ b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java @@ -12,19 +12,38 @@ import java.util.function.Predicate; /** - * Creates {@link CallAdapter} that decorates a {@link Call} to provide integration with a - * Javaslang {@link CircuitBreaker} using {@link RetrofitCircuitBreaker} + * Creates a Retrofit {@link CallAdapter.Factory} that decorates a Call to provide integration with a + * {@link CircuitBreaker} using {@link RetrofitCircuitBreaker} */ -public final class CircuitBreakerCallAdapterFactory extends CallAdapter.Factory { +public final class CircuitBreakerCallAdapter extends CallAdapter.Factory { private final CircuitBreaker circuitBreaker; private final Predicate successResponse; - public CircuitBreakerCallAdapterFactory(final CircuitBreaker circuitBreaker) { + /** + * Create a circuit-breaking call adapter that decorates retrofit calls + * @param circuitBreaker circuit breaker to use + * @return a {@link CallAdapter.Factory} that can be passed into the {@link Retrofit.Builder} + */ + public static CircuitBreakerCallAdapter of(final CircuitBreaker circuitBreaker) { + return new CircuitBreakerCallAdapter(circuitBreaker); + } + + /** + * Create a circuit-breaking call adapter that decorates retrofit calls + * @param circuitBreaker circuit breaker to use + * @param successResponse {@link Predicate} that determines whether the {@link Call} {@link Response} should be considered successful + * @return a {@link CallAdapter.Factory} that can be passed into the {@link Retrofit.Builder} + */ + public static CircuitBreakerCallAdapter of(final CircuitBreaker circuitBreaker, final Predicate successResponse) { + return new CircuitBreakerCallAdapter(circuitBreaker, successResponse); + } + + private CircuitBreakerCallAdapter(final CircuitBreaker circuitBreaker) { this(circuitBreaker, Response::isSuccessful); } - public CircuitBreakerCallAdapterFactory(final CircuitBreaker circuitBreaker, Predicate successResponse) { + private CircuitBreakerCallAdapter(final CircuitBreaker circuitBreaker, final Predicate successResponse) { this.circuitBreaker = circuitBreaker; this.successResponse = successResponse; } @@ -49,7 +68,7 @@ public Call adapt(Call call) { }; } - static Type getCallResponseType(Type returnType) { + private static Type getCallResponseType(Type returnType) { if (!(returnType instanceof ParameterizedType)) { throw new IllegalArgumentException( "Call return type must be parameterized as Call or Call"); diff --git a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java index 0b026234ab..b4cb603fc1 100644 --- a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java +++ b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java @@ -27,8 +27,7 @@ import static org.mockito.Mockito.mock; /** - * Tests the integration of the Retrofit HTTP client and JavaSlang-circuitbreaker. - * Validates that connection timeouts will trip circuit breaking + * Tests the integration of the Retrofit HTTP client and {@link CircuitBreaker} */ public class RetrofitCircuitBreakerTest { @@ -42,7 +41,6 @@ public class RetrofitCircuitBreakerTest { .build(); private final CircuitBreaker circuitBreaker = CircuitBreaker.of("test", circuitBreakerConfig); - @Before public void setUp() { final long TIMEOUT = 300; // ms @@ -53,7 +51,7 @@ public void setUp() { .build(); this.service = new Retrofit.Builder() - .addCallAdapterFactory(new CircuitBreakerCallAdapterFactory(circuitBreaker)) + .addCallAdapterFactory(CircuitBreakerCallAdapter.of(circuitBreaker)) .addConverterFactory(ScalarsConverterFactory.create()) .baseUrl("http://localhost:8080/") .client(client) @@ -85,7 +83,7 @@ public void decorateTimingOutCall() throws Exception { try { service.greeting().execute(); - } catch (Throwable t) { + } catch (Throwable ignored) { } final CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics(); @@ -96,12 +94,12 @@ public void decorateTimingOutCall() throws Exception { try { service.greeting().execute(); - } catch (Throwable t) { + } catch (Throwable ignored) { } try { service.greeting().execute(); - } catch (Throwable t) { + } catch (Throwable ignored) { } assertEquals(3, metrics.getNumberOfFailedCalls()); From 524dd509b6b7b2a6099d377de90a755c5c94a21a Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 11:56:58 +0100 Subject: [PATCH 08/10] Removed some duplication in constructor/factory method --- .../resilience4j/retrofit/CircuitBreakerCallAdapter.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java index cbe109dd60..405ae80a08 100644 --- a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java +++ b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java @@ -26,7 +26,7 @@ public final class CircuitBreakerCallAdapter extends CallAdapter.Factory { * @return a {@link CallAdapter.Factory} that can be passed into the {@link Retrofit.Builder} */ public static CircuitBreakerCallAdapter of(final CircuitBreaker circuitBreaker) { - return new CircuitBreakerCallAdapter(circuitBreaker); + return of(circuitBreaker, Response::isSuccessful); } /** @@ -39,10 +39,6 @@ public static CircuitBreakerCallAdapter of(final CircuitBreaker circuitBreaker, return new CircuitBreakerCallAdapter(circuitBreaker, successResponse); } - private CircuitBreakerCallAdapter(final CircuitBreaker circuitBreaker) { - this(circuitBreaker, Response::isSuccessful); - } - private CircuitBreakerCallAdapter(final CircuitBreaker circuitBreaker, final Predicate successResponse) { this.circuitBreaker = circuitBreaker; this.successResponse = successResponse; From a73104992a20ecdb04e0cfeef05e887afaa3f2bc Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 12:17:42 +0100 Subject: [PATCH 09/10] Recreated the README for the module as LICENSE included, improved description of module --- .../src/docs/asciidoc/retrofit.adoc | 14 +++--- resilience4j-retrofit/README.adoc | 44 +++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 resilience4j-retrofit/README.adoc diff --git a/resilience4j-documentation/src/docs/asciidoc/retrofit.adoc b/resilience4j-documentation/src/docs/asciidoc/retrofit.adoc index f1d3812eca..a7b4d60fbc 100644 --- a/resilience4j-documentation/src/docs/asciidoc/retrofit.adoc +++ b/resilience4j-documentation/src/docs/asciidoc/retrofit.adoc @@ -1,6 +1,10 @@ = resilience4j-retrofit -https://square.github.io/retrofit/[Retrofit] client circuit breaking +https://square.github.io/retrofit/[Retrofit] client circuit breaking. Short-circuits http client calls based upon the policy +associated to the CircuitBreaker instance provided. + +For circuit breaking triggered by timeout the thresholds can be set +on a OkHttpClient which can be set on the Retrofit.Builder. [source,java] ---- @@ -17,8 +21,7 @@ Retrofit retrofit = new Retrofit.Builder() RetrofitService service = retrofit.create(RetrofitService.class); ---- -By default, all exceptions and responses where -`!Response.isSuccessful()` will be recorded as an error in the CircuitBreaker. +By default, all exceptions and responses where `!Response.isSuccessful()` will be recorded as an error in the CircuitBreaker. Customising what is considered a _successful_ response is possible like so: @@ -28,7 +31,4 @@ Retrofit retrofit = new Retrofit.Builder() .addCallAdapterFactory(CircuitBreakerCallAdapter.of(circuitBreaker, (r) -> r.code() < 500)); .baseUrl("http://localhost:8080/") .build(); ----- - -For circuit breaking triggered by timeout the thresholds can be set on the OkHttpClient - +---- \ No newline at end of file diff --git a/resilience4j-retrofit/README.adoc b/resilience4j-retrofit/README.adoc new file mode 100644 index 0000000000..f2ce755df0 --- /dev/null +++ b/resilience4j-retrofit/README.adoc @@ -0,0 +1,44 @@ += resilience4j-retrofit + +https://square.github.io/retrofit/[Retrofit] client circuit breaking. Short-circuits http client calls based upon the policy +associated to the CircuitBreaker instance provided. + +For circuit breaking triggered by timeout the thresholds can be set +on a OkHttpClient which can be set on the Retrofit.Builder. + +[source,java] +---- +// Create a CircuitBreaker +private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName"); + +// Create a retrofit instance with CircuitBreaker call adapter +Retrofit retrofit = new Retrofit.Builder() + .addCallAdapterFactory(CircuitBreakerCallAdapter.of(circuitBreaker)) + .baseUrl("http://localhost:8080/") + .build(); + +// Get an instance of your service with circuit breaking built in. +RetrofitService service = retrofit.create(RetrofitService.class); +---- + +By default, all exceptions and responses where `!Response.isSuccessful()` will be recorded as an error in the CircuitBreaker. + +Customising what is considered a _successful_ response is possible like so: + +[source,java] +---- +Retrofit retrofit = new Retrofit.Builder() + .addCallAdapterFactory(CircuitBreakerCallAdapter.of(circuitBreaker, (r) -> r.code() < 500)); + .baseUrl("http://localhost:8080/") + .build(); +---- + +== License + +Copyright 2017 Christopher Pilsworth + +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. \ No newline at end of file From 3831d19276b933b5bf508adb30a87b7178107d25 Mon Sep 17 00:00:00 2001 From: Chris Pilsworth Date: Wed, 29 Mar 2017 12:21:36 +0100 Subject: [PATCH 10/10] Updated Java source files with license header --- .../retrofit/CircuitBreakerCallAdapter.java | 18 ++++++++++++++++++ .../retrofit/RetrofitCircuitBreaker.java | 18 ++++++++++++++++++ .../retrofit/RetrofitCircuitBreakerTest.java | 18 ++++++++++++++++++ .../resilience4j/retrofit/RetrofitService.java | 18 ++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java index 405ae80a08..b0f3f12de4 100644 --- a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java +++ b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/CircuitBreakerCallAdapter.java @@ -1,3 +1,21 @@ +/* + * + * Copyright 2017 Christopher Pilsworth + * + * 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.retrofit; import io.github.resilience4j.circuitbreaker.CircuitBreaker; diff --git a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java index 209de5d2e3..d9748e4c89 100644 --- a/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java +++ b/resilience4j-retrofit/src/main/java/io/github/resilience4j/retrofit/RetrofitCircuitBreaker.java @@ -1,3 +1,21 @@ +/* + * + * Copyright 2017 Christopher Pilsworth + * + * 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.retrofit; import io.github.resilience4j.circuitbreaker.CircuitBreaker; diff --git a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java index b4cb603fc1..131c86845a 100644 --- a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java +++ b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitCircuitBreakerTest.java @@ -1,3 +1,21 @@ +/* + * + * Copyright 2017 Christopher Pilsworth + * + * 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.retrofit; import com.github.tomakehurst.wiremock.junit.WireMockRule; diff --git a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitService.java b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitService.java index 5c88e1fb43..ec456dba33 100644 --- a/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitService.java +++ b/resilience4j-retrofit/src/test/java/io/github/resilience4j/retrofit/RetrofitService.java @@ -1,3 +1,21 @@ +/* + * + * Copyright 2017 Christopher Pilsworth + * + * 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.retrofit; import retrofit2.Call;