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, failureCountfailureThreshold, and lastFailureTime is nearly equal to current time, //state should be half-open assertEquals(circuitBreaker.getState(), "HALF_OPEN"); //Since failureCount>failureThreshold, and lastFailureTime is much lesser current time, //state should be open circuitBreaker.lastFailureTime = System.nanoTime() - 1000 * 1000 * 1000 * 1000; - circuitBreaker.setState(); + circuitBreaker.evaluateState(); assertEquals(circuitBreaker.getState(), "OPEN"); //Now set it back again to closed to test idempotency circuitBreaker.failureCount = 0; - circuitBreaker.setState(); + circuitBreaker.evaluateState(); assertEquals(circuitBreaker.getState(), "CLOSED"); } @Test public void testSetStateForBypass() { - var circuitBreaker = new CircuitBreaker(1, 1, 100); + var circuitBreaker = new DefaultCircuitBreaker(null, 1, 1, 2000 * 1000 * 1000); //Right now, failureCount { + var obj = new DelayedRemoteService(); + obj.call(); + }); + } + + /** + * Testing server started in past (2 seconds ago) and with a simulated delay of 1 second. + * + * @throws RemoteServiceException + */ + @Test + public void testParameterizedConstructor() throws RemoteServiceException { + var obj = new DelayedRemoteService(System.nanoTime()-2000*1000*1000,1); + assertEquals("Delayed service is working",obj.call()); + } +} diff --git a/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/MonitoringServiceTest.java b/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/MonitoringServiceTest.java index 71ede3e1c..f6b802b96 100644 --- a/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/MonitoringServiceTest.java +++ b/circuit-breaker/src/test/java/com/iluwatar/circuitbreaker/MonitoringServiceTest.java @@ -35,28 +35,45 @@ public class MonitoringServiceTest { //long timeout, int failureThreshold, long retryTimePeriod @Test public void testLocalResponse() { - var monitoringService = new MonitoringService(); + var monitoringService = new MonitoringService(null,null); var response = monitoringService.localResourceResponse(); assertEquals(response, "Local Service is working"); } @Test - public void testRemoteResponse() { - var monitoringService = new MonitoringService(); - var circuitBreaker = new CircuitBreaker(1, 1, 100); + public void testDelayedRemoteResponseSuccess() { + var delayedService = new DelayedRemoteService(System.nanoTime()-2*1000*1000*1000, 2); + var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000, + 1, + 2 * 1000 * 1000 * 1000); + + var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,null); //Set time in past to make the server work - var serverStartTime = System.nanoTime() / 10; - var response = monitoringService.remoteResourceResponse(circuitBreaker, serverStartTime); + var response = monitoringService.delayedServiceResponse(); assertEquals(response, "Delayed service is working"); } @Test - public void testRemoteResponse2() { - var monitoringService = new MonitoringService(); - var circuitBreaker = new CircuitBreaker(1, 1, 100); + public void testDelayedRemoteResponseFailure() { + var delayedService = new DelayedRemoteService(System.nanoTime(), 2); + var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000, + 1, + 2 * 1000 * 1000 * 1000); + var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,null); //Set time as current time as initially server fails - var serverStartTime = System.nanoTime(); - var response = monitoringService.remoteResourceResponse(circuitBreaker, serverStartTime); - assertEquals(response, "Remote service not responding"); + var response = monitoringService.delayedServiceResponse(); + assertEquals(response, "Delayed service is down"); + } + + @Test + public void testQuickRemoteServiceResponse() { + var delayedService = new QuickRemoteService(); + var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000, + 1, + 2 * 1000 * 1000 * 1000); + var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,null); + //Set time as current time as initially server fails + var response = monitoringService.delayedServiceResponse(); + assertEquals(response, "Quick Service is working"); } } diff --git a/pom.xml b/pom.xml index 102668588..2e1f904c3 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ Java Design Patterns - abstract-factory + circuit-breaker - role-object +