#1510 Improvments done in Circuit Breaker

This commit is contained in:
swarajsaaj 2020-10-01 21:09:39 +05:30
parent 9088ac51f6
commit b29bd66369
16 changed files with 754 additions and 252 deletions

View File

@ -55,20 +55,57 @@ public class App {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class); private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
/**
* Program entry point.
*
* @param args command line args
*/
public static void main(String[] args) { public static void main(String[] args) {
var obj = new MonitoringService();
var circuitBreaker = new CircuitBreaker(3000, 1, 2000 * 1000 * 1000);
var serverStartTime = System.nanoTime(); var serverStartTime = System.nanoTime();
while (true) {
LOGGER.info(obj.localResourceResponse()); var delayedService = new DelayedRemoteService(serverStartTime, 5);
LOGGER.info(obj.remoteResourceResponse(circuitBreaker, serverStartTime)); //Set the circuit Breaker parameters
LOGGER.info(circuitBreaker.getState()); 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 { try {
Thread.sleep(5 * 1000); Thread.sleep(5000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOGGER.error(e.getMessage()); 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());
} }
} }
``` ```
@ -78,14 +115,42 @@ The monitoring service:
```java ```java
public class MonitoringService { 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() { public String localResourceResponse() {
return "Local Service is working"; 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 { try {
return circuitBreaker.call("delayedService", serverStartTime); return this.delayedService.attemptRequest();
} catch (Exception e) { } 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(); 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: remote (costly) service in a circuit breaker object, which prevents faults as follows:
```java ```java
public class CircuitBreaker { public class DefaultCircuitBreaker implements CircuitBreaker {
private final long timeout; private final long timeout;
private final long retryTimePeriod; private final long retryTimePeriod;
private final RemoteService service;
long lastFailureTime; long lastFailureTime;
int failureCount; int failureCount;
private final int failureThreshold; private final int failureThreshold;
private State state; private State state;
private final long futureTime = 1000 * 1000 * 1000 * 1000; 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.state = State.CLOSED;
this.failureThreshold = failureThreshold; 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.timeout = timeout;
this.retryTimePeriod = retryTimePeriod; this.retryTimePeriod = retryTimePeriod;
//An absurd amount of time in future which basically indicates the last failure never happened
this.lastFailureTime = System.nanoTime() + futureTime; this.lastFailureTime = System.nanoTime() + futureTime;
this.failureCount = 0; this.failureCount = 0;
} }
private void reset() { //Reset everything to defaults
@Override
public void recordSuccess() {
this.failureCount = 0; this.failureCount = 0;
this.lastFailureTime = System.nanoTime() + futureTime; this.lastFailureTime = System.nanoTime() + futureTime;
this.state = State.CLOSED; this.state = State.CLOSED;
} }
private void recordFailure() { @Override
public void recordFailure() {
failureCount = failureCount + 1; failureCount = failureCount + 1;
this.lastFailureTime = System.nanoTime(); this.lastFailureTime = System.nanoTime();
} }
protected void setState() { //Evaluate the current state based on failureThreshold, failureCount and lastFailureTime.
if (failureCount > failureThreshold) { protected void evaluateState() {
if (failureCount >= failureThreshold) { //Then something is wrong with remote service
if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) { if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
//We have waited long enough and should try checking if service is up
state = State.HALF_OPEN; state = State.HALF_OPEN;
} else { } else {
//Service would still probably be down
state = State.OPEN; state = State.OPEN;
} }
} else { } else {
//Everything is working fine
state = State.CLOSED; state = State.CLOSED;
} }
} }
@Override
public String getState() { public String getState() {
evaluateState();
return state.name(); 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; 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) { if (state == State.OPEN) {
// return cached response if no the circuit is in OPEN state
return "This is stale response from API"; return "This is stale response from API";
} else { } else {
if (serviceToCall.equals("delayedService")) { // Make the API request if the circuit is not OPEN
var delayedService = new DelayedService(20); try {
var response = delayedService.response(serverStartTime); //In a real application, this would be run in a thread and the timeout
if (response.split(" ")[3].equals("working")) { //parameter of the circuit breaker would be utilized to know if service
reset(); //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; return response;
} else { } catch (RemoteServiceException ex) {
recordFailure(); recordFailure();
throw new Exception("Remote service not responding"); throw ex;
}
} else {
throw new Exception("Unknown Service Name");
} }
} }
} }
} }
``` ```
How does the above pattern prevent failures? Let's understand via this finite state machine How does the above pattern prevent failures? Let's understand via this finite state machine

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -5,32 +5,51 @@ package com.iluwatar.circuitbreaker {
+ App() + App()
+ main(args : String[]) {static} + 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 ~ failureCount : int
- failureThreshold : int - failureThreshold : int
- futureTime : long - futureTime : long
~ lastFailureTime : long ~ lastFailureTime : long
- retryTimePeriod : long - retryTimePeriod : long
- service : RemoteService
- state : State - state : State
- timeout : long - timeout : long
~ CircuitBreaker(timeout : long, failureThreshold : int, retryTimePeriod : long) ~ DefaultCircuitBreaker(serviceToCall : RemoteService, timeout : long, failureThreshold : int, retryTimePeriod : long)
+ call(serviceToCall : String, serverStartTime : long) : String + attemptRequest() : String
# evaluateState()
+ getState() : String + getState() : String
- recordFailure() + recordFailure()
- reset() + recordSuccess()
# setState() + setState(state : State)
+ setStateForBypass(state : State)
} }
class DelayedService { class DelayedRemoteService {
- delay : int - delay : int
+ DelayedService() - serverStartTime : long
+ DelayedService(delay : int) + DelayedRemoteService()
+ response(serverStartTime : long) : String + DelayedRemoteService(serverStartTime : long, delay : int)
+ call() : String
} }
class MonitoringService { class MonitoringService {
+ MonitoringService() - delayedService : CircuitBreaker
- quickService : CircuitBreaker
+ MonitoringService(delayedService : CircuitBreaker, quickService : CircuitBreaker)
+ delayedServiceResponse() : String
+ localResourceResponse() : String + localResourceResponse() : String
+ remoteResourceResponse(circuitBreaker : CircuitBreaker, serverStartTime : long) : String + quickServiceResponse() : String
}
class QuickRemoteService {
+ QuickRemoteService()
+ call() : String
}
interface RemoteService {
+ call() : String {abstract}
} }
enum State { enum State {
+ CLOSED {static} + CLOSED {static}
@ -40,5 +59,10 @@ package com.iluwatar.circuitbreaker {
+ values() : State[] {static} + values() : State[] {static}
} }
} }
CircuitBreaker --> "-state" State DefaultCircuitBreaker --> "-state" State
MonitoringService --> "-delayedService" CircuitBreaker
DefaultCircuitBreaker --> "-service" RemoteService
DefaultCircuitBreaker ..|> CircuitBreaker
DelayedRemoteService ..|> RemoteService
QuickRemoteService ..|> RemoteService
@enduml @enduml

