Skip to content

Commit

Permalink
Refactor circuit breaker aspect to remove API type and add completabl… (
Browse files Browse the repository at this point in the history
ReactiveX#388)

* refactor circuit breaker aspect to remove API type and add completable future support

* Add Rxjava 2 support to Sprung aspect of circuit breaker

* fix the gradle build

* tmp fix the gradle build dependency issue

* make the circuit breaker aspect smart ! :) to know which logic need to be done based into the runtime situation

* code cleanup and adding needed java doc

* review comments

* make it public for the extension aspects

* review comments and spring config generalization

* review comments

* execption handling properly for the circuit breaker aspects

* javadoc update
  • Loading branch information
Romeh authored and RobWin committed Apr 3, 2019
1 parent 0ef7c41 commit 34074be
Show file tree
Hide file tree
Showing 19 changed files with 741 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,4 @@
* @return the name of the circuit breaker
*/
String name();

/**
* @return the type of circuit breaker (default or webflux which is reactor circuit breaker)
*/
ApiType type() default ApiType.DEFAULT;


}
18 changes: 9 additions & 9 deletions resilience4j-spring-boot/build.gradle
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
dependencies {
compile ( libraries.spring_boot_aop )
compile ( libraries.spring_boot_actuator )
compile ( libraries.spring_boot_web )
compile ( libraries.spring_reactor )
compile ( libraries.hibernate_validator )
compileOnly ( libraries.spring_boot_config_processor )
compileOnly ( libraries.spring_boot_autoconfigure_processor )
compile(libraries.spring_boot_aop)
compile(libraries.spring_boot_actuator)
compile(libraries.spring_boot_web)
compile(libraries.spring_reactor)
compile(libraries.hibernate_validator)
compileOnly(libraries.spring_boot_config_processor)
compileOnly(libraries.spring_boot_autoconfigure_processor)
compile project(':resilience4j-annotations')
compile project(':resilience4j-spring')
compile project(':resilience4j-circuitbreaker')
compile project(':resilience4j-ratelimiter')
compile project(':resilience4j-consumer')
compileOnly project(':resilience4j-prometheus')
compileOnly project(':resilience4j-metrics')
testCompile ( libraries.spring_boot_test )
testCompile(libraries.spring_boot_test)
testCompile project(':resilience4j-prometheus')
testCompile project(':resilience4j-metrics')
testCompile ( libraries.prometheus_spring_boot )
testCompile(libraries.prometheus_spring_boot)
}

compileJava.dependsOn(processResources)
17 changes: 9 additions & 8 deletions resilience4j-spring-boot2/build.gradle
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
dependencies {
compile ( libraries.spring_boot2_aop )
compile ( libraries.spring_boot2_actuator )
compile ( libraries.hibernate_validator )
compileOnly ( libraries.spring_boot2_config_processor )
compileOnly ( libraries.spring_boot2_autoconfigure_processor )
compile(libraries.spring_boot2_aop)
compile(libraries.spring_boot2_actuator)
compile(libraries.hibernate_validator)
compileOnly(libraries.spring_boot2_config_processor)
compileOnly(libraries.spring_boot2_autoconfigure_processor)
compile project(':resilience4j-annotations')
compile project(':resilience4j-spring')
compile project(':resilience4j-micrometer')
compile project(':resilience4j-circuitbreaker')
compile project(':resilience4j-ratelimiter')
compile project(':resilience4j-consumer')
testCompile ( libraries.spring_boot2_test )
testCompile ( libraries.micrometer_prometheus )
testCompile ( libraries.spring_boot2_web )
testCompile(libraries.spring_boot2_test)
testCompile(libraries.micrometer_prometheus)
testCompile(libraries.spring_boot2_web)
testCompile project(':resilience4j-reactor')
testCompile project(':resilience4j-rxjava2')
}

