/* CircuitBreaker.java * * Copyright 2009-2015 Comcast Interactive Media, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.fishwife.jrugged; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicLong; /** A {@link CircuitBreaker} can be used with a service to throttle traffic * to a failed subsystem (particularly one we might not be able to monitor, * such as a peer system which must be accessed over the network). Service * calls are wrapped by the <code>CircuitBreaker</code>. * <p> * When everything is operating normally, the <code>CircuitBreaker</code> * is CLOSED and the calls are allowed through. * <p> * When a call fails, however, the <code>CircuitBreaker</code> "trips" and * moves to an OPEN state. Client calls are not allowed through while * the <code>CircuitBreaker</code> is OPEN. * <p> * After a certain "cooldown" period, the <code>CircuitBreaker</code> will * transition to a HALF_CLOSED state, where one call is allowed to go through * as a test. If that call succeeds, the <code>CircuitBreaker</code> moves * back to the CLOSED state; if it fails, it moves back to the OPEN state * for another cooldown period. * <p> * Sample usage: * <pre> public class Service implements Monitorable { private CircuitBreaker cb = new CircuitBreaker(); public String doSomething(final Object arg) throws Exception { return cb.invoke(new Callable<String>() { public String call() { // make the call ... } }); } public Status getStatus() { return cb.getStatus(); } } * </pre> */ public class CircuitBreaker implements MonitoredService, ServiceWrapper { /** * Represents whether a {@link CircuitBreaker} is OPEN, HALF_CLOSED, * or CLOSED. */ protected enum BreakerState { /** An OPEN breaker has tripped and will not allow requests through. */ OPEN, /** A HALF_CLOSED breaker has completed its cooldown period and will allow one request through as a "test request." */ HALF_CLOSED, /** A CLOSED breaker is operating normally and allowing requests through. */ CLOSED } private Throwable tripException = null; /** * Returns the last exception that caused the breaker to trip, NULL if never tripped. * * @return Throwable */ public Throwable getTripException() { return tripException; } /** * Returns the last exception that caused the breaker to trip, empty <code>String </code> * if never tripped. * * @return Throwable */ public String getTripExceptionAsString() { if (tripException == null) { return ""; } else { return getFullStackTrace(tripException); } } /** Current state of the breaker. */ protected volatile BreakerState state = BreakerState.CLOSED; /** The time the breaker last tripped, in milliseconds since the epoch. */ protected AtomicLong lastFailure = new AtomicLong(0L); /** How many times the breaker has tripped during its lifetime. */ protected AtomicLong openCount = new AtomicLong(0L); /** How long the cooldown period is in milliseconds. */ protected AtomicLong resetMillis = new AtomicLong(15 * 1000L); /** The {@link FailureInterpreter} to use to determine whether a given failure should cause the breaker to trip. */ protected FailureInterpreter failureInterpreter = new DefaultFailureInterpreter(); /** Helper class to allow throwing an application-specific * exception rather than the default {@link * CircuitBreakerException}. */ protected CircuitBreakerExceptionMapper<? extends Exception> exceptionMapper; protected List<CircuitBreakerNotificationCallback> cbNotifyList = Collections.synchronizedList(new ArrayList<CircuitBreakerNotificationCallback>()); private boolean isHardTrip; /** * Bypass this CircuitBreaker - used for testing, or other operational * situations where verification of the Break might be required. */ protected boolean byPass = false; /** * Whether the "test" attempt permitted in the HALF_CLOSED state * is currently in-flight. */ protected boolean isAttemptLive = false; /** The default name if none is provided. */ private static final String DEFAULT_NAME="CircuitBreaker"; /** The name for the CircuitBreaker. */ protected String name = DEFAULT_NAME; /** Creates a {@link CircuitBreaker} with a {@link * DefaultFailureInterpreter} and the default "tripped" exception * behavior (throwing a {@link CircuitBreakerException}). */ public CircuitBreaker() { } /** Creates a {@link CircuitBreaker} with a {@link * DefaultFailureInterpreter} and the default "tripped" exception * behavior (throwing a {@link CircuitBreakerException}). * @param name the name for the {@link CircuitBreaker}. */ public CircuitBreaker(String name) { this.name = name; } /** Creates a {@link CircuitBreaker} with the specified {@link * FailureInterpreter} and the default "tripped" exception * behavior (throwing a {@link CircuitBreakerException}). * @param fi the <code>FailureInterpreter</code> to use when * determining whether a specific failure ought to cause the * breaker to trip */ public CircuitBreaker(FailureInterpreter fi) { failureInterpreter = fi; } /** Creates a {@link CircuitBreaker} with the specified {@link * FailureInterpreter} and the default "tripped" exception * behavior (throwing a {@link CircuitBreakerException}). * @param name the name for the {@link CircuitBreaker}. * @param fi the <code>FailureInterpreter</code> to use when * determining whether a specific failure ought to cause the * breaker to trip */ public CircuitBreaker(String name, FailureInterpreter fi) { this.name = name; failureInterpreter = fi; } /** Creates a {@link CircuitBreaker} with a {@link * DefaultFailureInterpreter} and using the supplied {@link * CircuitBreakerExceptionMapper} when client calls are made * while the breaker is tripped. * @param name the name for the {@link CircuitBreaker}. * @param mapper helper used to translate a {@link * CircuitBreakerException} into an application-specific one */ public CircuitBreaker(String name, CircuitBreakerExceptionMapper<? extends Exception> mapper) { this.name = name; exceptionMapper = mapper; } /** Creates a {@link CircuitBreaker} with the provided {@link * FailureInterpreter} and using the provided {@link * CircuitBreakerExceptionMapper} when client calls are made * while the breaker is tripped. * @param name the name for the {@link CircuitBreaker}. * @param fi the <code>FailureInterpreter</code> to use when * determining whether a specific failure ought to cause the * breaker to trip * @param mapper helper used to translate a {@link * CircuitBreakerException} into an application-specific one */ public CircuitBreaker(String name, FailureInterpreter fi, CircuitBreakerExceptionMapper<? extends Exception> mapper) { this.name = name; failureInterpreter = fi; exceptionMapper = mapper; } /** Wrap the given service call with the {@link CircuitBreaker} * protection logic. * @param c the {@link Callable} to attempt * @return whatever c would return on success * @throws CircuitBreakerException if the * breaker was OPEN or HALF_CLOSED and this attempt wasn't the * reset attempt * @throws Exception if <code>c</code> throws one during * execution */ public <V> V invoke(Callable<V> c) throws Exception { if (!byPass) { if (!allowRequest()) { throw mapException(new CircuitBreakerException()); } try { isAttemptLive = true; V result = c.call(); close(); return result; } catch (Throwable cause) { handleFailure(cause); } throw new IllegalStateException("not possible"); } else { return c.call(); } } /** Wrap the given service call with the {@link CircuitBreaker} * protection logic. * @param r the {@link Runnable} to attempt * @throws CircuitBreakerException if the * breaker was OPEN or HALF_CLOSED and this attempt wasn't the * reset attempt * @throws Exception if <code>c</code> throws one during * execution */ public void invoke(Runnable r) throws Exception { if (!byPass) { if (!allowRequest()) { throw mapException(new CircuitBreakerException()); } try { isAttemptLive = true; r.run(); close(); return; } catch (Throwable cause) { handleFailure(cause); } throw new IllegalStateException("not possible"); } else { r.run(); } } /** Wrap the given service call with the {@link CircuitBreaker} * protection logic. * @param r the {@link Runnable} to attempt * @param result what to return after <code>r</code> succeeds * @return result * @throws CircuitBreakerException if the * breaker was OPEN or HALF_CLOSED and this attempt wasn't the * reset attempt * @throws Exception if <code>c</code> throws one during * execution */ public <V> V invoke(Runnable r, V result) throws Exception { if (!byPass) { if (!allowRequest()) { throw mapException(new CircuitBreakerException()); } try { isAttemptLive = true; r.run(); close(); return result; } catch (Throwable cause) { handleFailure(cause); } throw new IllegalStateException("not possible"); } else { r.run(); return result; } } /** * When called with true - causes the {@link CircuitBreaker} to byPass * its functionality allowing requests to be executed unmolested * until the <code>CircuitBreaker</code> is reset or the byPass * is manually set to false. * * @param b Set this breaker into bypass mode */ public void setByPassState(boolean b) { byPass = b; notifyBreakerStateChange(getStatus()); } /** * Get the current state of the {@link CircuitBreaker} byPass * * @return boolean the byPass flag's current value */ public boolean getByPassState() { return byPass; } /** * Causes the {@link CircuitBreaker} to trip and OPEN; no new * requests will be allowed until the <code>CircuitBreaker</code> * resets. */ public void trip() { if (state != BreakerState.OPEN) { openCount.getAndIncrement(); } state = BreakerState.OPEN; lastFailure.set(System.currentTimeMillis()); isAttemptLive = false; notifyBreakerStateChange(getStatus()); } /** * Manually trips the CircuitBreaker until {@link #reset()} is invoked. */ public void tripHard() { this.trip(); isHardTrip = true; } /** * Returns the last time the breaker tripped OPEN, measured in * milliseconds since the Epoch. * @return long the last failure time */ public long getLastTripTime() { return lastFailure.get(); } /** * Returns the number of times the breaker has tripped OPEN during * its lifetime. * @return long the number of times the circuit breaker tripped */ public long getTripCount() { return openCount.get(); } /** * Manually set the breaker to be reset and ready for use. This * is only useful after a manual trip otherwise the breaker will * trip automatically again if the service is still unavailable. * Just like a real breaker. WOOT!!! */ public void reset() { state = BreakerState.CLOSED; isHardTrip = false; byPass = false; isAttemptLive = false; notifyBreakerStateChange(getStatus()); } /** * Returns the current {@link org.fishwife.jrugged.Status} of the * {@link CircuitBreaker}. In this case, it really refers to the * status of the client service. If the * <code>CircuitBreaker</code> is CLOSED, we report that the * client is UP; if it is HALF_CLOSED, we report that the client * is DEGRADED; if it is OPEN, we report the client is DOWN. * * @return Status the current status of the breaker */ public Status getStatus() { return getServiceStatus().getStatus(); } /** * Get the current {@link ServiceStatus} of the * {@link CircuitBreaker}, including the name, * {@link org.fishwife.jrugged.Status}, and reason. * @return the {@link ServiceStatus}. */ public ServiceStatus getServiceStatus() { boolean canSendProbeRequest = !isHardTrip && lastFailure.get() > 0 && allowRequest(); if (byPass) { return new ServiceStatus(name, Status.DEGRADED, "Bypassed"); } switch(state) { case OPEN: return (canSendProbeRequest ? new ServiceStatus(name, Status.DEGRADED, "Send Probe Request") : new ServiceStatus(name, Status.DOWN, "Open")); case HALF_CLOSED: return new ServiceStatus(name, Status.DEGRADED, "Half Closed"); case CLOSED: default: return new ServiceStatus(name, Status.UP); } } /** * Returns the cooldown period in milliseconds. * @return long */ public long getResetMillis() { return resetMillis.get(); } /** Sets the reset period to the given number of milliseconds. The * default is 15,000 (make one retry attempt every 15 seconds). * * @param l number of milliseconds to "cool down" after tripping * before allowing a "test request" through again */ public void setResetMillis(long l) { resetMillis.set(l); } /** Returns a {@link String} representation of the breaker's * status; potentially useful for exposing to monitoring software. * @return <code>String</code> which is <code>"GREEN"</code> if * the breaker is CLOSED; <code>"YELLOW"</code> if the breaker * is HALF_CLOSED; and <code>"RED"</code> if the breaker is * OPEN (tripped). */ public String getHealthCheck() { return getStatus().getSignal(); } /** * Specifies the failure tolerance limit for the {@link * DefaultFailureInterpreter} that comes with a {@link * CircuitBreaker} by default. * @see DefaultFailureInterpreter * @param limit the number of tolerated failures in a window */ public void setLimit(int limit) { FailureInterpreter fi = getFailureInterpreter(); if (!(fi instanceof DefaultFailureInterpreter)) { throw new IllegalStateException("setLimit() not supported: this CircuitBreaker's FailureInterpreter isn't a DefaultFailureInterpreter."); } ((DefaultFailureInterpreter)fi).setLimit(limit); } /** * Specifies a set of {@link Throwable} classes that should not * be considered failures by the {@link CircuitBreaker}. * @see DefaultFailureInterpreter * @param ignore a {@link java.util.Collection} of {@link Throwable} * classes */ public void setIgnore(Collection<Class<? extends Throwable>> ignore) { FailureInterpreter fi = getFailureInterpreter(); if (!(fi instanceof DefaultFailureInterpreter)) { throw new IllegalStateException("setIgnore() not supported: this CircuitBreaker's FailureInterpreter isn't a DefaultFailureInterpreter."); } @SuppressWarnings("unchecked") Class<? extends Throwable>[] classes = new Class[ignore.size()]; int i = 0; for(Class<? extends Throwable> c : ignore) { classes[i] = c; i++; } ((DefaultFailureInterpreter)fi).setIgnore(classes); } /** * Specifies the tolerance window in milliseconds for the {@link * DefaultFailureInterpreter} that comes with a {@link * CircuitBreaker} by default. * @see DefaultFailureInterpreter * @param windowMillis length of the window in milliseconds */ public void setWindowMillis(long windowMillis) { FailureInterpreter fi = getFailureInterpreter(); if (!(fi instanceof DefaultFailureInterpreter)) { throw new IllegalStateException("setWindowMillis() not supported: this CircuitBreaker's FailureInterpreter isn't a DefaultFailureInterpreter."); } ((DefaultFailureInterpreter)fi).setWindowMillis(windowMillis); } /** * Specifies a helper that determines whether a given failure will * cause the breaker to trip or not. * * @param failureInterpreter the {@link FailureInterpreter} to use */ public void setFailureInterpreter(FailureInterpreter failureInterpreter) { this.failureInterpreter = failureInterpreter; } /** * Get the failure interpreter for this instance. The failure * interpreter provides the configuration for determining which * exceptions trip the circuit breaker, in what time interval, * etc. * * @return {@link FailureInterpreter} for this instance or null if no * failure interpreter was set. */ public FailureInterpreter getFailureInterpreter() { return this.failureInterpreter; } /** * A helper that converts CircuitBreakerExceptions into a known * 'application' exception. * * @param mapper my converter object */ public void setExceptionMapper(CircuitBreakerExceptionMapper<? extends Exception> mapper) { this.exceptionMapper = mapper; } /** * Add an interested party for {@link CircuitBreaker} events, like up, * down, degraded status state changes. * * @param listener an interested party for {@link CircuitBreaker} status events. */ public void addListener(CircuitBreakerNotificationCallback listener) { cbNotifyList.add(listener); } /** * Set a list of interested parties for {@link CircuitBreaker} events, like up, * down, degraded status state changes. * * @param listeners a list of interested parties for {@link CircuitBreaker} status events. */ public void setListeners(ArrayList<CircuitBreakerNotificationCallback> listeners) { cbNotifyList = Collections.synchronizedList(listeners); } /** * Get the helper that converts {@link CircuitBreakerException}s into * application-specific exceptions. * @return {@link CircuitBreakerExceptionMapper} my converter object, or * <code>null</code> if one is not currently set. */ public CircuitBreakerExceptionMapper<? extends Exception> getExceptionMapper(){ return this.exceptionMapper; } protected Exception mapException(CircuitBreakerException cbe) { if (exceptionMapper == null) return cbe; return exceptionMapper.map(this, cbe); } protected void handleFailure(Throwable cause) throws Exception { if (failureInterpreter == null || failureInterpreter.shouldTrip(cause)) { this.tripException = cause; trip(); } else if (isAttemptLive) { close(); } if (cause instanceof Exception) { throw (Exception)cause; } else if (cause instanceof Error) { throw (Error)cause; } else { throw (RuntimeException)cause; } } /** * Reports a successful service call to the {@link CircuitBreaker}, * putting the <code>CircuitBreaker</code> back into the CLOSED * state serving requests. */ protected void close() { state = BreakerState.CLOSED; isAttemptLive = false; notifyBreakerStateChange(getStatus()); } private synchronized boolean canAttempt() { if (!(BreakerState.HALF_CLOSED == state) || isAttemptLive) { return false; } return true; } private void notifyBreakerStateChange(Status status) { if (cbNotifyList != null && cbNotifyList.size() >= 1) { for (CircuitBreakerNotificationCallback notifyObject : cbNotifyList) { notifyObject.notify(status); } } } /** * @return boolean whether the breaker will allow a request * through or not. */ protected boolean allowRequest() { if (this.isHardTrip) { return false; } else if (BreakerState.CLOSED == state) { return true; } if (BreakerState.OPEN == state && System.currentTimeMillis() - lastFailure.get() >= resetMillis.get()) { state = BreakerState.HALF_CLOSED; } return canAttempt(); } private String getFullStackTrace(Throwable t) { StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); return sw.toString(); } }