handler);
97 |
98 | /**
99 | * Executes the given operation with the circuit breaker control. The operation is generally calling an
100 | * external system. The operation receives a {@link Promise} object as parameter and must
101 | * call {@link Promise#complete(Object)} when the operation has terminated successfully. The operation must also
102 | * call {@link Promise#fail(Throwable)} in case of a failure.
103 | *
104 | * The operation is not invoked if the circuit breaker is open, and the given fallback is called instead.
105 | * The circuit breaker also monitors whether the operation completes in time. The operation is considered failed
106 | * if it does not terminate before the configured timeout.
107 | *
108 | * This method returns a {@link Future} object to retrieve the status and result of the operation, with the status
109 | * being a success or a failure. If the fallback is called, the returned future is successfully completed with the
110 | * value returned from the fallback. If the fallback throws an exception, the returned future is marked as failed.
111 | *
112 | * @param command the operation
113 | * @param fallback the fallback function; gets an exception as parameter and returns the fallback result
114 | * @param the type of result
115 | * @return a future object completed when the operation or the fallback completes
116 | */
117 | Future executeWithFallback(Handler> command, Function fallback);
118 |
119 | /**
120 | * Executes the given operation with the circuit breaker control. The operation is generally calling an
121 | * external system. The operation receives a {@link Promise} object as parameter and must
122 | * call {@link Promise#complete(Object)} when the operation has terminated successfully. The operation must also
123 | * call {@link Promise#fail(Throwable)} in case of a failure.
124 | *
125 | * The operation is not invoked if the circuit breaker is open, and the given fallback is called instead.
126 | * The circuit breaker also monitors whether the operation completes in time. The operation is considered failed
127 | * if it does not terminate before the configured timeout.
128 | *
129 | * This method returns a {@link Future} object to retrieve the status and result of the operation, with the status
130 | * being a success or a failure. If the fallback is called, the returned future is successfully completed with the
131 | * value returned from the fallback. If the fallback throws an exception, the returned future is marked as failed.
132 | *
133 | * @param command the operation
134 | * @param fallback the fallback function; gets an exception as parameter and returns the fallback result
135 | * @param the type of result
136 | * @return a future object completed when the operation or the fallback completes
137 | */
138 | Future executeWithFallback(Supplier> command, Function fallback);
139 |
140 | /**
141 | * Same as {@link #executeWithFallback(Handler, Function)} but using the circuit breaker
142 | * {@linkplain #fallback(Function) default fallback}.
143 | *
144 | * @param command the operation
145 | * @param the type of result
146 | * @return a future object completed when the operation or its fallback completes
147 | */
148 | Future execute(Handler> command);
149 |
150 | /**
151 | * Same as {@link #executeWithFallback(Supplier, Function)} but using the circuit breaker
152 | * {@linkplain #fallback(Function) default fallback}.
153 | *
154 | * @param command the operation
155 | * @param the type of result
156 | * @return a future object completed when the operation or its fallback completes
157 | */
158 | Future execute(Supplier> command);
159 |
160 | /**
161 | * Same as {@link #executeAndReportWithFallback(Promise, Handler, Function)} but using the circuit breaker
162 | * {@linkplain #fallback(Function) default fallback}.
163 | *
164 | * @param resultPromise the promise on which the operation result is reported
165 | * @param command the operation
166 | * @param the type of result
167 | * @return this {@link CircuitBreaker}
168 | */
169 | @Fluent
170 | CircuitBreaker executeAndReport(Promise resultPromise, Handler> command);
171 |
172 | /**
173 | * Executes the given operation with the circuit breaker control. The operation is generally calling an
174 | * external system. The operation receives a {@link Promise} object as parameter and must
175 | * call {@link Promise#complete(Object)} when the operation has terminated successfully. The operation must also
176 | * call {@link Promise#fail(Throwable)} in case of a failure.
177 | *
178 | * The operation is not invoked if the circuit breaker is open, and the given fallback is called instead.
179 | * The circuit breaker also monitors whether the operation completes in time. The operation is considered failed
180 | * if it does not terminate before the configured timeout.
181 | *
182 | * Unlike {@link #executeWithFallback(Handler, Function)}, this method does not return a {@link Future} object, but
183 | * lets the caller pass a {@link Promise} object on which the result is reported. If the fallback is called, the promise
184 | * is successfully completed with the value returned by the fallback function. If the fallback throws an exception,
185 | * the promise is marked as failed.
186 | *
187 | * @param resultPromise the promise on which the operation result is reported
188 | * @param command the operation
189 | * @param fallback the fallback function; gets an exception as parameter and returns the fallback result
190 | * @param the type of result
191 | * @return this {@link CircuitBreaker}
192 | */
193 | @Fluent
194 | CircuitBreaker executeAndReportWithFallback(Promise resultPromise, Handler> command,
195 | Function fallback);
196 |
197 | /**
198 | * Sets a default fallback {@link Function} to be invoked when the circuit breaker is open or when failure
199 | * occurs and {@link CircuitBreakerOptions#isFallbackOnFailure()} is enabled.
200 | *
201 | * The function gets the exception as parameter and returns the fallback result.
202 | *
203 | * @param handler the fallback handler
204 | * @return this {@link CircuitBreaker}
205 | */
206 | @Fluent
207 | CircuitBreaker fallback(Function handler);
208 |
209 | /**
210 | * Configures the failure policy for this circuit-breaker.
211 | *
212 | * @return the current {@link CircuitBreaker}
213 | * @see FailurePolicy
214 | */
215 | @Fluent
216 | default CircuitBreaker failurePolicy(FailurePolicy failurePolicy) {
217 | return this;
218 | }
219 |
220 | /**
221 | * Resets the circuit breaker state. The number of recent failures is set to 0 and if the state is half-open,
222 | * it is set to closed.
223 | *
224 | * @return this {@link CircuitBreaker}
225 | */
226 | @Fluent
227 | CircuitBreaker reset();
228 |
229 | /**
230 | * Explicitly opens the circuit breaker.
231 | *
232 | * @return this {@link CircuitBreaker}
233 | */
234 | @Fluent
235 | CircuitBreaker open();
236 |
237 | /**
238 | * @return the current state of this circuit breaker
239 | */
240 | CircuitBreakerState state();
241 |
242 | /**
243 | * @return the current number of recorded failures
244 | */
245 | long failureCount();
246 |
247 | /**
248 | * @return the name of this circuit breaker
249 | */
250 | @CacheReturn
251 | String name();
252 |
253 | /**
254 | * Set a {@link RetryPolicy} which computes a delay before a retry attempt.
255 | */
256 | @Fluent
257 | CircuitBreaker retryPolicy(RetryPolicy retryPolicy);
258 | }
259 |
--------------------------------------------------------------------------------
/src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptions.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2016 The original author or authors
3 | *
4 | * All rights reserved. This program and the accompanying materials
5 | * are made available under the terms of the Eclipse Public License v1.0
6 | * and Apache License v2.0 which accompanies this distribution.
7 | *
8 | * The Eclipse Public License is available at
9 | * http://www.eclipse.org/legal/epl-v10.html
10 | *
11 | * The Apache License v2.0 is available at
12 | * http://www.opensource.org/licenses/apache2.0.php
13 | *
14 | * You may elect to redistribute this code under either of these licenses.
15 | */
16 |
17 | package io.vertx.circuitbreaker;
18 |
19 | import io.vertx.codegen.annotations.DataObject;
20 | import io.vertx.codegen.json.annotations.JsonGen;
21 | import io.vertx.core.json.JsonObject;
22 |
23 | /**
24 | * Circuit breaker configuration options. All time values are in milliseconds.
25 | *
26 | * @author Clement Escoffier
27 | */
28 | @DataObject
29 | @JsonGen(publicConverter = false)
30 | public class CircuitBreakerOptions {
31 |
32 | /**
33 | * Default timeout in milliseconds.
34 | */
35 | public static final long DEFAULT_TIMEOUT = 10_000L;
36 |
37 | /**
38 | * Default number of failures after which a closed circuit breaker moves to open.
39 | */
40 | public static final int DEFAULT_MAX_FAILURES = 5;
41 |
42 | /**
43 | * Default value of the {@linkplain #isFallbackOnFailure() fallback on failure} property.
44 | */
45 | public static final boolean DEFAULT_FALLBACK_ON_FAILURE = false;
46 |
47 | /**
48 | * Default time after which an open circuit breaker moves to half-open (in an attempt to re-close) in milliseconds.
49 | */
50 | public static final long DEFAULT_RESET_TIMEOUT = 30_000;
51 |
52 | /**
53 | * Default value of whether circuit breaker state events should be delivered only to local consumers.
54 | */
55 | public static final boolean DEFAULT_NOTIFICATION_LOCAL_ONLY = true;
56 |
57 | /**
58 | * A default address on which the circuit breakers can send their updates.
59 | */
60 | public static final String DEFAULT_NOTIFICATION_ADDRESS = "vertx.circuit-breaker";
61 |
62 | /**
63 | * Default notification period in milliseconds.
64 | */
65 | public static final long DEFAULT_NOTIFICATION_PERIOD = 2_000;
66 |
67 | /**
68 | * Default length of rolling window for metrics in milliseconds.
69 | */
70 | public static final long DEFAULT_METRICS_ROLLING_WINDOW = 10_000;
71 |
72 | /**
73 | * Default number of buckets used for the metrics rolling window.
74 | */
75 | public static final int DEFAULT_METRICS_ROLLING_BUCKETS = 10;
76 |
77 | /**
78 | * Default number of retries.
79 | */
80 | private static final int DEFAULT_MAX_RETRIES = 0;
81 |
82 | /**
83 | * Default length of rolling window for failures in milliseconds.
84 | */
85 | private static final int DEFAULT_FAILURES_ROLLING_WINDOW = 10_000;
86 |
87 | /**
88 | * The operation timeout.
89 | */
90 | private long timeout = DEFAULT_TIMEOUT;
91 |
92 | /**
93 | * The max failures.
94 | */
95 | private int maxFailures = DEFAULT_MAX_FAILURES;
96 |
97 | /**
98 | * Whether the fallback should be called upon failures.
99 | */
100 | private boolean fallbackOnFailure = DEFAULT_FALLBACK_ON_FAILURE;
101 |
102 | /**
103 | * The reset timeout.
104 | */
105 | private long resetTimeout = DEFAULT_RESET_TIMEOUT;
106 |
107 | /**
108 | * Whether circuit breaker state should be delivered only to local consumers.
109 | */
110 | private boolean notificationLocalOnly = DEFAULT_NOTIFICATION_LOCAL_ONLY;
111 |
112 | /**
113 | * The event bus address on which the circuit breaker state is published.
114 | */
115 | private String notificationAddress = null;
116 |
117 | /**
118 | * The state publication period in ms.
119 | */
120 | private long notificationPeriod = DEFAULT_NOTIFICATION_PERIOD;
121 |
122 | /**
123 | * The number of retries
124 | */
125 | private int maxRetries = DEFAULT_MAX_RETRIES;
126 |
127 | /**
128 | * The metric rolling window in ms.
129 | */
130 | private long metricsRollingWindow = DEFAULT_METRICS_ROLLING_WINDOW;
131 |
132 | /**
133 | * The number of buckets used for the metric rolling window.
134 | */
135 | private int metricsRollingBuckets = DEFAULT_METRICS_ROLLING_BUCKETS;
136 |
137 | /**
138 | * The failure rolling window in ms.
139 | */
140 | private long failuresRollingWindow = DEFAULT_FAILURES_ROLLING_WINDOW;
141 |
142 | /**
143 | * Creates a new instance of {@link CircuitBreakerOptions} using the default values.
144 | */
145 | public CircuitBreakerOptions() {
146 | // Empty constructor
147 | }
148 |
149 | /**
150 | * Creates a new instance of {@link CircuitBreakerOptions} by copying the other instance.
151 | *
152 | * @param other the instance fo copy
153 | */
154 | public CircuitBreakerOptions(CircuitBreakerOptions other) {
155 | this.timeout = other.timeout;
156 | this.maxFailures = other.maxFailures;
157 | this.fallbackOnFailure = other.fallbackOnFailure;
158 | this.notificationLocalOnly = other.notificationLocalOnly;
159 | this.notificationAddress = other.notificationAddress;
160 | this.notificationPeriod = other.notificationPeriod;
161 | this.resetTimeout = other.resetTimeout;
162 | this.maxRetries = other.maxRetries;
163 | this.metricsRollingBuckets = other.metricsRollingBuckets;
164 | this.metricsRollingWindow = other.metricsRollingWindow;
165 | this.failuresRollingWindow = other.failuresRollingWindow;
166 | }
167 |
168 | /**
169 | * Creates a new instance of {@link CircuitBreakerOptions} from the given JSON object.
170 | *
171 | * @param json the JSON object
172 | */
173 | public CircuitBreakerOptions(JsonObject json) {
174 | this();
175 | CircuitBreakerOptionsConverter.fromJson(json, this);
176 | }
177 |
178 | /**
179 | * @return a JSON object representing this configuration
180 | */
181 | public JsonObject toJson() {
182 | JsonObject json = new JsonObject();
183 | CircuitBreakerOptionsConverter.toJson(this, json);
184 | return json;
185 | }
186 |
187 | /**
188 | * @return the maximum number of failures before opening the circuit breaker
189 | */
190 | public int getMaxFailures() {
191 | return maxFailures;
192 | }
193 |
194 | /**
195 | * Sets the maximum number of failures before opening the circuit breaker.
196 | *
197 | * @param maxFailures the number of failures.
198 | * @return this {@link CircuitBreakerOptions}
199 | */
200 | public CircuitBreakerOptions setMaxFailures(int maxFailures) {
201 | this.maxFailures = maxFailures;
202 | return this;
203 | }
204 |
205 | /**
206 | * @return the configured timeout in milliseconds
207 | */
208 | public long getTimeout() {
209 | return timeout;
210 | }
211 |
212 | /**
213 | * Sets the timeout in milliseconds. If an action does not complete before this timeout, the action is considered as
214 | * a failure.
215 | *
216 | * @param timeoutInMs the timeout, -1 to disable the timeout
217 | * @return this {@link CircuitBreakerOptions}
218 | */
219 | public CircuitBreakerOptions setTimeout(long timeoutInMs) {
220 | this.timeout = timeoutInMs;
221 | return this;
222 | }
223 |
224 | /**
225 | * @return whether the fallback is executed on failures, even when the circuit breaker is closed
226 | */
227 | public boolean isFallbackOnFailure() {
228 | return fallbackOnFailure;
229 | }
230 |
231 | /**
232 | * Sets whether the fallback is executed on failure, even when the circuit breaker is closed.
233 | *
234 | * @param fallbackOnFailure {@code true} to enable it.
235 | * @return this {@link CircuitBreakerOptions}
236 | */
237 | public CircuitBreakerOptions setFallbackOnFailure(boolean fallbackOnFailure) {
238 | this.fallbackOnFailure = fallbackOnFailure;
239 | return this;
240 | }
241 |
242 | /**
243 | * @return the time in milliseconds before an open circuit breaker moves to half-open (in an attempt to re-close)
244 | */
245 | public long getResetTimeout() {
246 | return resetTimeout;
247 | }
248 |
249 | /**
250 | * Sets the time in milliseconds before an open circuit breaker moves to half-open (in an attempt to re-close).
251 | * If the circuit breaker is closed when the timeout is reached, nothing happens. {@code -1} disables this feature.
252 | *
253 | * @param resetTimeout the time in ms
254 | * @return this {@link CircuitBreakerOptions}
255 | */
256 | public CircuitBreakerOptions setResetTimeout(long resetTimeout) {
257 | this.resetTimeout = resetTimeout;
258 | return this;
259 | }
260 |
261 | /**
262 | * @return {@code true} if circuit breaker state events should be delivered only to local consumers,
263 | * {@code false} otherwise
264 | */
265 | public boolean isNotificationLocalOnly() {
266 | return notificationLocalOnly;
267 | }
268 |
269 | /**
270 | * Sets whether circuit breaker state events should be delivered only to local consumers.
271 | *
272 | * @param notificationLocalOnly {@code true} if circuit breaker state events should be delivered only to local consumers, {@code false} otherwise
273 | * @return this {@link CircuitBreakerOptions}
274 | */
275 | public CircuitBreakerOptions setNotificationLocalOnly(boolean notificationLocalOnly) {
276 | this.notificationLocalOnly = notificationLocalOnly;
277 | return this;
278 | }
279 |
280 | /**
281 | * @return the eventbus address on which the circuit breaker events are published, or {@code null} if this feature has
282 | * been disabled
283 | */
284 | public String getNotificationAddress() {
285 | return notificationAddress;
286 | }
287 |
288 | /**
289 | * Sets the event bus address on which the circuit breaker publishes its state changes.
290 | *
291 | * @param notificationAddress the address, {@code null} to disable this feature
292 | * @return this {@link CircuitBreakerOptions}
293 | */
294 | public CircuitBreakerOptions setNotificationAddress(String notificationAddress) {
295 | this.notificationAddress = notificationAddress;
296 | return this;
297 | }
298 |
299 | /**
300 | * @return the period in milliseconds in which the circuit breaker sends notifications about its state
301 | */
302 | public long getNotificationPeriod() {
303 | return notificationPeriod;
304 | }
305 |
306 | /**
307 | * Sets the period in milliseconds in which the circuit breaker sends notifications on the event bus with its
308 | * current state.
309 | *
310 | * @param notificationPeriod the period, 0 to disable this feature.
311 | * @return this {@link CircuitBreakerOptions}
312 | */
313 | public CircuitBreakerOptions setNotificationPeriod(long notificationPeriod) {
314 | this.notificationPeriod = notificationPeriod;
315 | return this;
316 | }
317 |
318 | /**
319 | * @return the configured length of rolling window for metrics
320 | */
321 | public long getMetricsRollingWindow() {
322 | return metricsRollingWindow;
323 | }
324 |
325 | /**
326 | * Sets the rolling window length used for metrics.
327 | *
328 | * @param metricsRollingWindow the period in milliseconds
329 | * @return this {@link CircuitBreakerOptions}
330 | */
331 | public CircuitBreakerOptions setMetricsRollingWindow(long metricsRollingWindow) {
332 | this.metricsRollingWindow = metricsRollingWindow;
333 | return this;
334 | }
335 |
336 | /**
337 | * @return the configured length of rolling window for failures
338 | */
339 | public long getFailuresRollingWindow() {
340 | return failuresRollingWindow;
341 | }
342 |
343 | /**
344 | * Sets the rolling window length used for failures.
345 | *
346 | * @param failureRollingWindow the period in milliseconds
347 | * @return this {@link CircuitBreakerOptions}
348 | */
349 | public CircuitBreakerOptions setFailuresRollingWindow(long failureRollingWindow) {
350 | this.failuresRollingWindow = failureRollingWindow;
351 | return this;
352 | }
353 |
354 | /**
355 | * @return the configured number of buckets the metrics rolling window is divided into
356 | */
357 | public int getMetricsRollingBuckets() {
358 | return metricsRollingBuckets;
359 | }
360 |
361 | /**
362 | * Sets the number of buckets the metrics rolling window is divided into.
363 | *
364 | * The following must be true: {@code metricsRollingWindow % metricsRollingBuckets == 0},
365 | * otherwise an exception will be thrown.
366 | * For example, 10000/10 is okay, so is 10000/20, but 10000/7 is not.
367 | *
368 | * @param metricsRollingBuckets the number of buckets
369 | * @return this {@link CircuitBreakerOptions}
370 | */
371 | public CircuitBreakerOptions setMetricsRollingBuckets(int metricsRollingBuckets) {
372 | this.metricsRollingBuckets = metricsRollingBuckets;
373 | return this;
374 | }
375 |
376 | /**
377 | * @return the number of times the circuit breaker retries an operation before failing
378 | */
379 | public int getMaxRetries() {
380 | return maxRetries;
381 | }
382 |
383 | /**
384 | * Sets the number of times the circuit breaker retries an operation before failing.
385 | *
386 | * @param maxRetries the number of retries, 0 to disable retrying
387 | * @return this {@link CircuitBreakerOptions}
388 | */
389 | public CircuitBreakerOptions setMaxRetries(int maxRetries) {
390 | this.maxRetries = maxRetries;
391 | return this;
392 | }
393 | }
394 |
--------------------------------------------------------------------------------
/src/main/java/io/vertx/circuitbreaker/CircuitBreakerState.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2016 The original author or authors
3 | *
4 | * All rights reserved. This program and the accompanying materials
5 | * are made available under the terms of the Eclipse Public License v1.0
6 | * and Apache License v2.0 which accompanies this distribution.
7 | *
8 | * The Eclipse Public License is available at
9 | * http://www.eclipse.org/legal/epl-v10.html
10 | *
11 | * The Apache License v2.0 is available at
12 | * http://www.opensource.org/licenses/apache2.0.php
13 | *
14 | * You may elect to redistribute this code under either of these licenses.
15 | */
16 |
17 | package io.vertx.circuitbreaker;
18 |
19 | import io.vertx.codegen.annotations.VertxGen;
20 |
21 | /**
22 | * Circuit breaker states.
23 | *
24 | * @author Clement Escoffier
25 | */
26 | @VertxGen
27 | public enum CircuitBreakerState {
28 | /**
29 | * The {@code OPEN} state. The circuit breaker is executing the fallback, and switches to the {@link #HALF_OPEN}
30 | * state after the specified time.
31 | */
32 | OPEN,
33 | /**
34 | * The {@code CLOSED} state. The circuit breaker lets invocations pass and collects the failures. If the number of
35 | * failures reach the specified threshold, the circuit breaker switches to the {@link #OPEN} state.
36 | */
37 | CLOSED,
38 | /**
39 | * The {@code HALF_OPEN} state. The circuit breaker has been opened, and is now checking the current situation. It
40 | * lets the next invocation pass and determines from the result (failure or success) if the circuit breaker can
41 | * be switched to the {@link #CLOSED} state again.
42 | */
43 | HALF_OPEN
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/io/vertx/circuitbreaker/FailurePolicy.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker;
2 |
3 | import io.vertx.codegen.annotations.VertxGen;
4 | import io.vertx.core.AsyncResult;
5 | import io.vertx.core.Future;
6 |
7 | import java.util.function.Predicate;
8 |
9 | /**
10 | * A failure policy for the {@link CircuitBreaker}.
11 | *
12 | * The default policy is to consider an asynchronous result as a failure if {@link AsyncResult#failed()} returns {@code true}.
13 | * Nevertheless, sometimes this is not good enough. For example, an HTTP Client could return a response, but with an unexpected status code.
14 | *
15 | * In this case, a custom failure policy can be configured with {@link CircuitBreaker#failurePolicy(FailurePolicy)}.
16 | */
17 | @VertxGen
18 | public interface FailurePolicy extends Predicate> {
19 |
20 | /**
21 | * The default policy, which considers an asynchronous result as a failure if {@link AsyncResult#failed()} returns {@code true}.
22 | */
23 | static FailurePolicy defaultPolicy() {
24 | return AsyncResult::failed;
25 | }
26 |
27 | /**
28 | * Invoked by the {@link CircuitBreaker} when an operation completes.
29 | *
30 | * @param future a completed future
31 | * @return {@code true} if the asynchronous result should be considered as a failure, {@code false} otherwise
32 | */
33 | @Override
34 | boolean test(Future future);
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/io/vertx/circuitbreaker/OpenCircuitException.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker;
2 |
3 | /**
4 | * Exception reported when the circuit breaker is open.
5 | *
6 | * For performance reason, this exception does not carry a stack trace. You are not allowed to set a stack trace or a
7 | * cause to this exception. This immutability allows using a singleton instance.
8 | *
9 | * @author Clement Escoffier
10 | */
11 | public class OpenCircuitException extends RuntimeException {
12 |
13 | public static OpenCircuitException INSTANCE = new OpenCircuitException();
14 |
15 | private OpenCircuitException() {
16 | super("open circuit", null, false, false);
17 | }
18 |
19 | @Override
20 | public void setStackTrace(StackTraceElement[] stackTrace) {
21 | throw new UnsupportedOperationException();
22 | }
23 |
24 | @Override
25 | public synchronized Throwable initCause(Throwable cause) {
26 | throw new UnsupportedOperationException();
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/io/vertx/circuitbreaker/RetryPolicy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
3 | *
4 | * This program and the accompanying materials are made available under the
5 | * terms of the Eclipse Public License 2.0 which is available at
6 | * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7 | * which is available at https://www.apache.org/licenses/LICENSE-2.0.
8 | *
9 | * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10 | */
11 |
12 | package io.vertx.circuitbreaker;
13 |
14 | import io.vertx.codegen.annotations.VertxGen;
15 |
16 | import java.util.concurrent.ThreadLocalRandom;
17 |
18 | import static java.lang.Math.*;
19 |
20 | /**
21 | * A policy for retry execution.
22 | */
23 | @VertxGen
24 | @FunctionalInterface
25 | public interface RetryPolicy {
26 |
27 | /**
28 | * Create a constant delay retry policy.
29 | *
30 | * @param delay the constant delay in milliseconds
31 | */
32 | static RetryPolicy constantDelay(long delay) {
33 | if (delay <= 0) {
34 | throw new IllegalArgumentException("delay must be strictly positive");
35 | }
36 | return (failure, retryCount) -> delay;
37 | }
38 |
39 | /**
40 | * Create a linear delay retry policy.
41 | *
42 | * @param initialDelay the initial delay in milliseconds
43 | * @param maxDelay maximum delay in milliseconds
44 | */
45 | static RetryPolicy linearDelay(long initialDelay, long maxDelay) {
46 | if (initialDelay <= 0) {
47 | throw new IllegalArgumentException("initialDelay must be strictly positive");
48 | }
49 | if (maxDelay < initialDelay) {
50 | throw new IllegalArgumentException("maxDelay must be greater than initialDelay");
51 | }
52 | return (failure, retryCount) -> min(maxDelay, initialDelay * retryCount);
53 | }
54 |
55 | /**
56 | * Create an exponential delay with jitter retry policy.
57 | *
58 | * Based on the Full Jitter approach described in
59 | * Exponential Backoff And Jitter.
60 | *
61 | * @param initialDelay the initial delay in milliseconds
62 | * @param maxDelay maximum delay in milliseconds
63 | */
64 | static RetryPolicy exponentialDelayWithJitter(long initialDelay, long maxDelay) {
65 | if (initialDelay <= 0) {
66 | throw new IllegalArgumentException("initialDelay must be strictly positive");
67 | }
68 | if (maxDelay < initialDelay) {
69 | throw new IllegalArgumentException("maxDelay must be greater than initialDelay");
70 | }
71 | return (failure, retryCount) -> {
72 | ThreadLocalRandom random = ThreadLocalRandom.current();
73 | long delay = initialDelay * (1L << retryCount);
74 | return random.nextLong(0, delay < 0 ? maxDelay : min(maxDelay, delay));
75 | };
76 | }
77 |
78 | /**
79 | * Compute a delay in milliseconds before retry is executed.
80 | *
81 | * @param failure the failure of the previous execution attempt
82 | * @param retryCount the number of times operation has been retried already
83 | * @return a delay in milliseconds before retry is executed
84 | */
85 | long delay(Throwable failure, int retryCount);
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/java/io/vertx/circuitbreaker/TimeoutException.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker;
2 |
3 | /**
4 | * Exception reported when the monitored operation timed out.
5 | *
6 | * For performance reason, this exception does not carry a stack trace. You are not allowed to set a stack trace or a
7 | * cause to this exception. This immutability allows using a singleton instance.
8 | *
9 | * @author Clement Escoffier
10 | */
11 | public class TimeoutException extends RuntimeException {
12 |
13 | public static TimeoutException INSTANCE = new TimeoutException();
14 |
15 | private TimeoutException() {
16 | super("operation timeout", null, false, false);
17 | }
18 |
19 | @Override
20 | public void setStackTrace(StackTraceElement[] stackTrace) {
21 | throw new UnsupportedOperationException();
22 | }
23 |
24 | @Override
25 | public synchronized Throwable initCause(Throwable cause) {
26 | throw new UnsupportedOperationException();
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2016 The original author or authors
3 | *
4 | * All rights reserved. This program and the accompanying materials
5 | * are made available under the terms of the Eclipse Public License v1.0
6 | * and Apache License v2.0 which accompanies this distribution.
7 | *
8 | * The Eclipse Public License is available at
9 | * http://www.eclipse.org/legal/epl-v10.html
10 | *
11 | * The Apache License v2.0 is available at
12 | * http://www.opensource.org/licenses/apache2.0.php
13 | *
14 | * You may elect to redistribute this code under either of these licenses.
15 | */
16 |
17 | package io.vertx.circuitbreaker.impl;
18 |
19 | import io.vertx.circuitbreaker.*;
20 | import io.vertx.core.*;
21 | import io.vertx.core.eventbus.DeliveryOptions;
22 | import io.vertx.core.internal.ContextInternal;
23 | import io.vertx.core.internal.PromiseInternal;
24 | import io.vertx.core.json.JsonObject;
25 |
26 | import java.util.LinkedHashMap;
27 | import java.util.Map;
28 | import java.util.Objects;
29 | import java.util.concurrent.TimeUnit;
30 | import java.util.concurrent.atomic.AtomicInteger;
31 | import java.util.function.Function;
32 | import java.util.function.Supplier;
33 |
34 | /**
35 | * @author Clement Escoffier
36 | */
37 | public class CircuitBreakerImpl implements CircuitBreaker {
38 |
39 | private static final Handler NOOP = (v) -> {
40 | // Nothing...
41 | };
42 |
43 | private final Vertx vertx;
44 | private final CircuitBreakerOptions options;
45 | private final String name;
46 | private final long periodicUpdateTask;
47 |
48 | private Handler openHandler = NOOP;
49 | private Handler halfOpenHandler = NOOP;
50 | private Handler closeHandler = NOOP;
51 | private Function fallback = null;
52 | private FailurePolicy failurePolicy = FailurePolicy.defaultPolicy();
53 |
54 | private CircuitBreakerState state = CircuitBreakerState.CLOSED;
55 | private RollingCounter rollingFailures;
56 |
57 | private final AtomicInteger passed = new AtomicInteger();
58 |
59 | private final CircuitBreakerMetrics metrics;
60 | private RetryPolicy retryPolicy = (failure, retryCount) -> 0L;
61 |
62 | public CircuitBreakerImpl(String name, Vertx vertx, CircuitBreakerOptions options) {
63 | Objects.requireNonNull(name);
64 | Objects.requireNonNull(vertx);
65 | this.vertx = vertx;
66 | this.name = name;
67 |
68 | if (options == null) {
69 | this.options = new CircuitBreakerOptions();
70 | } else {
71 | this.options = new CircuitBreakerOptions(options);
72 | }
73 |
74 | this.rollingFailures = new RollingCounter(this.options.getFailuresRollingWindow() / 1000, TimeUnit.SECONDS);
75 |
76 | if (this.options.getNotificationAddress() != null) {
77 | this.metrics = new CircuitBreakerMetrics(vertx, this, this.options);
78 | sendUpdateOnEventBus();
79 | if (this.options.getNotificationPeriod() > 0) {
80 | this.periodicUpdateTask = vertx.setPeriodic(this.options.getNotificationPeriod(), l -> sendUpdateOnEventBus());
81 | } else {
82 | this.periodicUpdateTask = -1;
83 | }
84 | } else {
85 | this.metrics = null;
86 | this.periodicUpdateTask = -1;
87 | }
88 | }
89 |
90 | @Override
91 | public CircuitBreaker close() {
92 | if (metrics != null) {
93 | if (periodicUpdateTask != -1) {
94 | vertx.cancelTimer(periodicUpdateTask);
95 | }
96 | metrics.close();
97 | }
98 | return this;
99 | }
100 |
101 | @Override
102 | public synchronized CircuitBreaker openHandler(Handler handler) {
103 | Objects.requireNonNull(handler);
104 | openHandler = handler;
105 | return this;
106 | }
107 |
108 | @Override
109 | public synchronized CircuitBreaker halfOpenHandler(Handler handler) {
110 | Objects.requireNonNull(handler);
111 | halfOpenHandler = handler;
112 | return this;
113 | }
114 |
115 | @Override
116 | public synchronized CircuitBreaker closeHandler(Handler handler) {
117 | Objects.requireNonNull(handler);
118 | closeHandler = handler;
119 | return this;
120 | }
121 |
122 | @Override
123 | public CircuitBreaker fallback(Function handler) {
124 | Objects.requireNonNull(handler);
125 | fallback = handler;
126 | return this;
127 | }
128 |
129 | @Override
130 | public CircuitBreaker failurePolicy(FailurePolicy failurePolicy) {
131 | Objects.requireNonNull(failurePolicy);
132 | this.failurePolicy = failurePolicy;
133 | return this;
134 | }
135 |
136 | /**
137 | * A version of {@link #reset()} that can forcefully change the state to closed even if the circuit breaker is open.
138 | *
139 | * This is an internal API.
140 | *
141 | * @param force whether we force the state change and allow an illegal transition
142 | * @return this circuit breaker
143 | */
144 | public synchronized CircuitBreaker reset(boolean force) {
145 | rollingFailures.reset();
146 |
147 | if (state == CircuitBreakerState.CLOSED) {
148 | // Do nothing else.
149 | return this;
150 | }
151 |
152 | if (!force && state == CircuitBreakerState.OPEN) {
153 | // Resetting the circuit breaker while we are in the open state is an illegal transition
154 | return this;
155 | }
156 |
157 | state = CircuitBreakerState.CLOSED;
158 | closeHandler.handle(null);
159 | sendUpdateOnEventBus();
160 | return this;
161 | }
162 |
163 | @Override
164 | public synchronized CircuitBreaker reset() {
165 | return reset(false);
166 | }
167 |
168 | private synchronized void sendUpdateOnEventBus() {
169 | if (metrics != null) {
170 | DeliveryOptions deliveryOptions = new DeliveryOptions()
171 | .setLocalOnly(options.isNotificationLocalOnly());
172 | vertx.eventBus().publish(options.getNotificationAddress(), metrics.toJson(), deliveryOptions);
173 | }
174 | }
175 |
176 | @Override
177 | public synchronized CircuitBreaker open() {
178 | state = CircuitBreakerState.OPEN;
179 | openHandler.handle(null);
180 | sendUpdateOnEventBus();
181 |
182 | // Set up the attempt reset timer
183 | long period = options.getResetTimeout();
184 | if (period != -1) {
185 | vertx.setTimer(period, l -> attemptReset());
186 | }
187 |
188 | return this;
189 | }
190 |
191 | @Override
192 | public synchronized long failureCount() {
193 | return rollingFailures.count();
194 | }
195 |
196 | @Override
197 | public synchronized CircuitBreakerState state() {
198 | return state;
199 | }
200 |
201 | private synchronized CircuitBreaker attemptReset() {
202 | if (state == CircuitBreakerState.OPEN) {
203 | passed.set(0);
204 | state = CircuitBreakerState.HALF_OPEN;
205 | halfOpenHandler.handle(null);
206 | sendUpdateOnEventBus();
207 | }
208 | return this;
209 | }
210 |
211 | @Override
212 | public CircuitBreaker executeAndReportWithFallback(Promise resultPromise,
213 | Handler> command,
214 | Function fallback) {
215 | ContextInternal context = (ContextInternal) vertx.getOrCreateContext();
216 | executeAndReportWithFallback(context, resultPromise, convert(context, command), fallback);
217 | return this;
218 | }
219 |
220 | public void executeAndReportWithFallback(
221 | ContextInternal context,
222 | Promise resultPromise,
223 | Supplier> command,
224 | Function fallback) {
225 |
226 | CircuitBreakerState currentState;
227 | synchronized (this) {
228 | currentState = state;
229 | }
230 |
231 | CircuitBreakerMetrics.Operation operationMetrics = metrics != null ? metrics.enqueue() : null;
232 |
233 | // this future object tracks the completion of the operation
234 | // This future is marked as failed on operation failures and timeout.
235 | Promise operationResult = context.promise();
236 |
237 | if (currentState == CircuitBreakerState.CLOSED) {
238 | Future opFuture = operationResult.future();
239 | opFuture.onComplete(new ClosedCircuitCompletion<>(context, resultPromise, fallback, operationMetrics));
240 | if (options.getMaxRetries() > 0) {
241 | executeOperation(command, retryPromise(context, 0, command, operationResult, operationMetrics), operationMetrics);
242 | } else {
243 | executeOperation(command, operationResult, operationMetrics);
244 | }
245 | } else if (currentState == CircuitBreakerState.OPEN) {
246 | // Fallback immediately
247 | if (operationMetrics != null) {
248 | operationMetrics.shortCircuited();
249 | }
250 | invokeFallback(OpenCircuitException.INSTANCE, resultPromise, fallback, operationMetrics);
251 | } else if (currentState == CircuitBreakerState.HALF_OPEN) {
252 | if (passed.incrementAndGet() == 1) {
253 | Future opFuture = operationResult.future();
254 | opFuture.onComplete(new HalfOpenedCircuitCompletion<>(context, resultPromise, fallback, operationMetrics));
255 | // Execute the operation
256 | executeOperation(command, operationResult, operationMetrics);
257 | } else {
258 | // Not selected, fallback.
259 | if (operationMetrics != null) {
260 | operationMetrics.shortCircuited();
261 | }
262 | invokeFallback(OpenCircuitException.INSTANCE, resultPromise, fallback, operationMetrics);
263 | }
264 | }
265 | }
266 |
267 | private Promise retryPromise(ContextInternal context, int retryCount, Supplier> command,
268 | Promise operationResult, CircuitBreakerMetrics.Operation operationMetrics) {
269 |
270 | Promise promise = context.promise();
271 | promise.future().onComplete(event -> {
272 | if (event.succeeded()) {
273 | reset();
274 | operationResult.complete(event.result());
275 | return;
276 | }
277 |
278 | CircuitBreakerState currentState;
279 | synchronized (this) {
280 | currentState = state;
281 | }
282 |
283 | if (currentState == CircuitBreakerState.CLOSED) {
284 | if (retryCount < options.getMaxRetries() - 1) {
285 | executeRetryWithDelay(event.cause(), retryCount, l -> {
286 | // Don't report timeout or error in the retry attempt, only the last one.
287 | executeOperation(command, retryPromise(context, retryCount + 1, command, operationResult, null),
288 | operationMetrics);
289 | });
290 | } else {
291 | executeRetryWithDelay(event.cause(), retryCount, l -> {
292 | executeOperation(command, operationResult, operationMetrics);
293 | });
294 | }
295 | } else {
296 | operationResult.fail(OpenCircuitException.INSTANCE);
297 | }
298 | });
299 | return promise;
300 | }
301 |
302 | private void executeRetryWithDelay(Throwable failure, int retryCount, Handler action) {
303 | long retryDelay = retryPolicy.delay(failure, retryCount + 1);
304 |
305 | if (retryDelay > 0) {
306 | vertx.setTimer(retryDelay, l -> {
307 | action.handle(null);
308 | });
309 | } else {
310 | action.handle(null);
311 | }
312 | }
313 |
314 | private void invokeFallback(Throwable reason, Promise resultPromise,
315 | Function fallback, CircuitBreakerMetrics.Operation operationMetrics) {
316 | if (fallback == null) {
317 | // No fallback, mark the user future as failed.
318 | resultPromise.fail(reason);
319 | return;
320 | }
321 |
322 | try {
323 | T apply = fallback.apply(reason);
324 | if (operationMetrics != null) {
325 | operationMetrics.fallbackSucceed();
326 | }
327 | resultPromise.complete(apply);
328 | } catch (Exception e) {
329 | resultPromise.fail(e);
330 | if (operationMetrics != null) {
331 | operationMetrics.fallbackFailed();
332 | }
333 | }
334 | }
335 |
336 | private Supplier> convert(ContextInternal context, Handler> handler) {
337 | // We use an intermediate future to avoid the passed future to complete or fail after a timeout.
338 | return () -> {
339 | Promise passedFuture = context.promise();
340 | handler.handle(passedFuture);
341 | return passedFuture.future();
342 | };
343 | }
344 |
345 | private void executeOperation(Supplier> supplier, Promise operationResult,
346 | CircuitBreakerMetrics.Operation operationMetrics) {
347 | // Execute the operation
348 | Future fut;
349 | try {
350 | fut = supplier.get();
351 | } catch (Throwable e) {
352 | if (!operationResult.future().isComplete()) {
353 | if (operationMetrics != null) {
354 | operationMetrics.error();
355 | }
356 | operationResult.fail(e);
357 | }
358 | return;
359 | }
360 |
361 | if (options.getTimeout() != -1) {
362 | long timerId = vertx.setTimer(options.getTimeout(), (l) -> {
363 | // Check if the operation has not already been completed
364 | if (!operationResult.future().isComplete()) {
365 | if (operationMetrics != null) {
366 | operationMetrics.timeout();
367 | }
368 | operationResult.fail(TimeoutException.INSTANCE);
369 | }
370 | // Else Operation has completed
371 | });
372 | fut.onComplete(v -> vertx.cancelTimer(timerId));
373 | }
374 |
375 | fut.onComplete(ar -> {
376 | if (ar.failed()) {
377 | if (!operationResult.future().isComplete()) {
378 | operationResult.fail(ar.cause());
379 | }
380 | } else {
381 | if (!operationResult.future().isComplete()) {
382 | operationResult.complete(ar.result());
383 | }
384 | }
385 | });
386 | }
387 |
388 | @Override
389 | public Future executeWithFallback(Handler> operation, Function fallback) {
390 | // be careful to not create a new context, to preserve existing (sometimes synchronous) behavior
391 | ContextInternal context = ContextInternal.current();
392 | Promise promise = context != null ? context.promise() : Promise.promise();
393 | executeAndReportWithFallback(promise, operation, fallback);
394 | return promise.future();
395 | }
396 |
397 | @Override
398 | public Future executeWithFallback(Supplier> command, Function fallback) {
399 | ContextInternal context = (ContextInternal) vertx.getOrCreateContext();
400 | Promise resultPromise = context.promise();
401 | executeAndReportWithFallback(context, resultPromise, command, fallback);
402 | return resultPromise.future();
403 | }
404 |
405 | public Future execute(Handler> operation) {
406 | return executeWithFallback(operation, fallback);
407 | }
408 |
409 | @Override
410 | public Future execute(Supplier> command) {
411 | return executeWithFallback(command, fallback);
412 | }
413 |
414 | @Override
415 | public CircuitBreaker executeAndReport(Promise resultPromise, Handler> operation) {
416 | return executeAndReportWithFallback(resultPromise, operation, fallback);
417 | }
418 |
419 | @Override
420 | public String name() {
421 | return name;
422 | }
423 |
424 | private synchronized void incrementFailures() {
425 | rollingFailures.increment();
426 | if (rollingFailures.count() >= options.getMaxFailures()) {
427 | if (state != CircuitBreakerState.OPEN) {
428 | open();
429 | } else {
430 | // `open()` calls `sendUpdateOnEventBus()`, so no need to repeat it in the previous case
431 | sendUpdateOnEventBus();
432 | }
433 | } else {
434 | // Number of failure has changed, send update.
435 | sendUpdateOnEventBus();
436 | }
437 | }
438 |
439 | /**
440 | * For testing purpose only.
441 | *
442 | * @return retrieve the metrics.
443 | */
444 | public JsonObject getMetrics() {
445 | return metrics.toJson();
446 | }
447 |
448 | public CircuitBreakerOptions options() {
449 | return options;
450 | }
451 |
452 | @Override
453 | public CircuitBreaker retryPolicy(RetryPolicy retryPolicy) {
454 | this.retryPolicy = retryPolicy;
455 | return this;
456 | }
457 |
458 | static class RollingCounter {
459 | // all `RollingCounter` methods are called in a `synchronized (CircuitBreakerImpl.this)` block,
460 | // which therefore guards access to these fields
461 |
462 | private Map window;
463 | private long timeUnitsInWindow;
464 | private TimeUnit windowTimeUnit;
465 |
466 | public RollingCounter(long timeUnitsInWindow, TimeUnit windowTimeUnit) {
467 | this.windowTimeUnit = windowTimeUnit;
468 | this.window = new LinkedHashMap((int) timeUnitsInWindow + 1) {
469 | @Override
470 | protected boolean removeEldestEntry(Map.Entry eldest) {
471 | return size() > timeUnitsInWindow;
472 | }
473 | };
474 | this.timeUnitsInWindow = timeUnitsInWindow;
475 | }
476 |
477 | public void increment() {
478 | long timeSlot = windowTimeUnit.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS);
479 | Long current = window.getOrDefault(timeSlot, 0L);
480 | window.put(timeSlot, ++current);
481 | }
482 |
483 | public long count() {
484 | long windowStartTime = windowTimeUnit.convert(System.currentTimeMillis() - windowTimeUnit.toMillis(timeUnitsInWindow), TimeUnit.MILLISECONDS);
485 |
486 | long result = 0;
487 | for (Map.Entry entry : window.entrySet()) {
488 | if (entry.getKey() >= windowStartTime) {
489 | result += entry.getValue();
490 | }
491 | }
492 | return result;
493 | }
494 |
495 | public void reset() {
496 | window.clear();
497 | }
498 | }
499 |
500 | @SuppressWarnings("unchecked")
501 | private abstract class Completion implements Handler> {
502 |
503 | final Context context;
504 | final Promise resultFuture;
505 | final Function fallback;
506 | final CircuitBreakerMetrics.Operation operationMetrics;
507 |
508 | protected Completion(Context context, Promise resultFuture, Function fallback, CircuitBreakerMetrics.Operation operationMetrics) {
509 | this.context = context;
510 | this.resultFuture = resultFuture;
511 | this.fallback = fallback;
512 | this.operationMetrics = operationMetrics;
513 | }
514 |
515 | @Override
516 | public void handle(AsyncResult ar) {
517 | context.runOnContext(v -> {
518 | if (failurePolicy.test(asFuture(ar))) {
519 | failureAction();
520 | if (operationMetrics != null) {
521 | operationMetrics.failed();
522 | }
523 | if (options.isFallbackOnFailure()) {
524 | invokeFallback(ar.cause(), resultFuture, fallback, operationMetrics);
525 | } else {
526 | resultFuture.fail(ar.cause());
527 | }
528 | } else {
529 | if (operationMetrics != null) {
530 | operationMetrics.complete();
531 | }
532 | reset();
533 | //The event may pass due to a user given predicate. We still want to push up the failure for the user
534 | //to do any work
535 | resultFuture.handle(ar);
536 | }
537 | });
538 | }
539 |
540 | private Future asFuture(AsyncResult ar) {
541 | Future result;
542 | if (ar instanceof Future) {
543 | result = (Future) ar;
544 | } else if (ar.succeeded()) {
545 | result = Future.succeededFuture(ar.result());
546 | } else {
547 | result = Future.failedFuture(ar.cause());
548 | }
549 | return result;
550 | }
551 |
552 | protected abstract void failureAction();
553 | }
554 |
555 | private class ClosedCircuitCompletion extends Completion {
556 |
557 | ClosedCircuitCompletion(Context context, Promise userFuture, Function fallback, CircuitBreakerMetrics.Operation call) {
558 | super(context, userFuture, fallback, call);
559 | }
560 |
561 | @Override
562 | protected void failureAction() {
563 | incrementFailures();
564 | }
565 | }
566 |
567 | private class HalfOpenedCircuitCompletion extends Completion {
568 |
569 | HalfOpenedCircuitCompletion(Context context, Promise userFuture, Function fallback, CircuitBreakerMetrics.Operation call) {
570 | super(context, userFuture, fallback, call);
571 | }
572 |
573 | @Override
574 | protected void failureAction() {
575 | open();
576 | }
577 | }
578 | }
579 |
--------------------------------------------------------------------------------
/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerMetrics.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker.impl;
2 |
3 | import io.vertx.circuitbreaker.CircuitBreakerOptions;
4 | import io.vertx.core.Vertx;
5 | import io.vertx.core.internal.VertxInternal;
6 | import io.vertx.core.json.JsonObject;
7 | import org.HdrHistogram.Histogram;
8 |
9 | /**
10 | * Circuit breaker metrics.
11 | *
12 | * @author Clement Escoffier
13 | */
14 | public class CircuitBreakerMetrics {
15 | private final CircuitBreakerImpl circuitBreaker;
16 | private final String node;
17 |
18 | private final long circuitBreakerResetTimeout;
19 | private final long circuitBreakerTimeout;
20 |
21 | // Global statistics
22 |
23 | private final RollingWindow rollingWindow;
24 |
25 | CircuitBreakerMetrics(Vertx vertx, CircuitBreakerImpl circuitBreaker, CircuitBreakerOptions options) {
26 | this.circuitBreaker = circuitBreaker;
27 | this.circuitBreakerTimeout = circuitBreaker.options().getTimeout();
28 | this.circuitBreakerResetTimeout = circuitBreaker.options().getResetTimeout();
29 | this.node = vertx.isClustered() ? ((VertxInternal) vertx).clusterManager().getNodeId() : "local";
30 | this.rollingWindow = new RollingWindow(options.getMetricsRollingWindow(), options.getMetricsRollingBuckets());
31 | }
32 |
33 | private synchronized void evictOutdatedOperations() {
34 | rollingWindow.updateTime();
35 | }
36 |
37 | public void close() {
38 | // do nothing by default.
39 | }
40 |
41 | class Operation {
42 | final long begin;
43 | private volatile long end;
44 | private boolean complete;
45 | private boolean failed;
46 | private boolean timeout;
47 | private boolean exception;
48 | private boolean fallbackFailed;
49 | private boolean fallbackSucceed;
50 | private boolean shortCircuited;
51 |
52 | Operation() {
53 | begin = System.nanoTime();
54 | }
55 |
56 | synchronized void complete() {
57 | end = System.nanoTime();
58 | complete = true;
59 | CircuitBreakerMetrics.this.complete(this);
60 | }
61 |
62 | synchronized void failed() {
63 | if (timeout || exception) {
64 | // Already completed.
65 | return;
66 | }
67 | end = System.nanoTime();
68 | failed = true;
69 | CircuitBreakerMetrics.this.complete(this);
70 | }
71 |
72 | synchronized void timeout() {
73 | end = System.nanoTime();
74 | failed = false;
75 | timeout = true;
76 | CircuitBreakerMetrics.this.complete(this);
77 | }
78 |
79 | synchronized void error() {
80 | end = System.nanoTime();
81 | failed = false;
82 | exception = true;
83 | CircuitBreakerMetrics.this.complete(this);
84 | }
85 |
86 | synchronized void fallbackFailed() {
87 | fallbackFailed = true;
88 | }
89 |
90 | synchronized void fallbackSucceed() {
91 | fallbackSucceed = true;
92 | }
93 |
94 | synchronized void shortCircuited() {
95 | end = System.nanoTime();
96 | shortCircuited = true;
97 | CircuitBreakerMetrics.this.complete(this);
98 | }
99 |
100 | long durationInMs() {
101 | return (end - begin) / 1_000_000;
102 | }
103 | }
104 |
105 | Operation enqueue() {
106 | return new Operation();
107 | }
108 |
109 | public synchronized void complete(Operation operation) {
110 | rollingWindow.add(operation);
111 | }
112 |
113 | public synchronized JsonObject toJson() {
114 | JsonObject json = new JsonObject();
115 |
116 | // Configuration
117 | json.put("resetTimeout", circuitBreakerResetTimeout);
118 | json.put("timeout", circuitBreakerTimeout);
119 | json.put("metricRollingWindow", rollingWindow.getMetricRollingWindowSizeInMs());
120 | json.put("name", circuitBreaker.name());
121 | json.put("node", node);
122 |
123 | // Current state
124 | json.put("state", circuitBreaker.state());
125 | json.put("failures", circuitBreaker.failureCount());
126 |
127 | // Global metrics
128 | addSummary(json, rollingWindow.totalSummary(), MetricNames.TOTAL);
129 |
130 | // Window metrics
131 | evictOutdatedOperations();
132 | addSummary(json, rollingWindow.windowSummary(), MetricNames.ROLLING);
133 |
134 | return json;
135 | }
136 |
137 | private void addSummary(JsonObject json, RollingWindow.Summary summary, MetricNames names) {
138 | long calls = summary.count();
139 | int errorCount = summary.failures + summary.exceptions + summary.timeouts;
140 |
141 | json.put(names.operationCountName, calls - summary.shortCircuited);
142 | json.put(names.errorCountName, errorCount);
143 | json.put(names.successCountName, summary.successes);
144 | json.put(names.timeoutCountName, summary.timeouts);
145 | json.put(names.exceptionCountName, summary.exceptions);
146 | json.put(names.failureCountName, summary.failures);
147 |
148 | if (calls == 0) {
149 | json.put(names.successPercentageName, 0);
150 | json.put(names.errorPercentageName, 0);
151 | } else {
152 | json.put(names.successPercentageName, ((double) summary.successes / calls) * 100);
153 | json.put(names.errorPercentageName, ((double) (errorCount) / calls) * 100);
154 | }
155 |
156 | json.put(names.fallbackSuccessCountName, summary.fallbackSuccess);
157 | json.put(names.fallbackFailureCountName, summary.fallbackFailure);
158 | json.put(names.shortCircuitedCountName, summary.shortCircuited);
159 |
160 | addLatency(json, summary.statistics, names);
161 | }
162 |
163 |
164 | private void addLatency(JsonObject json, Histogram histogram, MetricNames names) {
165 | json.put(names.latencyMeanName, histogram.getMean());
166 | json.put(names.latencyName, new JsonObject()
167 | .put("0", histogram.getValueAtPercentile(0))
168 | .put("25", histogram.getValueAtPercentile(25))
169 | .put("50", histogram.getValueAtPercentile(50))
170 | .put("75", histogram.getValueAtPercentile(75))
171 | .put("90", histogram.getValueAtPercentile(90))
172 | .put("95", histogram.getValueAtPercentile(95))
173 | .put("99", histogram.getValueAtPercentile(99))
174 | .put("99.5", histogram.getValueAtPercentile(99.5))
175 | .put("100", histogram.getValueAtPercentile(100)));
176 | }
177 |
178 | private enum MetricNames {
179 | ROLLING("rolling"), TOTAL("total");
180 |
181 | private final String operationCountName;
182 | private final String errorCountName;
183 | private final String successCountName;
184 | private final String timeoutCountName;
185 | private final String exceptionCountName;
186 | private final String failureCountName;
187 | private final String successPercentageName;
188 | private final String errorPercentageName;
189 | private final String fallbackSuccessCountName;
190 | private final String fallbackFailureCountName;
191 | private final String shortCircuitedCountName;
192 |
193 | private final String latencyMeanName;
194 | private final String latencyName;
195 |
196 | MetricNames(String prefix){
197 | operationCountName = prefix + "OperationCount";
198 | errorCountName = prefix + "ErrorCount";
199 | successCountName = prefix + "SuccessCount";
200 | timeoutCountName = prefix + "TimeoutCount";
201 | exceptionCountName = prefix + "ExceptionCount";
202 | failureCountName = prefix + "FailureCount";
203 | successPercentageName = prefix + "SuccessPercentage";
204 | errorPercentageName = prefix + "ErrorPercentage";
205 | fallbackSuccessCountName = prefix + "FallbackSuccessCount";
206 | fallbackFailureCountName = prefix + "FallbackFailureCount";
207 | shortCircuitedCountName = prefix + "ShortCircuitedCount";
208 |
209 | latencyName = prefix + "Latency";
210 | latencyMeanName = prefix + "LatencyMean";
211 | }
212 | }
213 |
214 | private static class RollingWindow {
215 | private final Summary history;
216 | private final Summary[] buckets;
217 | private final long bucketSizeInNs;
218 |
219 | RollingWindow(long windowSizeInMs, int numberOfBuckets) {
220 | if (windowSizeInMs % numberOfBuckets != 0) {
221 | throw new IllegalArgumentException("Window size should be divisible by number of buckets.");
222 | }
223 | this.buckets = new Summary[numberOfBuckets];
224 | for (int i = 0; i < buckets.length; i++) {
225 | this.buckets[i] = new Summary();
226 | }
227 | this.bucketSizeInNs = 1_000_000 * windowSizeInMs / numberOfBuckets;
228 | this.history = new Summary();
229 | }
230 |
231 | public void add(Operation operation) {
232 | getBucket(operation.end).add(operation);
233 | }
234 |
235 | public Summary totalSummary() {
236 | Summary total = new Summary();
237 |
238 | total.add(history);
239 | total.add(windowSummary());
240 |
241 | return total;
242 | }
243 |
244 | public Summary windowSummary() {
245 | Summary window = new Summary(buckets[0].bucketIndex);
246 | for (Summary bucket : buckets) {
247 | window.add(bucket);
248 | }
249 |
250 | return window;
251 | }
252 |
253 | public void updateTime() {
254 | getBucket(System.nanoTime());
255 | }
256 |
257 | private Summary getBucket(long timeInNs) {
258 | long bucketIndex = timeInNs / bucketSizeInNs;
259 |
260 | //sample too old:
261 | if (bucketIndex < buckets[0].bucketIndex) {
262 | return history;
263 | }
264 |
265 | shiftIfNecessary(bucketIndex);
266 |
267 | return buckets[(int) (bucketIndex - buckets[0].bucketIndex)];
268 | }
269 |
270 | private void shiftIfNecessary(long bucketIndex) {
271 | long shiftUnlimited = bucketIndex - buckets[buckets.length - 1].bucketIndex;
272 | if (shiftUnlimited <= 0) {
273 | return;
274 | }
275 | int shift = (int) Long.min(buckets.length, shiftUnlimited);
276 |
277 | // Add old buckets to history
278 | for(int i = 0; i < shift; i++) {
279 | history.add(buckets[i]);
280 | }
281 |
282 | System.arraycopy(buckets, shift, buckets, 0, buckets.length - shift);
283 |
284 | for(int i = buckets.length - shift; i < buckets.length; i++) {
285 | buckets[i] = new Summary(bucketIndex + i + 1 - buckets.length);
286 | }
287 | }
288 |
289 | public long getMetricRollingWindowSizeInMs() {
290 | return bucketSizeInNs * buckets.length / 1_000_000;
291 | }
292 |
293 | private static class Summary {
294 | final long bucketIndex;
295 | final Histogram statistics;
296 |
297 | private int successes;
298 | private int failures;
299 | private int exceptions;
300 | private int timeouts;
301 | private int fallbackSuccess;
302 | private int fallbackFailure;
303 | private int shortCircuited;
304 |
305 | private Summary() {
306 | this(-1);
307 | }
308 |
309 | private Summary(long bucketIndex) {
310 | this.bucketIndex = bucketIndex;
311 | statistics = new Histogram(2);
312 | }
313 |
314 | public void add(Summary other) {
315 | statistics.add(other.statistics);
316 |
317 | successes += other.successes;
318 | failures += other.failures;
319 | exceptions += other.exceptions;
320 | timeouts += other.timeouts;
321 | fallbackSuccess += other.fallbackSuccess;
322 | fallbackFailure += other.fallbackFailure;
323 | shortCircuited += other.shortCircuited;
324 | }
325 |
326 | public void add(Operation operation) {
327 | statistics.recordValue(operation.durationInMs());
328 | if (operation.complete) {
329 | successes++;
330 | } else if (operation.failed) {
331 | failures++;
332 | } else if (operation.exception) {
333 | exceptions++;
334 | } else if (operation.timeout) {
335 | timeouts++;
336 | }
337 |
338 | if (operation.fallbackSucceed) {
339 | fallbackSuccess++;
340 | } else if (operation.fallbackFailed) {
341 | fallbackFailure++;
342 | }
343 |
344 | if (operation.shortCircuited) {
345 | shortCircuited++;
346 | }
347 | }
348 |
349 | public long count() {
350 | return statistics.getTotalCount();
351 | }
352 | }
353 | }
354 | }
355 |
--------------------------------------------------------------------------------
/src/main/java/io/vertx/circuitbreaker/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2016 The original author or authors
3 | *
4 | * All rights reserved. This program and the accompanying materials
5 | * are made available under the terms of the Eclipse Public License v1.0
6 | * and Apache License v2.0 which accompanies this distribution.
7 | *
8 | * The Eclipse Public License is available at
9 | * http://www.eclipse.org/legal/epl-v10.html
10 | *
11 | * The Apache License v2.0 is available at
12 | * http://www.opensource.org/licenses/apache2.0.php
13 | *
14 | * You may elect to redistribute this code under either of these licenses.
15 | */
16 | @ModuleGen(name = "vertx-circuit-breaker", groupPackage = "io.vertx")
17 | package io.vertx.circuitbreaker;
18 |
19 | import io.vertx.codegen.annotations.ModuleGen;
20 |
--------------------------------------------------------------------------------
/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
3 | *
4 | * This program and the accompanying materials are made available under the
5 | * terms of the Eclipse Public License 2.0 which is available at
6 | * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7 | * which is available at https://www.apache.org/licenses/LICENSE-2.0.
8 | *
9 | * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10 | */
11 | module io.vertx.circuitbreaker {
12 |
13 | requires static io.vertx.docgen;
14 | requires static io.vertx.codegen.api;
15 | requires static io.vertx.codegen.json;
16 |
17 | requires static HdrHistogram;
18 | requires io.vertx.core;
19 | requires io.vertx.core.logging;
20 |
21 | exports io.vertx.circuitbreaker;
22 | exports io.vertx.circuitbreaker.impl to io.vertx.circuitbreaker.tests;
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/JsonFactory.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker.tests;
2 |
3 | import io.vertx.core.json.jackson.JacksonCodec;
4 | import io.vertx.core.spi.json.JsonCodec;
5 |
6 | public class JsonFactory implements io.vertx.core.spi.JsonFactory {
7 |
8 | @Override
9 | public JsonCodec codec() {
10 | return new JacksonCodec();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/PredefinedRetryPolicyTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
3 | *
4 | * This program and the accompanying materials are made available under the
5 | * terms of the Eclipse Public License 2.0 which is available at
6 | * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7 | * which is available at https://www.apache.org/licenses/LICENSE-2.0.
8 | *
9 | * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10 | */
11 |
12 | package io.vertx.circuitbreaker.tests;
13 |
14 | import io.vertx.circuitbreaker.RetryPolicy;
15 | import org.junit.Test;
16 |
17 | import static org.junit.Assert.*;
18 |
19 | public class PredefinedRetryPolicyTest {
20 |
21 | @Test(expected = IllegalArgumentException.class)
22 | public void testConstantDelayNegative() {
23 | RetryPolicy.constantDelay(-1);
24 | }
25 |
26 | @Test
27 | public void testConstantDelay() {
28 | RetryPolicy retryPolicy = RetryPolicy.constantDelay(10);
29 | for (int i = 1; i <= 50; i++) {
30 | assertEquals(10, retryPolicy.delay(null, i));
31 | }
32 | }
33 |
34 | @Test(expected = IllegalArgumentException.class)
35 | public void testLinearDelayNegative() {
36 | RetryPolicy.linearDelay(-1, 30000);
37 | }
38 |
39 | @Test(expected = IllegalArgumentException.class)
40 | public void testLinearDelayGreatherThanMax() {
41 | RetryPolicy.linearDelay(50000, 30000);
42 | }
43 |
44 | @Test
45 | public void testLinearDelay() {
46 | RetryPolicy retryPolicy = RetryPolicy.linearDelay(10, 250);
47 | for (int i = 1; i <= 50; i++) {
48 | long delay = retryPolicy.delay(null, i);
49 | if (i <= 25) {
50 | assertEquals(10 * i, delay);
51 | } else {
52 | assertEquals(250, delay);
53 | }
54 | }
55 | }
56 |
57 | @Test(expected = IllegalArgumentException.class)
58 | public void testExponentialDelayNegative() {
59 | RetryPolicy.exponentialDelayWithJitter(-1, 30000);
60 | }
61 |
62 | @Test(expected = IllegalArgumentException.class)
63 | public void testExponentialDelayGreatherThanMax() {
64 | RetryPolicy.exponentialDelayWithJitter(50000, 30000);
65 | }
66 |
67 | @Test
68 | public void testExponentialDelayWithJitter() {
69 | int maxDelay = 30000;
70 | RetryPolicy retryPolicy = RetryPolicy.exponentialDelayWithJitter(3, maxDelay);
71 | for (int i = 1; i <= 50; i++) {
72 | long delay = retryPolicy.delay(null, i);
73 | assertTrue(0 <= delay && delay <= maxDelay);
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/impl/APITest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2016 The original author or authors
3 | *
4 | * All rights reserved. This program and the accompanying materials
5 | * are made available under the terms of the Eclipse Public License v1.0
6 | * and Apache License v2.0 which accompanies this distribution.
7 | *
8 | * The Eclipse Public License is available at
9 | * http://www.eclipse.org/legal/epl-v10.html
10 | *
11 | * The Apache License v2.0 is available at
12 | * http://www.opensource.org/licenses/apache2.0.php
13 | *
14 | * You may elect to redistribute this code under either of these licenses.
15 | */
16 |
17 | package io.vertx.circuitbreaker.tests.impl;
18 |
19 | import io.vertx.circuitbreaker.CircuitBreakerOptions;
20 | import io.vertx.circuitbreaker.CircuitBreakerState;
21 | import io.vertx.core.Promise;
22 | import io.vertx.core.Vertx;
23 | import io.vertx.circuitbreaker.CircuitBreaker;
24 | import org.junit.After;
25 | import org.junit.Before;
26 | import org.junit.Test;
27 |
28 | import java.util.concurrent.atomic.AtomicBoolean;
29 | import java.util.concurrent.atomic.AtomicInteger;
30 |
31 | import static org.awaitility.Awaitility.await;
32 | import static org.hamcrest.core.Is.is;
33 | import static org.junit.Assert.assertEquals;
34 | import static org.junit.Assert.assertNotNull;
35 |
36 | /**
37 | * @author Clement Escoffier
38 | */
39 | public class APITest {
40 |
41 | private CircuitBreaker breaker;
42 | private Vertx vertx;
43 |
44 | @Before
45 | public void setUp() {
46 | vertx = Vertx.vertx();
47 | }
48 |
49 | @After
50 | public void tearDown() {
51 | if (breaker != null) {
52 | breaker.close();
53 | }
54 | AtomicBoolean completed = new AtomicBoolean();
55 | vertx.close().onComplete(ar -> completed.set(ar.succeeded()));
56 | await().untilAtomic(completed, is(true));
57 | }
58 |
59 | /**
60 | * Reproducer of https://github.com/vert-x3/vertx-circuit-breaker/issues/9
61 | */
62 | @Test
63 | public void testWhenOptionsAreNull() {
64 | CircuitBreaker cb = CircuitBreaker.create("name", vertx, null);
65 | assertNotNull(cb);
66 | assertEquals("name", cb.name());
67 | assertEquals(CircuitBreakerState.CLOSED, cb.state());
68 | }
69 |
70 |
71 | @Test
72 | public void testWithOperationWithHandler() {
73 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions());
74 |
75 | AtomicInteger result = new AtomicInteger();
76 |
77 | breaker.executeWithFallback(fut -> {
78 | MyAsyncOperations.operation(1, 1, fut);
79 | }, v -> 0)
80 | .onComplete(ar -> result.set(ar.result()));
81 |
82 | await().untilAtomic(result, is(2));
83 | }
84 |
85 | @Test
86 | public void testWithOperationWithCompletionHandler() {
87 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions());
88 |
89 | AtomicInteger result = new AtomicInteger();
90 |
91 | breaker.executeWithFallback(fut -> {
92 | MyAsyncOperations.operation(1, 1, fut);
93 | }, v -> 0).onComplete(ar -> result.set(ar.result()));
94 |
95 | await().untilAtomic(result, is(2));
96 | }
97 |
98 | @Test
99 | public void testWithFailingOperationWithHandler() {
100 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()
101 | .setFallbackOnFailure(true));
102 |
103 | AtomicInteger result = new AtomicInteger();
104 |
105 | breaker.executeWithFallback(fut -> {
106 | MyAsyncOperations.fail(fut);
107 | }, v -> -1)
108 | .onComplete(ar -> result.set(ar.result()));
109 |
110 | await().untilAtomic(result, is(-1));
111 | }
112 |
113 | @Test
114 | public void testWithFailingOperationWithCompletionHandler() {
115 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()
116 | .setFallbackOnFailure(true));
117 |
118 | AtomicInteger result = new AtomicInteger();
119 |
120 | breaker.executeWithFallback(fut -> {
121 | MyAsyncOperations.fail(fut);
122 | }, v -> -1).onComplete(ar -> result.set(ar.result()));
123 |
124 | await().untilAtomic(result, is(-1));
125 | }
126 |
127 |
128 | @Test
129 | public void testWithOperationWithFuture() {
130 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()
131 | .setFallbackOnFailure(true));
132 |
133 | AtomicInteger result = new AtomicInteger();
134 | Promise operationResult = Promise.promise();
135 | operationResult.future().onComplete(ar -> {
136 | result.set(ar.result());
137 | });
138 |
139 | breaker.executeAndReport(operationResult, future -> MyAsyncOperations.operation(future, 1, 1));
140 |
141 | await().untilAtomic(result, is(2));
142 | }
143 |
144 | @Test
145 | public void testWithFailingOperationWithFuture() {
146 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()
147 | .setFallbackOnFailure(true));
148 |
149 | AtomicInteger result = new AtomicInteger();
150 |
151 | Promise operationResult = Promise.promise();
152 | operationResult.future().onComplete(ar -> result.set(ar.result()));
153 |
154 | breaker.executeAndReportWithFallback(operationResult, MyAsyncOperations::fail, t -> -1);
155 |
156 | await().untilAtomic(result, is(-1));
157 | }
158 |
159 |
160 | }
161 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/impl/AsyncBreakerTest.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker.tests.impl;
2 |
3 | import io.vertx.circuitbreaker.CircuitBreaker;
4 | import io.vertx.circuitbreaker.CircuitBreakerOptions;
5 | import io.vertx.core.Vertx;
6 | import io.vertx.core.internal.logging.Logger;
7 | import io.vertx.core.internal.logging.LoggerFactory;
8 | import io.vertx.ext.unit.Async;
9 | import io.vertx.ext.unit.TestContext;
10 | import io.vertx.ext.unit.junit.VertxUnitRunner;
11 | import org.junit.After;
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.junit.runner.RunWith;
15 |
16 | /**
17 | * Reproducer for https://github.com/vert-x3/issues/issues/294 (copied to
18 | * https://github.com/vert-x3/vertx-circuit-breaker/issues/14).
19 | */
20 | @RunWith(VertxUnitRunner.class)
21 | public class AsyncBreakerTest {
22 |
23 | private Vertx vertx;
24 | private CircuitBreaker breaker;
25 | private int count;
26 |
27 | private static Logger LOG = LoggerFactory.getLogger(AsyncBreakerTest.class);
28 |
29 | @Before
30 | public void setUp() {
31 | vertx = Vertx.vertx();
32 |
33 | breaker = CircuitBreaker.create("collector-circuit-breaker", vertx,
34 | new CircuitBreakerOptions()
35 | .setMaxFailures(2)
36 | .setTimeout(1_000)
37 | .setFallbackOnFailure(false)
38 | .setResetTimeout(2_000)
39 | .setNotificationPeriod(0));
40 |
41 | count = 0;
42 | }
43 |
44 | @After
45 | public void tearDown(TestContext tc) {
46 | vertx.close().onComplete(tc.asyncAssertSuccess());
47 | }
48 |
49 | private void x(TestContext tc, int id) {
50 | Async async = tc.async(10);
51 | breaker.executeWithFallback(future -> {
52 | vertx.setTimer(100 + (id * 10), handler -> {
53 | synchronized (this) {
54 | count++;
55 | if (count < 5 || count > 12) {
56 | future.complete("OK");
57 | async.complete();
58 | } else {
59 | future.fail("kapot");
60 | async.complete();
61 | }
62 | }
63 | });
64 |
65 | }, fallback -> {
66 | LOG.info("OPEN " + id);
67 | async.complete();
68 | return "OPEN";
69 | });
70 | }
71 |
72 | @Test
73 | public void test1(TestContext tc) throws InterruptedException {
74 |
75 | for (int i = 0; i < 20; ++i) {
76 | x(tc, i);
77 | }
78 |
79 | breaker.openHandler(h -> LOG.info("Breaker open"));
80 | breaker.closeHandler(h -> tc.fail("should not close"));
81 | breaker.halfOpenHandler(h -> LOG.info("Breaker half open"));
82 | }
83 |
84 | @Test
85 | public void test2(TestContext tc) throws InterruptedException {
86 | Async async = tc.async();
87 |
88 | for (int i = 0; i < 20; ++i) {
89 | x(tc, i);
90 | }
91 |
92 | breaker.openHandler(h -> LOG.info("Breaker open"));
93 | breaker.closeHandler(h -> {
94 | LOG.info("Breaker closed");
95 | async.complete();
96 | });
97 | breaker.halfOpenHandler(h -> LOG.info("Breaker half open"));
98 |
99 | LOG.info("Waiting for test to complete");
100 |
101 | Thread.sleep(3000);
102 | LOG.info("Sleep done");
103 | for (int i = 0; i < 5; ++i) {
104 | x(tc, i);
105 | }
106 | }
107 |
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/impl/CircuitBreakerImplTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2016 The original author or authors
3 | *
4 | * All rights reserved. This program and the accompanying materials
5 | * are made available under the terms of the Eclipse Public License v1.0
6 | * and Apache License v2.0 which accompanies this distribution.
7 | *
8 | * The Eclipse Public License is available at
9 | * http://www.eclipse.org/legal/epl-v10.html
10 | *
11 | * The Apache License v2.0 is available at
12 | * http://www.opensource.org/licenses/apache2.0.php
13 | *
14 | * You may elect to redistribute this code under either of these licenses.
15 | */
16 |
17 | package io.vertx.circuitbreaker.tests.impl;
18 |
19 | import io.vertx.circuitbreaker.*;
20 | import io.vertx.circuitbreaker.impl.CircuitBreakerImpl;
21 | import io.vertx.core.*;
22 | import io.vertx.ext.unit.Async;
23 | import io.vertx.ext.unit.TestContext;
24 | import io.vertx.ext.unit.junit.Repeat;
25 | import io.vertx.ext.unit.junit.RepeatRule;
26 | import io.vertx.ext.unit.junit.VertxUnitRunner;
27 | import org.junit.After;
28 | import org.junit.Before;
29 | import org.junit.Rule;
30 | import org.junit.Test;
31 | import org.junit.runner.RunWith;
32 |
33 | import java.util.ArrayList;
34 | import java.util.List;
35 | import java.util.concurrent.TimeUnit;
36 | import java.util.concurrent.atomic.AtomicBoolean;
37 | import java.util.concurrent.atomic.AtomicInteger;
38 | import java.util.concurrent.atomic.AtomicReference;
39 | import java.util.stream.IntStream;
40 |
41 | import static org.awaitility.Awaitility.await;
42 | import static org.hamcrest.core.Is.is;
43 | import static org.junit.Assert.*;
44 |
45 | /**
46 | * Test the basic behavior of the circuit breaker.
47 | *
48 | * @author Clement Escoffier
49 | */
50 | @RunWith(VertxUnitRunner.class)
51 | public class CircuitBreakerImplTest {
52 | private Vertx vertx;
53 | private CircuitBreaker breaker;
54 |
55 | @Rule
56 | public RepeatRule rule = new RepeatRule();
57 |
58 | @Before
59 | public void setUp() {
60 | vertx = Vertx.vertx();
61 | }
62 |
63 | @After
64 | public void tearDown() {
65 | if (breaker != null) {
66 | breaker.close();
67 | }
68 | AtomicBoolean completed = new AtomicBoolean();
69 | vertx.close().onComplete(ar -> completed.set(ar.succeeded()));
70 | await().untilAtomic(completed, is(true));
71 | }
72 |
73 | @Test
74 | public void testCreationWithDefault() {
75 | breaker = CircuitBreaker.create("name", vertx);
76 | assertEquals("name", breaker.name());
77 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
78 | }
79 |
80 | @Test
81 | @Repeat(5)
82 | public void testOk() {
83 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions());
84 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
85 |
86 | AtomicBoolean operationCalled = new AtomicBoolean();
87 | AtomicReference completionCalled = new AtomicReference<>();
88 | breaker.execute(fut -> {
89 | operationCalled.set(true);
90 | fut.complete("hello");
91 | }).onComplete(ar -> completionCalled.set(ar.result()));
92 |
93 | await().until(operationCalled::get);
94 | await().until(() -> completionCalled.get().equalsIgnoreCase("hello"));
95 | }
96 |
97 | @Test
98 | @Repeat(5)
99 | public void testWithCustomPredicateOk() {
100 | breaker = CircuitBreaker.create("test", vertx).failurePolicy(ar -> {
101 | return ar.failed() && ar.cause().getStackTrace().length > 0;
102 | });
103 |
104 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
105 |
106 | AtomicBoolean operationCalled = new AtomicBoolean();
107 | AtomicReference completionCalled = new AtomicReference<>();
108 | breaker.execute(fut -> {
109 | operationCalled.set(true);
110 | fut.fail("some fake exception");
111 | }).onComplete(ar -> {
112 | completionCalled.set(ar.cause().getMessage());
113 | assertTrue(ar.failed());
114 | });
115 |
116 | await().until(operationCalled::get);
117 | await().until(() -> completionCalled.get().equalsIgnoreCase("some fake exception"));
118 | }
119 |
120 | @Test
121 | @Repeat(5)
122 | public void testWithUserFutureOk() {
123 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions());
124 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
125 |
126 | AtomicBoolean operationCalled = new AtomicBoolean();
127 | AtomicReference completionCalled = new AtomicReference<>();
128 |
129 | Promise userFuture = Promise.promise();
130 | userFuture.future().onComplete(ar ->
131 | completionCalled.set(ar.result()));
132 |
133 | breaker.executeAndReport(userFuture, fut -> {
134 | operationCalled.set(true);
135 | fut.complete("hello");
136 | });
137 |
138 | await().until(operationCalled::get);
139 | await().until(() -> completionCalled.get().equalsIgnoreCase("hello"));
140 | }
141 |
142 | @Test
143 | @Repeat(5)
144 | public void testWithUserFutureWithCustomPredicateOk() {
145 | breaker = CircuitBreaker.create("test", vertx).failurePolicy(ar -> {
146 | return ar.failed() && ar.cause().getStackTrace().length > 0;
147 | });
148 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
149 |
150 | AtomicBoolean operationCalled = new AtomicBoolean();
151 | AtomicReference completionCalled = new AtomicReference<>();
152 |
153 | Promise userFuture = Promise.promise();
154 | userFuture.future().onComplete(ar -> {
155 | completionCalled.set(ar.cause().getMessage());
156 | assertTrue(ar.failed());
157 | });
158 |
159 | breaker.executeAndReport(userFuture, fut -> {
160 | operationCalled.set(true);
161 | fut.fail("some custom exception");
162 | });
163 |
164 | await().until(operationCalled::get);
165 | await().until(() -> completionCalled.get().equalsIgnoreCase("some custom exception"));
166 | }
167 |
168 | @Test
169 | public void testAsynchronousOk() {
170 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions());
171 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
172 |
173 | AtomicBoolean called = new AtomicBoolean();
174 | AtomicReference result = new AtomicReference<>();
175 | breaker.execute(future ->
176 | vertx.setTimer(100, l -> {
177 | called.set(true);
178 | future.complete("hello");
179 | })
180 | ).onComplete(ar -> result.set(ar.result()));
181 |
182 | await().until(called::get);
183 | await().untilAtomic(result, is("hello"));
184 | }
185 |
186 | @Test
187 | public void testAsynchronousWithCustomPredicateOk() {
188 | breaker = CircuitBreaker.create("test", vertx).failurePolicy(ar -> {
189 | return ar.failed() && ar.cause().getStackTrace().length > 0;
190 | });
191 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
192 |
193 | AtomicBoolean called = new AtomicBoolean();
194 | AtomicReference result = new AtomicReference<>();
195 | breaker.execute(future ->
196 | vertx.setTimer(100, l -> {
197 | called.set(true);
198 | future.fail("some custom exception");
199 | })
200 | ).onComplete(ar -> {
201 | result.set(ar.cause().getMessage());
202 | assertTrue(ar.failed());
203 | });
204 | ;
205 |
206 | await().until(called::get);
207 | await().untilAtomic(result, is("some custom exception"));
208 | }
209 |
210 | @Test
211 | public void testAsynchronousWithUserFutureOk() {
212 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions());
213 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
214 |
215 | AtomicBoolean called = new AtomicBoolean();
216 | AtomicReference result = new AtomicReference<>();
217 |
218 | Promise userFuture = Promise.promise();
219 | userFuture.future().onComplete(ar -> result.set(ar.result()));
220 |
221 | breaker.executeAndReport(userFuture, future ->
222 | vertx.setTimer(100, l -> {
223 | called.set(true);
224 | future.complete("hello");
225 | })
226 | );
227 |
228 | await().until(called::get);
229 | await().untilAtomic(result, is("hello"));
230 | }
231 |
232 | @Test
233 | public void testAsynchronousWithUserFutureAndWithCustomPredicateOk() {
234 | breaker = CircuitBreaker.create("test", vertx).failurePolicy(ar -> {
235 | return ar.failed() && ar.cause() instanceof ClassNotFoundException;
236 | });
237 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
238 |
239 | AtomicBoolean called = new AtomicBoolean();
240 | AtomicReference result = new AtomicReference<>();
241 |
242 | Promise userFuture = Promise.promise();
243 | userFuture.future().onComplete(ar -> {
244 | result.set(ar.cause().getMessage());
245 | assertTrue(ar.failed());
246 | });
247 | ;
248 |
249 | breaker.executeAndReport(userFuture, future ->
250 | vertx.setTimer(100, l -> {
251 | called.set(true);
252 | future.fail(new NullPointerException("some custom exception"));
253 | })
254 | );
255 |
256 | await().until(called::get);
257 | await().untilAtomic(result, is("some custom exception"));
258 | }
259 |
260 | @Test
261 | public void testRollingWindowFailuresAreDecreased() {
262 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()
263 | .setMaxFailures(10)
264 | .setFailuresRollingWindow(10000));
265 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
266 |
267 | IntStream.range(0, 9).forEach(i -> breaker.execute(v -> v.fail(new RuntimeException("oh no, but this is expected"))));
268 | await().until(() -> breaker.failureCount() == 9);
269 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
270 |
271 | await().atMost(11, TimeUnit.SECONDS).until(() -> breaker.failureCount() < 9);
272 |
273 | assertTrue(breaker.failureCount() < 9);
274 | }
275 |
276 | @Test
277 | @Repeat(5)
278 | public void testOpenAndCloseHandler() {
279 | AtomicInteger spyOpen = new AtomicInteger();
280 | AtomicInteger spyClosed = new AtomicInteger();
281 |
282 | AtomicReference lastException = new AtomicReference<>();
283 |
284 | breaker = CircuitBreaker.create("name", vertx, new CircuitBreakerOptions().setResetTimeout(-1))
285 | .openHandler((v) -> spyOpen.incrementAndGet())
286 | .closeHandler((v) -> spyClosed.incrementAndGet());
287 |
288 | assertEquals(0, spyOpen.get());
289 | assertEquals(0, spyClosed.get());
290 |
291 | // First failure
292 | breaker.execute(v -> {
293 | throw new RuntimeException("oh no, but this is expected");
294 | })
295 | .onComplete(ar -> lastException.set(ar.cause()));
296 |
297 | assertEquals(0, spyOpen.get());
298 | assertEquals(0, spyClosed.get());
299 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED);
300 | assertNotNull(lastException.get());
301 | lastException.set(null);
302 |
303 | for (int i = 1; i < CircuitBreakerOptions.DEFAULT_MAX_FAILURES; i++) {
304 | breaker.execute(v -> {
305 | throw new RuntimeException("oh no, but this is expected");
306 | })
307 | .onComplete(ar -> lastException.set(ar.cause()));
308 | }
309 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN);
310 | assertEquals(1, spyOpen.get());
311 | assertNotNull(lastException.get());
312 |
313 | ((CircuitBreakerImpl) breaker).reset(true);
314 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
315 | assertEquals(1, spyOpen.get());
316 | assertEquals(1, spyClosed.get());
317 | }
318 |
319 | @Test
320 | @Repeat(5)
321 | public void testHalfOpen() {
322 | AtomicBoolean thrown = new AtomicBoolean(false);
323 | Context ctx = vertx.getOrCreateContext()
324 | .exceptionHandler(ex -> // intercept exceptions
325 | thrown.set(true));
326 |
327 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()
328 | .setResetTimeout(200)
329 | .setMaxFailures(1));
330 |
331 | Handler> fail = p -> p.fail("fail");
332 | Handler> success = Promise::complete;
333 |
334 | ctx.runOnContext(v -> {
335 | breaker.execute(fail);
336 | breaker.execute(fail);
337 | });
338 |
339 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN);
340 |
341 | ctx.runOnContext(v -> {
342 | breaker.execute(fail);
343 | });
344 |
345 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN);
346 |
347 | ctx.runOnContext(v -> {
348 | breaker.execute(success);
349 | });
350 |
351 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED);
352 |
353 | assertFalse(thrown.get());
354 | }
355 |
356 | @Test
357 | @Repeat(5)
358 | public void testExceptionOnSynchronousCode() {
359 | AtomicBoolean called = new AtomicBoolean(false);
360 | CircuitBreakerOptions options = new CircuitBreakerOptions()
361 | .setFallbackOnFailure(false)
362 | .setResetTimeout(-1);
363 | breaker = CircuitBreaker.create("test", vertx, options)
364 | .fallback(t -> {
365 | called.set(true);
366 | return "fallback";
367 | });
368 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
369 |
370 | for (int i = 0; i < options.getMaxFailures(); i++) {
371 | breaker.execute(v -> {
372 | throw new RuntimeException("oh no, but this is expected");
373 | });
374 | }
375 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN ||
376 | breaker.state() == CircuitBreakerState.HALF_OPEN);
377 | assertFalse(called.get());
378 |
379 | AtomicBoolean spy = new AtomicBoolean();
380 | breaker.execute(v -> spy.set(true));
381 | assertFalse(spy.get());
382 | assertTrue(called.get());
383 | }
384 |
385 | @Test
386 | @Repeat(5)
387 | public void testExceptionOnSynchronousCodeWithExecute() {
388 | CircuitBreakerOptions options = new CircuitBreakerOptions()
389 | .setFallbackOnFailure(false)
390 | .setResetTimeout(-1);
391 | breaker = CircuitBreaker.create("test", vertx, options)
392 | .fallback(t -> "fallback");
393 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
394 |
395 | for (int i = 0; i < options.getMaxFailures(); i++) {
396 | Promise future = Promise.promise();
397 | AtomicReference result = new AtomicReference<>();
398 | breaker.executeAndReport(future, v -> {
399 | throw new RuntimeException("oh no, but this is expected");
400 | });
401 | future.future().onComplete(ar -> result.set(ar.result()));
402 | assertNull(result.get());
403 | }
404 |
405 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN);
406 | assertEquals(CircuitBreakerState.OPEN, breaker.state());
407 |
408 | AtomicBoolean spy = new AtomicBoolean();
409 | AtomicReference result = new AtomicReference<>();
410 | Promise fut = Promise.promise();
411 | fut.future().onComplete(ar ->
412 | result.set(ar.result())
413 | );
414 | breaker.executeAndReport(fut, v -> spy.set(true));
415 | assertFalse(spy.get());
416 | assertEquals("fallback", result.get());
417 | }
418 |
419 | @Test
420 | public void testFailureOnAsynchronousCode() {
421 | AtomicBoolean called = new AtomicBoolean(false);
422 | AtomicReference result = new AtomicReference<>();
423 | CircuitBreakerOptions options = new CircuitBreakerOptions().setResetTimeout(-1);
424 | breaker = CircuitBreaker.create("test", vertx, options)
425 | .fallback(v -> {
426 | called.set(true);
427 | return "fallback";
428 | });
429 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
430 |
431 | for (int i = 0; i < options.getMaxFailures(); i++) {
432 | breaker.execute(
433 | future -> vertx.setTimer(100, l -> future.fail("expected failure"))
434 | ).onComplete(ar -> result.set(ar.result()));
435 | }
436 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN);
437 | assertFalse(called.get());
438 |
439 | AtomicBoolean spy = new AtomicBoolean();
440 | breaker.execute(
441 | future -> vertx.setTimer(100, l -> {
442 | future.fail("expected failure");
443 | spy.set(true);
444 | }))
445 | .onComplete(ar -> result.set(ar.result()));
446 | await().untilAtomic(called, is(true));
447 | assertFalse(spy.get());
448 | assertEquals("fallback", result.get());
449 | }
450 |
451 | @Test
452 | public void testFailureOnAsynchronousCodeWithCustomPredicate() {
453 | AtomicBoolean called = new AtomicBoolean(false);
454 | AtomicReference result = new AtomicReference<>();
455 | CircuitBreakerOptions options = new CircuitBreakerOptions().setResetTimeout(-1);
456 | breaker = CircuitBreaker.create("test", vertx, options)
457 | .fallback(v -> {
458 | called.set(true);
459 | return "fallback";
460 | })
461 | .failurePolicy(ar -> {
462 | return ar.failed() && ar.cause().getStackTrace().length == 0;
463 | });
464 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
465 |
466 | for (int i = 0; i < options.getMaxFailures(); i++) {
467 | breaker.execute(
468 | future -> vertx.setTimer(100, l -> future.fail("expected failure"))
469 | ).onComplete(ar -> result.set(ar.result()));
470 | }
471 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN);
472 | assertFalse(called.get());
473 |
474 | AtomicBoolean spy = new AtomicBoolean();
475 | breaker.execute(
476 | future -> vertx.setTimer(100, l -> {
477 | future.fail("expected failure");
478 | spy.set(true);
479 | }))
480 | .onComplete(ar -> {
481 | result.set(ar.result());
482 | });
483 | ;
484 | await().untilAtomic(called, is(true));
485 | assertFalse(spy.get());
486 | assertEquals("fallback", result.get());
487 | }
488 |
489 | @Test
490 | @Repeat(5)
491 | public void testResetAttempt() {
492 | AtomicBoolean called = new AtomicBoolean(false);
493 | CircuitBreakerOptions options = new CircuitBreakerOptions().setResetTimeout(100);
494 | breaker = CircuitBreaker.create("test", vertx, options)
495 | .fallback(v -> {
496 | called.set(true);
497 | return "fallback";
498 | });
499 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
500 |
501 | for (int i = 0; i < options.getMaxFailures(); i++) {
502 | breaker.execute(v -> {
503 | throw new RuntimeException("oh no, but this is expected");
504 | });
505 | }
506 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN);
507 | assertFalse(called.get());
508 |
509 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN);
510 |
511 | AtomicBoolean spy = new AtomicBoolean();
512 | breaker.execute(v -> {
513 | spy.set(true);
514 | v.complete();
515 | });
516 | assertTrue(spy.get());
517 | assertFalse(called.get());
518 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED);
519 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
520 | }
521 |
522 | @Test
523 | @Repeat(5)
524 | public void testResetAttemptThatFails() {
525 | AtomicBoolean called = new AtomicBoolean(false);
526 | CircuitBreakerOptions options = new CircuitBreakerOptions()
527 | .setResetTimeout(100)
528 | .setFallbackOnFailure(true);
529 | breaker = CircuitBreaker.create("test", vertx, options)
530 | .fallback(v -> {
531 | called.set(true);
532 | return "fallback";
533 | });
534 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED);
535 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
536 |
537 | for (int i = 0; i < options.getMaxFailures(); i++) {
538 | breaker.execute(v -> {
539 | throw new RuntimeException("oh no, but this is expected");
540 | });
541 | }
542 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN);
543 | assertTrue(called.get());
544 |
545 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN);
546 | called.set(false);
547 |
548 | AtomicReference result = new AtomicReference<>();
549 | breaker.execute(v -> {
550 | throw new RuntimeException("oh no, but this is expected");
551 | }).onComplete(ar -> result.set(ar.result()));
552 |
553 | await().until(called::get);
554 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN);
555 | assertEquals("fallback", result.get());
556 | }
557 |
558 | @Test
559 | public void testTimeout() {
560 | AtomicBoolean called = new AtomicBoolean(false);
561 | CircuitBreakerOptions options = new CircuitBreakerOptions().setTimeout(100);
562 | breaker = CircuitBreaker.create("test", vertx, options)
563 | .fallback(v -> {
564 | called.set(true);
565 | return "fallback";
566 | });
567 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
568 |
569 | AtomicInteger failureCount = new AtomicInteger();
570 | for (int i = 0; i < options.getMaxFailures(); i++) {
571 | try {
572 | breaker.execute(v -> {
573 | vertx.setTimer(500, id -> {
574 | v.complete("done");
575 | });
576 | }).onComplete(ar -> {
577 | if (ar.failed()) failureCount.incrementAndGet();
578 | }).await();
579 | } catch (TimeoutException e) {
580 | // Timeout
581 | }
582 | }
583 |
584 | assertEquals(CircuitBreakerState.OPEN, breaker.state());
585 | assertFalse(called.get());
586 | assertEquals(options.getMaxFailures(), failureCount.get());
587 |
588 | AtomicBoolean spy = new AtomicBoolean();
589 | AtomicReference result = new AtomicReference<>();
590 | breaker.execute(v -> {
591 | spy.set(true);
592 | v.complete();
593 | })
594 | .onComplete(ar -> result.set(ar.result()));
595 | assertFalse(spy.get());
596 | assertTrue(called.get());
597 | assertEquals("fallback", result.get());
598 | }
599 |
600 | @Test
601 | public void testTimeoutWithFallbackCalled() {
602 | AtomicBoolean called = new AtomicBoolean(false);
603 | CircuitBreakerOptions options = new CircuitBreakerOptions().setTimeout(100)
604 | .setResetTimeout(5000)
605 | .setFallbackOnFailure(true);
606 | breaker = CircuitBreaker.create("test", vertx, options)
607 | .fallback(v -> {
608 | called.set(true);
609 | return "fallback";
610 | });
611 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
612 |
613 | AtomicInteger count = new AtomicInteger();
614 | for (int i = 0; i < options.getMaxFailures() + 3; i++) {
615 | breaker.execute(v -> {
616 | vertx.setTimer(500, id -> {
617 | v.complete("done");
618 | });
619 | }).onComplete(ar -> {
620 | if (ar.result().equals("fallback")) {
621 | count.incrementAndGet();
622 | }
623 | }).await();
624 | }
625 |
626 | assertEquals(CircuitBreakerState.OPEN, breaker.state());
627 | assertTrue(called.get());
628 | assertEquals(options.getMaxFailures() + 3, count.get());
629 | }
630 |
631 | @Test
632 | public void testResetAttemptOnTimeout() {
633 | AtomicBoolean called = new AtomicBoolean(false);
634 | AtomicBoolean hasBeenOpened = new AtomicBoolean(false);
635 | CircuitBreakerOptions options = new CircuitBreakerOptions()
636 | .setResetTimeout(100)
637 | .setTimeout(10)
638 | .setFallbackOnFailure(true);
639 | breaker = CircuitBreaker.create("test", vertx, options)
640 | .fallback(v -> {
641 | called.set(true);
642 | return "fallback";
643 | })
644 | .openHandler(v -> hasBeenOpened.set(true));
645 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
646 |
647 | for (int i = 0; i < options.getMaxFailures(); i++) {
648 | breaker.execute(future -> {
649 | // Do nothing with the future, this is a very bad thing.
650 | });
651 | }
652 | await().untilAtomic(hasBeenOpened, is(true));
653 | assertTrue(called.get());
654 |
655 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN);
656 | called.set(false);
657 |
658 | breaker.execute(Promise::complete);
659 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED);
660 | await().untilAtomic(called, is(false));
661 | }
662 |
663 | @Test
664 | @Repeat(10)
665 | public void testResetAttemptThatFailsOnTimeout() {
666 | AtomicBoolean called = new AtomicBoolean(false);
667 | AtomicBoolean hasBeenOpened = new AtomicBoolean(false);
668 | CircuitBreakerOptions options = new CircuitBreakerOptions()
669 | .setResetTimeout(100)
670 | .setTimeout(10)
671 | .setFallbackOnFailure(true);
672 | breaker = CircuitBreaker.create("test", vertx, options)
673 | .fallback(v -> {
674 | called.set(true);
675 | return "fallback";
676 | })
677 | .openHandler(v -> hasBeenOpened.set(true));
678 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
679 |
680 | for (int i = 0; i < options.getMaxFailures(); i++) {
681 | breaker.execute(future -> {
682 | // Do nothing with the future, this is a very bad thing.
683 | });
684 | }
685 | await().untilAtomic(hasBeenOpened, is(true));
686 | assertTrue(called.get());
687 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN);
688 | hasBeenOpened.set(false);
689 | called.set(false);
690 |
691 | breaker.execute(future -> {
692 | // Do nothing with the future, this is a very bad thing.
693 | });
694 | // Failed again, open circuit
695 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN);
696 | await().untilAtomic(called, is(true));
697 | await().untilAtomic(hasBeenOpened, is(true));
698 |
699 | hasBeenOpened.set(false);
700 | called.set(false);
701 |
702 | breaker.execute(future -> {
703 | // Do nothing with the future, this is a very bad thing.
704 | });
705 | // Failed again, open circuit
706 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN);
707 | await().untilAtomic(called, is(true));
708 | await().untilAtomic(hasBeenOpened, is(true));
709 |
710 | hasBeenOpened.set(false);
711 | called.set(false);
712 |
713 | hasBeenOpened.set(false);
714 | called.set(false);
715 |
716 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED || breaker.state() == CircuitBreakerState.HALF_OPEN);
717 |
718 | // If HO - need to get next request executed and wait until we are closed
719 | breaker.execute(Promise::complete);
720 | await().until(() -> {
721 | if (breaker.state() == CircuitBreakerState.CLOSED) {
722 | return true;
723 | } else {
724 | breaker.execute(Promise::complete);
725 | return false;
726 | }
727 | });
728 | called.set(false);
729 | for (int i = 0; i < options.getMaxFailures(); i++) {
730 | breaker.execute(f -> f.complete(null));
731 | }
732 |
733 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED);
734 | await().untilAtomic(hasBeenOpened, is(false));
735 | }
736 |
737 | @Test
738 | public void testThatOnlyOneRequestIsCheckedInHalfOpen() {
739 | AtomicBoolean called = new AtomicBoolean(false);
740 | AtomicBoolean hasBeenOpened = new AtomicBoolean(false);
741 | CircuitBreakerOptions options = new CircuitBreakerOptions()
742 | .setResetTimeout(1000)
743 | .setFallbackOnFailure(true);
744 | breaker = CircuitBreaker.create("test", vertx, options)
745 | .fallback(v -> {
746 | called.set(true);
747 | return "fallback";
748 | })
749 | .openHandler(v -> hasBeenOpened.set(true));
750 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
751 |
752 | for (int i = 0; i < options.getMaxFailures(); i++) {
753 | breaker.execute(future -> future.fail("expected failure"));
754 | }
755 | await().untilAtomic(hasBeenOpened, is(true));
756 | assertTrue(called.get());
757 |
758 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN);
759 | called.set(false);
760 |
761 | AtomicInteger fallbackCalled = new AtomicInteger();
762 | for (int i = 0; i < options.getMaxFailures(); i++) {
763 | breaker.executeWithFallback(
764 | future -> vertx.setTimer(500, l -> future.complete()),
765 | v -> {
766 | fallbackCalled.incrementAndGet();
767 | return "fallback";
768 | });
769 | }
770 |
771 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED);
772 | assertEquals(options.getMaxFailures() - 1, fallbackCalled.get());
773 | }
774 |
775 | @Test
776 | public void testFailureWhenThereIsNoFallback() {
777 | CircuitBreakerOptions options = new CircuitBreakerOptions()
778 | .setResetTimeout(50000)
779 | .setTimeout(300)
780 | .setFallbackOnFailure(true);
781 | breaker = CircuitBreaker.create("test", vertx, options);
782 |
783 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
784 |
785 | List> results = new ArrayList<>();
786 | for (int i = 0; i < options.getMaxFailures(); i++) {
787 | breaker.execute(future -> future.fail("expected failure"))
788 | .onComplete(ar -> results.add(ar));
789 | }
790 | await().until(() -> results.size() == options.getMaxFailures());
791 | results.forEach(ar -> {
792 | assertTrue(ar.failed());
793 | assertNotNull(ar.cause());
794 | assertEquals("expected failure", ar.cause().getMessage());
795 | });
796 |
797 | results.clear();
798 |
799 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN);
800 | breaker.execute(future -> future.fail("expected failure"))
801 | .onComplete(ar -> results.add(ar));
802 | await().until(() -> results.size() == 1);
803 | results.forEach(ar -> {
804 | assertTrue(ar.failed());
805 | assertNotNull(ar.cause());
806 | assertTrue(ar.cause() instanceof OpenCircuitException);
807 | assertEquals("open circuit", ar.cause().getMessage());
808 | });
809 |
810 | ((CircuitBreakerImpl) breaker).reset(true);
811 |
812 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
813 | results.clear();
814 |
815 | breaker.execute(future -> {
816 | try {
817 | Thread.sleep(500);
818 | } catch (InterruptedException e) {
819 | // Ignored.
820 | }
821 | })
822 | .onComplete(ar -> results.add(ar));
823 | await().until(() -> results.size() == 1);
824 | results.forEach(ar -> {
825 | assertTrue(ar.failed());
826 | assertNotNull(ar.cause());
827 | assertTrue(ar.cause() instanceof TimeoutException);
828 | assertEquals("operation timeout", ar.cause().getMessage());
829 | });
830 | }
831 |
832 | @Test
833 | public void testWhenFallbackThrowsAnException() {
834 | CircuitBreakerOptions options = new CircuitBreakerOptions()
835 | .setResetTimeout(5000)
836 | .setFallbackOnFailure(true);
837 | breaker = CircuitBreaker.create("test", vertx, options);
838 |
839 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
840 |
841 | List> results = new ArrayList<>();
842 | for (int i = 0; i < options.getMaxFailures(); i++) {
843 | breaker.executeWithFallback(
844 | future -> future.fail("expected failure"),
845 | t -> {
846 | throw new RuntimeException("boom");
847 | })
848 | .onComplete(ar -> results.add(ar));
849 | }
850 | await().until(() -> results.size() == options.getMaxFailures());
851 | results.forEach(ar -> {
852 | assertTrue(ar.failed());
853 | assertNotNull(ar.cause());
854 | assertEquals("boom", ar.cause().getMessage());
855 | });
856 |
857 | results.clear();
858 |
859 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN);
860 | breaker.executeWithFallback(
861 | future -> future.fail("expected failure"),
862 | t -> {
863 | throw new RuntimeException("boom");
864 | })
865 | .onComplete(ar -> results.add(ar));
866 | await().until(() -> results.size() == 1);
867 | results.forEach(ar -> {
868 | assertTrue(ar.failed());
869 | assertNotNull(ar.cause());
870 | assertEquals("boom", ar.cause().getMessage());
871 | });
872 | }
873 |
874 |
875 | @Test
876 | public void testTheExceptionReceivedByFallback() {
877 | CircuitBreakerOptions options = new CircuitBreakerOptions()
878 | .setResetTimeout(50000)
879 | .setTimeout(300)
880 | .setFallbackOnFailure(true);
881 | List failures = new ArrayList<>();
882 |
883 | breaker = CircuitBreaker.create("test", vertx, options)
884 | .fallback(failures::add);
885 |
886 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
887 |
888 | for (int i = 0; i < options.getMaxFailures(); i++) {
889 | breaker.execute(future -> future.fail("expected failure"));
890 | }
891 | await().until(() -> failures.size() == options.getMaxFailures());
892 | failures.forEach(ar -> {
893 | assertNotNull(ar);
894 | assertEquals("expected failure", ar.getMessage());
895 | });
896 |
897 | failures.clear();
898 |
899 | ((CircuitBreakerImpl) breaker).reset(true);
900 |
901 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
902 | failures.clear();
903 |
904 | breaker.execute(future -> {
905 | try {
906 | Thread.sleep(500);
907 | } catch (InterruptedException e) {
908 | // Ignored.
909 | }
910 | });
911 | await().until(() -> failures.size() == 1);
912 | failures.forEach(ar -> {
913 | assertNotNull(ar);
914 | assertTrue(ar instanceof TimeoutException);
915 | assertEquals("operation timeout", ar.getMessage());
916 | });
917 |
918 | ((CircuitBreakerImpl) breaker).reset(true);
919 |
920 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
921 | failures.clear();
922 |
923 | breaker.execute(future -> {
924 | throw new RuntimeException("boom");
925 | });
926 | await().until(() -> failures.size() == 1);
927 | failures.forEach(ar -> {
928 | assertNotNull(ar);
929 | assertEquals("boom", ar.getMessage());
930 | });
931 | }
932 |
933 | @Test
934 | @Repeat(5)
935 | public void testRetries() {
936 | CircuitBreakerOptions options = new CircuitBreakerOptions().setMaxRetries(5).setMaxFailures(4).setTimeout(100)
937 | .setFallbackOnFailure(true);
938 | List failures = new ArrayList<>();
939 |
940 | AtomicInteger calls = new AtomicInteger();
941 | breaker = CircuitBreaker.create("test", vertx, options);
942 |
943 |
944 | final AtomicReference result = new AtomicReference<>();
945 | vertx.runOnContext(v -> {
946 | result.set(breaker.execute(future -> {
947 | calls.incrementAndGet();
948 | future.fail("boom");
949 | }));
950 | });
951 |
952 | await().untilAtomic(calls, is(6));
953 | assertTrue(result.get().failed());
954 | assertEquals(1, breaker.failureCount());
955 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
956 |
957 | ((CircuitBreakerImpl) breaker).reset(true);
958 | calls.set(0);
959 | result.set(null);
960 |
961 | vertx.runOnContext(v -> {
962 | result.set(breaker.execute(future -> {
963 | if (calls.incrementAndGet() >= 4) {
964 | future.complete();
965 | } else {
966 | future.fail("boom");
967 | }
968 | }));
969 | });
970 |
971 |
972 | await().untilAtomic(calls, is(4));
973 | assertTrue(result.get().succeeded());
974 | assertEquals(0, breaker.failureCount());
975 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
976 |
977 | ((CircuitBreakerImpl) breaker).reset(true);
978 | calls.set(0);
979 |
980 | vertx.runOnContext(v -> {
981 | for (int i = 0; i < options.getMaxFailures() + 1; i++) {
982 | breaker.execute(future -> {
983 | calls.incrementAndGet();
984 | });
985 | }
986 | });
987 |
988 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN);
989 |
990 | calls.set(0);
991 | ((CircuitBreakerImpl) breaker).reset(true);
992 | AtomicReference result2 = new AtomicReference<>();
993 | vertx.runOnContext(v -> {
994 | result2.set(breaker.execute(future -> {
995 | if (calls.incrementAndGet() == 4) {
996 | future.complete();
997 | } else {
998 | }
999 | }));
1000 | for (int i = 0; i < options.getMaxFailures(); i++) {
1001 | breaker.execute(future -> {
1002 | future.fail("boom");
1003 | });
1004 | }
1005 | });
1006 |
1007 |
1008 | await().until(() -> result2.get() != null && result2.get().failed());
1009 | assertEquals(options.getMaxFailures() + 1, breaker.failureCount());
1010 | assertEquals(CircuitBreakerState.OPEN, breaker.state());
1011 |
1012 |
1013 | ((CircuitBreakerImpl) breaker).reset(true);
1014 | breaker.fallback(failures::add);
1015 | calls.set(0);
1016 | result.set(null);
1017 |
1018 | vertx.runOnContext(v -> {
1019 | result.set(breaker.execute(future -> {
1020 | try {
1021 | Thread.sleep(150);
1022 | } catch (InterruptedException e) {
1023 | Thread.currentThread().interrupt();
1024 | }
1025 | }));
1026 | });
1027 |
1028 |
1029 | await().until(() -> failures.size() == 1);
1030 | failures.forEach(ar -> {
1031 | assertNotNull(ar);
1032 | assertTrue(ar instanceof TimeoutException);
1033 | assertEquals("operation timeout", ar.getMessage());
1034 | });
1035 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
1036 |
1037 | ((CircuitBreakerImpl) breaker).reset(true);
1038 | calls.set(0);
1039 | result.set(null);
1040 |
1041 |
1042 | vertx.runOnContext(v -> {
1043 | result.set(breaker.execute(future -> {
1044 | if (calls.incrementAndGet() == 4) {
1045 | future.complete();
1046 | } else {
1047 | try {
1048 | Thread.sleep(150);
1049 | } catch (InterruptedException e) {
1050 | Thread.currentThread().interrupt();
1051 | }
1052 | }
1053 | }));
1054 | });
1055 |
1056 |
1057 | await().untilAtomic(calls, is(4));
1058 | assertTrue(result.get().succeeded());
1059 | assertEquals(0, breaker.failureCount());
1060 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
1061 |
1062 | }
1063 |
1064 | @Test(expected = IllegalArgumentException.class)
1065 | public void testInvalidBucketSize() {
1066 | CircuitBreakerOptions options = new CircuitBreakerOptions()
1067 | .setNotificationAddress(CircuitBreakerOptions.DEFAULT_NOTIFICATION_ADDRESS)
1068 | .setMetricsRollingBuckets(7);
1069 | CircuitBreaker.create("test", vertx, options);
1070 | }
1071 |
1072 | @Test
1073 | public void operationTimersShouldBeRemovedToAvoidOOM(TestContext ctx) {
1074 | breaker = CircuitBreaker.create("cb", vertx, new CircuitBreakerOptions().setTimeout(600_000));
1075 | Async async = ctx.async(3000);
1076 | long id = vertx.setPeriodic(1, l -> {
1077 | breaker.execute(prom -> prom.complete(new byte[10 * 1024 * 1024])).onSuccess(v -> async.countDown()).onFailure(ctx::fail);
1078 | });
1079 | async.await();
1080 | // Test will throw OOM if operation timers are not removed
1081 | }
1082 | }
1083 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/impl/CircuitBreakerMetricsTest.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker.tests.impl;
2 |
3 | import io.vertx.circuitbreaker.CircuitBreaker;
4 | import io.vertx.circuitbreaker.CircuitBreakerOptions;
5 | import io.vertx.circuitbreaker.CircuitBreakerState;
6 | import io.vertx.circuitbreaker.impl.CircuitBreakerImpl;
7 | import io.vertx.core.*;
8 | import io.vertx.core.json.JsonObject;
9 | import io.vertx.ext.unit.Async;
10 | import io.vertx.ext.unit.TestContext;
11 | import io.vertx.ext.unit.junit.Repeat;
12 | import io.vertx.ext.unit.junit.RepeatRule;
13 | import io.vertx.ext.unit.junit.VertxUnitRunner;
14 | import org.junit.After;
15 | import org.junit.Before;
16 | import org.junit.Rule;
17 | import org.junit.Test;
18 | import org.junit.runner.RunWith;
19 |
20 | import java.util.ArrayList;
21 | import java.util.List;
22 | import java.util.concurrent.atomic.AtomicBoolean;
23 | import java.util.stream.IntStream;
24 |
25 | import static org.awaitility.Awaitility.await;
26 | import static java.util.stream.Collectors.collectingAndThen;
27 | import static java.util.stream.Collectors.toList;
28 | import static org.hamcrest.core.Is.is;
29 | import static org.junit.Assert.assertEquals;
30 | import static org.junit.Assert.assertTrue;
31 |
32 | /**
33 | * @author Clement Escoffier
34 | */
35 | @RunWith(VertxUnitRunner.class)
36 | public class CircuitBreakerMetricsTest {
37 |
38 |
39 | private Vertx vertx;
40 | private CircuitBreaker breaker;
41 |
42 | @Rule
43 | public RepeatRule rule = new RepeatRule();
44 |
45 |
46 | @Before
47 | public void setUp(TestContext tc) {
48 | vertx = Vertx.vertx();
49 | vertx.exceptionHandler(tc.exceptionHandler());
50 | }
51 |
52 | @After
53 | public void tearDown() {
54 | vertx.exceptionHandler(null);
55 | if (breaker != null) {
56 | breaker.close();
57 | }
58 | AtomicBoolean completed = new AtomicBoolean();
59 | vertx.close().onComplete(ar -> completed.set(ar.succeeded()));
60 | await().untilAtomic(completed, is(true));
61 | }
62 |
63 |
64 | @Test
65 | @Repeat(10)
66 | public void testWithSuccessfulCommands(TestContext tc) {
67 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions());
68 | Async async = tc.async();
69 |
70 |
71 | Future command1 = breaker.execute(commandThatWorks());
72 | Future command2 = breaker.execute(commandThatWorks());
73 | Future command3 = breaker.execute(commandThatWorks());
74 |
75 | Future.all(command1, command2, command3)
76 | .onComplete(ar -> {
77 | assertTrue(ar.succeeded());
78 | assertEquals("some-circuit-breaker", metrics().getString("name"));
79 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state"));
80 | assertEquals(0, (int)metrics().getInteger("failures"));
81 | assertEquals(0, (int)metrics().getInteger("totalErrorCount"));
82 | assertEquals(3, (int)metrics().getInteger("totalSuccessCount"));
83 | assertEquals(0, (int)metrics().getInteger("totalTimeoutCount"));
84 | assertEquals(0, (int)metrics().getInteger("totalExceptionCount"));
85 | assertEquals(0, (int)metrics().getInteger("totalFailureCount"));
86 | assertEquals(100, (int)metrics().getInteger("totalSuccessPercentage"));
87 | assertEquals(0, (int)metrics().getInteger("totalErrorPercentage"));
88 | async.complete();
89 | });
90 | }
91 |
92 | private CircuitBreakerOptions getOptions() {
93 | return new CircuitBreakerOptions()
94 | .setNotificationAddress(CircuitBreakerOptions.DEFAULT_NOTIFICATION_ADDRESS);
95 | }
96 |
97 | @Test
98 | @Repeat(10)
99 | public void testWithFailedCommands(TestContext tc) {
100 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions());
101 | Async async = tc.async();
102 |
103 | Future command1 = breaker.execute(commandThatFails());
104 | Future command2 = breaker.execute(commandThatWorks());
105 | Future command3 = breaker.execute(commandThatWorks());
106 | Future command4 = breaker.execute(commandThatFails());
107 |
108 | Future.join(command1, command2, command3, command4)
109 | .onComplete(ar -> {
110 | assertEquals("some-circuit-breaker", metrics().getString("name"));
111 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state"));
112 | assertEquals(2, (int)metrics().getInteger("totalErrorCount"));
113 | assertEquals(2, (int)metrics().getInteger("totalSuccessCount"));
114 | assertEquals(0, (int)metrics().getInteger("totalTimeoutCount"));
115 | assertEquals(0, (int)metrics().getInteger("totalExceptionCount"));
116 | assertEquals(2, (int)metrics().getInteger("totalFailureCount"));
117 | assertEquals(4, (int)metrics().getInteger("totalOperationCount"));
118 | assertEquals(50, (int)metrics().getInteger("totalSuccessPercentage"));
119 | assertEquals(50, (int)metrics().getInteger("totalErrorPercentage"));
120 | async.complete();
121 | });
122 | }
123 |
124 | @Test
125 | @Repeat(10)
126 | public void testWithCrashingCommands(TestContext tc) {
127 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions());
128 | Async async = tc.async();
129 |
130 | Future command1 = breaker.execute(commandThatFails());
131 | Future command2 = breaker.execute(commandThatWorks());
132 | Future command3 = breaker.execute(commandThatWorks());
133 | Future command4 = breaker.execute(commandThatFails());
134 | Future command5 = breaker.execute(commandThatCrashes());
135 |
136 | Future.join(command1, command2, command3, command4, command5)
137 | .onComplete(ar -> {
138 | assertEquals("some-circuit-breaker", metrics().getString("name"));
139 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state"));
140 | assertEquals(3, (int)metrics().getInteger("totalErrorCount"));
141 | assertEquals(2, (int)metrics().getInteger("totalSuccessCount"));
142 | assertEquals(0, (int)metrics().getInteger("totalTimeoutCount"));
143 | assertEquals(1, (int)metrics().getInteger("totalExceptionCount"));
144 | assertEquals(2, (int)metrics().getInteger("totalFailureCount"));
145 | assertEquals(5, (int)metrics().getInteger("totalOperationCount"));
146 | assertEquals((2.0 / 5 * 100), (float)metrics().getFloat("totalSuccessPercentage"), 0.1);
147 | assertEquals((3.0 / 5 * 100), (float)metrics().getFloat("totalErrorPercentage"), 0.1);
148 | async.complete();
149 | });
150 | }
151 |
152 | @Test
153 | @Repeat(10)
154 | public void testWithTimeoutCommands(TestContext tc) {
155 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions().setTimeout(100));
156 | Async async = tc.async();
157 |
158 | Future command1 = breaker.execute(commandThatFails());
159 | Future command2 = breaker.execute(commandThatWorks());
160 | Future command3 = breaker.execute(commandThatWorks());
161 | Future command4 = breaker.execute(commandThatFails());
162 | Future command5 = breaker.execute(commandThatTimeout(100));
163 |
164 | Future.join(command1, command2, command3, command4, command5)
165 | .onComplete(ar -> {
166 | assertEquals("some-circuit-breaker", metrics().getString("name"));
167 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state"));
168 | assertEquals(3, (int)metrics().getInteger("totalErrorCount"));
169 | assertEquals(2, (int)metrics().getInteger("totalSuccessCount"));
170 | assertEquals(1, (int)metrics().getInteger("totalTimeoutCount"));
171 | assertEquals(0, (int)metrics().getInteger("totalExceptionCount"));
172 | assertEquals(2, (int)metrics().getInteger("totalFailureCount"));
173 | assertEquals(5, (int)metrics().getInteger("totalOperationCount"));
174 | assertEquals((2.0 / 5 * 100), (float)metrics().getFloat("totalSuccessPercentage"), 0.1);
175 | assertEquals((3.0 / 5 * 100), (float)metrics().getFloat("totalErrorPercentage"), 0.1);
176 | async.complete();
177 | });
178 | }
179 |
180 |
181 | @Test
182 | @Repeat(10)
183 | public void testLatencyComputation(TestContext tc) {
184 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions());
185 | Async async = tc.async();
186 |
187 |
188 | int count = 1000;
189 |
190 | IntStream.range(0, count)
191 | .mapToObj(i -> breaker.execute(commandThatWorks()))
192 | .collect(collectingAndThen(toList(), Future::all))
193 | .onComplete(ar -> {
194 | assertTrue(ar.succeeded());
195 | assertEquals("some-circuit-breaker", metrics().getString("name"));
196 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state"));
197 | assertEquals(0, (int)metrics().getInteger("failures"));
198 | assertEquals(0, (int)metrics().getInteger("totalErrorCount"));
199 | assertEquals(count, (int)metrics().getInteger("totalSuccessCount"));
200 | assertEquals(0, (int)metrics().getInteger("totalTimeoutCount"));
201 | assertEquals(0, (int)metrics().getInteger("totalExceptionCount"));
202 | assertEquals(0, (int)metrics().getInteger("totalFailureCount"));
203 | assertEquals(count, (int)metrics().getInteger("totalOperationCount"));
204 | assertEquals(100, metrics().getFloat("totalSuccessPercentage"), 0.1);
205 | assertEquals(0, metrics().getFloat("totalErrorPercentage"), 0.1);
206 | async.complete();
207 | });
208 | }
209 |
210 | @Test
211 | @Repeat(100)
212 | public void testEviction(TestContext tc) {
213 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions().setMetricsRollingWindow(10));
214 | Async async = tc.async();
215 |
216 |
217 | int count = 1000;
218 |
219 | List> list = new ArrayList<>();
220 | for (int i = 0; i < count; i++) {
221 | list.add(breaker.execute(commandThatWorks()));
222 | }
223 |
224 | Future.all(list)
225 | .onComplete(ar -> {
226 | assertTrue(ar.succeeded());
227 | assertEquals(1000, (int)metrics().getInteger("totalOperationCount"));
228 | assertTrue(metrics().getInteger("rollingOperationCount") <= 1000);
229 | async.complete();
230 | });
231 | }
232 |
233 |
234 | private Handler> commandThatWorks() {
235 | return (future -> vertx.setTimer(5, l -> future.complete(null)));
236 | }
237 |
238 | private Handler> commandThatFails() {
239 | return (future -> vertx.setTimer(5, l -> future.fail("expected failure")));
240 | }
241 |
242 | private Handler> commandThatCrashes() {
243 | return (future -> {
244 | throw new RuntimeException("Expected error");
245 | });
246 | }
247 |
248 | private Handler> commandThatTimeout(int timeout) {
249 | return (future -> vertx.setTimer(timeout + 500, l -> future.complete(null)));
250 | }
251 |
252 | private JsonObject metrics() {
253 | return ((CircuitBreakerImpl) breaker).getMetrics();
254 | }
255 |
256 | }
257 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/impl/CircuitBreakerWithHTTPTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2016 The original author or authors
3 | *
4 | * All rights reserved. This program and the accompanying materials
5 | * are made available under the terms of the Eclipse Public License v1.0
6 | * and Apache License v2.0 which accompanies this distribution.
7 | *
8 | * The Eclipse Public License is available at
9 | * http://www.eclipse.org/legal/epl-v10.html
10 | *
11 | * The Apache License v2.0 is available at
12 | * http://www.opensource.org/licenses/apache2.0.php
13 | *
14 | * You may elect to redistribute this code under either of these licenses.
15 | */
16 |
17 | package io.vertx.circuitbreaker.tests.impl;
18 |
19 | import io.vertx.circuitbreaker.CircuitBreaker;
20 | import io.vertx.circuitbreaker.CircuitBreakerOptions;
21 | import io.vertx.circuitbreaker.CircuitBreakerState;
22 | import io.vertx.core.Future;
23 | import io.vertx.core.Promise;
24 | import io.vertx.core.Vertx;
25 | import io.vertx.core.VertxException;
26 | import io.vertx.core.buffer.Buffer;
27 | import io.vertx.core.http.HttpClient;
28 | import io.vertx.core.http.HttpClientRequest;
29 | import io.vertx.core.http.HttpClientResponse;
30 | import io.vertx.core.http.HttpMethod;
31 | import io.vertx.core.http.HttpServer;
32 | import org.junit.After;
33 | import org.junit.Before;
34 | import org.junit.Ignore;
35 | import org.junit.Test;
36 |
37 | import java.time.Duration;
38 | import java.time.LocalDateTime;
39 | import java.util.ArrayList;
40 | import java.util.Collections;
41 | import java.util.List;
42 | import java.util.concurrent.atomic.AtomicBoolean;
43 | import java.util.concurrent.atomic.AtomicInteger;
44 |
45 | import static org.awaitility.Awaitility.*;
46 | import static io.vertx.core.http.HttpHeaders.*;
47 | import static java.util.concurrent.TimeUnit.*;
48 | import static org.hamcrest.core.Is.*;
49 | import static org.junit.Assert.*;
50 |
51 | /**
52 | * Test the circuit breaker when doing HTTP calls.
53 | *
54 | * @author Clement Escoffier
55 | */
56 | public class CircuitBreakerWithHTTPTest {
57 | private Vertx vertx;
58 | private HttpServer http;
59 | private HttpClient client;
60 | private CircuitBreaker breaker;
61 |
62 | @Before
63 | public void setUp() throws Exception {
64 | vertx = Vertx.vertx();
65 | AtomicBoolean invoked = new AtomicBoolean();
66 | http = vertx
67 | .createHttpServer()
68 | .requestHandler(request -> {
69 |
70 | switch (request.path()) {
71 | case "/":
72 | request.response().end("hello");
73 | break;
74 | case "/error":
75 | request.response().setStatusCode(500).end("failed !");
76 | break;
77 | case "/long":
78 | try {
79 | Thread.sleep(2000);
80 | } catch (Exception e) {
81 | // Ignored.
82 | }
83 | request.response().end("hello");
84 | break;
85 | case "/flaky":
86 | if (invoked.compareAndSet(false, true)) {
87 | request.response().setStatusCode(503).putHeader(RETRY_AFTER, "2").end();
88 | } else {
89 | request.response().setStatusCode(200).end();
90 | }
91 | break;
92 | }
93 | })
94 | .listen(8080)
95 | .await(20, SECONDS);
96 | client = vertx.createHttpClient();
97 | }
98 |
99 | @After
100 | public void tearDown() throws Exception {
101 | if (breaker != null) {
102 | breaker.close();
103 | }
104 | try {
105 | vertx
106 | .close()
107 | .await(20, SECONDS);
108 | } finally {
109 | vertx = null;
110 | http = null;
111 | client = null;
112 | }
113 | }
114 |
115 | @Test
116 | public void testOk() {
117 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions());
118 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
119 |
120 | Promise result = Promise.promise();
121 | breaker.executeAndReport(result, v -> client.request(HttpMethod.GET, 8080, "localhost", "/")
122 | .compose(req -> req
123 | .send()
124 | .compose(resp -> resp
125 | .body()
126 | .map(Buffer::toString)))
127 | .onComplete(v));
128 |
129 | await().until(() -> result.future().result() != null);
130 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
131 | }
132 |
133 | @Test
134 | public void testFailure() {
135 | CircuitBreakerOptions options = new CircuitBreakerOptions();
136 | breaker = CircuitBreaker.create("test", vertx, options);
137 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
138 |
139 | AtomicInteger count = new AtomicInteger();
140 |
141 | for (int i = 0; i < options.getMaxFailures(); i++) {
142 | Promise result = Promise.promise();
143 | breaker.executeAndReport(result, future ->
144 | client.request(HttpMethod.GET, 8080, "localhost", "/error").compose(req ->
145 | req.send().compose(resp -> Future.succeededFuture(resp.statusCode()))
146 | ).onSuccess(sc -> {
147 | if (sc != 200) {
148 | future.fail("http error");
149 | } else {
150 | future.complete();
151 | }
152 | count.incrementAndGet();
153 | })
154 | );
155 | }
156 |
157 | await().untilAtomic(count, is(options.getMaxFailures()));
158 | assertEquals(CircuitBreakerState.OPEN, breaker.state());
159 |
160 | Promise result = Promise.promise();
161 | breaker.executeAndReportWithFallback(result, future ->
162 | client.request(HttpMethod.GET, 8080, "localhost", "/error")
163 | .compose(req -> req.send().compose(resp -> Future.succeededFuture(resp.statusCode())))
164 | .onSuccess(sc -> {
165 | if (sc != 200) {
166 | future.fail("http error");
167 | } else {
168 | future.complete();
169 | }
170 | }), v -> "fallback");
171 |
172 | await().until(() -> result.future().result().equals("fallback"));
173 | assertEquals(CircuitBreakerState.OPEN, breaker.state());
174 |
175 | }
176 |
177 | @Test
178 | public void testTimeout() {
179 | CircuitBreakerOptions options = new CircuitBreakerOptions().setTimeout(100).setMaxFailures(2);
180 | breaker = CircuitBreaker.create("test", vertx, options);
181 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
182 |
183 | AtomicInteger count = new AtomicInteger();
184 |
185 | for (int i = 0; i < options.getMaxFailures(); i++) {
186 | breaker.execute(future ->
187 | client.request(HttpMethod.GET,8080, "localhost", "/long").compose(req ->
188 | req.send().compose(HttpClientResponse::body).onSuccess(body -> {
189 | count.incrementAndGet();
190 | future.complete();
191 | })));
192 | }
193 |
194 | await().untilAtomic(count, is(options.getMaxFailures()));
195 | assertEquals(CircuitBreakerState.OPEN, breaker.state());
196 |
197 | Promise result = Promise.promise();
198 | breaker.executeAndReportWithFallback(result, future ->
199 | client.request(HttpMethod.GET, 8080, "localhost", "/long")
200 | .compose(HttpClientRequest::send).onSuccess(response -> {
201 | System.out.println("Got response");
202 | future.complete();
203 | }), v -> "fallback");
204 |
205 | await().until(() -> result.future().result().equals("fallback"));
206 | assertEquals(CircuitBreakerState.OPEN, breaker.state());
207 | }
208 |
209 | private static class ServiceUnavailableException extends VertxException {
210 | final int delay;
211 |
212 | ServiceUnavailableException(int delay) {
213 | super("unavailable", true);
214 | this.delay = delay;
215 | }
216 | }
217 |
218 | @Test
219 | public void testUseRetryAfterHeaderValue() {
220 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions().setMaxRetries(1))
221 | .retryPolicy((failure, retryCount) -> {
222 | if (failure instanceof ServiceUnavailableException) {
223 | ServiceUnavailableException sue = (ServiceUnavailableException) failure;
224 | return MILLISECONDS.convert(sue.delay, SECONDS);
225 | }
226 | return 0;
227 | });
228 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
229 |
230 | List requestLocalDateTimes = Collections.synchronizedList(new ArrayList<>());
231 | Promise result = Promise.promise();
232 | breaker.executeAndReport(result, v -> {
233 | requestLocalDateTimes.add(LocalDateTime.now());
234 | client.request(HttpMethod.GET, 8080, "localhost", "/flaky")
235 | .compose(req -> req
236 | .send()
237 | .compose(resp -> {
238 | if (resp.statusCode() == 503) {
239 | ServiceUnavailableException sue = new ServiceUnavailableException(Integer.parseInt(resp.getHeader(RETRY_AFTER)));
240 | return Future.failedFuture(sue);
241 | } else {
242 | return resp.body().map(Buffer::toString);
243 | }
244 | })
245 | )
246 | .onComplete(v);
247 | });
248 |
249 | await().until(() -> result.future().result() != null);
250 | assertEquals(CircuitBreakerState.CLOSED, breaker.state());
251 |
252 | assertEquals(2, requestLocalDateTimes.size());
253 | assertTrue(Duration.between(requestLocalDateTimes.get(0), requestLocalDateTimes.get(1)).toMillis() >= 2000);
254 | }
255 |
256 | }
257 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/impl/MyAsyncOperations.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2011-2016 The original author or authors
3 | *
4 | * All rights reserved. This program and the accompanying materials
5 | * are made available under the terms of the Eclipse Public License v1.0
6 | * and Apache License v2.0 which accompanies this distribution.
7 | *
8 | * The Eclipse Public License is available at
9 | * http://www.eclipse.org/legal/epl-v10.html
10 | *
11 | * The Apache License v2.0 is available at
12 | * http://www.opensource.org/licenses/apache2.0.php
13 | *
14 | * You may elect to redistribute this code under either of these licenses.
15 | */
16 |
17 | package io.vertx.circuitbreaker.tests.impl;
18 |
19 | import io.vertx.core.*;
20 |
21 | /**
22 | * Some methods using asynchronous patterns.
23 | * @author Clement Escoffier
24 | */
25 | public class MyAsyncOperations {
26 |
27 | public static void operation(int a, int b, Completable handler) {
28 | handler.succeed(a + b);
29 | }
30 |
31 | public static void fail(Handler> handler) {
32 | handler.handle(Future.failedFuture("boom"));
33 | }
34 |
35 | public static void operation(Promise future, int a, int b) {
36 | future.complete(a + b);
37 | }
38 |
39 | public static void fail(Promise future) {
40 | future.fail("boom");
41 | }
42 |
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/impl/NumberOfRetryTest.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker.tests.impl;
2 |
3 | import io.vertx.circuitbreaker.CircuitBreaker;
4 | import io.vertx.circuitbreaker.CircuitBreakerOptions;
5 | import io.vertx.core.Vertx;
6 | import org.junit.After;
7 | import org.junit.Before;
8 | import org.junit.Test;
9 |
10 | import java.util.concurrent.atomic.AtomicInteger;
11 |
12 | import static org.awaitility.Awaitility.await;
13 | import static org.hamcrest.Matchers.is;
14 |
15 | /**
16 | * Checks the number of retries.
17 | *
18 | * @author Clement Escoffier
19 | */
20 | public class NumberOfRetryTest {
21 |
22 | private Vertx vertx;
23 |
24 | @Before
25 | public void setup() {
26 | vertx = Vertx.vertx();
27 | }
28 |
29 | @After
30 | public void tearDown() {
31 | vertx.close();
32 | }
33 |
34 |
35 | @Test
36 | public void testWithoutRetry() {
37 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx,
38 | new CircuitBreakerOptions().setMaxFailures(5));
39 | AtomicInteger counter = new AtomicInteger();
40 |
41 | breaker.execute(future -> {
42 | counter.incrementAndGet();
43 | future.fail("FAILED");
44 | }).onComplete(ar -> {
45 |
46 | });
47 |
48 | await().untilAtomic(counter, is(1));
49 | }
50 |
51 | @Test
52 | public void testWithRetrySetToZero() {
53 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx,
54 | new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(0));
55 | AtomicInteger counter = new AtomicInteger();
56 |
57 | breaker.execute(future -> {
58 | counter.incrementAndGet();
59 | future.fail("FAILED");
60 | }).onComplete(ar -> {
61 |
62 | });
63 |
64 | await().untilAtomic(counter, is(1));
65 | }
66 |
67 | @Test
68 | public void testWithRetrySetToOne() {
69 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx,
70 | new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(1));
71 | AtomicInteger counter = new AtomicInteger();
72 |
73 | breaker.execute(future -> {
74 | counter.incrementAndGet();
75 | future.fail("FAILED");
76 | }).onComplete(ar -> {
77 |
78 | });
79 |
80 | await().untilAtomic(counter, is(2));
81 | }
82 |
83 | @Test
84 | public void testWithRetrySetToFive() {
85 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx,
86 | new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(5));
87 | AtomicInteger counter = new AtomicInteger();
88 |
89 | breaker.execute(future -> {
90 | counter.incrementAndGet();
91 | future.fail("FAILED");
92 | }).onComplete(ar -> {
93 |
94 | });
95 |
96 | await().untilAtomic(counter, is(6));
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/impl/RetryPolicyTest.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker.tests.impl;
2 |
3 | import io.vertx.circuitbreaker.CircuitBreaker;
4 | import io.vertx.circuitbreaker.CircuitBreakerOptions;
5 | import io.vertx.circuitbreaker.RetryPolicy;
6 | import io.vertx.core.Vertx;
7 | import org.junit.After;
8 | import org.junit.Before;
9 | import org.junit.Test;
10 |
11 | import java.util.concurrent.atomic.AtomicInteger;
12 |
13 | import static org.awaitility.Awaitility.*;
14 | import static org.hamcrest.Matchers.*;
15 |
16 | /**
17 | * Checks that retry policy is being applied
18 | */
19 | public class RetryPolicyTest {
20 | private Vertx vertx;
21 |
22 | @Before
23 | public void setup() {
24 | vertx = Vertx.vertx();
25 | }
26 |
27 | @After
28 | public void tearDown() {
29 | vertx.close();
30 | }
31 |
32 | @Test
33 | public void testWithRetryPolicy() {
34 | runRetryPolicyTest(RetryPolicy.linearDelay(100, 10000));
35 | }
36 |
37 | @Test
38 | public void testWithZeroRetryPolicy() {
39 | runRetryPolicyTest((failure, retryCount) -> 0);
40 | }
41 |
42 | @Test
43 | public void testWithNegativeRetryPolicy() {
44 | runRetryPolicyTest((failure, retryCount) -> -1);
45 | }
46 |
47 | /**
48 | * Helper method to run retry policy tests
49 | */
50 | private void runRetryPolicyTest(RetryPolicy retryPolicy) {
51 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx,
52 | new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(5));
53 | AtomicInteger counter = new AtomicInteger();
54 | AtomicInteger retryPolicyCounter = new AtomicInteger();
55 |
56 | breaker.retryPolicy((failure, retryCount) -> {
57 | retryPolicyCounter.incrementAndGet();
58 | return retryPolicy.delay(null, retryCount);
59 | });
60 |
61 | breaker.execute(future -> {
62 | counter.incrementAndGet();
63 | future.fail("FAILED");
64 | }).onComplete(ar -> {
65 |
66 | });
67 |
68 | await().untilAtomic(counter, is(6));
69 | await().untilAtomic(retryPolicyCounter, is(5));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/test/java/io/vertx/circuitbreaker/tests/impl/UsageTest.java:
--------------------------------------------------------------------------------
1 | package io.vertx.circuitbreaker.tests.impl;
2 |
3 | import io.vertx.circuitbreaker.CircuitBreaker;
4 | import io.vertx.circuitbreaker.CircuitBreakerOptions;
5 | import io.vertx.core.Future;
6 | import io.vertx.core.Promise;
7 | import io.vertx.core.Vertx;
8 | import io.vertx.core.buffer.Buffer;
9 | import io.vertx.core.eventbus.Message;
10 | import io.vertx.core.http.HttpClient;
11 | import io.vertx.core.http.HttpHeaders;
12 | import io.vertx.core.http.HttpMethod;
13 | import io.vertx.core.http.HttpServer;
14 | import io.vertx.core.json.JsonObject;
15 | import io.vertx.ext.unit.junit.Repeat;
16 | import io.vertx.ext.unit.junit.RepeatRule;
17 | import io.vertx.ext.unit.junit.VertxUnitRunner;
18 | import org.junit.After;
19 | import org.junit.Before;
20 | import org.junit.Rule;
21 | import org.junit.Test;
22 | import org.junit.runner.RunWith;
23 |
24 | import java.util.concurrent.ThreadLocalRandom;
25 | import java.util.concurrent.TimeUnit;
26 | import java.util.concurrent.atomic.AtomicReference;
27 |
28 | import static org.awaitility.Awaitility.await;
29 | import static org.hamcrest.CoreMatchers.*;
30 | import static org.junit.Assert.assertEquals;
31 |
32 | /**
33 | * @author Clement Escoffier
34 | */
35 | @RunWith(VertxUnitRunner.class)
36 | public class UsageTest {
37 |
38 | @Rule
39 | public RepeatRule repeatRule = new RepeatRule();
40 |
41 | private Vertx vertx;
42 | private CircuitBreaker cb;
43 | private HttpServer server;
44 |
45 | @Before
46 | public void setUp() {
47 | vertx = Vertx.vertx();
48 | cb = CircuitBreaker.create("circuit-breaker", vertx, new CircuitBreakerOptions()
49 | .setFallbackOnFailure(true)
50 | .setTimeout(500)
51 | .setResetTimeout(1000));
52 |
53 | vertx.eventBus().consumer("ok", message -> message.reply("OK"));
54 |
55 | vertx.eventBus().consumer("fail", message -> message.fail(100, "Bad bad bad"));
56 |
57 | vertx.eventBus().consumer("exception", message -> {
58 | throw new RuntimeException("RT - Bad bad bad");
59 | });
60 |
61 | vertx.eventBus().consumer("timeout", message -> vertx.setTimer(2000, x -> message.reply("Too late")));
62 | }
63 |
64 | @After
65 | public void tearDown() {
66 | if (server != null) {
67 | server.close().await();
68 | }
69 | cb.close();
70 | vertx.close().await();
71 | }
72 |
73 | @Test
74 | @Repeat(10)
75 | public void testCBWithReadOperation() throws Exception {
76 | server = vertx.createHttpServer().requestHandler(req -> {
77 | switch (req.path()) {
78 | case "/resource":
79 | req.response()
80 | .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
81 | .end(new JsonObject().put("status", "OK").encode());
82 | break;
83 | case "/delayed":
84 | vertx.setTimer(2000, id -> {
85 | req.response().end();
86 | });
87 | break;
88 | case "/error":
89 | req.response()
90 | .setStatusCode(500)
91 | .end("This is an error");
92 | break;
93 | }
94 | }).listen(8089)
95 | .await(20, TimeUnit.SECONDS);
96 |
97 |
98 | HttpClient client = vertx.createHttpClient();
99 |
100 | AtomicReference json = new AtomicReference<>();
101 | cb.executeWithFallback(
102 | promise -> {
103 | client.request(HttpMethod.GET, 8089, "localhost", "/resource")
104 | .compose(req -> req
105 | .putHeader("Accept", "application/json")
106 | .send().compose(resp -> resp
107 | .body()
108 | .map(Buffer::toJsonObject))
109 | ).onComplete(promise);
110 | },
111 | t -> null
112 | ).onComplete(ar -> json.set(ar.result()));
113 | await().atMost(1, TimeUnit.MINUTES).untilAtomic(json, is(notNullValue()));
114 | assertEquals("OK", json.get().getString("status"));
115 |
116 | json.set(null);
117 | cb.executeWithFallback(
118 | promise -> {
119 | client.request(HttpMethod.GET, 8089, "localhost", "/error")
120 | .compose(req -> req
121 | .putHeader("Accept", "application/json")
122 | .send().compose(resp -> {
123 | if (resp.statusCode() != 200) {
124 | return Future.failedFuture("Invalid response");
125 | } else {
126 | return resp.body().map(Buffer::toJsonObject);
127 | }
128 | })
129 | ).onComplete(promise);
130 | },
131 | t -> new JsonObject().put("status", "KO")
132 | ).onComplete(ar -> json.set(ar.result()));
133 | await().untilAtomic(json, is(notNullValue()));
134 | assertEquals("KO", json.get().getString("status"));
135 |
136 | json.set(null);
137 | cb.executeWithFallback(
138 | promise -> {
139 | client.request(HttpMethod.GET, 8089, "localhost", "/delayed")
140 | .compose(req -> req
141 | .putHeader("Accept", "application/json")
142 | .send().compose(resp -> {
143 | if (resp.statusCode() != 200) {
144 | return Future.failedFuture("Invalid response");
145 | } else {
146 | return resp.body().map(Buffer::toJsonObject);
147 | }
148 | })
149 | ).onComplete(promise);
150 | },
151 | t -> new JsonObject().put("status", "KO")
152 | ).onComplete(ar -> json.set(ar.result()));
153 | await().untilAtomic(json, is(notNullValue()));
154 | assertEquals("KO", json.get().getString("status"));
155 | }
156 |
157 | private void asyncWrite(Scenario scenario, Promise promise) {
158 | long delay;
159 | switch (scenario) {
160 | case RUNTIME_EXCEPTION:
161 | throw new RuntimeException("Bad bad bad");
162 | case TIMEOUT:
163 | delay = 2000;
164 | break;
165 | default:
166 | delay = ThreadLocalRandom.current().nextLong(1, 250); // Must be less than CB timeout
167 | break;
168 | }
169 |
170 | vertx.setTimer(delay, l -> {
171 | if (scenario == Scenario.FAILURE) {
172 | promise.fail("Bad Bad Bad");
173 | } else {
174 | promise.succeed("foo");
175 | }
176 | });
177 | }
178 |
179 | private enum Scenario {
180 | OK,
181 | FAILURE,
182 | RUNTIME_EXCEPTION,
183 | TIMEOUT
184 | }
185 |
186 | @Test
187 | @Repeat(10)
188 | public void testCBWithWriteOperation() {
189 | AtomicReference str = new AtomicReference<>();
190 | cb.executeWithFallback(
191 | promise -> asyncWrite(Scenario.OK, promise),
192 | t -> "bar"
193 | ).onComplete(ar -> str.set(ar.result()));
194 | await().untilAtomic(str, is(equalTo("foo")));
195 |
196 | str.set(null);
197 | cb.executeWithFallback(
198 | promise -> asyncWrite(Scenario.FAILURE, promise),
199 | t -> "bar"
200 | ).onComplete(ar -> str.set(ar.result()));
201 | await().untilAtomic(str, is(equalTo("bar")));
202 |
203 | str.set(null);
204 | cb.executeWithFallback(
205 | promise -> asyncWrite(Scenario.TIMEOUT, promise),
206 | t -> "bar"
207 | ).onComplete(ar -> str.set(ar.result()));
208 | await().untilAtomic(str, is(equalTo("bar")));
209 |
210 | str.set(null);
211 | cb.executeWithFallback(
212 | promise -> asyncWrite(Scenario.RUNTIME_EXCEPTION, promise),
213 | t -> "bar"
214 | ).onComplete(ar -> str.set(ar.result()));
215 | await().untilAtomic(str, is(equalTo("bar")));
216 | }
217 |
218 |
219 | @Test
220 | public void testCBWithEventBus() {
221 | AtomicReference str = new AtomicReference<>();
222 | cb.executeWithFallback(
223 | promise -> vertx.eventBus().request("ok", "").map(Message::body).onComplete(promise),
224 | t -> "KO"
225 | ).onComplete(ar -> str.set(ar.result()));
226 | await().untilAtomic(str, is(equalTo("OK")));
227 |
228 | str.set(null);
229 | cb.executeWithFallback(
230 | promise -> vertx.eventBus().request("timeout", "").map(Message::body).onComplete(promise),
231 | t -> "KO"
232 | ).onComplete(ar -> str.set(ar.result()));
233 | await().untilAtomic(str, is(equalTo("KO")));
234 |
235 | str.set(null);
236 | cb.executeWithFallback(
237 | promise -> vertx.eventBus().request("fail", "").map(Message::body).onComplete(promise),
238 | t -> "KO"
239 | ).onComplete(ar -> str.set(ar.result()));
240 | await().untilAtomic(str, is(equalTo("KO")));
241 |
242 | str.set(null);
243 | cb.executeWithFallback(
244 | promise -> vertx.eventBus().request("exception", "").map(Message::body).onComplete(promise),
245 | t -> "KO"
246 | ).onComplete(ar -> str.set(ar.result()));
247 | await().untilAtomic(str, is(equalTo("KO")));
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/src/test/java/module-info.java:
--------------------------------------------------------------------------------
1 | open module io.vertx.circuitbreaker.tests {
2 | requires io.vertx.circuitbreaker;
3 | requires io.vertx.core;
4 | requires io.vertx.core.logging;
5 | requires io.vertx.testing.unit;
6 | requires junit;
7 | requires awaitility;
8 | requires org.hamcrest;
9 | }
10 |
--------------------------------------------------------------------------------
/src/test/resources/META-INF/services/io.vertx.core.spi.JsonFactory:
--------------------------------------------------------------------------------
1 | io.vertx.circuitbreaker.tests.JsonFactory
2 |
--------------------------------------------------------------------------------