diff --git a/circuit-breaker/README.md b/circuit-breaker/README.md index 8d2205f2b..f8c9aca69 100644 --- a/circuit-breaker/README.md +++ b/circuit-breaker/README.md @@ -52,40 +52,105 @@ In terms of code, the end user application is: ```java public class App { - + private static final Logger LOGGER = LoggerFactory.getLogger(App.class); - + + /** + * Program entry point. + * + * @param args command line args + */ public static void main(String[] args) { - var obj = new MonitoringService(); - var circuitBreaker = new CircuitBreaker(3000, 1, 2000 * 1000 * 1000); + var serverStartTime = System.nanoTime(); - while (true) { - LOGGER.info(obj.localResourceResponse()); - LOGGER.info(obj.remoteResourceResponse(circuitBreaker, serverStartTime)); - LOGGER.info(circuitBreaker.getState()); - try { - Thread.sleep(5 * 1000); - } catch (InterruptedException e) { - LOGGER.error(e.getMessage()); - } + + var delayedService = new DelayedRemoteService(serverStartTime, 5); + //Set the circuit Breaker parameters + var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000, 2, + 2000 * 1000 * 1000); + + var quickService = new QuickRemoteService(); + //Set the circuit Breaker parameters + var quickServiceCircuitBreaker = new DefaultCircuitBreaker(quickService, 3000, 2, + 2000 * 1000 * 1000); + + //Create an object of monitoring service which makes both local and remote calls + var monitoringService = new MonitoringService(delayedServiceCircuitBreaker, + quickServiceCircuitBreaker); + + //Fetch response from local resource + LOGGER.info(monitoringService.localResourceResponse()); + + //Fetch response from delayed service 2 times, to meet the failure threshold + LOGGER.info(monitoringService.delayedServiceResponse()); + LOGGER.info(monitoringService.delayedServiceResponse()); + + //Fetch current state of delayed service circuit breaker after crossing failure threshold limit + //which is OPEN now + LOGGER.info(delayedServiceCircuitBreaker.getState()); + + //Meanwhile, the delayed service is down, fetch response from the healthy quick service + LOGGER.info(monitoringService.quickServiceResponse()); + LOGGER.info(quickServiceCircuitBreaker.getState()); + + //Wait for the delayed service to become responsive + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); } + //Check the state of delayed circuit breaker, should be HALF_OPEN + LOGGER.info(delayedServiceCircuitBreaker.getState()); + + //Fetch response from delayed service, which should be healthy by now + LOGGER.info(monitoringService.delayedServiceResponse()); + //As successful response is fetched, it should be CLOSED again. + LOGGER.info(delayedServiceCircuitBreaker.getState()); } } ``` The monitoring service: -``` java +```java public class MonitoringService { + private final CircuitBreaker delayedService; + + private final CircuitBreaker quickService; + + public MonitoringService(CircuitBreaker delayedService, CircuitBreaker quickService) { + this.delayedService = delayedService; + this.quickService = quickService; + } + + //Assumption: Local service won't fail, no need to wrap it in a circuit breaker logic public String localResourceResponse() { return "Local Service is working"; } - public String remoteResourceResponse(CircuitBreaker circuitBreaker, long serverStartTime) { + /** + * Fetch response from the delayed service (with some simulated startup time). + * + * @return response string + */ + public String delayedServiceResponse() { try { - return circuitBreaker.call("delayedService", serverStartTime); - } catch (Exception e) { + return this.delayedService.attemptRequest(); + } catch (RemoteServiceException e) { + return e.getMessage(); + } + } + + /** + * Fetches response from a healthy service without any failure. + * + * @return response string + */ + public String quickServiceResponse() { + try { + return this.quickService.attemptRequest(); + } catch (RemoteServiceException e) { return e.getMessage(); } } @@ -95,76 +160,129 @@ As it can be seen, it does the call to get local resources directly, but it wrap remote (costly) service in a circuit breaker object, which prevents faults as follows: ```java -public class CircuitBreaker { +public class DefaultCircuitBreaker implements CircuitBreaker { + private final long timeout; private final long retryTimePeriod; + private final RemoteService service; long lastFailureTime; int failureCount; private final int failureThreshold; private State state; private final long futureTime = 1000 * 1000 * 1000 * 1000; - CircuitBreaker(long timeout, int failureThreshold, long retryTimePeriod) { + /** + * Constructor to create an instance of Circuit Breaker. + * + * @param timeout Timeout for the API request. Not necessary for this simple example + * @param failureThreshold Number of failures we receive from the depended service before changing + * state to 'OPEN' + * @param retryTimePeriod Time period after which a new request is made to remote service for + * status check. + */ + DefaultCircuitBreaker(RemoteService serviceToCall, long timeout, int failureThreshold, + long retryTimePeriod) { + this.service = serviceToCall; + // We start in a closed state hoping that everything is fine this.state = State.CLOSED; this.failureThreshold = failureThreshold; + // Timeout for the API request. + // Used to break the calls made to remote resource if it exceeds the limit this.timeout = timeout; this.retryTimePeriod = retryTimePeriod; + //An absurd amount of time in future which basically indicates the last failure never happened this.lastFailureTime = System.nanoTime() + futureTime; this.failureCount = 0; } - - private void reset() { + + //Reset everything to defaults + @Override + public void recordSuccess() { this.failureCount = 0; - this.lastFailureTime = System.nanoTime() + futureTime; + this.lastFailureTime = System.nanoTime() + futureTime; this.state = State.CLOSED; } - private void recordFailure() { + @Override + public void recordFailure() { failureCount = failureCount + 1; this.lastFailureTime = System.nanoTime(); } - - protected void setState() { - if (failureCount > failureThreshold) { + + //Evaluate the current state based on failureThreshold, failureCount and lastFailureTime. + protected void evaluateState() { + if (failureCount >= failureThreshold) { //Then something is wrong with remote service if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) { + //We have waited long enough and should try checking if service is up state = State.HALF_OPEN; } else { + //Service would still probably be down state = State.OPEN; } } else { + //Everything is working fine state = State.CLOSED; } } - + + @Override public String getState() { + evaluateState(); return state.name(); } - - public void setStateForBypass(State state) { + + /** + * Break the circuit beforehand if it is known service is down Or connect the circuit manually if + * service comes online before expected. + * + * @param state State at which circuit is in + */ + @Override + public void setState(State state) { this.state = state; + switch (state) { + case OPEN: + this.failureCount = failureThreshold; + this.lastFailureTime = System.nanoTime(); + break; + case HALF_OPEN: + this.failureCount = failureThreshold; + this.lastFailureTime = System.nanoTime() - retryTimePeriod; + break; + default: + this.failureCount = 0; + } } - - public String call(String serviceToCall, long serverStartTime) throws Exception { - setState(); + + /** + * Executes service call. + * + * @return Value from the remote resource, stale response or a custom exception + */ + @Override + public String attemptRequest() throws RemoteServiceException { + evaluateState(); if (state == State.OPEN) { + // return cached response if no the circuit is in OPEN state return "This is stale response from API"; } else { - if (serviceToCall.equals("delayedService")) { - var delayedService = new DelayedService(20); - var response = delayedService.response(serverStartTime); - if (response.split(" ")[3].equals("working")) { - reset(); - return response; - } else { - recordFailure(); - throw new Exception("Remote service not responding"); - } - } else { - throw new Exception("Unknown Service Name"); + // Make the API request if the circuit is not OPEN + try { + //In a real application, this would be run in a thread and the timeout + //parameter of the circuit breaker would be utilized to know if service + //is working. Here, we simulate that based on server response itself + var response = service.call(); + // Yay!! the API responded fine. Let's reset everything. + recordSuccess(); + return response; + } catch (RemoteServiceException ex) { + recordFailure(); + throw ex; } } } } + ``` How does the above pattern prevent failures? Let's understand via this finite state machine diff --git a/circuit-breaker/etc/circuit-breaker.urm.png b/circuit-breaker/etc/circuit-breaker.urm.png index 9278ce216..cfa7214ba 100644 Binary files a/circuit-breaker/etc/circuit-breaker.urm.png and b/circuit-breaker/etc/circuit-breaker.urm.png differ diff --git a/circuit-breaker/etc/circuit-breaker.urm.puml b/circuit-breaker/etc/circuit-breaker.urm.puml index 214719002..8128c539a 100644 --- a/circuit-breaker/etc/circuit-breaker.urm.puml +++ b/circuit-breaker/etc/circuit-breaker.urm.puml @@ -5,32 +5,51 @@ package com.iluwatar.circuitbreaker { + App() + main(args : String[]) {static} } - class CircuitBreaker { + interface CircuitBreaker { + + attemptRequest() : String {abstract} + + getState() : String {abstract} + + recordFailure() {abstract} + + recordSuccess() {abstract} + + setState(State) {abstract} + } + class DefaultCircuitBreaker { ~ failureCount : int - failureThreshold : int - futureTime : long ~ lastFailureTime : long - retryTimePeriod : long + - service : RemoteService - state : State - timeout : long - ~ CircuitBreaker(timeout : long, failureThreshold : int, retryTimePeriod : long) - + call(serviceToCall : String, serverStartTime : long) : String + ~ DefaultCircuitBreaker(serviceToCall : RemoteService, timeout : long, failureThreshold : int, retryTimePeriod : long) + + attemptRequest() : String + # evaluateState() + getState() : String - - recordFailure() - - reset() - # setState() - + setStateForBypass(state : State) + + recordFailure() + + recordSuccess() + + setState(state : State) } - class DelayedService { + class DelayedRemoteService { - delay : int - + DelayedService() - + DelayedService(delay : int) - + response(serverStartTime : long) : String + - serverStartTime : long + + DelayedRemoteService() + + DelayedRemoteService(serverStartTime : long, delay : int) + + call() : String } class MonitoringService { - + MonitoringService() + - delayedService : CircuitBreaker + - quickService : CircuitBreaker + + MonitoringService(delayedService : CircuitBreaker, quickService : CircuitBreaker) + + delayedServiceResponse() : String + localResourceResponse() : String - + remoteResourceResponse(circuitBreaker : CircuitBreaker, serverStartTime : long) : String + + quickServiceResponse() : String + } + class QuickRemoteService { + + QuickRemoteService() + + call() : String + } + interface RemoteService { + + call() : String {abstract} } enum State { + CLOSED {static} @@ -40,5 +59,10 @@ package com.iluwatar.circuitbreaker { + values() : State[] {static} } } -CircuitBreaker --> "-state" State +DefaultCircuitBreaker --> "-state" State +MonitoringService --> "-delayedService" CircuitBreaker +DefaultCircuitBreaker --> "-service" RemoteService +DefaultCircuitBreaker ..|> CircuitBreaker +DelayedRemoteService ..|> RemoteService +QuickRemoteService ..|> RemoteService @enduml \ No newline at end of file diff --git a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/App.java b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/App.java index c3465d801..0a88e4e5a 100644 --- a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/App.java +++ b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/App.java @@ -36,17 +36,18 @@ import org.slf4j.LoggerFactory; * operational again, so that we can use it *
*- * In this example, the circuit breaker pattern is demonstrated by using two services: {@link - * MonitoringService} and {@link DelayedService}. The monitoring service is responsible for calling - * two services: a local service and a remote service {@link DelayedService} , and by using the - * circuit breaker construction we ensure that if the call to remote service is going to fail, we - * are going to save our resources and not make the function call at all, by wrapping our call to - * the remote service in the circuit breaker object. + * In this example, the circuit breaker pattern is demonstrated by using three services: {@link + * DelayedRemoteService}, {@link QuickRemoteService} and {@link MonitoringService}. The monitoring + * service is responsible for calling three services: a local service, a quick remove service + * {@link QuickRemoteService} and a delayed remote service {@link DelayedRemoteService} , and by + * using the circuit breaker construction we ensure that if the call to remote service is going to + * fail, we are going to save our resources and not make the function call at all, by wrapping our + * call to the remote services in the {@link DefaultCircuitBreaker} implementation object. *
*
- * This works as follows: The {@link CircuitBreaker} object can be in one of three states:
- * Open, Closed and Half-Open, which represents the real world circuits. If the
- * state is closed (initial), we assume everything is alright and perform the function call.
+ * This works as follows: The {@link DefaultCircuitBreaker} object can be in one of three states:
+ * Open, Closed and Half-Open, which represents the real world circuits. If
+ * the state is closed (initial), we assume everything is alright and perform the function call.
* However, every time the call fails, we note it and once it crosses a threshold, we set the state
* to Open, preventing any further calls to the remote server. Then, after a certain retry period
* (during which we expect thee service to recover), we make another call to the remote server and
@@ -63,22 +64,51 @@ public class App {
*
* @param args command line args
*/
- @SuppressWarnings("squid:S2189")
public static void main(String[] args) {
- //Create an object of monitoring service which makes both local and remote calls
- var obj = new MonitoringService();
- //Set the circuit Breaker parameters
- var circuitBreaker = new CircuitBreaker(3000, 1, 2000 * 1000 * 1000);
+
var serverStartTime = System.nanoTime();
- while (true) {
- LOGGER.info(obj.localResourceResponse());
- LOGGER.info(obj.remoteResourceResponse(circuitBreaker, serverStartTime));
- LOGGER.info(circuitBreaker.getState());
- try {
- Thread.sleep(5 * 1000);
- } catch (InterruptedException e) {
- LOGGER.error(e.getMessage());
- }
+
+ var delayedService = new DelayedRemoteService(serverStartTime, 5);
+ //Set the circuit Breaker parameters
+ var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000, 2,
+ 2000 * 1000 * 1000);
+
+ var quickService = new QuickRemoteService();
+ //Set the circuit Breaker parameters
+ var quickServiceCircuitBreaker = new DefaultCircuitBreaker(quickService, 3000, 2,
+ 2000 * 1000 * 1000);
+
+ //Create an object of monitoring service which makes both local and remote calls
+ var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,
+ quickServiceCircuitBreaker);
+
+ //Fetch response from local resource
+ LOGGER.info(monitoringService.localResourceResponse());
+
+ //Fetch response from delayed service 2 times, to meet the failure threshold
+ LOGGER.info(monitoringService.delayedServiceResponse());
+ LOGGER.info(monitoringService.delayedServiceResponse());
+
+ //Fetch current state of delayed service circuit breaker after crossing failure threshold limit
+ //which is OPEN now
+ LOGGER.info(delayedServiceCircuitBreaker.getState());
+
+ //Meanwhile, the delayed service is down, fetch response from the healthy quick service
+ LOGGER.info(monitoringService.quickServiceResponse());
+ LOGGER.info(quickServiceCircuitBreaker.getState());
+
+ //Wait for the delayed service to become responsive
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
}
+ //Check the state of delayed circuit breaker, should be HALF_OPEN
+ LOGGER.info(delayedServiceCircuitBreaker.getState());
+
+ //Fetch response from delayed service, which should be healthy by now
+ LOGGER.info(monitoringService.delayedServiceResponse());
+ //As successful response is fetched, it should be CLOSED again.
+ LOGGER.info(delayedServiceCircuitBreaker.getState());
}
}
diff --git a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/CircuitBreaker.java b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/CircuitBreaker.java
index 18268b1ce..b05e72104 100644
--- a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/CircuitBreaker.java
+++ b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/CircuitBreaker.java
@@ -24,114 +24,22 @@
package com.iluwatar.circuitbreaker;
/**
- * The circuit breaker class with all configurations.
+ * The Circuit breaker interface.
*/
-public class CircuitBreaker {
- private final long timeout;
- private final long retryTimePeriod;
- long lastFailureTime;
- int failureCount;
- private final int failureThreshold;
- private State state;
- private final long futureTime = 1000 * 1000 * 1000 * 1000;
+public interface CircuitBreaker {
- /**
- * Constructor to create an instance of Circuit Breaker.
- *
- * @param timeout Timeout for the API request. Not necessary for this simple example
- * @param failureThreshold Number of failures we receive from the depended service before changing
- * state to 'OPEN'
- * @param retryTimePeriod Time period after which a new request is made to remote service for
- * status check.
- */
- CircuitBreaker(long timeout, int failureThreshold, long retryTimePeriod) {
- // We start in a closed state hoping that everything is fine
- this.state = State.CLOSED;
- this.failureThreshold = failureThreshold;
- // Timeout for the API request.
- // Used to break the calls made to remote resource if it exceeds the limit
- this.timeout = timeout;
- this.retryTimePeriod = retryTimePeriod;
- //An absurd amount of time in future which basically indicates the last failure never happened
- this.lastFailureTime = System.nanoTime() + futureTime;
- this.failureCount = 0;
- }
+ //Success response. Reset everything to defaults
+ void recordSuccess();
- //Reset everything to defaults
- private void reset() {
- this.failureCount = 0;
- this.lastFailureTime = System.nanoTime() + futureTime;
- this.state = State.CLOSED;
- }
+ //Failure response. Handle accordingly and change state if required.
+ void recordFailure();
- private void recordFailure() {
- failureCount = failureCount + 1;
- this.lastFailureTime = System.nanoTime();
- }
+ //Get the current state of circuit breaker
+ String getState();
- protected void setState() {
- if (failureCount > failureThreshold) { //Then something is wrong with remote service
- if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
- //We have waited long enough and should try checking if service is up
- state = State.HALF_OPEN;
- } else {
- //Service would still probably be down
- state = State.OPEN;
- }
- } else {
- //Everything is working fine
- state = State.CLOSED;
- }
- }
+ //Set the specific state manually.
+ void setState(State state);
- public String getState() {
- return state.name();
- }
-
- /**
- * Break the circuit beforehand if it is known service is down Or connect the circuit manually if
- * service comes online before expected.
- *
- * @param state State at which circuit is in
- */
- public void setStateForBypass(State state) {
- this.state = state;
- }
-
- /**
- * Executes service call.
- *
- * @param serviceToCall The name of the service in String. Can be changed to data URLs in case
- * of web applications
- * @param serverStartTime Time at which actual server was started which makes calls to this
- * service
- * @return Value from the remote resource, stale response or a custom exception
- */
- public String call(String serviceToCall, long serverStartTime) throws Exception {
- setState();
- if (state == State.OPEN) {
- // return cached response if no the circuit is in OPEN state
- return "This is stale response from API";
- } else {
- // Make the API request if the circuit is not OPEN
- if (serviceToCall.equals("delayedService")) {
- var delayedService = new DelayedService(20);
- var response = delayedService.response(serverStartTime);
- //In a real application, this would be run in a thread and the timeout
- //parameter of the circuit breaker would be utilized to know if service
- //is working. Here, we simulate that based on server response itself
- if (response.split(" ")[3].equals("working")) {
- // Yay!! the API responded fine. Let's reset everything.
- reset();
- return response;
- } else {
- // Uh-oh!! the call still failed. Let's update that in our records.
- recordFailure();
- throw new Exception("Remote service not responding");
- }
- } else {
- throw new Exception("Unknown Service Name");
- }
- }
- }
-}
\ No newline at end of file
+ //Attempt to fetch response from the remote service.
+ String attemptRequest() throws RemoteServiceException;
+}
diff --git a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/DefaultCircuitBreaker.java b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/DefaultCircuitBreaker.java
new file mode 100644
index 000000000..1d48c142a
--- /dev/null
+++ b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/DefaultCircuitBreaker.java
@@ -0,0 +1,153 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2019 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.circuitbreaker;
+
+/**
+ * The delay based Circuit breaker implementation that works in a
+ * CLOSED->OPEN-(retry_time_period)->HALF_OPEN->CLOSED flow with some retry time period for failed
+ * services and a failure threshold for service to open
+ * circuit.
+ */
+public class DefaultCircuitBreaker implements CircuitBreaker {
+
+ private final long timeout;
+ private final long retryTimePeriod;
+ private final RemoteService service;
+ long lastFailureTime;
+ int failureCount;
+ private final int failureThreshold;
+ private State state;
+ private final long futureTime = 1000 * 1000 * 1000 * 1000;
+
+ /**
+ * Constructor to create an instance of Circuit Breaker.
+ *
+ * @param timeout Timeout for the API request. Not necessary for this simple example
+ * @param failureThreshold Number of failures we receive from the depended service before changing
+ * state to 'OPEN'
+ * @param retryTimePeriod Time period after which a new request is made to remote service for
+ * status check.
+ */
+ DefaultCircuitBreaker(RemoteService serviceToCall, long timeout, int failureThreshold,
+ long retryTimePeriod) {
+ this.service = serviceToCall;
+ // We start in a closed state hoping that everything is fine
+ this.state = State.CLOSED;
+ this.failureThreshold = failureThreshold;
+ // Timeout for the API request.
+ // Used to break the calls made to remote resource if it exceeds the limit
+ this.timeout = timeout;
+ this.retryTimePeriod = retryTimePeriod;
+ //An absurd amount of time in future which basically indicates the last failure never happened
+ this.lastFailureTime = System.nanoTime() + futureTime;
+ this.failureCount = 0;
+ }
+
+ //Reset everything to defaults
+ @Override
+ public void recordSuccess() {
+ this.failureCount = 0;
+ this.lastFailureTime = System.nanoTime() + futureTime;
+ this.state = State.CLOSED;
+ }
+
+ @Override
+ public void recordFailure() {
+ failureCount = failureCount + 1;
+ this.lastFailureTime = System.nanoTime();
+ }
+
+ //Evaluate the current state based on failureThreshold, failureCount and lastFailureTime.
+ protected void evaluateState() {
+ if (failureCount >= failureThreshold) { //Then something is wrong with remote service
+ if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
+ //We have waited long enough and should try checking if service is up
+ state = State.HALF_OPEN;
+ } else {
+ //Service would still probably be down
+ state = State.OPEN;
+ }
+ } else {
+ //Everything is working fine
+ state = State.CLOSED;
+ }
+ }
+
+ @Override
+ public String getState() {
+ evaluateState();
+ return state.name();
+ }
+
+ /**
+ * Break the circuit beforehand if it is known service is down Or connect the circuit manually if
+ * service comes online before expected.
+ *
+ * @param state State at which circuit is in
+ */
+ @Override
+ public void setState(State state) {
+ this.state = state;
+ switch (state) {
+ case OPEN:
+ this.failureCount = failureThreshold;
+ this.lastFailureTime = System.nanoTime();
+ break;
+ case HALF_OPEN:
+ this.failureCount = failureThreshold;
+ this.lastFailureTime = System.nanoTime() - retryTimePeriod;
+ break;
+ default:
+ this.failureCount = 0;
+ }
+ }
+
+ /**
+ * Executes service call.
+ *
+ * @return Value from the remote resource, stale response or a custom exception
+ */
+ @Override
+ public String attemptRequest() throws RemoteServiceException {
+ evaluateState();
+ if (state == State.OPEN) {
+ // return cached response if no the circuit is in OPEN state
+ return "This is stale response from API";
+ } else {
+ // Make the API request if the circuit is not OPEN
+ try {
+ //In a real application, this would be run in a thread and the timeout
+ //parameter of the circuit breaker would be utilized to know if service
+ //is working. Here, we simulate that based on server response itself
+ var response = service.call();
+ // Yay!! the API responded fine. Let's reset everything.
+ recordSuccess();
+ return response;
+ } catch (RemoteServiceException ex) {
+ recordFailure();
+ throw ex;
+ }
+ }
+ }
+}
diff --git a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/DelayedService.java b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/DelayedRemoteService.java
similarity index 81%
rename from circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/DelayedService.java
rename to circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/DelayedRemoteService.java
index 13861923b..f4add64fc 100644
--- a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/DelayedService.java
+++ b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/DelayedRemoteService.java
@@ -27,7 +27,9 @@ package com.iluwatar.circuitbreaker;
* This simulates the remote service It responds only after a certain timeout period (default set to
* 20 seconds).
*/
-public class DelayedService {
+public class DelayedRemoteService implements RemoteService {
+
+ private final long serverStartTime;
private final int delay;
/**
@@ -35,22 +37,23 @@ public class DelayedService {
*
* @param delay the delay after which service would behave properly, in seconds
*/
- public DelayedService(int delay) {
+ public DelayedRemoteService(long serverStartTime, int delay) {
+ this.serverStartTime = serverStartTime;
this.delay = delay;
}
- public DelayedService() {
- this.delay = 60;
+ public DelayedRemoteService() {
+ this.serverStartTime = System.nanoTime();
+ this.delay = 20;
}
/**
* Responds based on delay, current time and server start time if the service is down / working.
*
- * @param serverStartTime Time at which actual server was started which makes calls to this
- * service
* @return The state of the service
*/
- public String response(long serverStartTime) {
+ @Override
+ public String call() throws RemoteServiceException {
var currentTime = System.nanoTime();
//Since currentTime and serverStartTime are both in nanoseconds, we convert it to
//seconds by diving by 10e9 and ensure floating point division by multiplying it
@@ -58,9 +61,8 @@ public class DelayedService {
//send the reply
if ((currentTime - serverStartTime) * 1.0 / (1000 * 1000 * 1000) < delay) {
//Can use Thread.sleep() here to block and simulate a hung server
- return "Delayed service is down";
- } else {
- return "Delayed service is working";
+ throw new RemoteServiceException("Delayed service is down");
}
+ return "Delayed service is working";
}
}
diff --git a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/MonitoringService.java b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/MonitoringService.java
index e91367175..3fb8d8396 100644
--- a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/MonitoringService.java
+++ b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/MonitoringService.java
@@ -24,28 +24,47 @@
package com.iluwatar.circuitbreaker;
/**
- * The service class which makes local and remote calls Uses {@link CircuitBreaker} object to ensure
- * remote calls don't use up resources.
+ * The service class which makes local and remote calls Uses {@link DefaultCircuitBreaker} object to
+ * ensure remote calls don't use up resources.
*/
public class MonitoringService {
+ private final CircuitBreaker delayedService;
+
+ private final CircuitBreaker quickService;
+
+ public MonitoringService(CircuitBreaker delayedService, CircuitBreaker quickService) {
+ this.delayedService = delayedService;
+ this.quickService = quickService;
+ }
+
//Assumption: Local service won't fail, no need to wrap it in a circuit breaker logic
public String localResourceResponse() {
return "Local Service is working";
}
/**
- * Try to get result from remote server.
+ * Fetch response from the delayed service (with some simulated startup time).
*
- * @param circuitBreaker The circuitBreaker object with all parameters
- * @param serverStartTime Time at which actual server was started which makes calls to this
- * service
- * @return result from the remote response or exception raised by it.
+ * @return response string
*/
- public String remoteResourceResponse(CircuitBreaker circuitBreaker, long serverStartTime) {
+ public String delayedServiceResponse() {
try {
- return circuitBreaker.call("delayedService", serverStartTime);
- } catch (Exception e) {
+ return this.delayedService.attemptRequest();
+ } catch (RemoteServiceException e) {
+ return e.getMessage();
+ }
+ }
+
+ /**
+ * Fetches response from a healthy service without any failure.
+ *
+ * @return response string
+ */
+ public String quickServiceResponse() {
+ try {
+ return this.quickService.attemptRequest();
+ } catch (RemoteServiceException e) {
return e.getMessage();
}
}
diff --git a/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/DelayedServiceTest.java b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/QuickRemoteService.java
similarity index 77%
rename from circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/DelayedServiceTest.java
rename to circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/QuickRemoteService.java
index af747f794..1b73bc0e3 100644
--- a/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/DelayedServiceTest.java
+++ b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/QuickRemoteService.java
@@ -23,19 +23,13 @@
package com.iluwatar.circuitbreaker;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-
/**
- * Monitoring Service test
+ * A quick response remote service, that responds healthy without any delay or failure.
*/
-public class DelayedServiceTest {
+public class QuickRemoteService implements RemoteService {
- //Improves code coverage
- @Test
- public void testDefaultConstructor() {
- var obj = new DelayedService();
- assertEquals(obj.response(System.nanoTime()), "Delayed service is down");
+ @Override
+ public String call() throws RemoteServiceException {
+ return "Quick Service is working";
}
}
diff --git a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/RemoteService.java b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/RemoteService.java
new file mode 100644
index 000000000..3c2fd9c78
--- /dev/null
+++ b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/RemoteService.java
@@ -0,0 +1,34 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2019 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.circuitbreaker;
+
+/**
+ * The Remote service interface, used by {@link CircuitBreaker} for fetching response from remote
+ * services.
+ */
+public interface RemoteService {
+
+ //Fetch response from remote service.
+ String call() throws RemoteServiceException;
+}
diff --git a/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/RemoteServiceException.java b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/RemoteServiceException.java
new file mode 100644
index 000000000..f384f5a41
--- /dev/null
+++ b/circuit-breaker/src/main/java/com/iluwatar/circuitbreaker/RemoteServiceException.java
@@ -0,0 +1,11 @@
+package com.iluwatar.circuitbreaker;
+
+/**
+ * Exception thrown when {@link RemoteService} does not respond successfully.
+ */
+public class RemoteServiceException extends Exception {
+
+ public RemoteServiceException(String message) {
+ super(message);
+ }
+}
diff --git a/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/AppTest.java b/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/AppTest.java
new file mode 100644
index 000000000..3b4041103
--- /dev/null
+++ b/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/AppTest.java
@@ -0,0 +1,129 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2019 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.circuitbreaker;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * App Test showing usage of circuit breaker.
+ */
+public class AppTest {
+
+ //Startup delay for delayed service (in seconds)
+ private static final int STARTUP_DELAY = 4;
+
+ //Number of failed requests for circuit breaker to open
+ private static final int FAILURE_THRESHOLD = 1;
+
+ //Time period in seconds for circuit breaker to retry service
+ private static final int RETRY_PERIOD = 2;
+
+ private MonitoringService monitoringService;
+
+ private CircuitBreaker delayedServiceCircuitBreaker;
+
+ private CircuitBreaker quickServiceCircuitBreaker;
+
+ /**
+ * Setup the circuit breakers and services, where {@link DelayedRemoteService} will be start with
+ * a delay of 4 seconds and a {@link QuickRemoteService} responding healthy. Both services are
+ * wrapped in a {@link DefaultCircuitBreaker} implementation with failure threshold of 1 failure
+ * and retry time period of 2 seconds.
+ */
+ @BeforeEach
+ public void setupCircuitBreakers() {
+ var delayedService = new DelayedRemoteService(System.nanoTime(), STARTUP_DELAY);
+ //Set the circuit Breaker parameters
+ delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000,
+ FAILURE_THRESHOLD,
+ RETRY_PERIOD * 1000 * 1000 * 1000);
+
+ var quickService = new QuickRemoteService();
+ //Set the circuit Breaker parameters
+ quickServiceCircuitBreaker = new DefaultCircuitBreaker(quickService, 3000, FAILURE_THRESHOLD,
+ RETRY_PERIOD * 1000 * 1000 * 1000);
+
+ monitoringService = new MonitoringService(delayedServiceCircuitBreaker,
+ quickServiceCircuitBreaker);
+
+ }
+
+ @Test
+ public void testFailure_OpenStateTransition() {
+ //Calling delayed service, which will be unhealthy till 4 seconds
+ assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
+ //As failure threshold is "1", the circuit breaker is changed to OPEN
+ assertEquals("OPEN", delayedServiceCircuitBreaker.getState());
+ //As circuit state is OPEN, we expect a quick fallback response from circuit breaker.
+ assertEquals("This is stale response from API", monitoringService.delayedServiceResponse());
+
+ //Meanwhile, the quick service is responding and the circuit state is CLOSED
+ assertEquals("Quick Service is working", monitoringService.quickServiceResponse());
+ assertEquals("CLOSED", quickServiceCircuitBreaker.getState());
+
+ }
+
+ @Test
+ public void testFailure_HalfOpenStateTransition() {
+ //Calling delayed service, which will be unhealthy till 4 seconds
+ assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
+ //As failure threshold is "1", the circuit breaker is changed to OPEN
+ assertEquals("OPEN", delayedServiceCircuitBreaker.getState());
+
+ //Waiting for recovery period of 2 seconds for circuit breaker to retry service.
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ //After 2 seconds, the circuit breaker should move to "HALF_OPEN" state and retry fetching response from service again
+ assertEquals("HALF_OPEN", delayedServiceCircuitBreaker.getState());
+
+ }
+
+ @Test
+ public void testRecovery_ClosedStateTransition() {
+ //Calling delayed service, which will be unhealthy till 4 seconds
+ assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
+ //As failure threshold is "1", the circuit breaker is changed to OPEN
+ assertEquals("OPEN", delayedServiceCircuitBreaker.getState());
+
+ //Waiting for 4 seconds, which is enough for DelayedService to become healthy and respond successfully.
+ try {
+ Thread.sleep(4000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ //As retry period is 2 seconds (<4 seconds of wait), hence the circuit breaker should be back in HALF_OPEN state.
+ assertEquals("HALF_OPEN", delayedServiceCircuitBreaker.getState());
+ //Check the success response from delayed service.
+ assertEquals("Delayed service is working", monitoringService.delayedServiceResponse());
+ //As the response is success, the state should be CLOSED
+ assertEquals("CLOSED", delayedServiceCircuitBreaker.getState());
+ }
+
+}
diff --git a/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/CircuitBreakerTest.java b/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/DefaultCircuitBreakerTest.java
similarity index 70%
rename from circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/CircuitBreakerTest.java
rename to circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/DefaultCircuitBreakerTest.java
index 98b59a6ae..4d300b36f 100644
--- a/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/CircuitBreakerTest.java
+++ b/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/DefaultCircuitBreakerTest.java
@@ -25,56 +25,60 @@ package com.iluwatar.circuitbreaker;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import java.rmi.Remote;
import org.junit.jupiter.api.Test;
/**
* Circuit Breaker test
*/
-public class CircuitBreakerTest {
+public class DefaultCircuitBreakerTest {
//long timeout, int failureThreshold, long retryTimePeriod
@Test
- public void testSetState() {
- var circuitBreaker = new CircuitBreaker(1, 1, 100);
+ public void testEvaluateState() {
+ var circuitBreaker = new DefaultCircuitBreaker(null, 1, 1, 100);
//Right now, failureCount