/* 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.util.Arrays; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * Trips a {@link org.fishwife.jrugged.CircuitBreaker} if the percentage of failures in a * given time window exceed a specified tolerance. By default, all {@link Throwable} * occurrences will be considered failures. */ public final class PercentErrPerTimeFailureInterpreter implements FailureInterpreter { private Set<Class<? extends Throwable>> ignore = new HashSet<>(); private int percent = 0; private long windowMillis = 0; private int requestThreshold = 0; private long previousRequestHighWaterMark = 0; // tracks times when exceptions occurred private final List<Long> errorTimes = new LinkedList<>(); // tracks times when exceptions occurred private final List<Long> requestCounts = new LinkedList<>(); private final Object modificationLock = new Object(); private RequestCounter requestCounter; @SuppressWarnings("rawtypes") private static Class[] defaultIgnore = {}; /** * Default constructor. Any {@link Throwable} will cause the breaker to trip. */ @SuppressWarnings("unchecked") public PercentErrPerTimeFailureInterpreter() { setIgnore(defaultIgnore); requestCounter = new RequestCounter(); } /** * Constructor that allows a tolerance for a certain number of failures within a given * window of time without tripping. * * @param rc * A {@link RequestCounter} wrapped around the same thing that this * {@link org.fishwife.jrugged.CircuitBreaker} is protecting. This is * needed in order to keep track of the total number of requests, enabling * a percentage calculation to be done. * @param percent * the whole number percentage of failures that will be tolerated (i.e. the * percentage of failures has to be strictly <em>greater than</em> this * than</em> this number in order to trip the breaker). For example, if * 3, any calculated failure percentage above that number during the window * will cause the breaker to trip. * @param windowMillis * length of the window in milliseconds */ @SuppressWarnings("unchecked") public PercentErrPerTimeFailureInterpreter(final RequestCounter rc, final int percent, final long windowMillis) { setIgnore(defaultIgnore); setPercent(percent); setWindowMillis(windowMillis); setRequestCounter(rc); } /** * Constructor that allows a tolerance for a certain number of failures within a given * window of time without tripping. * * @param p * A {@link PerformanceMonitor} from which we can get an underlying * {@link RequestCounter} that is wrapped around the same thing that this * {@link org.fishwife.jrugged.CircuitBreaker} is protecting. This is * needed in order to keep track of the total number of requests, enabling * a percentage calculation to be done. * @param percent * the whole number percentage of failures that will be tolerated (i.e. the * percentage of failures has to be strictly <em>greater than</em> this * than</em> this number in order to trip the breaker). For example, if * 3, any calculated failure percentage above that number during the window * will cause the breaker to trip. * @param windowMillis * length of the window in milliseconds */ @SuppressWarnings("unchecked") public PercentErrPerTimeFailureInterpreter(final PerformanceMonitor p, final int percent, final long windowMillis) { setIgnore(defaultIgnore); setPercent(percent); setWindowMillis(windowMillis); setRequestCounter(p.getRequestCounter()); } /** * Constructor where we specify certain {@link Throwable} classes that will be ignored * by the breaker and not be treated as failures (they will be passed through * transparently without causing the breaker to trip). * * @param ignore * an array of {@link Throwable} classes that will be ignored. Any given * <code>Throwable</code> that is a subclass of one of these classes will * be ignored. */ public PercentErrPerTimeFailureInterpreter( final Class<? extends Throwable>[] ignore) { setIgnore(ignore); } /** * Constructor where we specify tolerance and a set of ignored failures. * * @param rc * A {@link RequestCounter} wrapped around the same thing that this * {@link org.fishwife.jrugged.CircuitBreaker} is protecting. This is * needed in order to keep track of the total number of requests, enabling * a percentage calculation to be done. * @param ignore * an array of {@link Throwable} classes that will be ignored. Any given * <code>Throwable</code> that is a subclass of one of these classes will * be ignored. * @param percent * the whole number percentage of failures that will be tolerated (i.e. the * percentage of failures has to be strictly <em>greater than</em> this * than</em> this number in order to trip the breaker). For example, if * 3, any calculated failure percentage above that number during the window * will cause the breaker to trip. * @param windowMillis * length of the window in milliseconds */ public PercentErrPerTimeFailureInterpreter(final RequestCounter rc, final Class<? extends Throwable>[] ignore, final int percent, final long windowMillis) { setRequestCounter(rc); setIgnore(ignore); setPercent(percent); setWindowMillis(windowMillis); } private boolean hasWindowConditions() { return percent > 0 && windowMillis > 0; } @Override public boolean shouldTrip(final Throwable cause) { if (isExceptionIgnorable(cause)) { return false; } // if Exception is of specified type, and window conditions exist, // keep circuit open unless exception threshold has passed if (hasWindowConditions()) { Long currentRequestCount = -1L; long numberOfErrorsAfter; synchronized (modificationLock) { errorTimes.add(System.currentTimeMillis()); requestCounts.add(requestCounter.sample()[0]); // calculates time for which we remove any errors before final long removeTimeBeforeMillis = System.currentTimeMillis() - windowMillis; final int numberOfErrorsBefore = errorTimes.size(); removeErrorsPriorToCutoffTime(numberOfErrorsBefore, removeTimeBeforeMillis); numberOfErrorsAfter = errorTimes.size(); currentRequestCount = requestCounts.get(requestCounts.size() - 1); } final long windowRequests = currentRequestCount - previousRequestHighWaterMark; // Trip if the number of errors over the total of requests over the // same period // is over the percentage limit. return windowRequests >= requestThreshold && (double) numberOfErrorsAfter / (double) windowRequests * 100d >= percent; } return true; } @SuppressWarnings("rawtypes") private boolean isExceptionIgnorable(final Throwable cause) { for (final Class clazz : ignore) { if (clazz.isInstance(cause)) { return true; } } return false; } private void removeErrorsPriorToCutoffTime(final int numberOfErrorsBefore, final long removeTimeBeforeMillis) { boolean windowRemoval = false; // (could we speed this up by using binary search to find the entry // point, // then removing any items before that point?) for (int j = numberOfErrorsBefore - 1; j >= 0; j--) { final Long time = errorTimes.get(j); if (time < removeTimeBeforeMillis) { if (!windowRemoval) { previousRequestHighWaterMark = requestCounts.get(j); windowRemoval = true; } errorTimes.remove(j); requestCounts.remove(j); } } } /** * Returns the set of currently ignored {@link Throwable} classes. * * @return {@link Set} */ public Set<Class<? extends Throwable>> getIgnore() { return ignore; } /** * Specifies an array of {@link Throwable} classes to ignore. These will not be * considered failures. * * @param ignore * array of {@link Class} objects */ public synchronized void setIgnore(final Class<? extends Throwable>[] ignore) { this.ignore = new HashSet<>(Arrays.asList(ignore)); } /** * Returns the current percentage of failures within the window that will be tolerated * without tripping the breaker. * * @return int */ public int getPercent() { return percent; } /** * Specifies the percentage of tolerated failures within the configured time window. * If percentage is set to <em>n</em> then the <em>(n.000000000000001)</em>th failure * will trip the breaker. * * @param percent * <code>int</code> */ public void setPercent(final int percent) { this.percent = percent; } /** * Returns the length of the currently configured tolerance window in milliseconds. * * @return <code>long</code> */ public long getWindowMillis() { return windowMillis; } /** * Specifies the length of the tolerance window in milliseconds. * * @param windowMillis * <code>long</code> */ public void setWindowMillis(final long windowMillis) { this.windowMillis = windowMillis; } /** * Specifies the {@link RequestCounter} that will be supplying the "total" requests * made information for this interpreter. * * @param rc * A {@link RequestCounter} */ public void setRequestCounter(final RequestCounter rc) { requestCounter = rc; } /** * Sets the threshold at which the number of requests in the current window must be * above in order for the breaker to trip. This is intended to prevent the breaker * from being opened if the first request into this interpreter is a failure. * * @param requestThreshold * The threshold to set, defaults to 0 */ public void setRequestThreshold(final int requestThreshold) { this.requestThreshold = requestThreshold; } }