Skip to content

Commit 6b7078f

Browse files
RomehRobWin
authored andcommitted
Issue ReactiveX#258: Add webflux support circuitbreaker annotation
* Add response predicate to retry sync and async for enhancement ReactiveX#259 * ReactiveX#258 add the support to the webflux types in the circuit breaker annotation AOP * ReactiveX#258 review comments * ReactiveX#258 review comments
1 parent 2a53946 commit 6b7078f

File tree

9 files changed

+424
-201
lines changed

9 files changed

+424
-201
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2019 Mahmoud Romeh
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.resilience4j.circuitbreaker.annotation;
17+
18+
/**
19+
* API type support for circuit breaker annotation
20+
*/
21+
public enum ApiType {
22+
23+
DEFAULT, WEBFLUX
24+
}

resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/CircuitBreaker.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
*/
1616
package io.github.resilience4j.circuitbreaker.annotation;
1717

18-
import java.lang.annotation.*;
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
1923

2024
/**
2125
* This annotation can be applied to a class or a specific method.
@@ -29,11 +33,17 @@
2933
@Documented
3034
public @interface CircuitBreaker {
3135

32-
/**
33-
* Name of the circuit breaker.
34-
*
35-
* @return the name of the circuit breaker
36-
*/
37-
String name();
36+
/**
37+
* Name of the circuit breaker.
38+
*
39+
* @return the name of the circuit breaker
40+
*/
41+
String name();
42+
43+
/**
44+
* @return the type of circuit breaker (default or webflux which is reactor circuit breaker)
45+
*/
46+
ApiType type() default ApiType.DEFAULT;
47+
3848

3949
}

resilience4j-spring-boot2/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies {
1313
testCompile ( libraries.spring_boot2_test )
1414
testCompile ( libraries.micrometer_prometheus )
1515
testCompile ( libraries.spring_boot2_web )
16+
testCompile project(':resilience4j-reactor')
1617
}
1718

1819
compileJava.dependsOn(processResources)

resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java

Lines changed: 130 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
*/
1616
package io.github.resilience4j.circuitbreaker;
1717

18-
import io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakerProperties;
19-
import io.github.resilience4j.circuitbreaker.configure.CircuitBreakerAspect;
20-
import io.github.resilience4j.circuitbreaker.monitoring.endpoint.CircuitBreakerEndpointResponse;
21-
import io.github.resilience4j.circuitbreaker.monitoring.endpoint.CircuitBreakerEventsEndpointResponse;
22-
import io.github.resilience4j.service.test.DummyService;
23-
import io.github.resilience4j.service.test.TestApplication;
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.io.IOException;
21+
import java.time.Duration;
22+
import java.util.Map;
23+
2424
import org.junit.Test;
2525
import org.junit.runner.RunWith;
2626
import org.springframework.beans.factory.annotation.Autowired;
@@ -29,99 +29,155 @@
2929
import org.springframework.http.ResponseEntity;
3030
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
3131

32-
import java.io.IOException;
33-
import java.time.Duration;
34-
import java.util.Map;
35-
36-
import static org.assertj.core.api.Assertions.assertThat;
32+
import io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakerProperties;
33+
import io.github.resilience4j.circuitbreaker.configure.CircuitBreakerAspect;
34+
import io.github.resilience4j.circuitbreaker.monitoring.endpoint.CircuitBreakerEndpointResponse;
35+
import io.github.resilience4j.circuitbreaker.monitoring.endpoint.CircuitBreakerEventsEndpointResponse;
36+
import io.github.resilience4j.service.test.DummyService;
37+
import io.github.resilience4j.service.test.ReactiveDummyService;
38+
import io.github.resilience4j.service.test.TestApplication;
3739