compileJava.dependsOn(processResources)
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright 2017 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.circuitbreaker;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.util.Map;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakerProperties;
import io.github.resilience4j.circuitbreaker.configure.CircuitBreakerAspect;
import io.github.resilience4j.circuitbreaker.monitoring.endpoint.CircuitBreakerEndpointResponse;
import io.github.resilience4j.circuitbreaker.monitoring.endpoint.CircuitBreakerEventsEndpointResponse;
import io.github.resilience4j.service.test.DummyService;
import io.github.resilience4j.service.test.ReactiveDummyService;
import io.github.resilience4j.service.test.TestApplication;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = TestApplication.class)
public class CircuitBreakerAutoConfigurationRxJava2Test {

@Autowired
CircuitBreakerRegistry circuitBreakerRegistry;

@Autowired
CircuitBreakerProperties circuitBreakerProperties;

@Autowired
CircuitBreakerAspect circuitBreakerAspect;

@Autowired
DummyService dummyService;

@Autowired
private TestRestTemplate restTemplate;

@Autowired
private ReactiveDummyService reactiveDummyService;


/**
* The test verifies that a CircuitBreaker instance is created and configured properly when the DummyService is invoked and
* that the CircuitBreaker records successful and failed calls.
*/
@Test
@DirtiesContext
public void testCircuitBreakerAutoConfigurationReactiveRxJava2() throws IOException {
assertThat(circuitBreakerRegistry).isNotNull();
assertThat(circuitBreakerProperties).isNotNull();

try {
reactiveDummyService.doSomethingFlowable(true).blockingSubscribe(String::toUpperCase, Throwable::getCause);
} catch (Exception ex) {
// Do nothing. The IOException is recorded by the CircuitBreaker as part of the recordFailurePredicate as a failure.
}
// The invocation is recorded by the CircuitBreaker as a success.
reactiveDummyService.doSomethingFlowable(false).blockingSubscribe(String::toUpperCase, Throwable::getCause);

CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(ReactiveDummyService.BACKEND);
assertThat(circuitBreaker).isNotNull();

assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(2);
assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(1);
assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(1);

// expect circuitbreakers actuator endpoint contains both circuitbreakers
ResponseEntity<CircuitBreakerEndpointResponse> circuitBreakerList = restTemplate.getForEntity("/actuator/circuitbreakers", CircuitBreakerEndpointResponse.class);
assertThat(circuitBreakerList.getBody().getCircuitBreakers()).hasSize(2).containsExactly("backendA", "backendB");

// expect circuitbreaker-event actuator endpoint recorded both events
ResponseEntity<CircuitBreakerEventsEndpointResponse> circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents", CircuitBreakerEventsEndpointResponse.class);
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2);

circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents/backendB", CircuitBreakerEventsEndpointResponse.class);
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2);

// expect no health indicator for backendB, as it is disabled via properties
ResponseEntity<HealthResponse> healthResponse = restTemplate.getForEntity("/actuator/health", HealthResponse.class);
assertThat(healthResponse.getBody().getDetails()).isNotNull();
assertThat(healthResponse.getBody().getDetails().get("backendACircuitBreaker")).isNotNull();
assertThat(healthResponse.getBody().getDetails().get("backendBCircuitBreaker")).isNull();

// Observable test
try {
reactiveDummyService.doSomethingObservable(true).blockingSubscribe(String::toUpperCase, Throwable::getCause);
} catch (IOException ex) {
// Do nothing. The IOException is recorded by the CircuitBreaker as part of the recordFailurePredicate as a failure.
}
// The invocation is recorded by the CircuitBreaker as a success.
reactiveDummyService.doSomethingObservable(false).blockingSubscribe(String::toUpperCase, Throwable::getCause);

assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(4);
assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(2);
assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(2);

// Maybe test
try {
reactiveDummyService.doSomethingMaybe(true).blockingGet("goo");
} catch (Exception ex) {
// Do nothing. The IOException is recorded by the CircuitBreaker as part of the recordFailurePredicate as a failure.
}
// The invocation is recorded by the CircuitBreaker as a success.
reactiveDummyService.doSomethingMaybe(false).blockingGet();

assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(6);
assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(3);
assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(3);

// single test
try {
reactiveDummyService.doSomethingSingle(true).blockingGet();
} catch (Exception ex) {
// Do nothing. The IOException is recorded by the CircuitBreaker as part of the recordFailurePredicate as a failure.
}
// The invocation is recorded by the CircuitBreaker as a success.
reactiveDummyService.doSomethingSingle(false).blockingGet();

assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(8);
assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(4);
assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(4);

// Completable test

try {
reactiveDummyService.doSomethingCompletable(true).blockingAwait();
} catch (Exception ex) {
// Do nothing. The IOException is recorded by the CircuitBreaker as part of the recordFailurePredicate as a failure.
}
// The invocation is recorded by the CircuitBreaker as a success.
reactiveDummyService.doSomethingCompletable(false).blockingAwait();

assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(10);
assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(5);
assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(5);

}