View File

@ -36,17 +36,18 @@ import org.slf4j.LoggerFactory;
* operational again, so that we can use it * operational again, so that we can use it
* </p> * </p>
* <p> * <p>
* In this example, the circuit breaker pattern is demonstrated by using two services: {@link * In this example, the circuit breaker pattern is demonstrated by using three services: {@link
* MonitoringService} and {@link DelayedService}. The monitoring service is responsible for calling * DelayedRemoteService}, {@link QuickRemoteService} and {@link MonitoringService}. The monitoring
* two services: a local service and a remote service {@link DelayedService} , and by using the * service is responsible for calling three services: a local service, a quick remove service
* circuit breaker construction we ensure that if the call to remote service is going to fail, we * {@link QuickRemoteService} and a delayed remote service {@link DelayedRemoteService} , and by
* are going to save our resources and not make the function call at all, by wrapping our call to * using the circuit breaker construction we ensure that if the call to remote service is going to
* the remote service in the circuit breaker object. * 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.
* </p> * </p>
* <p> * <p>
* This works as follows: The {@link CircuitBreaker} object can be in one of three states: * This works as follows: The {@link DefaultCircuitBreaker} object can be in one of three states:
* <b>Open</b>, <b>Closed</b> and <b>Half-Open</b>, which represents the real world circuits. If the * <b>Open</b>, <b>Closed</b> and <b>Half-Open</b>, which represents the real world circuits. If
* state is closed (initial), we assume everything is alright and perform the function call. * 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 * 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 * 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 * (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 * @param args command line args
*/ */
@SuppressWarnings("squid:S2189")
public static void main(String[] args) { 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(); var serverStartTime = System.nanoTime();
while (true) {
LOGGER.info(obj.localResourceResponse()); var delayedService = new DelayedRemoteService(serverStartTime, 5);
LOGGER.info(obj.remoteResourceResponse(circuitBreaker, serverStartTime)); //Set the circuit Breaker parameters
LOGGER.info(circuitBreaker.getState()); 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 { try {
Thread.sleep(5 * 1000); Thread.sleep(5000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOGGER.error(e.getMessage()); 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());
} }
} }

View File

@ -24,114 +24,22 @@
package com.iluwatar.circuitbreaker; package com.iluwatar.circuitbreaker;
/** /**
* The circuit breaker class with all configurations. * The Circuit breaker interface.
*/ */
public class CircuitBreaker { public interface 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;
/** //Success response. Reset everything to defaults
* Constructor to create an instance of Circuit Breaker. void recordSuccess();
*
* @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;
}
//Reset everything to defaults //Failure response. Handle accordingly and change state if required.
private void reset() { void recordFailure();
this.failureCount = 0;
this.lastFailureTime = System.nanoTime() + futureTime;
this.state = State.CLOSED;
}
private void recordFailure() { //Get the current state of circuit breaker
failureCount = failureCount + 1; String getState();
this.lastFailureTime = System.nanoTime();
}
protected void setState() { //Set the specific state manually.
if (failureCount > failureThreshold) { //Then something is wrong with remote service void setState(State state);
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;
}
}
public String getState() { //Attempt to fetch response from the remote service.
return state.name(); String attemptRequest() throws RemoteServiceException;
}
/**
* 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");
}
}
}
} }

View File

@ -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;
}
}
}
}

View File

@ -27,7 +27,9 @@ package com.iluwatar.circuitbreaker;
* This simulates the remote service It responds only after a certain timeout period (default set to * This simulates the remote service It responds only after a certain timeout period (default set to
* 20 seconds). * 20 seconds).
*/ */
public class DelayedService { public class DelayedRemoteService implements RemoteService {
private final long serverStartTime;
private final int delay; private final int delay;
/** /**
@ -35,22 +37,23 @@ public class DelayedService {
* *
* @param delay the delay after which service would behave properly, in seconds * @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; this.delay = delay;
} }
public DelayedService() { public DelayedRemoteService() {
this.delay = 60; this.serverStartTime = System.nanoTime();
this.delay = 20;
} }
/** /**
* Responds based on delay, current time and server start time if the service is down / working. * 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 * @return The state of the service
*/ */
public String response(long serverStartTime) { @Override
public String call() throws RemoteServiceException {
var currentTime = System.nanoTime(); var currentTime = System.nanoTime();
//Since currentTime and serverStartTime are both in nanoseconds, we convert it to //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 //seconds by diving by 10e9 and ensure floating point division by multiplying it
@ -58,9 +61,8 @@ public class DelayedService {
//send the reply //send the reply
if ((currentTime - serverStartTime) * 1.0 / (1000 * 1000 * 1000) < delay) { if ((currentTime - serverStartTime) * 1.0 / (1000 * 1000 * 1000) < delay) {
//Can use Thread.sleep() here to block and simulate a hung server //Can use Thread.sleep() here to block and simulate a hung server
return "Delayed service is down"; throw new RemoteServiceException("Delayed service is down");
} else { }
return "Delayed service is working"; return "Delayed service is working";
} }
} }
}

View File

@ -24,28 +24,47 @@
package com.iluwatar.circuitbreaker; package com.iluwatar.circuitbreaker;
/** /**
* The service class which makes local and remote calls Uses {@link CircuitBreaker} object to ensure * The service class which makes local and remote calls Uses {@link DefaultCircuitBreaker} object to
* remote calls don't use up resources. * ensure remote calls don't use up resources.
*/ */
public class MonitoringService { 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 //Assumption: Local service won't fail, no need to wrap it in a circuit breaker logic
public String localResourceResponse() { public String localResourceResponse() {
return "Local Service is working"; 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 * @return response string
* @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.
*/ */
public String remoteResourceResponse(CircuitBreaker circuitBreaker, long serverStartTime) { public String delayedServiceResponse() {
try { try {
return circuitBreaker.call("delayedService", serverStartTime); return this.delayedService.attemptRequest();
} catch (Exception e) { } 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(); return e.getMessage();
} }
} }

View File

@ -23,19 +23,13 @@
package com.iluwatar.circuitbreaker; 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 @Override
@Test public String call() throws RemoteServiceException {
public void testDefaultConstructor() { return "Quick Service is working";
var obj = new DelayedService();
assertEquals(obj.response(System.nanoTime()), "Delayed service is down");
} }
} }

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -25,56 +25,60 @@ package com.iluwatar.circuitbreaker;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import java.rmi.Remote;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/** /**
* Circuit Breaker test * Circuit Breaker test
*/ */
public class CircuitBreakerTest { public class DefaultCircuitBreakerTest {
//long timeout, int failureThreshold, long retryTimePeriod //long timeout, int failureThreshold, long retryTimePeriod
@Test @Test
public void testSetState() { public void testEvaluateState() {
var circuitBreaker = new CircuitBreaker(1, 1, 100); var circuitBreaker = new DefaultCircuitBreaker(null, 1, 1, 100);
//Right now, failureCount<failureThreshold, so state should be closed //Right now, failureCount<failureThreshold, so state should be closed
assertEquals(circuitBreaker.getState(), "CLOSED"); assertEquals(circuitBreaker.getState(), "CLOSED");
circuitBreaker.failureCount = 4; circuitBreaker.failureCount = 4;
circuitBreaker.lastFailureTime = System.nanoTime(); circuitBreaker.lastFailureTime = System.nanoTime();
circuitBreaker.setState(); circuitBreaker.evaluateState();
//Since failureCount>failureThreshold, and lastFailureTime is nearly equal to current time, //Since failureCount>failureThreshold, and lastFailureTime is nearly equal to current time,
//state should be half-open //state should be half-open
assertEquals(circuitBreaker.getState(), "HALF_OPEN"); assertEquals(circuitBreaker.getState(), "HALF_OPEN");
//Since failureCount>failureThreshold, and lastFailureTime is much lesser current time, //Since failureCount>failureThreshold, and lastFailureTime is much lesser current time,
//state should be open //state should be open
circuitBreaker.lastFailureTime = System.nanoTime() - 1000 * 1000 * 1000 * 1000; circuitBreaker.lastFailureTime = System.nanoTime() - 1000 * 1000 * 1000 * 1000;
circuitBreaker.setState(); circuitBreaker.evaluateState();
assertEquals(circuitBreaker.getState(), "OPEN"); assertEquals(circuitBreaker.getState(), "OPEN");
//Now set it back again to closed to test idempotency //Now set it back again to closed to test idempotency
circuitBreaker.failureCount = 0; circuitBreaker.failureCount = 0;
circuitBreaker.setState(); circuitBreaker.evaluateState();
assertEquals(circuitBreaker.getState(), "CLOSED"); assertEquals(circuitBreaker.getState(), "CLOSED");
} }
@Test @Test
public void testSetStateForBypass() { public void testSetStateForBypass() {
var circuitBreaker = new CircuitBreaker(1, 1, 100); var circuitBreaker = new DefaultCircuitBreaker(null, 1, 1, 2000 * 1000 * 1000);
//Right now, failureCount<failureThreshold, so state should be closed //Right now, failureCount<failureThreshold, so state should be closed
//Bypass it and set it to open //Bypass it and set it to open
circuitBreaker.setStateForBypass(State.OPEN); circuitBreaker.setState(State.OPEN);
assertEquals(circuitBreaker.getState(), "OPEN"); assertEquals(circuitBreaker.getState(), "OPEN");
} }
@Test @Test
public void testApiResponses() { public void testApiResponses() throws RemoteServiceException {
var circuitBreaker = new CircuitBreaker(1, 1, 100); RemoteService mockService = new RemoteService() {
try { @Override
public String call() throws RemoteServiceException {
return "Remote Success";
}
};
var circuitBreaker = new DefaultCircuitBreaker(mockService, 1, 1, 100);
//Call with the paramater start_time set to huge amount of time in past so that service //Call with the paramater start_time set to huge amount of time in past so that service
//replies with "Ok". Also, state is CLOSED in start //replies with "Ok". Also, state is CLOSED in start
var serviceStartTime = System.nanoTime() - 60 * 1000 * 1000 * 1000; var serviceStartTime = System.nanoTime() - 60 * 1000 * 1000 * 1000;
var response = circuitBreaker.call("delayedService", serviceStartTime); var response = circuitBreaker.attemptRequest();
assertEquals(response, "Delayed service is working"); assertEquals(response, "Remote Success");
} catch (Exception e) {
System.out.println(e.getMessage());
}
} }
} }

View File

@ -0,0 +1,59 @@
/*
* 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.Assertions;
import org.junit.jupiter.api.Test;
/**
* Monitoring Service test
*/
public class DelayedRemoteServiceTest {
/**
* Testing immediate response of the delayed service.
*
* @throws RemoteServiceException
*/
@Test
public void testDefaultConstructor() throws RemoteServiceException {
Assertions.assertThrows(RemoteServiceException.class, () -> {
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());
}
}

View File

@ -35,28 +35,45 @@ public class MonitoringServiceTest {
//long timeout, int failureThreshold, long retryTimePeriod //long timeout, int failureThreshold, long retryTimePeriod
@Test @Test
public void testLocalResponse() { public void testLocalResponse() {
var monitoringService = new MonitoringService(); var monitoringService = new MonitoringService(null,null);
var response = monitoringService.localResourceResponse(); var response = monitoringService.localResourceResponse();
assertEquals(response, "Local Service is working"); assertEquals(response, "Local Service is working");
} }
@Test @Test
public void testRemoteResponse() { public void testDelayedRemoteResponseSuccess() {
var monitoringService = new MonitoringService(); var delayedService = new DelayedRemoteService(System.nanoTime()-2*1000*1000*1000, 2);
var circuitBreaker = new CircuitBreaker(1, 1, 100); 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 //Set time in past to make the server work
var serverStartTime = System.nanoTime() / 10; var response = monitoringService.delayedServiceResponse();
var response = monitoringService.remoteResourceResponse(circuitBreaker, serverStartTime);
assertEquals(response, "Delayed service is working"); assertEquals(response, "Delayed service is working");
} }
@Test @Test
public void testRemoteResponse2() { public void testDelayedRemoteResponseFailure() {
var monitoringService = new MonitoringService(); var delayedService = new DelayedRemoteService(System.nanoTime(), 2);
var circuitBreaker = new CircuitBreaker(1, 1, 100); 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 //Set time as current time as initially server fails
var serverStartTime = System.nanoTime(); var response = monitoringService.delayedServiceResponse();
var response = monitoringService.remoteResourceResponse(circuitBreaker, serverStartTime); assertEquals(response, "Delayed service is down");
assertEquals(response, "Remote service not responding"); }
@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");
} }
} }

View File

@ -63,7 +63,7 @@
<sonar.projectName>Java Design Patterns</sonar.projectName> <sonar.projectName>Java Design Patterns</sonar.projectName>
</properties> </properties>
<modules> <modules>
<module>abstract-factory</module> <!--<module>abstract-factory</module>
<module>tls</module> <module>tls</module>
<module>builder</module> <module>builder</module>
<module>factory-method</module> <module>factory-method</module>
@ -181,9 +181,9 @@
<module>bytecode</module> <module>bytecode</module>
<module>leader-election</module> <module>leader-election</module>
<module>data-locality</module> <module>data-locality</module>
<module>subclass-sandbox</module> <module>subclass-sandbox</module>-->
<module>circuit-breaker</module> <module>circuit-breaker</module>
<module>role-object</module> <!-- <module>role-object</module>
<module>saga</module> <module>saga</module>
<module>double-buffer</module> <module>double-buffer</module>
<module>sharding</module> <module>sharding</module>
@ -196,7 +196,7 @@
<module>transaction-script</module> <module>transaction-script</module>
<module>filterer</module> <module>filterer</module>
<module>factory</module> <module>factory</module>
<module>separated-interface</module> <module>separated-interface</module>-->
</modules> </modules>
<repositories> <repositories>