From 6b7078fc48933d893f598d4c962d94c91d99ddf1 Mon Sep 17 00:00:00 2001 From: MRomeh Date: Mon, 11 Mar 2019 13:57:38 +0100 Subject: [PATCH] Issue #258: Add webflux support circuitbreaker annotation * Add response predicate to retry sync and async for enhancement #259 * #258 add the support to the webflux types in the circuit breaker annotation AOP * #258 review comments * #258 review comments --- .../circuitbreaker/annotation/ApiType.java | 24 ++ .../annotation/CircuitBreaker.java | 24 +- resilience4j-spring-boot2/build.gradle | 1 + .../CircuitBreakerAutoConfigurationTest.java | 204 +++++++++++------ .../service/test/ReactiveDummyService.java | 33 +++ .../test/ReactiveDummyServiceImpl.java | 55 +++++ .../src/test/resources/application.yaml | 66 +++--- resilience4j-spring/build.gradle | 7 +- .../configure/CircuitBreakerAspect.java | 211 +++++++++++------- 9 files changed, 424 insertions(+), 201 deletions(-) create mode 100644 resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/ApiType.java create mode 100644 resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/ReactiveDummyService.java create mode 100644 resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/ReactiveDummyServiceImpl.java diff --git a/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/ApiType.java b/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/ApiType.java new file mode 100644 index 0000000000..56aea07284 --- /dev/null +++ b/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/ApiType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2019 Mahmoud Romeh + * + * 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.annotation; + +/** + * API type support for circuit breaker annotation + */ +public enum ApiType { + + DEFAULT, WEBFLUX +} diff --git a/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/CircuitBreaker.java b/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/CircuitBreaker.java index 2111e37ebe..b3acee4cb8 100644 --- a/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/CircuitBreaker.java +++ b/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/CircuitBreaker.java @@ -15,7 +15,11 @@ */ package io.github.resilience4j.circuitbreaker.annotation; -import java.lang.annotation.*; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** * This annotation can be applied to a class or a specific method. @@ -29,11 +33,17 @@ @Documented public @interface CircuitBreaker { - /** - * Name of the circuit breaker. - * - * @return the name of the circuit breaker - */ - String name(); + /** + * Name of the circuit breaker. + * + * @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; + } diff --git a/resilience4j-spring-boot2/build.gradle b/resilience4j-spring-boot2/build.gradle index 3afb375be5..f7c1387899 100644 --- a/resilience4j-spring-boot2/build.gradle +++ b/resilience4j-spring-boot2/build.gradle @@ -13,6 +13,7 @@ dependencies { testCompile ( libraries.spring_boot2_test ) testCompile ( libraries.micrometer_prometheus ) testCompile ( libraries.spring_boot2_web ) + testCompile project(':resilience4j-reactor') } compileJava.dependsOn(processResources) \ No newline at end of file diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java index d205884b0b..109359a364 100644 --- a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java @@ -15,12 +15,12 @@ */ package io.github.resilience4j.circuitbreaker; -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.TestApplication; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; + import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -29,99 +29,155 @@ import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import java.io.IOException; -import java.time.Duration; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; +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) + classes = TestApplication.class) public class CircuitBreakerAutoConfigurationTest { - @Autowired - CircuitBreakerRegistry circuitBreakerRegistry; + @Autowired + CircuitBreakerRegistry circuitBreakerRegistry; - @Autowired - CircuitBreakerProperties circuitBreakerProperties; + @Autowired + CircuitBreakerProperties circuitBreakerProperties; - @Autowired - CircuitBreakerAspect circuitBreakerAspect; + @Autowired + CircuitBreakerAspect circuitBreakerAspect; - @Autowired - DummyService dummyService; + @Autowired + DummyService dummyService; - @Autowired - private TestRestTemplate restTemplate; + @Autowired + private TestRestTemplate restTemplate; - /** - * 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 - public void testCircuitBreakerAutoConfiguration() throws IOException { - assertThat(circuitBreakerRegistry).isNotNull(); - assertThat(circuitBreakerProperties).isNotNull(); + @Autowired + private ReactiveDummyService reactiveDummyService; - try { - dummyService.doSomething(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. - dummyService.doSomething(false); + /** + * 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 + public void testCircuitBreakerAutoConfiguration() throws IOException { + assertThat(circuitBreakerRegistry).isNotNull(); + assertThat(circuitBreakerProperties).isNotNull(); - 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); + try { + dummyService.doSomething(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. + dummyService.doSomething(false); + + 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 circuitbreaker is configured as defined in application.yml + assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInClosedState()).isEqualTo(6); + assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInHalfOpenState()).isEqualTo(2); + assertThat(circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold()).isEqualTo(70f); + assertThat(circuitBreaker.getCircuitBreakerConfig().getWaitDurationInOpenState()).isEqualByComparingTo(Duration.ofSeconds(5L)); + + // expect circuitbreakers actuator endpoint contains both circuitbreakers + ResponseEntity 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 circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents", CircuitBreakerEventsEndpointResponse.class); + assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(4); + + 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 = 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(); + + assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new RecordedException())).isTrue(); + assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new IgnoredException())).isFalse(); + + // Verify that an exception for which recordFailurePredicate returns false and it is not included in + // recordExceptions evaluates to false. + assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new Exception())).isFalse(); + + // expect aspect configured as defined in application.yml + 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 + public void testCircuitBreakerAutoConfigurationReactive() throws IOException { + assertThat(circuitBreakerRegistry).isNotNull(); + assertThat(circuitBreakerProperties).isNotNull(); + + try { + reactiveDummyService.doSomethingFlux(true).subscribe(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.doSomethingFlux(false).subscribe(String::toUpperCase, Throwable::getCause); - // expect circuitbreaker is configured as defined in application.yml - assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInClosedState()).isEqualTo(6); - assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInHalfOpenState()).isEqualTo(2); - assertThat(circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold()).isEqualTo(70f); - assertThat(circuitBreaker.getCircuitBreakerConfig().getWaitDurationInOpenState()).isEqualByComparingTo(Duration.ofSeconds(5L)); + CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(ReactiveDummyService.BACKEND); + assertThat(circuitBreaker).isNotNull(); - // expect circuitbreakers actuator endpoint contains both circuitbreakers - ResponseEntity circuitBreakerList = restTemplate.getForEntity("/actuator/circuitbreakers", CircuitBreakerEndpointResponse.class); - assertThat(circuitBreakerList.getBody().getCircuitBreakers()).hasSize(2).containsExactly("backendA", "backendB"); + assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(2); + assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(1); + assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(1); - // expect circuitbreaker-event actuator endpoint recorded both events - ResponseEntity circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents", CircuitBreakerEventsEndpointResponse.class); - assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2); + // expect circuitbreaker is configured as defined in application.yml + assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInClosedState()).isEqualTo(6); + assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInHalfOpenState()).isEqualTo(2); + assertThat(circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold()).isEqualTo(70f); + assertThat(circuitBreaker.getCircuitBreakerConfig().getWaitDurationInOpenState()).isEqualByComparingTo(Duration.ofSeconds(5L)); - circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents?name=backendA", CircuitBreakerEventsEndpointResponse.class); - assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2); + // expect circuitbreakers actuator endpoint contains both circuitbreakers + ResponseEntity circuitBreakerList = restTemplate.getForEntity("/actuator/circuitbreakers", CircuitBreakerEndpointResponse.class); + assertThat(circuitBreakerList.getBody().getCircuitBreakers()).hasSize(2).containsExactly("backendA", "backendB"); - // expect no health indicator for backendB, as it is disabled via properties - ResponseEntity 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(); + // expect circuitbreaker-event actuator endpoint recorded both events + ResponseEntity circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents", CircuitBreakerEventsEndpointResponse.class); + assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2); - assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new RecordedException())).isTrue(); - assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new IgnoredException())).isFalse(); + circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents/backendB", CircuitBreakerEventsEndpointResponse.class); + assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2); - // Verify that an exception for which recordFailurePredicate returns false and it is not included in - // recordExceptions evaluates to false. - assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new Exception())).isFalse(); + // expect no health indicator for backendB, as it is disabled via properties + ResponseEntity 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(); - // expect aspect configured as defined in application.yml - assertThat(circuitBreakerAspect.getOrder()).isEqualTo(400); - } + // expect aspect configured as defined in application.yml + assertThat(circuitBreakerAspect.getOrder()).isEqualTo(400); + } - private final static class HealthResponse { - private Map details; + private final static class HealthResponse { + private Map details; - public Map getDetails() { + public Map getDetails() { return details; } - public void setDetails(Map details) { + public void setDetails(Map details) { this.details = details; } - } + } } diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/ReactiveDummyService.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/ReactiveDummyService.java new file mode 100644 index 0000000000..e16fd546e1 --- /dev/null +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/ReactiveDummyService.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 Mahmoud Romeh + * + * 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.service.test; + + +import java.io.IOException; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * reactive web service test using reactor types + */ +public interface ReactiveDummyService { + String BACKEND = "backendB"; + + Flux doSomethingFlux(boolean throwException) throws IOException; + + Mono doSomethingMono(boolean throwException) throws IOException; +} diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/ReactiveDummyServiceImpl.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/ReactiveDummyServiceImpl.java new file mode 100644 index 0000000000..293ea188ab --- /dev/null +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/ReactiveDummyServiceImpl.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019 Mahmoud Romeh + * + * 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.service.test; + +import java.io.IOException; + +import org.assertj.core.util.Arrays; +import org.springframework.stereotype.Component; + +import io.github.resilience4j.circuitbreaker.annotation.ApiType; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.ratelimiter.annotation.RateLimiter; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * reactive test service for web flux API type for the circuit brealer annotation AOP processing + */ +@CircuitBreaker(name = ReactiveDummyService.BACKEND, type = ApiType.WEBFLUX) +@RateLimiter(name = ReactiveDummyService.BACKEND) +@Component +public class ReactiveDummyServiceImpl implements ReactiveDummyService { + @Override + public Flux doSomethingFlux(boolean throwException) throws IOException { + + if (throwException) { + return Flux.error(new IllegalArgumentException("FailedFlux")); + } + + return Flux.fromArray(Arrays.array("test", "test2")); + } + + @Override + public Mono doSomethingMono(boolean throwException) throws IOException { + if (throwException) { + return Mono.error(new IllegalArgumentException("FailedFlux")); + } + + return Mono.just("testMono"); + } + +} diff --git a/resilience4j-spring-boot2/src/test/resources/application.yaml b/resilience4j-spring-boot2/src/test/resources/application.yaml index 932fe2af35..45523ef106 100644 --- a/resilience4j-spring-boot2/src/test/resources/application.yaml +++ b/resilience4j-spring-boot2/src/test/resources/application.yaml @@ -1,38 +1,40 @@ resilience4j.circuitbreaker: - circuitBreakerAspectOrder: 400 - backends: - backendA: - ringBufferSizeInClosedState: 6 - ringBufferSizeInHalfOpenState: 2 - waitDurationInOpenState: 5s - failureRateThreshold: 70 - eventConsumerBufferSize: 10 - recordFailurePredicate: io.github.resilience4j.circuitbreaker.RecordFailurePredicate - recordExceptions: - - io.github.resilience4j.circuitbreaker.RecordedException - ignoreExceptions: - - io.github.resilience4j.circuitbreaker.IgnoredException - backendB: - ringBufferSizeInClosedState: 10 - ringBufferSizeInHalfOpenState: 5 - waitDurationInOpenState: 5000 - failureRateThreshold: 50 - eventConsumerBufferSize: 10 - registerHealthIndicator: false + circuitBreakerAspectOrder: 400 + backends: + backendA: + ringBufferSizeInClosedState: 6 + ringBufferSizeInHalfOpenState: 2 + waitDurationInOpenState: 5s + failureRateThreshold: 70 + eventConsumerBufferSize: 10 + recordFailurePredicate: io.github.resilience4j.circuitbreaker.RecordFailurePredicate + recordExceptions: + - io.github.resilience4j.circuitbreaker.RecordedException + ignoreExceptions: + - io.github.resilience4j.circuitbreaker.IgnoredException + backendB: + ringBufferSizeInClosedState: 6 + ringBufferSizeInHalfOpenState: 2 + waitDurationInOpenState: 5s + failureRateThreshold: 70 + eventConsumerBufferSize: 10 + registerHealthIndicator: false resilience4j.ratelimiter: - rateLimiterAspectOrder: 401 - limiters: - backendA: - limitForPeriod: 10 - limitRefreshPeriodInMillis: 1000 - timeoutInMillis: 0 - subscribeForEvents: true - registerHealthIndicator: true - backendB: - limitForPeriod: 6 - limitRefreshPeriodInMillis: 500 - timeoutInMillis: 3000 + rateLimiterAspectOrder: 401 + limiters: + backendA: + limitForPeriod: 10 + limitRefreshPeriodInMillis: 1000 + timeoutInMillis: 0 + subscribeForEvents: true + registerHealthIndicator: true + backendB: + limitForPeriod: 6 + limitRefreshPeriodInMillis: 500 + timeoutInMillis: 3000 + subscribeForEvents: true + registerHealthIndicator: true management.security.enabled: false management.endpoints.web.exposure.include: '*' diff --git a/resilience4j-spring/build.gradle b/resilience4j-spring/build.gradle index dfae652903..ef39214b27 100644 --- a/resilience4j-spring/build.gradle +++ b/resilience4j-spring/build.gradle @@ -1,10 +1,11 @@ dependencies { - compile ( libraries.aspectj ) - compileOnly ( libraries.hibernate_validator ) - compileOnly ( libraries.spring_4_core, libraries.spring_4_context ) + compile(libraries.aspectj) + compileOnly(libraries.hibernate_validator) + compileOnly(libraries.spring_4_core, libraries.spring_4_context) compile project(':resilience4j-annotations') compile project(':resilience4j-consumer') compile project(':resilience4j-circuitbreaker') + compileOnly project(':resilience4j-reactor') compile project(':resilience4j-ratelimiter') compileOnly project(':resilience4j-prometheus') compileOnly project(':resilience4j-metrics') diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspect.java b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspect.java index 9af73cde77..6ac60dcf39 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspect.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspect.java @@ -27,8 +27,12 @@ import org.springframework.core.Ordered; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.circuitbreaker.annotation.ApiType; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.circuitbreaker.utils.CircuitBreakerUtils; +import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * This Spring AOP aspect intercepts all methods which are annotated with a {@link CircuitBreaker} annotation. @@ -38,89 +42,126 @@ @Aspect public class CircuitBreakerAspect implements Ordered { - private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerAspect.class); - - private final CircuitBreakerConfigurationProperties circuitBreakerProperties; - private final CircuitBreakerRegistry circuitBreakerRegistry; - - public CircuitBreakerAspect(CircuitBreakerConfigurationProperties backendMonitorPropertiesRegistry, CircuitBreakerRegistry circuitBreakerRegistry) { - this.circuitBreakerProperties = backendMonitorPropertiesRegistry; - this.circuitBreakerRegistry = circuitBreakerRegistry; - } - - @Pointcut(value = "@within(circuitBreaker) || @annotation(circuitBreaker)", argNames = "circuitBreaker") - public void matchAnnotatedClassOrMethod(CircuitBreaker circuitBreaker) { - } - - @Around(value = "matchAnnotatedClassOrMethod(backendMonitored)", argNames = "proceedingJoinPoint, backendMonitored") - public Object circuitBreakerAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, CircuitBreaker backendMonitored) throws Throwable { - Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod(); - String methodName = method.getDeclaringClass().getName() + "#" + method.getName(); - if (backendMonitored == null) { - backendMonitored = getBackendMonitoredAnnotation(proceedingJoinPoint); - } - String backend = backendMonitored.name(); - io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker = getOrCreateCircuitBreaker(methodName, backend); - return handleJoinPoint(proceedingJoinPoint, circuitBreaker, methodName); - } - - private io.github.resilience4j.circuitbreaker.CircuitBreaker getOrCreateCircuitBreaker(String methodName, String backend) { - io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(backend, - () -> circuitBreakerProperties.createCircuitBreakerConfig(backend)); - - if (logger.isDebugEnabled()) { - logger.debug("Created or retrieved circuit breaker '{}' with failure rate '{}' and wait interval'{}' for method: '{}'", - backend, circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), - circuitBreaker.getCircuitBreakerConfig().getWaitDurationInOpenState(), methodName); - } - - return circuitBreaker; - } - - private CircuitBreaker getBackendMonitoredAnnotation(ProceedingJoinPoint proceedingJoinPoint) { - if (logger.isDebugEnabled()) { - logger.debug("circuitBreaker parameter is null"); - } - CircuitBreaker circuitBreaker = null; - Class targetClass = proceedingJoinPoint.getTarget().getClass(); - if (targetClass.isAnnotationPresent(CircuitBreaker.class)) { - circuitBreaker = targetClass.getAnnotation(CircuitBreaker.class); - if (circuitBreaker == null) { - if (logger.isDebugEnabled()) { - logger.debug("TargetClass has no annotation 'CircuitBreaker'"); - } - circuitBreaker = targetClass.getDeclaredAnnotation(CircuitBreaker.class); - if (circuitBreaker == null) { - if (logger.isDebugEnabled()) { - logger.debug("TargetClass has no declared annotation 'CircuitBreaker'"); - } - } - } - } - return circuitBreaker; - } - - private Object handleJoinPoint(ProceedingJoinPoint proceedingJoinPoint, io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker, String methodName) throws Throwable { - CircuitBreakerUtils.isCallPermitted(circuitBreaker); - long start = System.nanoTime(); - try { - Object returnValue = proceedingJoinPoint.proceed(); - - long durationInNanos = System.nanoTime() - start; - circuitBreaker.onSuccess(durationInNanos); - return returnValue; - } catch (Throwable throwable) { - long durationInNanos = System.nanoTime() - start; - circuitBreaker.onError(durationInNanos, throwable); - if (logger.isDebugEnabled()) { - logger.debug("Invocation of method '" + methodName + "' failed!", throwable); - } - throw throwable; - } - } - - @Override - public int getOrder() { - return circuitBreakerProperties.getCircuitBreakerAspectOrder(); - } + private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerAspect.class); + + private final CircuitBreakerConfigurationProperties circuitBreakerProperties; + private final CircuitBreakerRegistry circuitBreakerRegistry; + + public CircuitBreakerAspect(CircuitBreakerConfigurationProperties backendMonitorPropertiesRegistry, CircuitBreakerRegistry circuitBreakerRegistry) { + this.circuitBreakerProperties = backendMonitorPropertiesRegistry; + this.circuitBreakerRegistry = circuitBreakerRegistry; + } + + @Pointcut(value = "@within(circuitBreaker) || @annotation(circuitBreaker)", argNames = "circuitBreaker") + public void matchAnnotatedClassOrMethod(CircuitBreaker circuitBreaker) { + } + + @Around(value = "matchAnnotatedClassOrMethod(backendMonitored)", argNames = "proceedingJoinPoint, backendMonitored") + public Object circuitBreakerAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, CircuitBreaker backendMonitored) throws Throwable { + Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod(); + String methodName = method.getDeclaringClass().getName() + "#" + method.getName(); + if (backendMonitored == null) { + backendMonitored = getBackendMonitoredAnnotation(proceedingJoinPoint); + } + String backend = backendMonitored.name(); + ApiType type = backendMonitored.type(); + io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker = getOrCreateCircuitBreaker(methodName, backend); + return handleJoinPoint(proceedingJoinPoint, circuitBreaker, methodName, type); + } + + private io.github.resilience4j.circuitbreaker.CircuitBreaker getOrCreateCircuitBreaker(String methodName, String backend) { + io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(backend, + () -> circuitBreakerProperties.createCircuitBreakerConfig(backend)); + + if (logger.isDebugEnabled()) { + logger.debug("Created or retrieved circuit breaker '{}' with failure rate '{}' and wait interval'{}' for method: '{}'", + backend, circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), + circuitBreaker.getCircuitBreakerConfig().getWaitDurationInOpenState(), methodName); + } + + return circuitBreaker; + } + + private CircuitBreaker getBackendMonitoredAnnotation(ProceedingJoinPoint proceedingJoinPoint) { + if (logger.isDebugEnabled()) { + logger.debug("circuitBreaker parameter is null"); + } + CircuitBreaker circuitBreaker = null; + Class targetClass = proceedingJoinPoint.getTarget().getClass(); + if (targetClass.isAnnotationPresent(CircuitBreaker.class)) { + circuitBreaker = targetClass.getAnnotation(CircuitBreaker.class); + if (circuitBreaker == null && logger.isDebugEnabled()) { + logger.debug("TargetClass has no annotation 'CircuitBreaker'"); + circuitBreaker = targetClass.getDeclaredAnnotation(CircuitBreaker.class); + if (circuitBreaker == null && logger.isDebugEnabled()) { + logger.debug("TargetClass has no declared annotation 'CircuitBreaker'"); + } + } + } + return circuitBreaker; + } + + private Object handleJoinPoint(ProceedingJoinPoint proceedingJoinPoint, io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker, String methodName, ApiType type) throws Throwable { + if (type == ApiType.WEBFLUX) { + return defaultWebFlux(proceedingJoinPoint, circuitBreaker, methodName); + } else { + return defaultHandling(proceedingJoinPoint, circuitBreaker, methodName); + } + } + + /** + * handle the Spring web flux (Flux /Mono) return types AOP based into reactor circuit-breaker + * See {@link io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator} for details. + */ + private Object defaultWebFlux(ProceedingJoinPoint proceedingJoinPoint, io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker, String methodName) throws Throwable { + CircuitBreakerUtils.isCallPermitted(circuitBreaker); + long start = System.nanoTime(); + try { + Object returnValue = proceedingJoinPoint.proceed(); + if (returnValue instanceof Flux) { + Flux fluxReturnValue = (Flux) returnValue; + return fluxReturnValue.transform(CircuitBreakerOperator.of(circuitBreaker)); + } else if (returnValue instanceof Mono) { + Mono monoReturnValue = (Mono) returnValue; + return monoReturnValue.transform(CircuitBreakerOperator.of(circuitBreaker)); + } else { + throw new IllegalArgumentException("Not Supported type for the circuit breaker in web flux :" + returnValue.getClass().getName()); + + } + } catch (Throwable throwable) { + long durationInNanos = System.nanoTime() - start; + circuitBreaker.onError(durationInNanos, throwable); + if (logger.isDebugEnabled()) { + logger.debug("Invocation of method '" + methodName + "' failed!", throwable); + } + throw throwable; + } + } + + /** + * the default Java types handling for the circuit breaker AOP + */ + private Object defaultHandling(ProceedingJoinPoint proceedingJoinPoint, io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker, String methodName) throws Throwable { + CircuitBreakerUtils.isCallPermitted(circuitBreaker); + long start = System.nanoTime(); + try { + Object returnValue = proceedingJoinPoint.proceed(); + + long durationInNanos = System.nanoTime() - start; + circuitBreaker.onSuccess(durationInNanos); + return returnValue; + } catch (Throwable throwable) { + long durationInNanos = System.nanoTime() - start; + circuitBreaker.onError(durationInNanos, throwable); + if (logger.isDebugEnabled()) { + logger.debug("Invocation of method '" + methodName + "' failed!", throwable); + } + throw throwable; + } + } + + @Override + public int getOrder() { + return circuitBreakerProperties.getCircuitBreakerAspectOrder(); + } }