#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

@ -36,17 +36,18 @@ import org.slf4j.LoggerFactory;
* operational again, so that we can use it
* </p>
* <p>
* 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.
* </p>
* <p>
* This works as follows: The {@link CircuitBreaker} 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
* 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:
* <b>Open</b>, <b>Closed</b> and <b>Half-Open</b>, 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());
}
}

View File

@ -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");
}
}
}
}
//Attempt to fetch response from the remote service.
String attemptRequest() throws RemoteServiceException;
}

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
* 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";
}
}

View File

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

View File

@ -0,0 +1,35 @@
/*
* 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;
/**
* A quick response remote service, that responds healthy without any delay or failure.
*/
public class QuickRemoteService implements RemoteService {
@Override
public String call() throws RemoteServiceException {
return "Quick Service is working";
}
}

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