private final static class HealthResponse {
private Map<String, Object> details;

public Map<String, Object> getDetails() {
return details;
}

public void setDetails(Map<String, Object> details) {
this.details = details;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
*/
package io.github.resilience4j.circuitbreaker;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -24,10 +32,6 @@
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.io.IOException;
import java.time.Duration;
import java.util.Map;

import io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakerProperties;
import io.github.resilience4j.circuitbreaker.configure.CircuitBreakerAspect;
import io.github.resilience4j.circuitbreaker.monitoring.endpoint.CircuitBreakerEndpointResponse;
Expand All @@ -36,8 +40,6 @@
import io.github.resilience4j.service.test.ReactiveDummyService;
import io.github.resilience4j.service.test.TestApplication;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = TestApplication.class)
Expand Down Expand Up @@ -120,6 +122,53 @@ public void testCircuitBreakerAutoConfiguration() throws IOException {
assertThat(circuitBreakerAspect.getOrder()).isEqualTo(400);
}


/**
* The test verifies that a CircuitBreaker instance is created and configured properly when the DummyService is invoked and
* that the CircuitBreaker records successful and failed calls.
*/
@Test
@DirtiesContext
public void testCircuitBreakerAutoConfigurationAsync() throws IOException, ExecutionException, InterruptedException {
assertThat(circuitBreakerRegistry).isNotNull();
assertThat(circuitBreakerProperties).isNotNull();

try {
dummyService.doSomethingAsync(true);
} catch (IOException ex) {
// Do nothing. The IOException is recorded by the CircuitBreaker as part of the recordFailurePredicate as a failure.
}
// The invocation is recorded by the CircuitBreaker as a success.
final CompletableFuture<String> stringCompletionStage = dummyService.doSomethingAsync(false);
assertThat(stringCompletionStage.get().equals("Test result"));

CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(DummyService.BACKEND);
assertThat(circuitBreaker).isNotNull();

assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(2);
assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(1);
assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(1);

// expect circuitbreakers actuator endpoint contains both circuitbreakers
ResponseEntity<CircuitBreakerEndpointResponse> circuitBreakerList = restTemplate.getForEntity("/actuator/circuitbreakers", CircuitBreakerEndpointResponse.class);
assertThat(circuitBreakerList.getBody().getCircuitBreakers()).hasSize(2).containsExactly("backendA", "backendB");

// expect circuitbreaker-event actuator endpoint recorded both events
ResponseEntity<CircuitBreakerEventsEndpointResponse> circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents", CircuitBreakerEventsEndpointResponse.class);
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2);

circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents/backendA", CircuitBreakerEventsEndpointResponse.class);
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2);

// expect no health indicator for backendB, as it is disabled via properties
ResponseEntity<HealthResponse> healthResponse = restTemplate.getForEntity("/actuator/health", HealthResponse.class);
assertThat(healthResponse.getBody().getDetails()).isNotNull();
assertThat(healthResponse.getBody().getDetails().get("backendACircuitBreaker")).isNotNull();
assertThat(healthResponse.getBody().getDetails().get("backendBCircuitBreaker")).isNull();


}

/**
* The test verifies that a CircuitBreaker instance is created and configured properly when the DummyService is invoked and
* that the CircuitBreaker records successful and failed calls.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@


import java.io.IOException;
import java.util.concurrent.CompletableFuture;

public interface DummyService {
String BACKEND = "backendA";

void doSomething(boolean throwException) throws IOException;

CompletableFuture<String> doSomethingAsync(boolean throwException) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


import java.io.IOException;
import java.util.concurrent.CompletableFuture;

import org.springframework.stereotype.Component;

Expand All @@ -12,10 +13,18 @@
@RateLimiter(name = DummyService.BACKEND)
@Component
public class DummyServiceImpl implements DummyService {
@Override
public void doSomething(boolean throwBackendTrouble) throws IOException {
if (throwBackendTrouble) {
throw new IOException("Test Message");
}
}
@Override
public void doSomething(boolean throwBackendTrouble) throws IOException {
if (throwBackendTrouble) {
throw new IOException("Test Message");
}
}

@Override
public CompletableFuture<String> doSomethingAsync(boolean throwBackendTrouble) throws IOException {
if (throwBackendTrouble) {
throw new IOException("Test Message");
}
return CompletableFuture.supplyAsync(() -> "Test result");
}
}
Loading

0 comments on commit 34074be

Please sign in to comment.