3840
@RunWith(SpringJUnit4ClassRunner.class)
3941
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
40-
classes = TestApplication.class)
42+
classes = TestApplication.class)
4143
public class CircuitBreakerAutoConfigurationTest {
4244

43-
@Autowired
44-
CircuitBreakerRegistry circuitBreakerRegistry;
45+
@Autowired
46+
CircuitBreakerRegistry circuitBreakerRegistry;
4547

46-
@Autowired
47-
CircuitBreakerProperties circuitBreakerProperties;
48+
@Autowired
49+
CircuitBreakerProperties circuitBreakerProperties;
4850

49-
@Autowired
50-
CircuitBreakerAspect circuitBreakerAspect;
51+
@Autowired
52+
CircuitBreakerAspect circuitBreakerAspect;
5153

52-
@Autowired
53-
DummyService dummyService;
54+
@Autowired
55+
DummyService dummyService;
5456

55-
@Autowired
56-
private TestRestTemplate restTemplate;
57+
@Autowired
58+
private TestRestTemplate restTemplate;
5759

58-
/**
59-
* The test verifies that a CircuitBreaker instance is created and configured properly when the DummyService is invoked and
60-
* that the CircuitBreaker records successful and failed calls.
61-
*/
62-
@Test
63-
public void testCircuitBreakerAutoConfiguration() throws IOException {
64-
assertThat(circuitBreakerRegistry).isNotNull();
65-
assertThat(circuitBreakerProperties).isNotNull();
60+
@Autowired
61+
private ReactiveDummyService reactiveDummyService;
6662

67-
try {
68-
dummyService.doSomething(true);
69-
} catch (IOException ex) {
70-
// Do nothing. The IOException is recorded by the CircuitBreaker as part of the recordFailurePredicate as a failure.
71-
}
72-
// The invocation is recorded by the CircuitBreaker as a success.
73-
dummyService.doSomething(false);
63+
/**
64+
* The test verifies that a CircuitBreaker instance is created and configured properly when the DummyService is invoked and
65+
* that the CircuitBreaker records successful and failed calls.
66+
*/
67+
@Test
68+
public void testCircuitBreakerAutoConfiguration() throws IOException {
69+
assertThat(circuitBreakerRegistry).isNotNull();
70+
assertThat(circuitBreakerProperties).isNotNull();
7471

75-
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(DummyService.BACKEND);
76-
assertThat(circuitBreaker).isNotNull();
77-
78-
assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(2);
79-
assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(1);
80-
assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(1);
72+
try {
73+
dummyService.doSomething(true);
74+
} catch (IOException ex) {
75+
// Do nothing. The IOException is recorded by the CircuitBreaker as part of the recordFailurePredicate as a failure.
76+
}
77+
// The invocation is recorded by the CircuitBreaker as a success.
78+
dummyService.doSomething(false);
79+
80+
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(DummyService.BACKEND);
81+
assertThat(circuitBreaker).isNotNull();
82+
83+
assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(2);
84+
assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(1);
85+
assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(1);
86+
87+
// expect circuitbreaker is configured as defined in application.yml
88+
assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInClosedState()).isEqualTo(6);
89+
assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInHalfOpenState()).isEqualTo(2);
90+
assertThat(circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold()).isEqualTo(70f);
91+
assertThat(circuitBreaker.getCircuitBreakerConfig().getWaitDurationInOpenState()).isEqualByComparingTo(Duration.ofSeconds(5L));
92+
93+
// expect circuitbreakers actuator endpoint contains both circuitbreakers
94+
ResponseEntity<CircuitBreakerEndpointResponse> circuitBreakerList = restTemplate.getForEntity("/actuator/circuitbreakers", CircuitBreakerEndpointResponse.class);
95+
assertThat(circuitBreakerList.getBody().getCircuitBreakers()).hasSize(2).containsExactly("backendA", "backendB");
96+
97+
// expect circuitbreaker-event actuator endpoint recorded both events
98+
ResponseEntity<CircuitBreakerEventsEndpointResponse> circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents", CircuitBreakerEventsEndpointResponse.class);
99+
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(4);
100+
101+
circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents/backendA", CircuitBreakerEventsEndpointResponse.class);
102+
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2);
103+
104+
// expect no health indicator for backendB, as it is disabled via properties
105+
ResponseEntity<HealthResponse> healthResponse = restTemplate.getForEntity("/actuator/health", HealthResponse.class);
106+
assertThat(healthResponse.getBody().getDetails()).isNotNull();
107+
assertThat(healthResponse.getBody().getDetails().get("backendACircuitBreaker")).isNotNull();
108+
assertThat(healthResponse.getBody().getDetails().get("backendBCircuitBreaker")).isNull();
109+
110+
assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new RecordedException())).isTrue();
111+
assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new IgnoredException())).isFalse();
112+
113+
// Verify that an exception for which recordFailurePredicate returns false and it is not included in
114+
// recordExceptions evaluates to false.
115+
assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new Exception())).isFalse();
116+
117+
// expect aspect configured as defined in application.yml
118+
assertThat(circuitBreakerAspect.getOrder()).isEqualTo(400);
119+
}
120+
121+
/**
122+
* The test verifies that a CircuitBreaker instance is created and configured properly when the DummyService is invoked and
123+
* that the CircuitBreaker records successful and failed calls.
124+
*/
125+
@Test
126+
public void testCircuitBreakerAutoConfigurationReactive() throws IOException {
127+
assertThat(circuitBreakerRegistry).isNotNull();
128+
assertThat(circuitBreakerProperties).isNotNull();
129+
130+
try {
131+
reactiveDummyService.doSomethingFlux(true).subscribe(String::toUpperCase, Throwable::getCause);
132+
} catch (IOException ex) {
133+
// Do nothing. The IOException is recorded by the CircuitBreaker as part of the recordFailurePredicate as a failure.
134+
}
135+
// The invocation is recorded by the CircuitBreaker as a success.
136+
reactiveDummyService.doSomethingFlux(false).subscribe(String::toUpperCase, Throwable::getCause);
81137

82-
// expect circuitbreaker is configured as defined in application.yml
83-
assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInClosedState()).isEqualTo(6);
84-
assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInHalfOpenState()).isEqualTo(2);
85-
assertThat(circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold()).isEqualTo(70f);
86-
assertThat(circuitBreaker.getCircuitBreakerConfig().getWaitDurationInOpenState()).isEqualByComparingTo(Duration.ofSeconds(5L));
138+
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(ReactiveDummyService.BACKEND);
139+
assertThat(circuitBreaker).isNotNull();
87140

