/* CircuitBreaker.java * * Copyright 2009-2012 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 ""; } 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. */ private 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(final 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(final 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(final String name, final 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(final String name, final 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(final String name, final FailureInterpreter fi, final 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 */ @Override public <V> V invoke(final Callable<V> c) throws Exception { if (!byPass) { if (!allowRequest()) { throw mapException(new CircuitBreakerException()); } try { final V result = c.call(); close(); return result; } catch (final Throwable cause) { handleFailure(cause); } throw new IllegalStateException("not possible"); } 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 */ @Override public void invoke(final Runnable r) throws Exception { if (!byPass) { if (!allowRequest()) { throw mapException(new CircuitBreakerException()); } try { r.run(); close(); return; } catch (final Throwable cause) { handleFailure(cause); } throw new IllegalStateException("not possible"); } 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 */ @Override public <V> V invoke(final Runnable r, final V result) throws Exception { if (!byPass) { if (!allowRequest()) { throw mapException(new CircuitBreakerException()); } try { r.run(); close(); return result; } catch (final Throwable cause) { handleFailure(cause); } throw new IllegalStateException("not possible"); } 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(final 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() { 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 org.fishwife.jrugged.ServiceStatus} of the * {@link CircuitBreaker}, including the name, {@link org.fishwife.jrugged.Status}, * and reason. * * @return the {@link org.fishwife.jrugged.ServiceStatus}. */ @Override public ServiceStatus getServiceStatus() { final boolean canSendProbeRequest = !isHardTrip && lastFailure.get() > 0 && System.currentTimeMillis() - lastFailure.get() >= resetMillis.get(); 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(final 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(final int limit) { final 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(final Collection<Class<? extends Throwable>> ignore) { final FailureInterpreter fi = getFailureInterpreter(); if (!(fi instanceof DefaultFailureInterpreter)) { throw new IllegalStateException( "setIgnore() not supported: this CircuitBreaker's FailureInterpreter isn't a DefaultFailureInterpreter."); } @SuppressWarnings("unchecked") final Class<? extends Throwable>[] classes = new Class[ignore.size()]; int i = 0; for (final 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(final long windowMillis) { final 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(final 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 failureInterpreter; } /** * A helper that converts CircuitBreakerExceptions into a known 'application' * exception. * * @param mapper * my converter object */ public void setExceptionMapper( final CircuitBreakerExceptionMapper<? extends Exception> mapper) { 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(final 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( final 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 exceptionMapper; } private Exception mapException(final CircuitBreakerException cbe) { if (exceptionMapper == null) { return cbe; } return exceptionMapper.map(this, cbe); } private void handleFailure(final Throwable cause) throws Exception { if (failureInterpreter == null || failureInterpreter.shouldTrip(cause)) { tripException = cause; trip(); } 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. */ private void close() { state = BreakerState.CLOSED; isAttemptLive = false; notifyBreakerStateChange(getStatus()); } private synchronized boolean canAttempt() { if (!(BreakerState.HALF_CLOSED == state) || isAttemptLive) { return false; } isAttemptLive = true; return true; } private void notifyBreakerStateChange(final Status status) { if (cbNotifyList != null && cbNotifyList.size() >= 1) { for (final CircuitBreakerNotificationCallback notifyObject : cbNotifyList) { notifyObject.notify(status); } } } /** * @return boolean whether the breaker will allow a request through or not. */ private boolean allowRequest() { if (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(final Throwable t) { final StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); return sw.toString(); } }