package io.vertx.circuitbreaker.impl; import com.github.tomakehurst.wiremock.junit.WireMockRule; import io.vertx.circuitbreaker.CircuitBreaker; import io.vertx.circuitbreaker.CircuitBreakerOptions; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; import io.vertx.core.eventbus.MessageConsumer; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientResponse; import io.vertx.core.http.HttpHeaders; import io.vertx.core.json.JsonObject; import io.vertx.ext.unit.junit.Repeat; import io.vertx.ext.unit.junit.RepeatRule; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.jayway.awaitility.Awaitility.await; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; /** * @author <a href="http://escoffier.me">Clement Escoffier</a> */ @RunWith(VertxUnitRunner.class) public class UsageTest { @Rule public RepeatRule repeatRule = new RepeatRule(); @Rule public WireMockRule wireMockRule = new WireMockRule(8089); private Vertx vertx; private CircuitBreaker cb; @Before public void setUp() { vertx = Vertx.vertx(); items.clear(); cb = CircuitBreaker.create("circuit-breaker", vertx, new CircuitBreakerOptions() .setFallbackOnFailure(true) .setTimeout(500) .setResetTimeout(1000)); vertx.eventBus().consumer("ok", message -> message.reply("OK")); vertx.eventBus().consumer("fail", message -> message.fail(100, "Bad bad bad")); vertx.eventBus().consumer("exception", message -> { throw new RuntimeException("RT - Bad bad bad"); }); vertx.eventBus().consumer("timeout", message -> vertx.setTimer(2000, x -> message.reply("Too late"))); } @After public void tearDown() { cb.close(); vertx.close(); } @Test @Repeat(10) public void testCBWithReadOperation() { prepareHttpServer(); HttpClient client = vertx.createHttpClient(); AtomicReference<JsonObject> json = new AtomicReference<>(); cb.<JsonObject>executeWithFallback( future -> { client.get(8089, "localhost", "/resource", HttpHeaders.set("Accept", "application/json"), ar -> { if (ar.succeeded()) { HttpClientResponse response = ar.result(); response.exceptionHandler(future::fail); response.bodyHandler(buffer -> { future.complete(buffer.toJsonObject()); }); } else { future.fail(ar.cause()); } }); }, t -> null ).onComplete(ar -> json.set(ar.result())); await().atMost(1, TimeUnit.MINUTES).untilAtomic(json, is(notNullValue())); assertThat(json.get().getString("status")).isEqualTo("OK"); json.set(null); cb.executeWithFallback( future -> { client.get(8089, "localhost", "/error", HttpHeaders.set("Accept", "application/json"), ar -> { if (ar.succeeded()) { HttpClientResponse response = ar.result(); if (response.statusCode() != 200) { future.fail("Invalid response"); } else { response.exceptionHandler(future::fail); response.bodyHandler(buffer -> future.complete(buffer.toJsonObject())); } } else { future.fail(ar.cause()); } }); }, t -> new JsonObject().put("status", "KO") ).onComplete(ar -> json.set(ar.result())); await().untilAtomic(json, is(notNullValue())); assertThat(json.get().getString("status")).isEqualTo("KO"); json.set(null); cb.executeWithFallback( future -> { client.get(8089, "localhost", "/delayed", HttpHeaders.set("Accept", "application/json"), ar -> { if (ar.succeeded()) { HttpClientResponse response = ar.result(); response.exceptionHandler(future::fail); if (response.statusCode() != 200) { future.fail("Invalid response"); } else { response.bodyHandler(buffer -> future.complete(buffer.toJsonObject())); } } else { future.fail(ar.cause()); } }); }, t -> new JsonObject().put("status", "KO") ).onComplete(ar -> json.set(ar.result())); await().untilAtomic(json, is(notNullValue())); assertThat(json.get().getString("status")).isEqualTo("KO"); } private void prepareHttpServer() { stubFor(get(urlEqualTo("/resource")) .withHeader("Accept", equalTo("application/json")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"status\":\"OK\"}"))); stubFor(get(urlEqualTo("/delayed")).willReturn( aResponse() .withStatus(200) .withFixedDelay(2000))); stubFor(get(urlEqualTo("/error")).willReturn( aResponse() .withStatus(500) .withBody("This is an error"))); } private List<String> items = new ArrayList<>(); public void asyncWrite(String content, Scenario scenario, Handler<AsyncResult<Void>> resultHandler) { long random = (long) (Math.random() * 1000); switch (scenario) { case TIMEOUT: random = 2000; break; case RUNTIME_EXCEPTION: throw new RuntimeException("Bad bad bad"); } vertx.setTimer(random, l -> { if (scenario == Scenario.FAILURE) { synchronized (UsageTest.this) { items.add("Error"); } resultHandler.handle(Future.failedFuture("Bad Bad Bad")); } else { synchronized (UsageTest.this) { items.add(content); } resultHandler.handle(Future.succeededFuture()); } }); } enum Scenario { OK, FAILURE, RUNTIME_EXCEPTION, TIMEOUT } @Test public void testCBWithWriteOperation() { cb.<Void>executeWithFallback( future -> { asyncWrite("Hello", Scenario.OK, future); }, t -> null ); await().until(() -> { synchronized (UsageTest.this) { return items.size() == 1; } }); items.clear(); AtomicBoolean fallbackCalled = new AtomicBoolean(); cb.<Void>executeWithFallback( future -> { asyncWrite("Hello", Scenario.FAILURE, future); }, t -> { fallbackCalled.set(true); return null; } ); await().until(() -> { synchronized (UsageTest.this) { return items.size() == 1; } }); assertThat(fallbackCalled.get()).isTrue(); items.clear(); fallbackCalled.set(false); cb.<Void>executeWithFallback( future -> asyncWrite("Hello", Scenario.TIMEOUT, future), t -> { fallbackCalled.set(true); return null; } ); await().untilAtomic(fallbackCalled, is(true)); assertThat(items).isEmpty(); items.clear(); fallbackCalled.set(false); cb.<Void>executeWithFallback( future -> asyncWrite("Hello", Scenario.RUNTIME_EXCEPTION, future), t -> { fallbackCalled.set(true); return null; } ); await().untilAtomic(fallbackCalled, is(true)); assertThat(items).isEmpty(); } @Test public void testCBWithEventBus() { cb.<Message<String>>executeWithFallback( future -> vertx.eventBus().request("ok", "", future), t -> null ).onComplete(ar -> items.add(ar.result().body())); await().until(() -> { synchronized (UsageTest.this) { return items.size() == 1; } }); items.clear(); AtomicBoolean fallbackCalled = new AtomicBoolean(); cb.<Message<String>>executeWithFallback( future -> vertx.eventBus().request("timeout", "", future), t -> { fallbackCalled.set(true); return null; } ); await().untilAtomic(fallbackCalled, is(true)); assertThat(items).isEmpty(); fallbackCalled.set(false); cb.<Message<String>>executeWithFallback( future -> vertx.eventBus().request("fail", "", future), t -> { fallbackCalled.set(true); return null; } ); await().untilAtomic(fallbackCalled, is(true)); assertThat(items).isEmpty(); fallbackCalled.set(false); cb.<Message<String>>executeWithFallback( future -> vertx.eventBus().request("exception", "", future), t -> { fallbackCalled.set(true); return null; } ); await().untilAtomic(fallbackCalled, is(true)); assertThat(items).isEmpty(); fallbackCalled.set(false); } }