88-
// expect circuitbreakers actuator endpoint contains both circuitbreakers
89-
ResponseEntity<CircuitBreakerEndpointResponse> circuitBreakerList = restTemplate.getForEntity("/actuator/circuitbreakers", CircuitBreakerEndpointResponse.class);
90-
assertThat(circuitBreakerList.getBody().getCircuitBreakers()).hasSize(2).containsExactly("backendA", "backendB");
141+
assertThat(circuitBreaker.getMetrics().getNumberOfBufferedCalls()).isEqualTo(2);
142+
assertThat(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()).isEqualTo(1);
143+
assertThat(circuitBreaker.getMetrics().getNumberOfFailedCalls()).isEqualTo(1);
91144

92-
// expect circuitbreaker-event actuator endpoint recorded both events
93-
ResponseEntity<CircuitBreakerEventsEndpointResponse> circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents", CircuitBreakerEventsEndpointResponse.class);
94-
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2);
145+
// expect circuitbreaker is configured as defined in application.yml
146+
assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInClosedState()).isEqualTo(6);
147+
assertThat(circuitBreaker.getCircuitBreakerConfig().getRingBufferSizeInHalfOpenState()).isEqualTo(2);
148+
assertThat(circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold()).isEqualTo(70f);
149+
assertThat(circuitBreaker.getCircuitBreakerConfig().getWaitDurationInOpenState()).isEqualByComparingTo(Duration.ofSeconds(5L));
95150

96-
circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents?name=backendA", CircuitBreakerEventsEndpointResponse.class);
97-
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2);
151+
// expect circuitbreakers actuator endpoint contains both circuitbreakers
152+
ResponseEntity<CircuitBreakerEndpointResponse> circuitBreakerList = restTemplate.getForEntity("/actuator/circuitbreakers", CircuitBreakerEndpointResponse.class);
153+
assertThat(circuitBreakerList.getBody().getCircuitBreakers()).hasSize(2).containsExactly("backendA", "backendB");
98154

99-
// expect no health indicator for backendB, as it is disabled via properties
100-
ResponseEntity<HealthResponse> healthResponse = restTemplate.getForEntity("/actuator/health", HealthResponse.class);
101-
assertThat(healthResponse.getBody().getDetails()).isNotNull();
102-
assertThat(healthResponse.getBody().getDetails().get("backendACircuitBreaker")).isNotNull();
103-
assertThat(healthResponse.getBody().getDetails().get("backendBCircuitBreaker")).isNull();
155+
// expect circuitbreaker-event actuator endpoint recorded both events
156+
ResponseEntity<CircuitBreakerEventsEndpointResponse> circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents", CircuitBreakerEventsEndpointResponse.class);
157+
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2);
104158

105-
assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new RecordedException())).isTrue();
106-
assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new IgnoredException())).isFalse();
159+
circuitBreakerEventList = restTemplate.getForEntity("/actuator/circuitbreakerevents/backendB", CircuitBreakerEventsEndpointResponse.class);
160+
assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2);
107161

108-
// Verify that an exception for which recordFailurePredicate returns false and it is not included in
109-
// recordExceptions evaluates to false.
110-
assertThat(circuitBreaker.getCircuitBreakerConfig().getRecordFailurePredicate().test(new Exception())).isFalse();
162+
// expect no health indicator for backendB, as it is disabled via properties
163+
ResponseEntity<HealthResponse> healthResponse = restTemplate.getForEntity("/actuator/health", HealthResponse.class);
164+
assertThat(healthResponse.getBody().getDetails()).isNotNull();
165+
assertThat(healthResponse.getBody().getDetails().get("backendACircuitBreaker")).isNotNull();
166+
assertThat(healthResponse.getBody().getDetails().get("backendBCircuitBreaker")).isNull();
111167

112-
// expect aspect configured as defined in application.yml
113-
assertThat(circuitBreakerAspect.getOrder()).isEqualTo(400);
114-
}
168+
// expect aspect configured as defined in application.yml
169+
assertThat(circuitBreakerAspect.getOrder()).isEqualTo(400);
170+
}
115171

116-
private final static class HealthResponse {
117-
private Map<String, Object> details;
172+
private final static class HealthResponse {
173+
private Map<String, Object> details;
118174

119-
public Map<String, Object> getDetails() {
175+
public Map<String, Object> getDetails() {
120176
return details;
121177
}
122178

123-
public void setDetails(Map<String, Object> details) {
179+
public void setDetails(Map<String, Object> details) {
124180
this.details = details;
125181
}
126-
}
182+
}
127183
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2019 Mahmoud Romeh
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.resilience4j.service.test;
17+
18+
19+
import java.io.IOException;
20+
21+
import reactor.core.publisher.Flux;
22+
import reactor.core.publisher.Mono;
23+
24+
/**
25+
* reactive web service test using reactor types
26+
*/
27+
public interface ReactiveDummyService {
28+
String BACKEND = "backendB";
29+
30+
Flux<String> doSomethingFlux(boolean throwException) throws IOException;
31+
32+
Mono<String> doSomethingMono(boolean throwException) throws IOException;
33+
}

0 commit comments

Comments
 (0)