diff --git a/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/CircuitBreakerAspect.java b/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/CircuitBreakerAspect.java index 63a45c56..04d0e081 100644 --- a/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/CircuitBreakerAspect.java +++ b/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/CircuitBreakerAspect.java @@ -19,7 +19,7 @@ import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.DeclarePrecedence; import org.fishwife.jrugged.CircuitBreakerConfig; -import org.fishwife.jrugged.CircuitBreakerFactory; +import org.fishwife.jrugged.BreakerFactory; import org.fishwife.jrugged.DefaultFailureInterpreter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,30 +39,30 @@ public class CircuitBreakerAspect { /** * Maps names to CircuitBreakers. */ - private CircuitBreakerFactory circuitBreakerFactory; + private BreakerFactory circuitBreakerFactory; /** Default constructor. */ public CircuitBreakerAspect() { - circuitBreakerFactory = new CircuitBreakerFactory(); + circuitBreakerFactory = new BreakerFactory(); } /** - * Sets the {@link org.fishwife.jrugged.CircuitBreakerFactory} to use when creating new + * Sets the {@link org.fishwife.jrugged.BreakerFactory} to use when creating new * {@link org.fishwife.jrugged.CircuitBreaker} instances. - * @param circuitBreakerFactory the {@link org.fishwife.jrugged.CircuitBreakerFactory} to + * @param circuitBreakerFactory the {@link org.fishwife.jrugged.BreakerFactory} to * use. */ public void setCircuitBreakerFactory( - CircuitBreakerFactory circuitBreakerFactory) { + BreakerFactory circuitBreakerFactory) { this.circuitBreakerFactory = circuitBreakerFactory; } /** - * Get the {@link org.fishwife.jrugged.CircuitBreakerFactory} that is being used to create + * Get the {@link org.fishwife.jrugged.BreakerFactory} that is being used to create * new {@link org.fishwife.jrugged.CircuitBreaker} instances. - * @return the {@link org.fishwife.jrugged.CircuitBreakerFactory}. + * @return the {@link org.fishwife.jrugged.BreakerFactory}. */ - public CircuitBreakerFactory getCircuitBreakerFactory() { + public BreakerFactory getCircuitBreakerFactory() { return circuitBreakerFactory; } diff --git a/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/SkepticBreaker.java b/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/SkepticBreaker.java new file mode 100644 index 00000000..739c6338 --- /dev/null +++ b/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/SkepticBreaker.java @@ -0,0 +1,87 @@ +/* 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.aspects; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Marks a method as one to be wrapped with a + * {@link org.fishwife.jrugged.SkepticBreaker}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface SkepticBreaker { + + /** + * Name of the skeptic. Each annotation with a shared value shares + * the same SkepticBreaker. + * @return the value + */ + String name(); + + /** + * Exception types that the {@link + * org.fishwife.jrugged.SkepticBreaker} will ignore (pass through + * transparently without tripping). + * @return the Exception types. + */ + Class[] ignore() default {}; + + /** + * Specifies the length of the measurement window for failure + * tolerances in milliseconds. i.e. if limit + * failures occur within windowMillis milliseconds, + * the breaker will trip. + * @return the length of the measurement window. + */ + long windowMillis() default -1; + + /** + * Specifies the number of failures that must occur within a + * configured time window in order to trip the skeptic breaker. + * @return the number of failures. + */ + int limit() default -1; + + + /** + * Input parameters used to determine length of timers during the + * OPEN (wait) and CLOSED (good) status of the + * {@link org.fishwife.jrugged.SkepticBreaker} + */ + long waitBase() default -1; + long waitMult() default -1; + long goodBase() default -1; + long goodMult() default -1; + + /** + * Specifies the current level of skepticism of the + * {@link org.fishwife.jrugged.SkepticBreaker} given past + * performance. + * @return the Skeptic Level. + */ + long skepticLevel() default -1; + + /** + * Specifies the maximum level of skepticism of the + * {@link org.fishwife.jrugged.SkepticBreaker} admissible. + * @return the maximum skeptic level. + */ + long maxLevel() default -1; +} diff --git a/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/SkepticBreakerAspect.java b/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/SkepticBreakerAspect.java new file mode 100644 index 00000000..55a54190 --- /dev/null +++ b/jrugged-aspects/src/main/java/org/fishwife/jrugged/aspects/SkepticBreakerAspect.java @@ -0,0 +1,129 @@ +/* Copyright 2009-2014 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.aspects; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.DeclarePrecedence; +import org.fishwife.jrugged.FailureInterpreter; +import org.fishwife.jrugged.SkepticBreakerConfig; +import org.fishwife.jrugged.BreakerFactory; +import org.fishwife.jrugged.DefaultFailureInterpreter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Callable; + +/** + * Surrounds methods decorated with the SkepticBreaker annotation with a named + * {@link org.fishwife.jrugged.SkepticBreaker}. + */ +@Aspect +public class SkepticBreakerAspect { + + private static final Logger logger = + LoggerFactory.getLogger(SkepticBreakerAspect.class); + + /** + * Maps names to SkepticBreakers. + */ + private BreakerFactory skepticBreakerFactory; + + /** Default constructor. */ + public SkepticBreakerAspect() { + skepticBreakerFactory = new BreakerFactory(); + } + + /** + * Sets the {@link org.fishwife.jrugged.BreakerFactory} to use when creating new + * {@link org.fishwife.jrugged.SkepticBreaker} instances. + * @param skepticBreakerFactory the {@link org.fishwife.jrugged.BreakerFactory} to + * use. + */ + public void setSkepticBreakerFactory( + BreakerFactory skepticBreakerFactory) { + this.skepticBreakerFactory = skepticBreakerFactory; + } + + /** + * Get the {@link org.fishwife.jrugged.BreakerFactory} that is being used to create + * new {@link org.fishwife.jrugged.SkepticBreaker} instances. + * @return the {@link org.fishwife.jrugged.BreakerFactory}. + */ + public BreakerFactory getSkepticBreakerFactory() { + return skepticBreakerFactory; + } + + /** Runs a method call through the configured + * {@link org.fishwife.jrugged.SkepticBreaker}. + * @param pjp a {@link ProceedingJoinPoint} representing an annotated + * method call. + * @param skepticBreakerAnnotation the {@link org.fishwife.jrugged.SkepticBreaker} annotation + * that wrapped the method. + * @throws Throwable if the method invocation itself or the wrapping + * {@link org.fishwife.jrugged.SkepticBreaker} throws one during execution. + * @return The return value from the method call. + */ + @Around("@annotation(skepticBreakerAnnotation)") + public Object monitor(final ProceedingJoinPoint pjp, + SkepticBreaker skepticBreakerAnnotation) throws Throwable { + final String name = skepticBreakerAnnotation.name(); + + org.fishwife.jrugged.SkepticBreaker skepticBreaker = + skepticBreakerFactory.findSkepticBreaker(name); + + if (skepticBreaker == null) { + DefaultFailureInterpreter dfi = + new DefaultFailureInterpreter( + skepticBreakerAnnotation.ignore(), + skepticBreakerAnnotation.limit(), + skepticBreakerAnnotation.windowMillis()); + + SkepticBreakerConfig config = new SkepticBreakerConfig(skepticBreakerAnnotation.goodBase(), + skepticBreakerAnnotation.goodMult(), skepticBreakerAnnotation.waitBase(), + skepticBreakerAnnotation.waitMult(), skepticBreakerAnnotation.maxLevel(), dfi); + + skepticBreaker = + skepticBreakerFactory.createSkepticBreaker(name, config); + } + + if (logger.isDebugEnabled()) { + logger.debug("Have @SkepticBreaker method with breaker name {}, " + + "wrapping call on method {} of target object {} with status {}", + new Object[]{ + name, + pjp.getSignature().getName(), + pjp.getTarget(), + skepticBreaker.getStatus()}); + } + + return skepticBreaker.invoke(new Callable() { + public Object call() throws Exception { + try { + return pjp.proceed(); + } catch (Throwable e) { + if (e instanceof Exception) { + throw (Exception) e; + } else if (e instanceof Error) { + throw (Error) e; + } else { + throw new RuntimeException(e); + } + } + } + }); + } +} diff --git a/jrugged-aspects/src/test/java/org/fishwife/jrugged/aspects/TestCircuitBreakerAspect.java b/jrugged-aspects/src/test/java/org/fishwife/jrugged/aspects/TestCircuitBreakerAspect.java index e2927e4c..1e9fc3cf 100644 --- a/jrugged-aspects/src/test/java/org/fishwife/jrugged/aspects/TestCircuitBreakerAspect.java +++ b/jrugged-aspects/src/test/java/org/fishwife/jrugged/aspects/TestCircuitBreakerAspect.java @@ -16,8 +16,8 @@ import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; -import org.fishwife.jrugged.CircuitBreakerException; -import org.fishwife.jrugged.CircuitBreakerFactory; +import org.fishwife.jrugged.BreakerException; +import org.fishwife.jrugged.BreakerFactory; import org.junit.Before; import org.junit.Test; @@ -104,7 +104,7 @@ public void testSetCircuitBreakerFactory() throws Throwable { expect(mockPjp.proceed()).andReturn(null); replay(mockPjp); - CircuitBreakerFactory factory = new CircuitBreakerFactory(); + BreakerFactory factory = new BreakerFactory(); aspect.setCircuitBreakerFactory(factory); aspect.monitor(mockPjp, mockAnnotation); @@ -169,7 +169,7 @@ public void testGetCircuitBreakerFactory() throws Throwable { replay(mockPjp); aspect.monitor(mockPjp, mockAnnotation); - CircuitBreakerFactory circuitBreakerFactory = + BreakerFactory circuitBreakerFactory = aspect.getCircuitBreakerFactory(); assertNotNull(circuitBreakerFactory); @@ -196,7 +196,7 @@ public void testTripBreaker() throws Throwable { callMonitorCatchThrowable(mockPjp, e); } - CircuitBreakerException cbe = new CircuitBreakerException(); + BreakerException cbe = new BreakerException(); callMonitorCatchThrowable(mockPjp, cbe); verifyBreakerExists(TEST_CIRCUIT_BREAKER); diff --git a/jrugged-core/pom.xml b/jrugged-core/pom.xml index ab1779be..ed0cf107 100644 --- a/jrugged-core/pom.xml +++ b/jrugged-core/pom.xml @@ -26,4 +26,12 @@ jar jrugged-core https://github.com/Comcast/jrugged + + + + org.easymock + easymock + 3.4 + + diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/Breaker.java b/jrugged-core/src/main/java/org/fishwife/jrugged/Breaker.java new file mode 100644 index 00000000..34b059e9 --- /dev/null +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/Breaker.java @@ -0,0 +1,479 @@ + +/* Breaker.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 Breaker} 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 Breaker. + *

+ * When everything is operating normally, the Breaker + * is CLOSED and the calls are allowed through. + *

+ * When a call fails, however, the Breaker and + * SkepticBreaker act differently. See corresponding + * documentation for more details. + */ +public abstract class Breaker implements MonitoredService, ServiceWrapper { + /** + * Represents whether a {@link Breaker} 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 + } + + protected 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 String + * 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); + + /** 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 + * BreakerException}. */ + protected BreakerExceptionMapper exceptionMapper; + + protected List cbNotifyList = + Collections.synchronizedList(new ArrayList()); + + protected boolean isHardTrip; + + /** + * Bypass this Breaker - used for testing, or other operational + * situations where verification of the Break might be required. + */ + protected boolean byPass = false; + + /* DEFAULT NAME WAS HERE */ + /* NOTE: I still included variable 'name' in SkepticBreaker */ + + /** The name for the Breaker. */ + protected String name = "Breaker"; + + /** Creates a {@link Breaker} with a {@link + * DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). */ + public Breaker() { + } + + /** Creates a {@link Breaker} with a {@link + * DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). + * @param name the name for the {@link Breaker}. + */ + public Breaker(String name) { + this.name = name; + } + + /** Creates a {@link Breaker} with the specified {@link + * FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public Breaker(FailureInterpreter fi) { + failureInterpreter = fi; + } + + /** Creates a {@link Breaker} with the specified {@link + * FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). + * @param name the name for the {@link Breaker}. + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public Breaker(String name, FailureInterpreter fi) { + this.name = name; + failureInterpreter = fi; + } + + /** Creates a {@link Breaker} with a {@link + * DefaultFailureInterpreter} and using the supplied {@link + * BreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link Breaker}. + * @param mapper helper used to translate a {@link + * BreakerException} into an application-specific one */ + public Breaker(String name, BreakerExceptionMapper mapper) { + this.name = name; + exceptionMapper = mapper; + } + + /** Creates a {@link Breaker} with the provided {@link + * FailureInterpreter} and using the provided {@link + * BreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link Breaker}. + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + * @param mapper helper used to translate a {@link + * BreakerException} into an application-specific one */ + public Breaker(String name, + FailureInterpreter fi, + BreakerExceptionMapper mapper) { + this.name = name; + failureInterpreter = fi; + exceptionMapper = mapper; + } + + /** Wrap the given service call with the {@link Breaker} + * protection logic. + * @param c the {@link Callable} to attempt + * @return whatever c would return on success + * @throws BreakerException if the + * breaker was OPEN or HALF_CLOSED and this attempt wasn't the + * reset attempt + * @throws Exception if c throws one during + * execution + */ + public abstract V invoke(Callable c) throws Exception; + + /** Wrap the given service call with the {@link Breaker} + * protection logic. + * @param r the {@link Runnable} to attempt + * @throws BreakerException if the + * breaker was OPEN or HALF_CLOSED and this attempt wasn't the + * reset attempt + * @throws Exception if c throws one during + * execution + */ + public abstract void invoke(Runnable r) throws Exception; + + /** Wrap the given service call with the {@link CircuitBreaker} + * protection logic. + * @param r the {@link Runnable} to attempt + * @param result what to return after r succeeds + * @return result + * @throws BreakerException if the + * breaker was OPEN or HALF_CLOSED and this attempt wasn't the + * reset attempt + * @throws Exception if c throws one during + * execution + */ + public abstract V invoke(Runnable r, V result) throws Exception; + + /** + * When called with true - causes the {@link Breaker} to byPass + * its functionality allowing requests to be executed unmolested + * until the Breaker 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 Breaker} byPass + * + * @return boolean the byPass flag's current value + */ + public boolean getByPassState() { + return byPass; + } + + /** + * Causes the {@link Breaker} to trip and OPEN; no new + * requests will be allowed until the Breaker + * resets. + */ + public abstract void trip(); + + /** + * Manually trips the Breaker 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 abstract void reset(); + + /** + * Returns the current {@link org.fishwife.jrugged.Status} of the + * {@link Breaker}. In this case, it really refers to the + * status of the client service. If the + * Breaker 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 abstract ServiceStatus getServiceStatus(); + + /* ----GET RESET MILLIS WAS HERE ---*/ + /* ----SET RESET MILLIS WAS HERE ---*/ + + /** Returns a {@link String} representation of the breaker's + * status; potentially useful for exposing to monitoring software. + * @return String which is "GREEN" if + * the breaker is CLOSED; "YELLOW" if the breaker + * is HALF_CLOSED; and "RED" 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 + * Breaker} 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 Breaker'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 Breaker}. + * @see DefaultFailureInterpreter + * @param ignore a {@link java.util.Collection} of {@link Throwable} + * classes + */ + public void setIgnore(Collection> ignore) { + FailureInterpreter fi = getFailureInterpreter(); + if (!(fi instanceof DefaultFailureInterpreter)) { + throw new IllegalStateException("setIgnore() not supported: this Breaker's FailureInterpreter isn't a DefaultFailureInterpreter."); + } + + @SuppressWarnings("unchecked") + Class[] classes = new Class[ignore.size()]; + int i = 0; + for(Class 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 + * Breaker} 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 Breaker'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 BreakerExceptions into a known + * 'application' exception. + * + * @param mapper my converter object + */ + public void setExceptionMapper(BreakerExceptionMapper mapper) { + this.exceptionMapper = mapper; + } + + /** + * Add an interested party for {@link Breaker} events, like up, + * down, degraded status state changes. + * + * @param listener an interested party for {@link Breaker} status events. + */ + public void addListener(BreakerNotificationCallback listener) { + cbNotifyList.add(listener); + } + + /** + * Set a list of interested parties for {@link Breaker} events, like up, + * down, degraded status state changes. + * + * @param listeners a list of interested parties for {@link Breaker} status events. + */ + public void setListeners(ArrayList listeners) { + cbNotifyList = Collections.synchronizedList(listeners); + } + + /** + * Get the helper that converts {@link BreakerException}s into + * application-specific exceptions. + * @return {@link BreakerExceptionMapper} my converter object, or + * null if one is not currently set. + */ + public BreakerExceptionMapper getExceptionMapper(){ + return this.exceptionMapper; + } + + protected Exception mapException(BreakerException cbe) { + if (exceptionMapper == null) + return cbe; + + return exceptionMapper.map(this, cbe); + } + + protected abstract void handleFailure(Throwable cause) throws Exception; + + /** + * Reports a successful service call to the {@link Breaker}, + * putting the Breaker back into the CLOSED + * state serving requests. + */ + protected abstract void close(); + + /* canAttempt() was here */ + + protected void notifyBreakerStateChange(Status status) { + if (cbNotifyList != null && cbNotifyList.size() >= 1) { + for (BreakerNotificationCallback notifyObject : cbNotifyList) { + notifyObject.notify(status); + } + } + } + + /** + * @return boolean whether the breaker will allow a request + * through or not. + */ + protected abstract boolean allowRequest(); + + private String getFullStackTrace(Throwable t) { + StringWriter sw = new StringWriter(); + t.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } +} diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerConfig.java b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerConfig.java new file mode 100644 index 00000000..0153e42f --- /dev/null +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerConfig.java @@ -0,0 +1,24 @@ +/* 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; + +/** + * The BreakerConfig class holds a + * {@link org.fishwife.jrugged.Breaker} configuration. + */ +public interface BreakerConfig { + + public FailureInterpreter getFailureInterpreter(); +} diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerException.java b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerException.java similarity index 89% rename from jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerException.java rename to jrugged-core/src/main/java/org/fishwife/jrugged/BreakerException.java index bfd28bcf..d421d458 100644 --- a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerException.java +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerException.java @@ -20,10 +20,10 @@ * This exception gets thrown by a {@link CircuitBreaker} if a wrapped * call is disallowed by a tripped (OPEN) breaker. */ -public class CircuitBreakerException extends RuntimeException { +public class BreakerException extends RuntimeException { private static final long serialVersionUID = 1L; /** Default constructor. */ - public CircuitBreakerException() { } + public BreakerException() { } } diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerExceptionMapper.java b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerExceptionMapper.java similarity index 72% rename from jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerExceptionMapper.java rename to jrugged-core/src/main/java/org/fishwife/jrugged/BreakerExceptionMapper.java index d08fae44..225b42b1 100644 --- a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerExceptionMapper.java +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerExceptionMapper.java @@ -15,21 +15,22 @@ package org.fishwife.jrugged; /** - * Allows the user to map the standard {@link CircuitBreakerException} + * Allows the user to map the standard {@link BreakerException} * thrown by a tripped {@link CircuitBreaker} into an application * specific exception. * * @param is the application specific exception type. */ -public interface CircuitBreakerExceptionMapper { +public interface BreakerExceptionMapper { /** - * Turns a {@link CircuitBreakerException} into the desired exception (T) + * Turns a {@link BreakerException} into the desired exception (T) * * @param breaker the {@link CircuitBreaker} - * @param e the CircuitBreakerException I get + * @param e the BreakerException I get * @return the {@link Exception} I want thrown instead */ - public T map(CircuitBreaker breaker, CircuitBreakerException e); - + public T map(Breaker breaker, BreakerException e); + + } diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerFactory.java b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerFactory.java new file mode 100644 index 00000000..9f2a3c32 --- /dev/null +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerFactory.java @@ -0,0 +1,461 @@ +/* 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.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory to create new {@link CircuitBreaker} and {@link SkepticBreaker} + * instances and keep track of them. + */ +public class BreakerFactory { + + public static final String CIRCUIT_BREAKER_CONFIG_KEY_PREFIX = "circuit"; + public static final String SKEPTIC_BREAKER_CONFIG_KEY_PREFIX = "skeptic"; + public static final String LIMIT_KEY = "limit"; + public static final String WINDOWMILLIS_KEY = "windowMillis"; + public static final String RESETMILLIS_KEY = "resetMillis"; + + public static final String WAITMULT_KEY = "waitMult"; + public static final String GOODMULT_KEY = "goodMult"; + public static final String WAITBASE_KEY = "waitBase"; + public static final String GOODBASE_KEY = "goodBase"; + public static final String MAXLEVEL_KEY = "maxLevel"; + + private final ConcurrentHashMap circuitBreakerMap = + new ConcurrentHashMap(); + private final ConcurrentHashMap skepticBreakerMap = + new ConcurrentHashMap(); + + private Properties circuitBreakerProperties; + private Properties skepticBreakerProperties; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Create a new {@link CircuitBreaker} and map it to the provided name. + * If the CircuitBreaker already exists, then the existing instance is + * returned. + * @param name the name of the {@link CircuitBreaker} + * @param config the {@link CircuitBreakerConfig} with the configuration + * values. + * @return the created {@link CircuitBreaker} + */ + public synchronized CircuitBreaker createCircuitBreaker(String name, + CircuitBreakerConfig config) { + CircuitBreaker circuitBreaker = findCircuitBreaker(name); + + if (circuitBreaker == null) { + circuitBreaker = new CircuitBreaker(name); + + configureCircuitBreaker(name, circuitBreaker, config); + addCircuitBreakerToMap(name, circuitBreaker); + } + + return circuitBreaker; + } + + /** + * Create a new {@link SkepticBreaker} and map it to the provided name. + * If the SkepticBreaker already exists, then the existing instance is + * returned. + * @param name the name of the {@link SkepticBreaker} + * @param config the {@link SkepticBreakerConfig} with the configuration + * values. + * @return the created {@link SkepticBreaker} + */ + public synchronized SkepticBreaker createSkepticBreaker(String name, + SkepticBreakerConfig config) { + SkepticBreaker skepticBreaker = findSkepticBreaker(name); + + if (skepticBreaker == null) { + skepticBreaker = new SkepticBreaker(name); + + configureSkepticBreaker(name, skepticBreaker, config); + addSkepticBreakerToMap(name, skepticBreaker); + } + + return skepticBreaker; + } + + /** + * Set the {@link Properties} object to search for {@link CircuitBreaker} + * property override values. The override values can be specified as:
+ * circuit.{circuit_name}.limit
+ * circuit.{circuit_name}.resetmillis
+ * circuit.{circuit_name}.windowmillis + * @param properties the {@link Properties} object to search. + */ + public void setCircuitBreakerProperties(Properties properties) { + this.circuitBreakerProperties = properties; + } + + /** + * Set the {@link Properties} object to search for {@link SkepticBreaker} + * property override values. The override values can be specified as:
+ * skeptic.{skeptic_name}.limit
+ * skeptic.{skeptic_name}.waitmult
+ * skeptic.{skeptic_name}.goodmult
+ * skeptic.{skeptic_name}.waitbase
+ * skeptic.{skeptic_name}.goodbase
+ * skeptic.{skeptic_name}.maxlevel
+ * skeptic.{skeptic_name}.windowmillis + * @param properties the {@link Properties} object to search. + */ + public void setSkepticBreakerProperties(Properties properties) { + this.skepticBreakerProperties = properties; + } + + /** + * Find an existing {@link CircuitBreaker} + * @param name the value for the {@link CircuitBreaker} + * @return the found {@link CircuitBreaker}, or null if it is not found. + */ + public CircuitBreaker findCircuitBreaker(String name) { + return circuitBreakerMap.get(name); + } + + /** + * Find an existing {@link SkepticBreaker} + * @param name the value for the {@link SkepticBreaker} + * @return the found {@link SkepticBreaker}, or null if it is not found. + */ + public SkepticBreaker findSkepticBreaker(String name) { + return skepticBreakerMap.get(name); + } + + /** + * Get the {@link Set} of created {@link CircuitBreaker} names. + * @return the {@link Set} of names. + */ + public Set getCircuitBreakerNames() { + return circuitBreakerMap.keySet(); + } + + /** + * Get the {@link Set} of created {@link SkepticBreaker} names. + * @return the {@link Set} of names. + */ + public Set getSkepticBreakerNames() { + return skepticBreakerMap.keySet(); + } + + protected void configureCircuitBreaker(String name, + CircuitBreaker circuit, + CircuitBreakerConfig config) { + + long resetMillis = config.getResetMillis(); + Long resetMillisOverride = getCircuitBreakerLongPropertyOverrideValue(name, RESETMILLIS_KEY); + if (resetMillisOverride != null) { + resetMillis = resetMillisOverride; + } + + FailureInterpreter fi = config.getFailureInterpreter(); + circuit.setFailureInterpreter(fi); + + if (resetMillis > 0) { + circuit.setResetMillis(resetMillis); + } + + if (fi instanceof DefaultFailureInterpreter) { + configureCircuitBreakerDefaultFailureInterpreter(name, resetMillis, circuit); + } + else { + logger.info( + "Created CircuitBreaker '{}', resetMillis={}", + new Object[] { + name, + resetMillis + }); + } + } + + protected void configureSkepticBreaker(String name, + SkepticBreaker skeptic, + SkepticBreakerConfig config) { + + long waitMult = config.getWaitMult(); + Long waitMultOverride = getSkepticBreakerLongPropertyOverrideValue(name, WAITMULT_KEY); + if (waitMultOverride != null) { + waitMult = waitMultOverride; + } + + long goodMult = config.getGoodMult(); + Long goodMultOverride = getSkepticBreakerLongPropertyOverrideValue(name, GOODMULT_KEY); + if (goodMultOverride != null) { + goodMult = goodMultOverride; + } + + long waitBase = config.getWaitBase(); + Long waitBaseOverride = getSkepticBreakerLongPropertyOverrideValue(name, WAITBASE_KEY); + if (waitBaseOverride != null) { + waitBase = waitBaseOverride; + } + + long goodBase = config.getGoodBase(); + Long goodBaseOverride = getSkepticBreakerLongPropertyOverrideValue(name, GOODBASE_KEY); + if (goodBaseOverride != null) { + goodBase = goodBaseOverride; + } + + long maxLevel = config.getMaxLevel(); + Long maxLevelOverride = getSkepticBreakerLongPropertyOverrideValue(name, MAXLEVEL_KEY); + if (maxLevelOverride != null) { + maxLevel = maxLevelOverride; + } + + FailureInterpreter fi = config.getFailureInterpreter(); + skeptic.setFailureInterpreter(fi); + + if (waitMult > 0) { + skeptic.setWaitMult(waitMult); + } + + if (goodMult > 0) { + skeptic.setGoodMult(goodMult); + } + + if (waitBase > 0) { + skeptic.setWaitBase(waitBase); + } + + if (goodBase > 0) { + skeptic.setGoodBase(goodBase); + } + + if (maxLevel > 0) { + skeptic.setMaxLevel(maxLevel); + } + + skeptic.updateTimers(); + + if (fi instanceof DefaultFailureInterpreter) { + configureSkepticBreakerDefaultFailureInterpreter(name, waitMult, goodMult, waitBase, + goodBase, maxLevel, skeptic); + } + else { + logger.info( + "Created SkepticBreaker '{}', waitMult={}, goodMult={}, waitBase={}, " + + "goodBase={}, maxLevel={}", + new Object[] { + name, + waitMult, + goodMult, + waitBase, + goodBase, + maxLevel + }); + } + } + + private void configureCircuitBreakerDefaultFailureInterpreter(String name, long resetMillis, CircuitBreaker circuit) { + DefaultFailureInterpreter fi = (DefaultFailureInterpreter) circuit.getFailureInterpreter(); + + Integer limitOverride = getCircuitBreakerIntegerPropertyOverrideValue(name, LIMIT_KEY); + + if (limitOverride != null) { + fi.setLimit(limitOverride); + } + + Long windowMillisOverride = getCircuitBreakerLongPropertyOverrideValue(name, WINDOWMILLIS_KEY); + + if (windowMillisOverride != null) { + fi.setWindowMillis(windowMillisOverride); + } + + logger.info( + "Created CircuitBreaker '{}', limit={}, windowMillis={}, resetMillis={}", + new Object[] { + name, + fi.getLimit(), + fi.getWindowMillis(), + resetMillis + }); + } + + private void configureSkepticBreakerDefaultFailureInterpreter(String name, long waitMult, + long goodMult, long waitBase, long goodBase, long maxLevel, SkepticBreaker skeptic) { + DefaultFailureInterpreter fi = (DefaultFailureInterpreter) skeptic.getFailureInterpreter(); + + Integer limitOverride = getSkepticBreakerIntegerPropertyOverrideValue(name, LIMIT_KEY); + + if (limitOverride != null) { + fi.setLimit(limitOverride); + } + + Long windowMillisOverride = getSkepticBreakerLongPropertyOverrideValue(name, WINDOWMILLIS_KEY); + + if (windowMillisOverride != null) { + fi.setWindowMillis(windowMillisOverride); + } + + logger.info( + "Created SkepticBreaker '{}', limit={}, windowMillis={}, waitMult={}, " + + "goodMult={}, waitBase={}, goodBase={}, maxLevel={}", + new Object[] { + name, + fi.getLimit(), + fi.getWindowMillis(), + waitMult, + goodMult, + waitBase, + goodBase, + maxLevel + }); + } + + /** + * Add a {@link CircuitBreaker} to the map. + * @param name the name for the {@link CircuitBreaker} + * @param circuitBreaker the {@link CircuitBreaker} to add. + */ + protected void addCircuitBreakerToMap(String name, CircuitBreaker circuitBreaker) { + circuitBreakerMap.put(name, circuitBreaker); + } + + /** + * Add a {@link SkepticBreaker} to the map. + * @param name the name for the {@link SkepticBreaker} + * @param skepticBreaker the {@link SkepticBreaker} to add. + */ + protected void addSkepticBreakerToMap(String name, SkepticBreaker skepticBreaker) { + skepticBreakerMap.put(name, skepticBreaker); + } + + /** + * Get the property name for a circuit name and key. + * @param name the circuit name. + * @param key the property key. + * @return the property name. + */ + private String getCircuitBreakerPropertyName(String name, String key) { + return CIRCUIT_BREAKER_CONFIG_KEY_PREFIX + '.' + name + '.' + key; + } + + /** + * Get the property name for a skeptic name and key. + * @param name the skeptic name. + * @param key the property key. + * @return the property name. + */ + private String getSkepticBreakerPropertyName(String name, String key) { + return SKEPTIC_BREAKER_CONFIG_KEY_PREFIX + '.' + name + '.' + key; + } + + /** + * Get an integer property override value. + * @param name the {@link CircuitBreaker} name. + * @param key the property override key. + * @return the property override value, or null if it is not found. + */ + private Integer getCircuitBreakerIntegerPropertyOverrideValue(String name, String key) { + if (circuitBreakerProperties != null) { + String propertyName = getCircuitBreakerPropertyName(name, key); + + String propertyOverrideValue = circuitBreakerProperties.getProperty(propertyName); + + if (propertyOverrideValue != null) { + try { + return Integer.parseInt(propertyOverrideValue); + } + catch (NumberFormatException e) { + logger.error("Could not parse property override key={}, value={}", + key, propertyOverrideValue); + } + } + } + return null; + } + + /** + * Get an integer property override value. + * @param name the {@link SkepticBreaker} name. + * @param key the property override key. + * @return the property override value, or null if it is not found. + */ + private Integer getSkepticBreakerIntegerPropertyOverrideValue(String name, String key) { + if (skepticBreakerProperties != null) { + String propertyName = getSkepticBreakerPropertyName(name, key); + + String propertyOverrideValue = skepticBreakerProperties.getProperty(propertyName); + + if (propertyOverrideValue != null) { + try { + return Integer.parseInt(propertyOverrideValue); + } + catch (NumberFormatException e) { + logger.error("Could not parse property override key={}, value={}", + key, propertyOverrideValue); + } + } + } + return null; + } + + /** + * Get an {@link Long} property override value. + * @param name the {@link CircuitBreaker} name. + * @param key the property override key. + * @return the property override value, or null if it is not found. + */ + private Long getCircuitBreakerLongPropertyOverrideValue(String name, String key) { + if (circuitBreakerProperties != null) { + String propertyName = getCircuitBreakerPropertyName(name, key); + + String propertyOverrideValue = circuitBreakerProperties.getProperty(propertyName); + + if (propertyOverrideValue != null) { + try { + return Long.parseLong(propertyOverrideValue); + } + catch (NumberFormatException e) { + logger.error("Could not parse property override key={}, value={}", + key, propertyOverrideValue); + } + } + } + return null; + } + + /** + * Get an {@link Long} property override value. + * @param name the {@link SkepticBreaker} name. + * @param key the property override key. + * @return the property override value, or null if it is not found. + */ + private Long getSkepticBreakerLongPropertyOverrideValue(String name, String key) { + if (skepticBreakerProperties != null) { + String propertyName = getSkepticBreakerPropertyName(name, key); + + String propertyOverrideValue = skepticBreakerProperties.getProperty(propertyName); + + if (propertyOverrideValue != null) { + try { + return Long.parseLong(propertyOverrideValue); + } + catch (NumberFormatException e) { + logger.error("Could not parse property override key={}, value={}", + key, propertyOverrideValue); + } + } + } + return null; + } +} diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerNotificationCallback.java b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerNotificationCallback.java similarity index 95% rename from jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerNotificationCallback.java rename to jrugged-core/src/main/java/org/fishwife/jrugged/BreakerNotificationCallback.java index 80ca2e92..87d249fb 100644 --- a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerNotificationCallback.java +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/BreakerNotificationCallback.java @@ -22,7 +22,7 @@ * the listeners to then take action based on what the state change * was. */ -public abstract class CircuitBreakerNotificationCallback { +public abstract class BreakerNotificationCallback { /** * The method needing an application specific implementation diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreaker.java b/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreaker.java index 80194f0e..d6ce9e90 100644 --- a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreaker.java +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreaker.java @@ -16,12 +16,6 @@ */ 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; @@ -58,85 +52,12 @@ public String call() { } * */ -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 String - * 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 exceptionMapper; - - protected List cbNotifyList = - Collections.synchronizedList(new ArrayList()); - - 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; - +public class CircuitBreaker extends Breaker implements MonitoredService, ServiceWrapper { + + + /** How long the cooldown period is in milliseconds. */ + protected AtomicLong resetMillis = new AtomicLong(15 * 1000L); + /** * Whether the "test" attempt permitted in the HALF_CLOSED state * is currently in-flight. @@ -148,81 +69,78 @@ public String getTripExceptionAsString() { /** 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}). */ + * behavior (throwing a {@link BreakerException}). */ public CircuitBreaker() { } - + /** Creates a {@link CircuitBreaker} with a {@link * DefaultFailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link CircuitBreakerException}). + * behavior (throwing a {@link BreakerException}). * @param name the name for the {@link CircuitBreaker}. */ public CircuitBreaker(String name) { - this.name = name; + super(name); } - + /** Creates a {@link CircuitBreaker} with the specified {@link * FailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link CircuitBreakerException}). + * behavior (throwing a {@link BreakerException}). * @param fi the FailureInterpreter to use when * determining whether a specific failure ought to cause the * breaker to trip */ public CircuitBreaker(FailureInterpreter fi) { - failureInterpreter = fi; + super(fi); } /** Creates a {@link CircuitBreaker} with the specified {@link * FailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link CircuitBreakerException}). + * behavior (throwing a {@link BreakerException}). * @param name the name for the {@link CircuitBreaker}. * @param fi the FailureInterpreter 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; + super(name, fi); } /** Creates a {@link CircuitBreaker} with a {@link * DefaultFailureInterpreter} and using the supplied {@link - * CircuitBreakerExceptionMapper} when client calls are made + * BreakerExceptionMapper} 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 mapper) { - this.name = name; - exceptionMapper = mapper; + * BreakerException} into an application-specific one */ + public CircuitBreaker(String name, BreakerExceptionMapper mapper) { + super(name, mapper); } /** Creates a {@link CircuitBreaker} with the provided {@link * FailureInterpreter} and using the provided {@link - * CircuitBreakerExceptionMapper} when client calls are made + * BreakerExceptionMapper} when client calls are made * while the breaker is tripped. * @param name the name for the {@link CircuitBreaker}. * @param fi the FailureInterpreter 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 */ + * BreakerException} into an application-specific one */ public CircuitBreaker(String name, FailureInterpreter fi, - CircuitBreakerExceptionMapper mapper) { - this.name = name; - failureInterpreter = fi; - exceptionMapper = mapper; + BreakerExceptionMapper mapper) { + super(name, fi, 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 + * @throws BreakerException if the * breaker was OPEN or HALF_CLOSED and this attempt wasn't the * reset attempt * @throws Exception if c throws one during @@ -231,7 +149,7 @@ public CircuitBreaker(String name, public V invoke(Callable c) throws Exception { if (!byPass) { if (!allowRequest()) { - throw mapException(new CircuitBreakerException()); + throw mapException(new BreakerException()); } try { @@ -252,7 +170,7 @@ public V invoke(Callable c) throws Exception { /** Wrap the given service call with the {@link CircuitBreaker} * protection logic. * @param r the {@link Runnable} to attempt - * @throws CircuitBreakerException if the + * @throws BreakerException if the * breaker was OPEN or HALF_CLOSED and this attempt wasn't the * reset attempt * @throws Exception if c throws one during @@ -261,7 +179,7 @@ public V invoke(Callable c) throws Exception { public void invoke(Runnable r) throws Exception { if (!byPass) { if (!allowRequest()) { - throw mapException(new CircuitBreakerException()); + throw mapException(new BreakerException()); } try { @@ -284,7 +202,7 @@ public void invoke(Runnable r) throws Exception { * @param r the {@link Runnable} to attempt * @param result what to return after r succeeds * @return result - * @throws CircuitBreakerException if the + * @throws BreakerException if the * breaker was OPEN or HALF_CLOSED and this attempt wasn't the * reset attempt * @throws Exception if c throws one during @@ -293,7 +211,7 @@ public void invoke(Runnable r) throws Exception { public V invoke(Runnable r, V result) throws Exception { if (!byPass) { if (!allowRequest()) { - throw mapException(new CircuitBreakerException()); + throw mapException(new BreakerException()); } try { @@ -312,28 +230,6 @@ public V invoke(Runnable r, V result) throws Exception { } } - /** - * When called with true - causes the {@link CircuitBreaker} to byPass - * its functionality allowing requests to be executed unmolested - * until the CircuitBreaker 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 CircuitBreaker @@ -350,32 +246,6 @@ public void trip() { 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 @@ -391,20 +261,6 @@ public void reset() { 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 - * CircuitBreaker 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, @@ -449,139 +305,6 @@ 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 String which is "GREEN" if - * the breaker is CLOSED; "YELLOW" if the breaker - * is HALF_CLOSED; and "RED" 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> 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[] classes = new Class[ignore.size()]; - int i = 0; - for(Class 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 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 listeners) { - cbNotifyList = Collections.synchronizedList(listeners); - } - - /** - * Get the helper that converts {@link CircuitBreakerException}s into - * application-specific exceptions. - * @return {@link CircuitBreakerExceptionMapper} my converter object, or - * null if one is not currently set. - */ - public CircuitBreakerExceptionMapper 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; @@ -618,14 +341,6 @@ private synchronized boolean canAttempt() { 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. @@ -647,9 +362,4 @@ else if (BreakerState.CLOSED == state) { } - private String getFullStackTrace(Throwable t) { - StringWriter sw = new StringWriter(); - t.printStackTrace(new PrintWriter(sw)); - return sw.toString(); - } } diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerConfig.java b/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerConfig.java index bd94d6d7..f61339dc 100644 --- a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerConfig.java +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerConfig.java @@ -18,7 +18,7 @@ * The CircuitBreakerConfig class holds a * {@link org.fishwife.jrugged.CircuitBreaker} configuration. */ -public class CircuitBreakerConfig { +public class CircuitBreakerConfig implements BreakerConfig{ private FailureInterpreter failureInterpreter; private long resetMillis; diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerFactory.java b/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerFactory.java deleted file mode 100644 index eb93dd22..00000000 --- a/jrugged-core/src/main/java/org/fishwife/jrugged/CircuitBreakerFactory.java +++ /dev/null @@ -1,218 +0,0 @@ -/* 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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Factory to create new {@link CircuitBreaker} instances and keep track of - * them. - */ -public class CircuitBreakerFactory { - - public static final String CONFIG_KEY_PREFIX = "circuit"; - public static final String LIMIT_KEY = "limit"; - public static final String WINDOWMILLIS_KEY = "windowMillis"; - public static final String RESETMILLIS_KEY = "resetMillis"; - - private final ConcurrentHashMap circuitBreakerMap = - new ConcurrentHashMap(); - - private Properties properties; - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - /** - * Create a new {@link CircuitBreaker} and map it to the provided name. - * If the CircuitBreaker already exists, then the existing instance is - * returned. - * @param name the name of the {@link CircuitBreaker} - * @param config the {@link CircuitBreakerConfig} with the configuration - * values. - * @return the created {@link CircuitBreaker} - */ - public synchronized CircuitBreaker createCircuitBreaker(String name, - CircuitBreakerConfig config) { - CircuitBreaker circuitBreaker = findCircuitBreaker(name); - - if (circuitBreaker == null) { - circuitBreaker = new CircuitBreaker(name); - - configureCircuitBreaker(name, circuitBreaker, config); - addCircuitBreakerToMap(name, circuitBreaker); - } - - return circuitBreaker; - } - - /** - * Set the {@link Properties} object to search for {@link CircuitBreaker} - * property override values. The override values can be specified as:
- * circuit.{circuit_name}.limit
- * circuit.{circuit_name}.resetmillis
- * circuit.{circuit_name}.windowmillis - * @param properties the {@link Properties} object to search. - */ - public void setProperties(Properties properties) { - this.properties = properties; - } - - /** - * Find an existing {@link CircuitBreaker} - * @param name the value for the {@link CircuitBreaker} - * @return the found {@link CircuitBreaker}, or null if it is not found. - */ - public CircuitBreaker findCircuitBreaker(String name) { - return circuitBreakerMap.get(name); - } - - /** - * Get the {@link Set} of created {@link CircuitBreaker} names. - * @return the {@link Set} of names. - */ - public Set getCircuitBreakerNames() { - return circuitBreakerMap.keySet(); - } - - protected void configureCircuitBreaker(String name, - CircuitBreaker circuit, - CircuitBreakerConfig config) { - - long resetMillis = config.getResetMillis(); - Long resetMillisOverride = getLongPropertyOverrideValue(name, RESETMILLIS_KEY); - if (resetMillisOverride != null) { - resetMillis = resetMillisOverride; - } - - FailureInterpreter fi = config.getFailureInterpreter(); - circuit.setFailureInterpreter(fi); - - if (resetMillis > 0) { - circuit.setResetMillis(resetMillis); - } - - if (fi instanceof DefaultFailureInterpreter) { - configureDefaultFailureInterpreter(name, resetMillis, circuit); - } - else { - logger.info( - "Created CircuitBreaker '{}', resetMillis={}", - new Object[] { - name, - resetMillis - }); - } - } - - private void configureDefaultFailureInterpreter(String name, long resetMillis, CircuitBreaker circuit) { - DefaultFailureInterpreter fi = (DefaultFailureInterpreter) circuit.getFailureInterpreter(); - - Integer limitOverride = getIntegerPropertyOverrideValue(name, LIMIT_KEY); - - if (limitOverride != null) { - fi.setLimit(limitOverride); - } - - Long windowMillisOverride = getLongPropertyOverrideValue(name, WINDOWMILLIS_KEY); - - if (windowMillisOverride != null) { - fi.setWindowMillis(windowMillisOverride); - } - - logger.info( - "Created CircuitBreaker '{}', limit={}, windowMillis={}, resetMillis={}", - new Object[] { - name, - fi.getLimit(), - fi.getWindowMillis(), - resetMillis - }); - } - - /** - * Add a {@link CircuitBreaker} to the map. - * @param name the name for the {@link CircuitBreaker} - * @param circuitBreaker the {@link CircuitBreaker} to add. - */ - protected void addCircuitBreakerToMap(String name, CircuitBreaker circuitBreaker) { - circuitBreakerMap.put(name, circuitBreaker); - } - - /** - * Get the property name for a circuit name and key. - * @param name the circuit name. - * @param key the property key. - * @return the property name. - */ - private String getPropertyName(String name, String key) { - return CONFIG_KEY_PREFIX + '.' + name + '.' + key; - } - - /** - * Get an integer property override value. - * @param name the {@link CircuitBreaker} name. - * @param key the property override key. - * @return the property override value, or null if it is not found. - */ - private Integer getIntegerPropertyOverrideValue(String name, String key) { - if (properties != null) { - String propertyName = getPropertyName(name, key); - - String propertyOverrideValue = properties.getProperty(propertyName); - - if (propertyOverrideValue != null) { - try { - return Integer.parseInt(propertyOverrideValue); - } - catch (NumberFormatException e) { - logger.error("Could not parse property override key={}, value={}", - key, propertyOverrideValue); - } - } - } - return null; - } - - /** - * Get an {@link Long} property override value. - * @param name the {@link CircuitBreaker} name. - * @param key the property override key. - * @return the property override value, or null if it is not found. - */ - private Long getLongPropertyOverrideValue(String name, String key) { - if (properties != null) { - String propertyName = getPropertyName(name, key); - - String propertyOverrideValue = properties.getProperty(propertyName); - - if (propertyOverrideValue != null) { - try { - return Long.parseLong(propertyOverrideValue); - } - catch (NumberFormatException e) { - logger.error("Could not parse property override key={}, value={}", - key, propertyOverrideValue); - } - } - } - return null; - } -} - diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/DefaultFailureInterpreter.java b/jrugged-core/src/main/java/org/fishwife/jrugged/DefaultFailureInterpreter.java index 5f45e4cc..93ff85f3 100644 --- a/jrugged-core/src/main/java/org/fishwife/jrugged/DefaultFailureInterpreter.java +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/DefaultFailureInterpreter.java @@ -21,175 +21,192 @@ import java.util.Set; /** - * Trips a {@link CircuitBreaker} if the number of failures in a given - * time window exceed a specified tolerance. By default, all {@link - * Throwable} occurrences will be considered failures. + * Trips a {@link CircuitBreaker} if the number of failures in a given time + * window exceed a specified tolerance. By default, all {@link Throwable} + * occurrences will be considered failures. */ public final class DefaultFailureInterpreter implements FailureInterpreter { - private Set> ignore = new HashSet>(); - private int limit = 0; - private long windowMillis = 0; - - private WindowedEventCounter counter; - - @SuppressWarnings("unchecked") - private static Class[] defaultIgnore = - new Class[0]; - - /** - * Default constructor. Any {@link Throwable} will cause the breaker to trip. - */ - public DefaultFailureInterpreter() { - setIgnore(defaultIgnore); - } - - /** - * Constructor that allows a tolerance for a certain number of - * failures within a given window of time without tripping. - * @param limit the number of failures that will be tolerated - * (i.e. the number of failures has to be strictly greater - * than this number in order to trip the breaker). For - * example, if the limit is 3, the fourth failure during - * the window will cause the breaker to trip. - * @param windowMillis length of the window in milliseconds - */ - public DefaultFailureInterpreter(int limit, long windowMillis) { - setIgnore(defaultIgnore); - setLimit(limit); - setWindowMillis(windowMillis); - initCounter(); - } - - /** - * 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 Throwable that is a - * subclass of one of these classes will be ignored. - */ - public DefaultFailureInterpreter(Class[] ignore) { - setIgnore(ignore); - } - - /** - * Constructor where we specify tolerance and a set of ignored failures. - * - * @param ignore an array of {@link Throwable} classes that will - * be ignored. Any given Throwable that is a - * subclass of one of these classes will be ignored. - * @param limit the number of failures that will be tolerated - * (i.e. the number of failures has to be strictly greater - * than this number in order to trip the breaker). For - * example, if the limit is 3, the fourth failure during - * the window will cause the breaker to trip. - * @param windowMillis length of the window in milliseconds - */ - public DefaultFailureInterpreter(Class[] ignore, - int limit, long windowMillis) { - setIgnore(ignore); - setLimit(limit); - setWindowMillis(windowMillis); - initCounter(); - } - - private boolean hasWindowConditions() { - return this.limit > 0 && this.windowMillis > 0; - } - - public boolean shouldTrip(Throwable cause) { - for(Class clazz : ignore) { - if (clazz.isInstance(cause)) { - return false; - } - } - - // if Exception is of specified type, and window conditions exist, - // keep circuit open unless exception threshold has passed - if (hasWindowConditions()) { - counter.mark(); - // Trip if the exception count has passed the limit - return (counter.tally() > limit); - } - - return true; - } - - private void initCounter() { - if (hasWindowConditions()) { - int capacity = limit + 1; - if (counter == null) { - this.counter = new WindowedEventCounter(capacity,windowMillis); - } else { - if (capacity != counter.getCapacity()) { - counter.setCapacity(capacity); - } - - if (windowMillis != counter.getWindowMillis()) { - counter.setWindowMillis(windowMillis); - } - } - } else { - // we're not under windowConditions, no counter needed - counter = null; - } - } - - /** - * Returns the set of currently ignored {@link Throwable} classes. - * @return {@link Set} - */ - public Set> getIgnore(){ - return this.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(Class[] ignore) { - this.ignore = new HashSet>(Arrays.asList(ignore)); - } - - /** - * Returns the current number of failures within the window that will be tolerated - * without tripping the breaker. - * @return int - */ - public int getLimit(){ - return this.limit; - } - - /** - * Specifies the number of tolerated failures within the - * configured time window. If limit is set to n then the - * (n+1)th failure will trip the breaker. Mutating the - * limit at runtime can reset previous failure counts. - * @param limit int - */ - public void setLimit(int limit) { - this.limit=limit; - initCounter(); - } - - /** - * Returns the length of the currently configured tolerance window - * in milliseconds. - * @return long - */ - public long getWindowMillis(){ - return this.windowMillis; - } - - /** - * Specifies the length of the tolerance window in milliseconds. - * @param windowMillis long - */ - public void setWindowMillis(long windowMillis) { - this.windowMillis=windowMillis; - initCounter(); - } + private Set> ignore = new HashSet>(); + private int limit = 0; + private long windowMillis = 0; + + private WindowedEventCounter counter; + + @SuppressWarnings("unchecked") + private static Class[] defaultIgnore = new Class[0]; + + /** + * Default constructor. Any {@link Throwable} will cause the breaker to + * trip. + */ + public DefaultFailureInterpreter() { + setIgnore(defaultIgnore); + } + + /** + * Constructor that allows a tolerance for a certain number of failures + * within a given window of time without tripping. + * + * @param limit + * the number of failures that will be tolerated (i.e. the number + * of failures has to be strictly greater + * than this number in order to trip the breaker). For example, if + * the limit is 3, the fourth failure during the window will + * cause the breaker to trip. + * @param windowMillis + * length of the window in milliseconds + */ + public DefaultFailureInterpreter(int limit, long windowMillis) { + setIgnore(defaultIgnore); + setLimit(limit); + setWindowMillis(windowMillis); + initCounter(); + } + + /** + * 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 Throwable that is a subclass of one of + * these classes will be ignored. + */ + public DefaultFailureInterpreter(Class[] ignore) { + setIgnore(ignore); + } + + /** + * Constructor where we specify tolerance and a set of ignored failures. + * + * @param ignore + * an array of {@link Throwable} classes that will be ignored. + * Any given Throwable that is a subclass of one of + * these classes will be ignored. + * @param limit + * the number of failures that will be tolerated (i.e. the number + * of failures has to be strictly greater + * than this number in order to trip the breaker). For example, if + * the limit is 3, the fourth failure during the window will + * cause the breaker to trip. + * @param windowMillis + * length of the window in milliseconds + */ + public DefaultFailureInterpreter(Class[] ignore, + int limit, long windowMillis) { + setIgnore(ignore); + setLimit(limit); + setWindowMillis(windowMillis); + initCounter(); + } + + private boolean hasWindowConditions() { + return this.limit > 0 && this.windowMillis > 0; + } + + public boolean shouldTrip(Throwable cause) { + for (Class clazz : ignore) { + if (clazz.isInstance(cause)) { + return false; + } + } + + // if Exception is of specified type, and window conditions exist, + // keep circuit open unless exception threshold has passed + if (hasWindowConditions()) { + counter.mark(); + // Trip if the exception count has passed the limit + return (counter.tally() > limit); + } + + return true; + } + + private void initCounter() { + if (hasWindowConditions()) { + int capacity = limit + 1; + if (counter == null) { + this.counter = new WindowedEventCounter(capacity, windowMillis); + } else { + if (capacity != counter.getCapacity()) { + counter.setCapacity(capacity); + } + + if (windowMillis != counter.getWindowMillis()) { + counter.setWindowMillis(windowMillis); + } + } + } else { + // we're not under windowConditions, no counter needed + counter = null; + } + } + + /** + * Returns the set of currently ignored {@link Throwable} classes. + * + * @return {@link Set} + */ + public Set> getIgnore() { + return this.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(Class[] ignore) { + this.ignore = new HashSet>( + Arrays.asList(ignore)); + } + + /** + * Returns the current number of failures within the window that will be + * tolerated without tripping the breaker. + * + * @return int + */ + public int getLimit() { + return this.limit; + } + + /** + * Specifies the number of tolerated failures within the configured time + * window. If limit is set to n then the (n+1)th failure + * will trip the breaker. Mutating the limit at runtime can reset previous + * failure counts. + * + * @param limit + * int + */ + public void setLimit(int limit) { + this.limit = limit; + initCounter(); + } + + /** + * Returns the length of the currently configured tolerance window in + * milliseconds. + * + * @return long + */ + public long getWindowMillis() { + return this.windowMillis; + } + + /** + * Specifies the length of the tolerance window in milliseconds. + * + * @param windowMillis + * long + */ + public void setWindowMillis(long windowMillis) { + this.windowMillis = windowMillis; + initCounter(); + } } diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/SkepticBreaker.java b/jrugged-core/src/main/java/org/fishwife/jrugged/SkepticBreaker.java new file mode 100644 index 00000000..8344e99b --- /dev/null +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/SkepticBreaker.java @@ -0,0 +1,471 @@ +/* SkepticBreaker.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.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicLong; + +import org.fishwife.jrugged.Breaker.BreakerState; +import static org.easymock.EasyMock.createMock; + +/** A {@link SkepticBreaker} 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 SkepticBreaker. + *

+ * When everything is operating normally, the SkepticBreaker + * is CLOSED and the calls are allowed through. If the 'good timer' runs out + * while in CLOSED state, Skepticism level decreases. + *

+ * When a call fails, however, the SkepticBreaker "trips" and + * moves to an OPEN state. Skepticism level increases. Client calls are not + * allowed through while the SkepticBreaker is OPEN. + *

+ * After a certain "cooldown" period where calls are not allowed to go through + * but internal sample calls are made, if no bad calls are encountered after the + * cooldown timer runs out (wait time), the SkepticBreaker will + * transition back to a CLOSED state. If bad calls are made, the timer restarts. + *

+ * Sample usage: + *

+    public class Service implements Monitorable {
+        private SkepticBreaker cb = new SkepticBreaker();
+        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(); }
+    }
+ * 
+ */ +public class SkepticBreaker extends Breaker implements MonitoredService, ServiceWrapper { + + /** The time the breaker last switched into good/CLOSED state. */ + protected AtomicLong lastCloseTime = new AtomicLong(System.currentTimeMillis()); + + /** Variables added to determine Wait Timer and Good Timer */ + protected AtomicLong waitBase = new AtomicLong(1000L); + protected AtomicLong waitMult = new AtomicLong(100L); + protected AtomicLong goodBase = new AtomicLong(600000L); + protected AtomicLong goodMult = new AtomicLong(100L); + protected AtomicLong skepticLevel = new AtomicLong(0L); + protected AtomicLong maxLevel = new AtomicLong(20L); + + /** How long the cooldown period is in milliseconds during Wait Phase. */ + protected AtomicLong waitTime = new AtomicLong((long) (waitBase.get() + + waitMult.get() * Math.pow(2, skepticLevel.get()))); + + /** How long the cooldown period is in milliseconds during Good Phase. */ + protected AtomicLong goodTime = new AtomicLong((long) (goodBase.get() + + goodMult.get() * Math.pow(2, skepticLevel.get()))); + + /** The default name if none is provided. */ + private static final String DEFAULT_NAME = "SkepticBreaker"; + + /** The name for the SkepticBreaker. */ + protected String name = DEFAULT_NAME; + + /** Creates a {@link SkepticBreaker} with a {@link + * DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). */ + public SkepticBreaker() { + } + + /** Creates a {@link SkepticBreaker} with a {@link + * DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). + * @param name the name for the {@link SkepticBreaker}. + */ + public SkepticBreaker(String name) { + super(name); + } + + /** Creates a {@link SkepticBreaker} with the specified {@link + * FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public SkepticBreaker(FailureInterpreter fi) { + super(fi); + } + + /** Creates a {@link SkepticBreaker} with the specified {@link + * FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). + * @param name the name for the {@link SkepticBreaker}. + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public SkepticBreaker(String name, FailureInterpreter fi) { + super(name, fi); + } + + /** Creates a {@link SkepticBreaker} with a {@link + * DefaultFailureInterpreter} and using the supplied {@link + * BreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link SkepticBreaker}. + * @param mapper helper used to translate a {@link + * BreakerException} into an application-specific one */ + public SkepticBreaker(String name, BreakerExceptionMapper mapper) { + super(name, mapper); + } + + /** Creates a {@link SkepticBreaker} with the provided {@link + * FailureInterpreter} and using the provided {@link + * BreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link SkepticBreaker}. + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + * @param mapper helper used to translate a {@link + * BreakerException} into an application-specific one */ + public SkepticBreaker(String name, + FailureInterpreter fi, + BreakerExceptionMapper mapper) { + super(name, fi, mapper); + } + + /** Wrap the given service call with the {@link SkepticBreaker} + * protection logic. + * @param c the {@link Callable} to attempt + * @return whatever c would return on success + * @throws BreakerException if the + * breaker was OPEN and this attempt wasn't the + * reset attempt + * @throws Exception if c throws one during + * execution + */ + public V invoke(Callable c) throws Exception { + if (!byPass) { + if (!allowRequest()) { + if (!isHardTrip) { + try { + Callable mockCallable = createMock(Callable.class); + mockCallable.call(); + } + catch (Throwable cause) { + handleFailure(cause); + } + } + throw mapException(new BreakerException()); + } + + try { + 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 SkepticBreaker} + * protection logic. + * @param r the {@link Runnable} to attempt + * @throws BreakerException if the + * breaker was OPEN and this attempt wasn't the + * reset attempt + * @throws Exception if c throws one during + * execution + */ + public void invoke(Runnable r) throws Exception { + if (!byPass) { + if (!allowRequest()) { + if(!isHardTrip){ + try{ + Runnable mockRunnable = createMock(Runnable.class); + mockRunnable.run(); + } + catch (Throwable cause){ + handleFailure(cause); + } + } + throw mapException(new BreakerException()); + } + + try { + 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 SkepticBreaker} + * protection logic. + * @param r the {@link Runnable} to attempt + * @param result what to return after r succeeds + * @return result + * @throws BreakerException if the + * breaker was OPEN and this attempt wasn't the + * reset attempt + * @throws Exception if c throws one during + * execution + */ + public V invoke(Runnable r, V result) throws Exception { + if (!byPass) { + if (!allowRequest()) { + if(!isHardTrip){ + try{ + Runnable mockRunnable = createMock(Runnable.class); + mockRunnable.run(); + } + catch (Throwable cause){ + handleFailure(cause); + } + } + throw mapException(new BreakerException()); + } + + try { + r.run(); + close(); + return result; + } catch (Throwable cause) { + handleFailure(cause); + } + throw new IllegalStateException("not possible"); + } + else { + r.run(); + return result; + } + } + + /** + * Causes the {@link SkepticBreaker} to trip and OPEN; no new + * requests will be allowed until the SkepticBreaker + * resets. Skeptic level increases and timers are updated accordingly. + */ + public void trip() { + if (state != BreakerState.OPEN) { + openCount.getAndIncrement(); + } + if (skepticLevel.get() < maxLevel.get()) { + increaseSkepticLevel(); + } + state = BreakerState.OPEN; + lastFailure.set(System.currentTimeMillis()); + + notifyBreakerStateChange(getStatus()); + } + + /** + * Increments the skeptic level and updates timers; occurs when the + * {@link SkepticBreaker} trips and moves from CLOSED to OPEN + */ + public void increaseSkepticLevel() { + skepticLevel.set(skepticLevel.incrementAndGet()); + updateTimers(); + } + + /** + * Updates wait timer and good timer based on current skeptic level. + */ + public void updateTimers() { + waitTime.set((long) (waitBase.get() + waitMult.get() * Math.pow(2, skepticLevel.get()))); + goodTime.set((long) (goodBase.get() + goodMult.get() * Math.pow(2, skepticLevel.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; + skepticLevel.set(0); + updateTimers(); + + notifyBreakerStateChange(getStatus()); + } + + /** + * Get the current {@link ServiceStatus} of the + * {@link SkepticBreaker}, including the name, + * {@link org.fishwife.jrugged.Status}, and reason. + * @return the {@link ServiceStatus}. + */ + public ServiceStatus getServiceStatus() { + if (byPass) { + return new ServiceStatus(name, Status.DEGRADED, "Bypassed"); + } + + return (state == BreakerState.CLOSED || (lastFailure.get() > 0 + && moveToClosed() && !isHardTrip) ? + new ServiceStatus(name, Status.UP) : + new ServiceStatus(name, Status.DOWN, "Open")); + } + + protected void handleFailure(Throwable cause) throws Exception { + if (failureInterpreter == null || failureInterpreter.shouldTrip(cause)) { + if (state == BreakerState.CLOSED) { + this.tripException = cause; + trip();// also updates skeptic level + } + else { + //update last failure - this is the case where we send 'ping' that fails + lastFailure.set(System.currentTimeMillis()); + } + } + + 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 SkepticBreaker}, + * putting the SkepticBreaker back into the CLOSED + * state serving requests. + */ + protected void close() { + state = BreakerState.CLOSED; + notifyBreakerStateChange(getStatus()); + } + + /** + * @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) { + decreaseSkepticLevel(); + return true; + } + + return moveToClosed(); + } + + private boolean moveToClosed() { + if (BreakerState.OPEN == state && + System.currentTimeMillis() - lastFailure.get() >= waitTime.get()) { + + state = BreakerState.CLOSED; + notifyBreakerStateChange(getStatus()); + + lastCloseTime.set(lastFailure.get() + waitTime.get()); + decreaseSkepticLevel(); + + return true; + } + + return false; + } + + /** + * Checks to see if SkepticLevel needs to be updated (if Good Timer has + * expired). If necessary, decrements the skeptic level and updates timers; + * occurs when the {@link SkepticBreaker} is in the CLOSED state + */ + protected void decreaseSkepticLevel() { + long currTime = System.currentTimeMillis(); + if (currTime - lastCloseTime.get() >= goodTime.get()) { + if(skepticLevel.get() > 0){ + skepticLevel.set(skepticLevel.decrementAndGet()); + lastCloseTime.set(goodTime.get() + lastCloseTime.get()); + updateTimers(); + decreaseSkepticLevel(); + } + } + } + + public long getWaitBase() { + return waitBase.get(); + } + + public void setWaitBase(long wBase) { + waitBase.set(wBase); + } + + public long getWaitMult() { + return waitMult.get(); + } + + public void setWaitMult(long wMult) { + waitMult.set(wMult); + } + + public long getGoodBase() { + return goodBase.get(); + } + + public void setGoodBase(long gBase) { + goodBase.set(gBase); + } + + public long getGoodMult() { + return goodMult.get(); + } + + public void setGoodMult(long gMult) { + goodMult.set(gMult); + } + + public long getMaxLevel() { + return maxLevel.get(); + } + + public void setMaxLevel(long mLevel) { + maxLevel.set(mLevel); + } + + public long getSkepticLevel() { + return skepticLevel.get(); + } + + public long getWaitTime() { + return waitTime.get(); + } + + public void setWaitTime(long l) { + waitTime.set(l); + } + + public long getGoodTime() { + return goodTime.get(); + } +} diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/SkepticBreakerConfig.java b/jrugged-core/src/main/java/org/fishwife/jrugged/SkepticBreakerConfig.java new file mode 100644 index 00000000..61ace818 --- /dev/null +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/SkepticBreakerConfig.java @@ -0,0 +1,69 @@ +/* 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; + +/** + * The SkepticBreakerConfig class holds a + * {@link org.fishwife.jrugged.SkepticBreaker} configuration. + */ +public class SkepticBreakerConfig implements BreakerConfig { + + private FailureInterpreter failureInterpreter; + + private long waitMult; + private long waitBase; + + private long goodMult; + private long goodBase; + + private long maxLevel; + + public SkepticBreakerConfig(long goodBase, long goodMult, long waitBase, long waitMult, + long maxLevel, FailureInterpreter failureInterpreter) { + + //ASSUMPTION: start level = 0 + this.waitMult = waitMult; + this.waitBase = waitBase; + this.goodMult = goodMult; + this.goodBase = goodBase; + this.maxLevel = maxLevel; + + this.failureInterpreter = failureInterpreter; + } + + public FailureInterpreter getFailureInterpreter() { + return failureInterpreter; + } + + public long getWaitMult() { + return waitMult; + } + + public long getWaitBase() { + return waitBase; + } + + public long getGoodMult() { + return goodMult; + } + + public long getGoodBase() { + return goodBase; + } + + public long getMaxLevel() { + return maxLevel; + } +} diff --git a/jrugged-core/src/test/java/org/fishwife/jrugged/TestBreakerFactory.java b/jrugged-core/src/test/java/org/fishwife/jrugged/TestBreakerFactory.java new file mode 100644 index 00000000..172875f3 --- /dev/null +++ b/jrugged-core/src/test/java/org/fishwife/jrugged/TestBreakerFactory.java @@ -0,0 +1,344 @@ +/* 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 org.junit.Before; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertSame; + +public class TestBreakerFactory { + + private BreakerFactory factory; + private CircuitBreakerConfig circuitBreakerConfig; + private SkepticBreakerConfig skepticBreakerConfig; + + private static final int TEST_LIMIT = 5; + private static final long TEST_WINDOW_MILLIS = 30000L; + private static final long TEST_RESET_MILLIS = 10000L; + + private static final long TEST_WAIT_BASE = 2000L; + private static final long TEST_WAIT_MULT = 200L; + private static final long TEST_GOOD_BASE = 300000L; + private static final long TEST_GOOD_MULT = 50L; + private static final long TEST_MAX_LEVEL = 20L; + + @Before + public void setUp() { + factory = new BreakerFactory(); + circuitBreakerConfig = new CircuitBreakerConfig(TEST_RESET_MILLIS, + new DefaultFailureInterpreter(TEST_LIMIT, TEST_WINDOW_MILLIS)); + skepticBreakerConfig = new SkepticBreakerConfig(TEST_GOOD_BASE, + TEST_GOOD_MULT, TEST_WAIT_BASE, TEST_WAIT_MULT, TEST_MAX_LEVEL, + new DefaultFailureInterpreter(TEST_LIMIT, TEST_WINDOW_MILLIS)); + } + + @Test + public void testCreateCircuitBreaker() { + CircuitBreaker breaker = factory.createCircuitBreaker("testCreate", circuitBreakerConfig); + checkCircuitBreaker(breaker, TEST_LIMIT, TEST_WINDOW_MILLIS, TEST_RESET_MILLIS); + } + + @Test + public void testCreateSkepticBreaker() { + SkepticBreaker breaker = factory.createSkepticBreaker("testCreate", skepticBreakerConfig); + checkSkepticBreaker(breaker, TEST_LIMIT, TEST_WINDOW_MILLIS, TEST_GOOD_BASE, + TEST_GOOD_MULT, TEST_WAIT_BASE, TEST_WAIT_MULT, TEST_MAX_LEVEL); + } + + @Test + public void testCreateDuplicateCircuitBreaker() { + String name = "testCreate"; + CircuitBreaker createdBreaker = factory.createCircuitBreaker(name, circuitBreakerConfig); + CircuitBreaker secondBreaker = factory.createCircuitBreaker(name, circuitBreakerConfig); + + assertSame(createdBreaker, secondBreaker); + checkCircuitBreaker(createdBreaker, TEST_LIMIT, TEST_WINDOW_MILLIS, TEST_RESET_MILLIS); + } + + @Test + public void testCreateDuplicateSkepticBreaker() { + String name = "testCreate"; + SkepticBreaker createdBreaker = factory.createSkepticBreaker(name, skepticBreakerConfig); + SkepticBreaker secondBreaker = factory.createSkepticBreaker(name, skepticBreakerConfig); + + assertSame(createdBreaker, secondBreaker); + checkSkepticBreaker(createdBreaker, TEST_LIMIT, TEST_WINDOW_MILLIS, TEST_GOOD_BASE, + TEST_GOOD_MULT, TEST_WAIT_BASE, TEST_WAIT_MULT, TEST_MAX_LEVEL); + } + + @Test + public void testCreateCircuitBreakerEmptyConfig() { + CircuitBreakerConfig emptyConfig = + new CircuitBreakerConfig(-1, new DefaultFailureInterpreter()); + CircuitBreaker breaker = factory.createCircuitBreaker("testCreateEmpty", emptyConfig); + + checkCircuitBreaker(breaker, 0, 0, 15000L); // These are the CircuitBreaker Defaults. + } + + @Test + public void testCreateSkepticBreakerEmptyConfig() { + SkepticBreakerConfig emptyConfig = + new SkepticBreakerConfig(-1, -1, -1, -1, -1, + new DefaultFailureInterpreter()); + SkepticBreaker breaker = factory.createSkepticBreaker("testCreateEmpty", emptyConfig); + + checkSkepticBreaker(breaker, 0, 0, 600000L, + 100L, 1000L, 100L, 20L); // These are the SkepticBreaker Defaults. + } + + @Test + public void testCreateCircuitBreakerNullFailureInterpreter() { + CircuitBreakerConfig emptyConfig = + new CircuitBreakerConfig(-1, null); + CircuitBreaker breaker = factory.createCircuitBreaker("testCreateEmpty", emptyConfig); + + checkCircuitBreakerNoFailureInterpreter(breaker, 15000L); // These are the CircuitBreaker Defaults. + } + + @Test + public void testCreateSkepticBreakerNullFailureInterpreter() { + SkepticBreakerConfig emptyConfig = + new SkepticBreakerConfig(-1, -1, -1, -1, -1, null); + SkepticBreaker breaker = factory.createSkepticBreaker("testCreateEmpty", emptyConfig); + + checkSkepticBreakerNoFailureInterpreter(breaker, 600000L, + 100L, 1000L, 100L, 20L); // These are the SkepticBreaker Defaults. + } + + @Test + public void testFindANamedCircuitBreaker() { + String monitorName = "testFind"; + CircuitBreaker createdBreaker = factory.createCircuitBreaker(monitorName, circuitBreakerConfig); + CircuitBreaker foundBreaker = factory.findCircuitBreaker(monitorName); + assertEquals(createdBreaker, foundBreaker); + } + + @Test + public void testFindANamedSkepticBreaker() { + String monitorName = "testFind"; + SkepticBreaker createdBreaker = factory.createSkepticBreaker(monitorName, skepticBreakerConfig); + SkepticBreaker foundBreaker = factory.findSkepticBreaker(monitorName); + assertEquals(createdBreaker, foundBreaker); + } + + @Test + public void testFindNonExistentCircuitBreaker() { + CircuitBreaker foundBreaker = factory.findCircuitBreaker("testNonExistent"); + assertNull(foundBreaker); + } + + @Test + public void testFindNonExistentSkepticBreaker() { + SkepticBreaker foundBreaker = factory.findSkepticBreaker("testNonExistent"); + assertNull(foundBreaker); + } + + @Test + public void testGetCircuitBreakerNames() { + Set testSet = new HashSet(); + testSet.add("one"); + testSet.add("two"); + testSet.add("three"); + testSet.add("four"); + + factory.createCircuitBreaker("one", circuitBreakerConfig); + factory.createCircuitBreaker("two", circuitBreakerConfig); + factory.createCircuitBreaker("three", circuitBreakerConfig); + factory.createCircuitBreaker("four", circuitBreakerConfig); + Set monitorNames = factory.getCircuitBreakerNames(); + assertEquals(testSet, monitorNames); + } + + @Test + public void testGetSkepticBreakerNames() { + Set testSet = new HashSet(); + testSet.add("one"); + testSet.add("two"); + testSet.add("three"); + testSet.add("four"); + + factory.createSkepticBreaker("one", skepticBreakerConfig); + factory.createSkepticBreaker("two", skepticBreakerConfig); + factory.createSkepticBreaker("three", skepticBreakerConfig); + factory.createSkepticBreaker("four", skepticBreakerConfig); + Set monitorNames = factory.getSkepticBreakerNames(); + assertEquals(testSet, monitorNames); + } + + @Test + public void testEmptyCircuitBreakerPropertyOverrides() { + Properties overrideProperties = new Properties(); + factory.setCircuitBreakerProperties(overrideProperties); + CircuitBreaker breaker = factory.createCircuitBreaker("emptyOverrides", circuitBreakerConfig); + checkCircuitBreaker(breaker, TEST_LIMIT, TEST_WINDOW_MILLIS, TEST_RESET_MILLIS); + } + + @Test + public void testEmptySkepticBreakerPropertyOverrides() { + Properties overrideProperties = new Properties(); + factory.setSkepticBreakerProperties(overrideProperties); + SkepticBreaker breaker = factory.createSkepticBreaker("emptyOverrides", skepticBreakerConfig); + checkSkepticBreaker(breaker, TEST_LIMIT, TEST_WINDOW_MILLIS, TEST_GOOD_BASE, + TEST_GOOD_MULT, TEST_WAIT_BASE, TEST_WAIT_MULT, TEST_MAX_LEVEL); + } + + @Test + public void testCircuitBreakerPropertyOverrides() { + Properties overrideProperties = new Properties(); + Integer overrideLimit = 10; + Long overrideResetMillis = 50000L; + Long overrideWindowMillis = 500000L; + String name = "testOverrides"; + + overrideProperties.put("circuit." + name + ".limit", overrideLimit.toString()); + overrideProperties.put("circuit." + name + ".resetMillis", overrideResetMillis.toString()); + overrideProperties.put("circuit." + name + ".windowMillis", overrideWindowMillis.toString()); + factory.setCircuitBreakerProperties(overrideProperties); + + CircuitBreaker breaker = factory.createCircuitBreaker(name, circuitBreakerConfig); + checkCircuitBreaker(breaker, overrideLimit, overrideWindowMillis, overrideResetMillis); + } + + @Test + public void testSkepticBreakerPropertyOverrides() { + Properties overrideProperties = new Properties(); + Integer overrideLimit = 10; + Long overrideWindowMillis = 500000L; + Long overrideGoodBase = 50000L; + Long overrideGoodMult = 500000L; + Long overrideWaitBase = 50000L; + Long overrideWaitMult = 500000L; + Long overrideMaxLevel = 25L; + String name = "testOverrides"; + + overrideProperties.put("skeptic." + name + ".limit", overrideLimit.toString()); + overrideProperties.put("skeptic." + name + ".windowMillis", overrideWindowMillis.toString()); + overrideProperties.put("skeptic." + name + ".goodBase", overrideGoodBase.toString()); + overrideProperties.put("skeptic." + name + ".goodMult", overrideGoodMult.toString()); + overrideProperties.put("skeptic." + name + ".waitBase", overrideWaitBase.toString()); + overrideProperties.put("skeptic." + name + ".waitMult", overrideWaitMult.toString()); + overrideProperties.put("skeptic." + name + ".maxLevel", overrideMaxLevel.toString()); + factory.setSkepticBreakerProperties(overrideProperties); + + SkepticBreaker breaker = factory.createSkepticBreaker(name, skepticBreakerConfig); + checkSkepticBreaker(breaker, overrideLimit, overrideWindowMillis, overrideGoodBase, + overrideGoodMult, overrideWaitBase, overrideWaitMult, overrideMaxLevel); + } + + @Test + public void testInvalidCircuitBreakerPropertyOverrides() { + Properties overrideProperties = new Properties(); + String name = "testOverrides"; + + overrideProperties.put("circuit." + name + ".limit", "badLimit"); + overrideProperties.put("circuit." + name + ".resetMillis", "badResetMillis"); + overrideProperties.put("circuit." + name + ".windowMillis", "badWindowMillis"); + factory.setCircuitBreakerProperties(overrideProperties); + + CircuitBreakerConfig emptyConfig = + new CircuitBreakerConfig(-1, new DefaultFailureInterpreter()); + CircuitBreaker breaker = factory.createCircuitBreaker(name, emptyConfig); + checkCircuitBreaker(breaker, 0, 0L, 15000L); // These are the CircuitBreaker defaults. + assertNotNull(breaker); + } + + @Test + public void testInvalidSkepticBreakerPropertyOverrides() { + Properties overrideProperties = new Properties(); + String name = "testOverrides"; + + overrideProperties.put("skeptic." + name + ".limit", "badLimit"); + overrideProperties.put("skeptic." + name + ".windowMillis", "badWindowMillis"); + overrideProperties.put("skeptic." + name + ".goodBase", "badGoodBase"); + overrideProperties.put("skeptic." + name + ".goodMult", "badGoodMult"); + overrideProperties.put("skeptic." + name + ".waitBase", "badWaitBase"); + overrideProperties.put("skeptic." + name + ".waitMult", "badWaitMult"); + overrideProperties.put("skeptic." + name + ".maxLevel", "badMaxLevel"); + factory.setSkepticBreakerProperties(overrideProperties); + + SkepticBreakerConfig emptyConfig = + new SkepticBreakerConfig(-1, -1, -1, -1, -1, + new DefaultFailureInterpreter()); + SkepticBreaker breaker = factory.createSkepticBreaker(name, emptyConfig); + checkSkepticBreaker(breaker, 0, 0, 600000L, + 100L, 1000L, 100L, 20L); // These are the SkepticBreaker Defaults. + assertNotNull(breaker); + } + + private void checkCircuitBreaker(CircuitBreaker breaker, + int expectedLimit, + long expectedWindowMillis, + long expectedResetMillis) { + + assertNotNull(breaker); + + DefaultFailureInterpreter failureInterpreter = (DefaultFailureInterpreter)breaker.getFailureInterpreter(); + + if (failureInterpreter != null) { + assertEquals(failureInterpreter.getLimit(), expectedLimit); + assertEquals(failureInterpreter.getWindowMillis(), expectedWindowMillis); + } + + assertEquals(breaker.getResetMillis(), expectedResetMillis); + } + + private void checkSkepticBreaker(SkepticBreaker breaker, int expectedLimit, + long expectedWindowMillis, long expectedGoodBase, long expectedGoodMult, + long expectedWaitBase, long expectedWaitMult, long expectedMaxLevel) { + + assertNotNull(breaker); + + DefaultFailureInterpreter failureInterpreter = (DefaultFailureInterpreter)breaker.getFailureInterpreter(); + + if (failureInterpreter != null) { + assertEquals(failureInterpreter.getLimit(), expectedLimit); + assertEquals(failureInterpreter.getWindowMillis(), expectedWindowMillis); + } + + assertEquals(breaker.getGoodBase(), expectedGoodBase); + assertEquals(breaker.getGoodMult(), expectedGoodMult); + assertEquals(breaker.getWaitBase(), expectedWaitBase); + assertEquals(breaker.getWaitMult(), expectedWaitMult); + assertEquals(breaker.getMaxLevel(), expectedMaxLevel); + } + + private void checkCircuitBreakerNoFailureInterpreter(CircuitBreaker breaker, + long expectedResetMillis) { + + assertNotNull(breaker); + assertEquals(breaker.getResetMillis(), expectedResetMillis); + } + + private void checkSkepticBreakerNoFailureInterpreter(SkepticBreaker breaker, + long expectedGoodBase, long expectedGoodMult, long expectedWaitBase, + long expectedWaitMult, long expectedMaxLevel) { + assertNotNull(breaker); + + assertEquals(breaker.getGoodBase(), expectedGoodBase); + assertEquals(breaker.getGoodMult(), expectedGoodMult); + assertEquals(breaker.getWaitBase(), expectedWaitBase); + assertEquals(breaker.getWaitMult(), expectedWaitMult); + assertEquals(breaker.getMaxLevel(), expectedMaxLevel); + } +} \ No newline at end of file diff --git a/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreaker.java b/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreaker.java index bcf3dbc0..24a00a32 100644 --- a/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreaker.java +++ b/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreaker.java @@ -76,7 +76,7 @@ public void testInvokeWithRunnableResultAndByPassReturnsResult() throws Exceptio assertSame(result, theReturned); } - @Test(expected = CircuitBreakerException.class) + @Test(expected = BreakerException.class) public void testInvokeWithRunnableResultAndTripHardReturnsException() throws Exception { final Object result = new Object(); impl.tripHard(); @@ -111,7 +111,7 @@ public void testInvokeWithRunnableAndByPassDoesNotError() throws Exception { verify(mockRunnable); } - @Test(expected = CircuitBreakerException.class) + @Test(expected = BreakerException.class) public void testInvokeWithRunnableAndTripHardReturnsException() throws Exception { impl.tripHard(); @@ -169,7 +169,7 @@ public void testOpenDuringCooldownThrowsCBException() try { impl.invoke(mockCallable); fail("should have thrown an exception"); - } catch (CircuitBreakerException expected) { + } catch (BreakerException expected) { } verify(mockCallable); @@ -241,7 +241,7 @@ public void testManualTripAndReset() throws Exception { try { impl.invoke(mockCallable); fail("Manual trip method failed."); - } catch (CircuitBreakerException e) { + } catch (BreakerException e) { } impl.reset(); @@ -263,7 +263,7 @@ public void testTripHard() throws Exception { try { impl.invoke(mockCallable); fail("exception expected after CircuitBreaker.tripHard()"); - } catch (CircuitBreakerException e) { + } catch (BreakerException e) { } assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); @@ -354,7 +354,7 @@ public void testByPassIgnoresBreakerStateAndCallsWrappedMethod() throws Exceptio try { impl.invoke(mockCallable); - } catch (CircuitBreakerException e) { + } catch (BreakerException e) { fail("exception not expected when CircuitBreaker is bypassed."); } assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); @@ -371,7 +371,7 @@ public void testByPassIgnoresBreakerStateAndCallsWrappedMethod() throws Exceptio @Test public void testNotificationCallback() throws Exception { - CircuitBreakerNotificationCallback cb = new CircuitBreakerNotificationCallback() { + BreakerNotificationCallback cb = new BreakerNotificationCallback() { public void notify(Status s) { theStatus = s; } diff --git a/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreakerFactory.java b/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreakerFactory.java deleted file mode 100644 index 97fa4f2e..00000000 --- a/jrugged-core/src/test/java/org/fishwife/jrugged/TestCircuitBreakerFactory.java +++ /dev/null @@ -1,174 +0,0 @@ -/* 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 org.junit.Before; -import org.junit.Test; - -import java.util.HashSet; -import java.util.Properties; -import java.util.Set; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertSame; - -public class TestCircuitBreakerFactory { - - private CircuitBreakerFactory factory; - private CircuitBreakerConfig config; - - private static final int TEST_LIMIT = 5; - private static final long TEST_WINDOW_MILLIS = 30000L; - private static final long TEST_RESET_MILLIS = 10000L; - - @Before - public void setUp() { - factory = new CircuitBreakerFactory(); - config = new CircuitBreakerConfig(TEST_RESET_MILLIS, - new DefaultFailureInterpreter(TEST_LIMIT, TEST_WINDOW_MILLIS)); - } - - @Test - public void testCreateCircuitBreaker() { - CircuitBreaker breaker = factory.createCircuitBreaker("testCreate", config); - checkBreaker(breaker, TEST_LIMIT, TEST_WINDOW_MILLIS, TEST_RESET_MILLIS); - } - - @Test - public void testCreateDuplicateCircuitBreaker() { - String name = "testCreate"; - CircuitBreaker createdBreaker = factory.createCircuitBreaker(name, config); - CircuitBreaker secondBreaker = factory.createCircuitBreaker(name, config); - - assertSame(createdBreaker, secondBreaker); - checkBreaker(createdBreaker, TEST_LIMIT, TEST_WINDOW_MILLIS, TEST_RESET_MILLIS); - } - - @Test - public void testCreateCircuitBreakerEmptyConfig() { - CircuitBreakerConfig emptyConfig = - new CircuitBreakerConfig(-1, new DefaultFailureInterpreter()); - CircuitBreaker breaker = factory.createCircuitBreaker("testCreateEmpty", emptyConfig); - - checkBreaker(breaker, 0, 0, 15000L); // These are the CircuitBreaker Defaults. - } - - @Test - public void testCreateCircuitBreakerNullFailureInterpreter() { - CircuitBreakerConfig emptyConfig = - new CircuitBreakerConfig(-1, null); - CircuitBreaker breaker = factory.createCircuitBreaker("testCreateEmpty", emptyConfig); - - checkBreakerNoFailureInterpreter(breaker, 15000L); // These are the CircuitBreaker Defaults. - } - - @Test - public void testFindANamedCircuitBreaker() { - String monitorName = "testFind"; - CircuitBreaker createdBreaker = factory.createCircuitBreaker(monitorName, config); - CircuitBreaker foundBreaker = factory.findCircuitBreaker(monitorName); - assertEquals(createdBreaker, foundBreaker); - } - - @Test - public void testFindNonExistentCircuitBreaker() { - CircuitBreaker foundBreaker = factory.findCircuitBreaker("testNonExistent"); - assertNull(foundBreaker); - } - - @Test - public void testGetCircuitBreakerNames() { - Set testSet = new HashSet(); - testSet.add("one"); - testSet.add("two"); - testSet.add("three"); - testSet.add("four"); - - factory.createCircuitBreaker("one", config); - factory.createCircuitBreaker("two", config); - factory.createCircuitBreaker("three", config); - factory.createCircuitBreaker("four", config); - Set monitorNames = factory.getCircuitBreakerNames(); - assertEquals(testSet, monitorNames); - } - - @Test - public void testEmptyPropertyOverrides() { - Properties overrideProperties = new Properties(); - factory.setProperties(overrideProperties); - CircuitBreaker breaker = factory.createCircuitBreaker("emptyOverrides", config); - checkBreaker(breaker, TEST_LIMIT, TEST_WINDOW_MILLIS, TEST_RESET_MILLIS); - } - - @Test - public void testPropertyOverrides() { - Properties overrideProperties = new Properties(); - Integer overrideLimit = 10; - Long overrideResetMillis = 50000L; - Long overrideWindowMillis = 500000L; - String name = "testOverrides"; - - overrideProperties.put("circuit." + name + ".limit", overrideLimit.toString()); - overrideProperties.put("circuit." + name + ".resetMillis", overrideResetMillis.toString()); - overrideProperties.put("circuit." + name + ".windowMillis", overrideWindowMillis.toString()); - factory.setProperties(overrideProperties); - - CircuitBreaker breaker = factory.createCircuitBreaker(name, config); - checkBreaker(breaker, overrideLimit, overrideWindowMillis, overrideResetMillis); - } - - @Test - public void testInvalidPropertyOverrides() { - Properties overrideProperties = new Properties(); - String name = "testOverrides"; - - overrideProperties.put("circuit." + name + ".limit", "badLimit"); - overrideProperties.put("circuit." + name + ".resetMillis", "badResetMillis"); - overrideProperties.put("circuit." + name + ".windowMillis", "badWindowMillis"); - factory.setProperties(overrideProperties); - - CircuitBreakerConfig emptyConfig = - new CircuitBreakerConfig(-1, new DefaultFailureInterpreter()); - CircuitBreaker breaker = factory.createCircuitBreaker(name, emptyConfig); - checkBreaker(breaker, 0, 0L, 15000L); // These are the CircuitBreaker defaults. - assertNotNull(breaker); - } - - private void checkBreaker(CircuitBreaker breaker, - int expectedLimit, - long expectedWindowMillis, - long expectedResetMillis) { - - assertNotNull(breaker); - - DefaultFailureInterpreter failureInterpreter = (DefaultFailureInterpreter)breaker.getFailureInterpreter(); - - if (failureInterpreter != null) { - assertEquals(failureInterpreter.getLimit(), expectedLimit); - assertEquals(failureInterpreter.getWindowMillis(), expectedWindowMillis); - } - - assertEquals(breaker.getResetMillis(), expectedResetMillis); - } - - private void checkBreakerNoFailureInterpreter(CircuitBreaker breaker, - long expectedResetMillis) { - - assertNotNull(breaker); - assertEquals(breaker.getResetMillis(), expectedResetMillis); - } -} diff --git a/jrugged-core/src/test/java/org/fishwife/jrugged/TestSkepticBreaker.java b/jrugged-core/src/test/java/org/fishwife/jrugged/TestSkepticBreaker.java new file mode 100644 index 00000000..a94dafe7 --- /dev/null +++ b/jrugged-core/src/test/java/org/fishwife/jrugged/TestSkepticBreaker.java @@ -0,0 +1,543 @@ +/* TestSkepticBreaker.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.util.concurrent.Callable; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertNull; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class TestSkepticBreaker { + private SkepticBreaker impl; + private Callable mockCallable; + private Runnable mockRunnable; + + Status theStatus; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + impl = new SkepticBreaker(); + mockCallable = createMock(Callable.class); + mockRunnable = createMock(Runnable.class); + } + + @Test + public void testInvokeWithRunnableResultAndResultReturnsResult() throws Exception { + final Object result = new Object(); + + mockRunnable.run(); + replay(mockRunnable); + + Object theReturned = impl.invoke(mockRunnable, result); + + verify(mockRunnable); + assertSame(result, theReturned); + } + + @Test + public void testInvokeWithRunnableResultAndByPassReturnsResult() throws Exception { + final Object result = new Object(); + impl.setByPassState(true); + + mockRunnable.run(); + replay(mockRunnable); + + Object theReturned = impl.invoke(mockRunnable, result); + + verify(mockRunnable); + assertSame(result, theReturned); + } + + @Test(expected = BreakerException.class) + public void testInvokeWithRunnableResultAndTripHardReturnsException() throws Exception { + final Object result = new Object(); + impl.tripHard(); + + mockRunnable.run(); + replay(mockRunnable); + + impl.invoke(mockRunnable, result); + + verify(mockRunnable); + } + + @Test + public void testInvokeWithRunnableDoesNotError() throws Exception { + mockRunnable.run(); + replay(mockRunnable); + + impl.invoke(mockRunnable); + + verify(mockRunnable); + } + + @Test + public void testInvokeWithRunnableAndByPassDoesNotError() throws Exception { + impl.setByPassState(true); + + mockRunnable.run(); + replay(mockRunnable); + + impl.invoke(mockRunnable); + + verify(mockRunnable); + } + + @Test(expected = BreakerException.class) + public void testInvokeWithRunnableAndTripHardReturnsException() throws Exception { + impl.tripHard(); + + mockRunnable.run(); + replay(mockRunnable); + + impl.invoke(mockRunnable); + + verify(mockRunnable); + } + + @Test + public void testStaysClosedOnSuccess() throws Exception { + impl.state = SkepticBreaker.BreakerState.CLOSED; + final Object obj = new Object(); + expect(mockCallable.call()).andReturn(obj); + replay(mockCallable); + + Object result = impl.invoke(mockCallable); + + verify(mockCallable); + assertSame(obj, result); + assertEquals(SkepticBreaker.BreakerState.CLOSED, impl.state); + } + + @Test + //Add check on timers here as well + public void testOpensOnFailure() throws Exception { + long start = System.currentTimeMillis(); + impl.state = SkepticBreaker.BreakerState.OPEN; + expect(mockCallable.call()).andThrow(new RuntimeException()); + replay(mockCallable); + + try { + impl.invoke(mockCallable); + fail("should have thrown an exception"); + } catch (RuntimeException expected) { + } + + long end = System.currentTimeMillis(); + + verify(mockCallable); + assertEquals(SkepticBreaker.BreakerState.OPEN, impl.state); + assertTrue(impl.lastFailure.get() >= start); + assertTrue(impl.lastFailure.get() <= end); + } + + @Test + public void testOpenDuringCooldownThrowsCBException() + throws Exception { + + impl.state = SkepticBreaker.BreakerState.OPEN; + impl.lastFailure.set(System.currentTimeMillis()); + replay(mockCallable); + + try { + impl.invoke(mockCallable); + fail("should have thrown an exception"); + } catch (BreakerException expected) { + } + + verify(mockCallable); + assertEquals(SkepticBreaker.BreakerState.OPEN, impl.state); + } + + @Test + public void testClosedAfterWaitTimeExpires() //testOpenAfterCooldownGoesClosed() + throws Exception { + impl.state = SkepticBreaker.BreakerState.OPEN; + impl.waitTime.set(1000);// waitTime is 1100 by default. set to 1000 in case default changes + impl.lastFailure.set(System.currentTimeMillis() - 2000); + + assertEquals(Status.UP, impl.getStatus()); + assertEquals(SkepticBreaker.BreakerState.CLOSED, impl.state); + } + + @Test + public void testOpenFailureOpensAgain() + throws Exception { + + impl.state = SkepticBreaker.BreakerState.OPEN; + impl.waitTime.set(1000); + impl.lastFailure.set(System.currentTimeMillis() - 2000); + + long start = System.currentTimeMillis(); + + expect(mockCallable.call()).andThrow(new RuntimeException()); + replay(mockCallable); + + try { + impl.invoke(mockCallable); + fail("should have thrown exception"); + } catch (RuntimeException expected) { + } + + long end = System.currentTimeMillis(); + + verify(mockCallable); + assertEquals(SkepticBreaker.BreakerState.OPEN, impl.state); + assertTrue(impl.lastFailure.get() >= start); + assertTrue(impl.lastFailure.get() <= end); + } + + @Test + public void testManualTripAndReset() throws Exception { + impl.state = SkepticBreaker.BreakerState.OPEN; + final Object obj = new Object(); + expect(mockCallable.call()).andReturn(obj); + replay(mockCallable); + + impl.trip(); + try { + impl.invoke(mockCallable); + fail("Manual trip method failed."); + } catch (BreakerException e) { + } + + impl.reset(); + + Object result = impl.invoke(mockCallable); + + verify(mockCallable); + assertSame(obj, result); + assertEquals(SkepticBreaker.BreakerState.CLOSED, impl.state); + } + + @Test + public void testTripHard() throws Exception { + expect(mockCallable.call()).andReturn("hi"); + + replay(mockCallable); + + impl.tripHard(); + try { + impl.invoke(mockCallable); + fail("exception expected after SkepticBreaker.tripHard()"); + } catch (BreakerException e) { + } + assertEquals(SkepticBreaker.BreakerState.OPEN, impl.state); + + impl.reset(); + impl.invoke(mockCallable); + assertEquals(SkepticBreaker.BreakerState.CLOSED, impl.state); + + verify(mockCallable); + } + + @Test + public void testGetTripCount() throws Exception { + long tripCount1 = impl.getTripCount(); + + impl.tripHard(); + long tripCount2 = impl.getTripCount(); + assertEquals(tripCount1 + 1, tripCount2); + + impl.tripHard(); + assertEquals(tripCount2, impl.getTripCount()); + } + + @Test + public void testGetStatusWhenOpen() { + impl.state = SkepticBreaker.BreakerState.OPEN; + Assert.assertEquals(Status.DOWN, impl.getStatus()); + } + + @Test + //CHANGED THIS (title), must change test to reflect + public void testGetStatusWhenClosedBeforeReset() { + impl.state = SkepticBreaker.BreakerState.CLOSED; + impl.waitTime.set(1000); + impl.lastFailure.set(System.currentTimeMillis() - 50); + + assertEquals(Status.UP, impl.getStatus()); + } + + @Test + public void testGetStatusWhenOpenAfterExpiration() { + impl.state = SkepticBreaker.BreakerState.OPEN; + impl.waitTime.set(1000); + impl.lastFailure.set(System.currentTimeMillis() - 2000); + + assertEquals(Status.UP, impl.getStatus()); + } + + @Test + public void testGetStatusAfterHardTrip() { + impl.tripHard(); + impl.waitTime.set(1000); + impl.lastFailure.set(System.currentTimeMillis() - 2000); + + assertEquals(Status.DOWN, impl.getStatus()); + } + + @Test + public void testStatusIsByPassWhenSet() { + impl.setByPassState(true); + assertEquals(Status.DEGRADED, impl.getStatus()); + } + + @Test + public void testByPassIgnoresCurrentBreakerStateWhenSet() { + impl.state = SkepticBreaker.BreakerState.OPEN; + assertEquals(Status.DOWN, impl.getStatus()); + + impl.setByPassState(true); + assertEquals(Status.DEGRADED, impl.getStatus()); + + impl.setByPassState(false); + assertEquals(Status.DOWN, impl.getStatus()); + } + + @Test + public void testByPassIgnoresBreakerStateAndCallsWrappedMethod() throws Exception { + expect(mockCallable.call()).andReturn("hi").anyTimes(); + + replay(mockCallable); + + impl.tripHard(); + impl.setByPassState(true); + + try { + impl.invoke(mockCallable); + } catch (BreakerException e) { + fail("exception not expected when SkepticBreaker is bypassed."); + } + assertEquals(SkepticBreaker.BreakerState.OPEN, impl.state); + assertEquals(Status.DEGRADED, impl.getStatus()); + + impl.reset(); + impl.setByPassState(false); + impl.invoke(mockCallable); + assertEquals(SkepticBreaker.BreakerState.CLOSED, impl.state); + + verify(mockCallable); + } + + @Test + public void testNotificationCallback() throws Exception { + + BreakerNotificationCallback cb = new BreakerNotificationCallback() { + public void notify(Status s) { + theStatus = s; + } + }; + + impl.addListener(cb); + impl.trip(); + + assertNotNull(theStatus); + assertEquals(Status.DOWN, theStatus); + } + + @Test(expected = Throwable.class) + public void SkepticBreakerKeepsExceptionThatTrippedIt() throws Throwable { + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + + Throwable tripException = impl.getTripException(); + assertEquals("broken", tripException.getMessage()); + throw tripException; + } + + @Test(expected = Throwable.class) + public void resetSkepticBreakerStillHasTripException() throws Throwable { + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + impl.reset(); + + Throwable tripException = impl.getTripException(); + assertEquals("broken", tripException.getMessage()); + throw tripException; + } + + @Test + public void skepticBreakerReturnsExceptionAsString() { + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + + String s = impl.getTripExceptionAsString(); + assertTrue(s.startsWith("java.lang.Exception: broken")); + assertTrue(s.contains("at org.fishwife.jrugged.TestSkepticBreaker$FailingCallable.call")); + assertTrue(s.contains("Caused by: java.lang.Exception: The Cause")); + } + + @Test + public void neverTrippedSkepticBreakerReturnsNullForTripException() throws Exception { + + impl.invoke(mockCallable); + + Throwable tripException = impl.getTripException(); + + assertNull(tripException); + } + + @Test + public void testResetsToInitialTimerValues() throws Exception { + long expectedWaitTime = impl.getWaitTime(); + long expectedGoodTime = impl.getGoodTime(); + long expectedSkepticLevel = impl.getSkepticLevel(); + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + impl.reset(); + + assertEquals(expectedWaitTime, impl.getWaitTime()); + assertEquals(expectedGoodTime, impl.getGoodTime()); + assertEquals(expectedSkepticLevel, impl.getSkepticLevel()); + } + + @Test + public void testTimerValuesAndSkepticLevelAfterOneTrip() throws Exception { + long expectedWaitTime = (long) (impl.getWaitBase() + + impl.getWaitMult() * Math.pow(2, impl.getSkepticLevel() + 1)); + long expectedGoodTime = (long) (impl.getGoodBase() + + impl.getGoodMult() * Math.pow(2, impl.getSkepticLevel() + 1)); + long expectedSkepticLevel = impl.getSkepticLevel() + 1; + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + + assertEquals(expectedWaitTime, impl.getWaitTime()); + assertEquals(expectedGoodTime, impl.getGoodTime()); + assertEquals(expectedSkepticLevel, impl.getSkepticLevel()); + } + + @Test + public void testTimerValuesAndSkepticLevelAfterThreeTrips() throws Exception { + int numTrips = 3; + long expectedWaitTime = (long) (impl.getWaitBase() + + impl.getWaitMult() * Math.pow(2, impl.getSkepticLevel() + numTrips)); + long expectedGoodTime = (long) (impl.getGoodBase() + + impl.getGoodMult() * Math.pow(2, impl.getSkepticLevel() + numTrips)); + long expectedSkepticLevel = impl.getSkepticLevel() + numTrips; + + for (int i = 0; i < numTrips; i++) { + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + Thread.sleep(impl.getWaitTime() + 100); + } + + assertEquals(expectedWaitTime, impl.getWaitTime()); + assertEquals(expectedGoodTime, impl.getGoodTime()); + assertEquals(expectedSkepticLevel, impl.getSkepticLevel()); + } + + @Test + public void testSkepticLevelStopsAtMaxLevel() throws Exception { + long expectedWaitTime = (long) (impl.getWaitBase() + + impl.getWaitMult() * Math.pow(2, impl.getMaxLevel())); + long expectedGoodTime = (long) (impl.getGoodBase() + + impl.getGoodMult() * Math.pow(2, impl.getMaxLevel())); + long expectedSkepticLevel = impl.getMaxLevel(); + impl.skepticLevel.set(impl.getMaxLevel()); + impl.updateTimers(); + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + + assertEquals(expectedWaitTime, impl.getWaitTime()); + assertEquals(expectedGoodTime, impl.getGoodTime()); + assertEquals(expectedSkepticLevel, impl.getSkepticLevel()); + } + + @Test + public void testDecreaseSkepticLevel() throws Exception { + impl.setGoodBase(100L); + impl.setWaitBase(100L); + impl.updateTimers(); + + long expectedWaitTime = impl.getWaitTime(); + long expectedGoodTime = impl.getGoodTime(); + long expectedSkepticLevel = impl.getSkepticLevel(); + + try { + impl.invoke(new FailingCallable("broken")); + } catch (Exception e) { + + } + Thread.sleep(impl.getWaitTime() + 100); + Thread.sleep(impl.getGoodTime() + 100); + + assertEquals(Status.UP, impl.getStatus()); + assertEquals(expectedWaitTime, impl.getWaitTime()); + assertEquals(expectedGoodTime, impl.getGoodTime()); + assertEquals(expectedSkepticLevel, impl.getSkepticLevel()); + } + + // test bad ping - not sure how? + + private class FailingCallable implements Callable { + + private final String exceptionMessage; + + public FailingCallable(String exceptionMessage) { + this.exceptionMessage = exceptionMessage; + } + + Exception causeException = new Exception("The Cause"); + + public Object call() throws Exception { + throw new Exception(exceptionMessage, causeException); + } + } + +} diff --git a/jrugged-examples/src/main/java/org/fishwife/jrugged/examples/webapp/InterceptCircuitBreakerExample.java b/jrugged-examples/src/main/java/org/fishwife/jrugged/examples/webapp/InterceptCircuitBreakerExample.java index 45f6cb57..cc59b861 100644 --- a/jrugged-examples/src/main/java/org/fishwife/jrugged/examples/webapp/InterceptCircuitBreakerExample.java +++ b/jrugged-examples/src/main/java/org/fishwife/jrugged/examples/webapp/InterceptCircuitBreakerExample.java @@ -50,7 +50,7 @@ public void setResponseTweaker(BreakerResponseTweaker breakerResponseTweaker) { @RequestMapping("/interceptCircuitBreaker") public ModelAndView viewMain(HttpServletRequest request, HttpServletResponse response) throws Exception { int delayedFor = breakerResponseTweaker.delay(); - ModelAndView view = new ModelAndView("interceptBreaker"); + ModelAndView view = new ModelAndView("interceptCircuitBreaker"); view.addObject("delay", new Integer(delayedFor)); return view; } diff --git a/jrugged-examples/src/main/java/org/fishwife/jrugged/examples/webapp/InterceptSkepticBreakerExample.java b/jrugged-examples/src/main/java/org/fishwife/jrugged/examples/webapp/InterceptSkepticBreakerExample.java new file mode 100644 index 00000000..2eba5e63 --- /dev/null +++ b/jrugged-examples/src/main/java/org/fishwife/jrugged/examples/webapp/InterceptSkepticBreakerExample.java @@ -0,0 +1,83 @@ +/* InterceptSkepticBreakerExample.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.examples.webapp; + +import org.fishwife.jrugged.examples.BreakerResponseTweaker; +import org.fishwife.jrugged.spring.SkepticBreakerBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; + +@Controller() +public class InterceptSkepticBreakerExample { + + @Autowired + private SkepticBreakerBean skepticBreakerBean; + public SkepticBreakerBean getSkepticBreakerBean() { + return skepticBreakerBean; + } + public void setSkepticBreakerBean(SkepticBreakerBean skepticBreakerBean) { + this.skepticBreakerBean = skepticBreakerBean; + } + + @Autowired + private BreakerResponseTweaker breakerResponseTweaker; + public BreakerResponseTweaker getResponseTweaker() { + return breakerResponseTweaker; + } + public void setResponseTweaker(BreakerResponseTweaker breakerResponseTweaker) { + this.breakerResponseTweaker = breakerResponseTweaker; + } + + @RequestMapping("/interceptSkepticBreaker") + public ModelAndView viewMain(HttpServletRequest request, HttpServletResponse response) throws Exception { + int delayedFor = breakerResponseTweaker.delay(); + ModelAndView view = new ModelAndView("interceptSkepticBreaker"); + view.addObject("delay", new Integer(delayedFor)); + return view; + } + + @RequestMapping("/interceptSkepticBreaker/stats") + public ModelAndView viewPerformanceMonitor(HttpServletRequest request, + HttpServletResponse response) throws Exception { + final StringBuilder sb = new StringBuilder(); + + // Go through all methods and invoke those with ManagedAttribute + // marker annotations + Method[] methods = skepticBreakerBean.getClass().getMethods(); + for (Method monitorMethod : methods) { + if (monitorMethod.getName().startsWith("get")) { + sb.append( + String.format("\t%s: %s\n", + monitorMethod.getName().substring(3), + monitorMethod.invoke(skepticBreakerBean, new Object[] {}) + ) + ); + } + } + sb.append("\n"); + + response.setContentType("text/plain"); + response.getWriter().println(sb.toString()); + return null; + } +} diff --git a/jrugged-examples/src/main/resources/applicationContext.xml b/jrugged-examples/src/main/resources/applicationContext.xml index b9600aa4..42b3fbee 100644 --- a/jrugged-examples/src/main/resources/applicationContext.xml +++ b/jrugged-examples/src/main/resources/applicationContext.xml @@ -86,15 +86,42 @@ + + + + + + + + + + + + + + + + + + - - + + circuitBreakerInterceptor + + + + + + skepticBreakerInterceptor + + + + + + JRugged Examples - Interceptor SkepticBreaker + + + + +

JRugged Examples - Interceptor SkepticBreaker

+ +

The response for this page was delayed by <%= request.getAttribute("delay") %> ms.

+ +

After loading this page a few times take a look at the breaker stats.

+ + + + diff --git a/jrugged-httpclient/src/main/java/org/fishwife/jrugged/httpclient/FailureHandlingHttpClient.java b/jrugged-httpclient/src/main/java/org/fishwife/jrugged/httpclient/FailureHandlingHttpClient.java index b3ccd133..6eebde30 100644 --- a/jrugged-httpclient/src/main/java/org/fishwife/jrugged/httpclient/FailureHandlingHttpClient.java +++ b/jrugged-httpclient/src/main/java/org/fishwife/jrugged/httpclient/FailureHandlingHttpClient.java @@ -23,7 +23,7 @@ import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.protocol.HttpContext; -import org.fishwife.jrugged.CircuitBreakerException; +import org.fishwife.jrugged.BreakerException; public class FailureHandlingHttpClient extends AbstractHttpClientDecorator { @@ -37,7 +37,7 @@ public HttpResponse execute(HttpHost host, HttpRequest req, HttpContext ctx) return backend.execute(host, req, ctx); } catch (UnsuccessfulResponseException ure) { return ure.getResponse(); - } catch (CircuitBreakerException cbe) { + } catch (BreakerException cbe) { throw new IOException(cbe); } } diff --git a/jrugged-httpclient/src/test/java/org/fishwife/jrugged/httpclient/TestFailureHandlingHttpClient.java b/jrugged-httpclient/src/test/java/org/fishwife/jrugged/httpclient/TestFailureHandlingHttpClient.java index 0d417cbf..e0993366 100644 --- a/jrugged-httpclient/src/test/java/org/fishwife/jrugged/httpclient/TestFailureHandlingHttpClient.java +++ b/jrugged-httpclient/src/test/java/org/fishwife/jrugged/httpclient/TestFailureHandlingHttpClient.java @@ -31,7 +31,7 @@ import org.apache.http.message.BasicHttpResponse; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; -import org.fishwife.jrugged.CircuitBreakerException; +import org.fishwife.jrugged.BreakerException; import org.junit.Before; import org.junit.Test; @@ -79,7 +79,7 @@ public void returnsEnclosedResponseOnUnsuccessfulException() throws Exception { @Test public void throwsIOExceptionForCircuitBreakerException() throws Exception { expect(mockBackend.execute(host, req, ctx)) - .andThrow(new CircuitBreakerException()); + .andThrow(new BreakerException()); replay(mockBackend); try { impl.execute(host, req, ctx); diff --git a/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/AsyncCircuitBreaker.java b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/AsyncCircuitBreaker.java index 2853ba52..de246e3f 100644 --- a/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/AsyncCircuitBreaker.java +++ b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/AsyncCircuitBreaker.java @@ -2,8 +2,8 @@ import java.util.concurrent.Callable; -import org.fishwife.jrugged.CircuitBreakerException; -import org.fishwife.jrugged.CircuitBreakerExceptionMapper; +import org.fishwife.jrugged.BreakerException; +import org.fishwife.jrugged.BreakerExceptionMapper; import org.fishwife.jrugged.DefaultFailureInterpreter; import org.fishwife.jrugged.FailureInterpreter; import org.springframework.util.concurrent.ListenableFuture; @@ -14,13 +14,13 @@ public class AsyncCircuitBreaker extends org.fishwife.jrugged.CircuitBreaker { /** Creates a {@link AsyncCircuitBreaker} with a {@link * DefaultFailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link CircuitBreakerException}). */ + * behavior (throwing a {@link BreakerException}). */ public AsyncCircuitBreaker() { } /** Creates a {@link AsyncCircuitBreaker} with a {@link * DefaultFailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link CircuitBreakerException}). + * behavior (throwing a {@link BreakerException}). * @param name the name for the {@link AsyncCircuitBreaker}. */ public AsyncCircuitBreaker(String name) { @@ -29,7 +29,7 @@ public AsyncCircuitBreaker(String name) { /** Creates a {@link AsyncCircuitBreaker} with the specified {@link * FailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link CircuitBreakerException}). + * behavior (throwing a {@link BreakerException}). * @param fi the FailureInterpreter to use when * determining whether a specific failure ought to cause the * breaker to trip @@ -40,7 +40,7 @@ public AsyncCircuitBreaker(FailureInterpreter fi) { /** Creates a {@link AsyncCircuitBreaker} with the specified {@link * FailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link CircuitBreakerException}). + * behavior (throwing a {@link BreakerException}). * @param name the name for the {@link AsyncCircuitBreaker}. * @param fi the FailureInterpreter to use when * determining whether a specific failure ought to cause the @@ -53,29 +53,29 @@ public AsyncCircuitBreaker(String name, FailureInterpreter fi) { /** Creates a {@link AsyncCircuitBreaker} with a {@link * DefaultFailureInterpreter} and using the supplied {@link - * CircuitBreakerExceptionMapper} when client calls are made + * BreakerExceptionMapper} when client calls are made * while the breaker is tripped. * @param name the name for the {@link AsyncCircuitBreaker}. * @param mapper helper used to translate a {@link - * CircuitBreakerException} into an application-specific one */ - public AsyncCircuitBreaker(String name, CircuitBreakerExceptionMapper mapper) { + * BreakerException} into an application-specific one */ + public AsyncCircuitBreaker(String name, BreakerExceptionMapper mapper) { this.name = name; exceptionMapper = mapper; } /** Creates a {@link AsyncCircuitBreaker} with the provided {@link * FailureInterpreter} and using the provided {@link - * CircuitBreakerExceptionMapper} when client calls are made + * BreakerExceptionMapper} when client calls are made * while the breaker is tripped. * @param name the name for the {@link AsyncCircuitBreaker}. * @param fi the FailureInterpreter 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 */ + * BreakerException} into an application-specific one */ public AsyncCircuitBreaker(String name, FailureInterpreter fi, - CircuitBreakerExceptionMapper mapper) { + BreakerExceptionMapper mapper) { this.name = name; failureInterpreter = fi; exceptionMapper = mapper; @@ -84,7 +84,7 @@ public AsyncCircuitBreaker(String name, /** Wrap the given service call with the {@link AsyncCircuitBreaker} protection logic. * @param callable the {@link java.util.concurrent.Callable} to attempt * @return {@link ListenableFuture} of whatever callable would return - * @throws org.fishwife.jrugged.CircuitBreakerException if the + * @throws org.fishwife.jrugged.BreakerException if the * breaker was OPEN or HALF_CLOSED and this attempt wasn't the * reset attempt * @throws Exception if the {@link org.fishwife.jrugged.CircuitBreaker} is in OPEN state @@ -111,7 +111,7 @@ public void onFailure(Throwable ex) { if (!byPass) { if (!allowRequest()) { - throw mapException(new CircuitBreakerException()); + throw mapException(new BreakerException()); } try { diff --git a/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/AsyncSkepticBreaker.java b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/AsyncSkepticBreaker.java new file mode 100644 index 00000000..c8cdf9b0 --- /dev/null +++ b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/AsyncSkepticBreaker.java @@ -0,0 +1,131 @@ +package org.fishwife.jrugged.spring; + +import java.util.concurrent.Callable; + +import org.fishwife.jrugged.BreakerException; +import org.fishwife.jrugged.BreakerExceptionMapper; +import org.fishwife.jrugged.DefaultFailureInterpreter; +import org.fishwife.jrugged.FailureInterpreter; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureCallback; +import org.springframework.util.concurrent.SettableListenableFuture; + +public class AsyncSkepticBreaker extends org.fishwife.jrugged.SkepticBreaker { + + /** Creates a {@link AsyncSkepticBreaker} with a {@link + * DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). */ + public AsyncSkepticBreaker() { + } + + /** Creates a {@link AsyncSkepticBreaker} with a {@link + * DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). + * @param name the name for the {@link AsyncSkepticBreaker}. + */ + public AsyncSkepticBreaker(String name) { + this.name = name; + } + + /** Creates a {@link AsyncSkepticBreaker} with the specified {@link + * FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public AsyncSkepticBreaker(FailureInterpreter fi) { + failureInterpreter = fi; + } + + /** Creates a {@link AsyncSkepticBreaker} with the specified {@link + * FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link BreakerException}). + * @param name the name for the {@link AsyncSkepticBreaker}. + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public AsyncSkepticBreaker(String name, FailureInterpreter fi) { + this.name = name; + failureInterpreter = fi; + } + + /** Creates a {@link AsyncSkepticBreaker} with a {@link + * DefaultFailureInterpreter} and using the supplied {@link + * BreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link AsyncSkepticBreaker}. + * @param mapper helper used to translate a {@link + * BreakerException} into an application-specific one */ + public AsyncSkepticBreaker(String name, BreakerExceptionMapper mapper) { + this.name = name; + exceptionMapper = mapper; + } + + /** Creates a {@link AsyncSkepticBreaker} with the provided {@link + * FailureInterpreter} and using the provided {@link + * BreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link AsyncSkepticBreaker}. + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + * @param mapper helper used to translate a {@link + * BreakerException} into an application-specific one */ + public AsyncSkepticBreaker(String name, + FailureInterpreter fi, + BreakerExceptionMapper mapper) { + this.name = name; + failureInterpreter = fi; + exceptionMapper = mapper; + } + + /** Wrap the given service call with the {@link AsyncSkepticBreaker} protection logic. + * @param callable the {@link java.util.concurrent.Callable} to attempt + * @return {@link ListenableFuture} of whatever callable would return + * @throws org.fishwife.jrugged.BreakerException if the + * breaker was OPEN or HALF_CLOSED and this attempt wasn't the + * reset attempt + * @throws Exception if the {@link org.fishwife.jrugged.CircuitBreaker} is in OPEN state + */ + public ListenableFuture invokeAsync(Callable> callable) throws Exception { + + final SettableListenableFuture response = new SettableListenableFuture(); + ListenableFutureCallback callback = new ListenableFutureCallback() { + @Override + public void onSuccess(T result) { + close(); + response.set(result); + } + + @Override + public void onFailure(Throwable ex) { + try { + handleFailure(ex); + } catch (Exception e) { + response.setException(e); + } + } + }; + + if (!byPass) { + if (!allowRequest()) { + throw mapException(new BreakerException()); + } + + try { + callable.call().addCallback(callback); + return response; + } catch (Throwable cause) { + // This shouldn't happen because Throwables are handled in the async onFailure callback + handleFailure(cause); + } + throw new IllegalStateException("not possible"); + } + else { + callable.call().addCallback(callback); + return response; + } + } +} diff --git a/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBean.java b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBean.java index 5b177261..c8ac3220 100644 --- a/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBean.java +++ b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBean.java @@ -15,7 +15,7 @@ package org.fishwife.jrugged.spring; import org.fishwife.jrugged.CircuitBreaker; -import org.fishwife.jrugged.CircuitBreakerExceptionMapper; +import org.fishwife.jrugged.BreakerExceptionMapper; import org.fishwife.jrugged.DefaultFailureInterpreter; import org.fishwife.jrugged.FailureInterpreter; import org.springframework.beans.factory.InitializingBean; @@ -43,14 +43,14 @@ public void afterPropertiesSet() throws Exception { /** * Creates a {@link CircuitBreakerBean} with a * {@link DefaultFailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link org.fishwife.jrugged.CircuitBreakerException}). + * behavior (throwing a {@link org.fishwife.jrugged.BreakerException}). */ public CircuitBreakerBean() { super(); } /** * Creates a {@link CircuitBreakerBean} with a * {@link DefaultFailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link org.fishwife.jrugged.CircuitBreakerException}). + * behavior (throwing a {@link org.fishwife.jrugged.BreakerException}). * @param name the name for the {@link CircuitBreakerBean} */ public CircuitBreakerBean(String name) { super(name); } @@ -58,7 +58,7 @@ public void afterPropertiesSet() throws Exception { /** * Creates a {@link CircuitBreakerBean} with the specified * {@link FailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link org.fishwife.jrugged.CircuitBreakerException}). + * behavior (throwing a {@link org.fishwife.jrugged.BreakerException}). * @param fi the FailureInterpreter to use when * determining whether a specific failure ought to cause the * breaker to trip @@ -70,7 +70,7 @@ public CircuitBreakerBean(FailureInterpreter fi) { /** * Creates a {@link CircuitBreakerBean} with the specified * {@link FailureInterpreter} and the default "tripped" exception - * behavior (throwing a {@link org.fishwife.jrugged.CircuitBreakerException}). + * behavior (throwing a {@link org.fishwife.jrugged.BreakerException}). * @param name the name for the {@link CircuitBreakerBean} * @param fi the FailureInterpreter to use when * determining whether a specific failure ought to cause the @@ -83,20 +83,20 @@ public CircuitBreakerBean(String name, FailureInterpreter fi) { /** * Creates a {@link CircuitBreaker} with a {@link * DefaultFailureInterpreter} and using the supplied {@link - * CircuitBreakerExceptionMapper} when client calls are made + * BreakerExceptionMapper} when client calls are made * while the breaker is tripped. * @param name the name for the {@link CircuitBreakerBean} * @param mapper helper used to translate a {@link - * org.fishwife.jrugged.CircuitBreakerException} into an application-specific one + * org.fishwife.jrugged.BreakerException} into an application-specific one */ - public CircuitBreakerBean(String name, CircuitBreakerExceptionMapper mapper) { + public CircuitBreakerBean(String name, BreakerExceptionMapper mapper) { super(name, mapper); } /** * Creates a {@link CircuitBreaker} with the provided {@link * FailureInterpreter} and using the provided {@link - * CircuitBreakerExceptionMapper} when client calls are made + * BreakerExceptionMapper} when client calls are made * while the breaker is tripped. * * @param name the name for the {@link CircuitBreakerBean} @@ -104,10 +104,10 @@ public CircuitBreakerBean(String name, CircuitBreakerExceptionMapper mapper) { + BreakerExceptionMapper mapper) { super(name, fi, mapper); } diff --git a/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBeanFactory.java b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBeanFactory.java index 9a0a4377..0410435a 100644 --- a/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBeanFactory.java +++ b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/CircuitBreakerBeanFactory.java @@ -21,7 +21,7 @@ import org.fishwife.jrugged.CircuitBreaker; import org.fishwife.jrugged.CircuitBreakerConfig; -import org.fishwife.jrugged.CircuitBreakerFactory; +import org.fishwife.jrugged.BreakerFactory; import org.fishwife.jrugged.DefaultFailureInterpreter; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; @@ -33,7 +33,7 @@ * them. If a {@link MBeanExportOperations} is set, then the CircuitBreakerBean will be * automatically exported as a JMX MBean. */ -public class CircuitBreakerBeanFactory extends CircuitBreakerFactory implements InitializingBean { +public class CircuitBreakerBeanFactory extends BreakerFactory implements InitializingBean { @Autowired(required = false) private MBeanExportOperations mBeanExportOperations; diff --git a/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/SkepticBreakerBean.java b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/SkepticBreakerBean.java new file mode 100644 index 00000000..527e3e0f --- /dev/null +++ b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/SkepticBreakerBean.java @@ -0,0 +1,344 @@ +/* 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.spring; + +import org.fishwife.jrugged.CircuitBreaker; +import org.fishwife.jrugged.BreakerExceptionMapper; +import org.fishwife.jrugged.DefaultFailureInterpreter; +import org.fishwife.jrugged.FailureInterpreter; +import org.fishwife.jrugged.SkepticBreaker; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; + +/** + * This is basically a {@link CircuitBreaker} that adds JMX + * annotations to some of the methods so that the core library + * doesn't have to depend on spring-context. + */ +@ManagedResource +public class SkepticBreakerBean extends SkepticBreaker implements InitializingBean { + + private boolean disabledAtStart = false; + + /** + * {@inheritDoc} + */ + public void afterPropertiesSet() throws Exception { + if (disabledAtStart) tripHard(); + } + + /** + * Creates a {@link SkepticBreakerBean} with a + * {@link DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link org.fishwife.jrugged.BreakerException}). + */ + public SkepticBreakerBean() { super(); } + + /** + * Creates a {@link SkepticBreakerBean} with a + * {@link DefaultFailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link org.fishwife.jrugged.BreakerException}). + * @param name the name for the {@link SkepticBreakerBean} + */ + public SkepticBreakerBean(String name) { super(name); } + + /** + * Creates a {@link SkepticBreakerBean} with the specified + * {@link FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link org.fishwife.jrugged.BreakerException}). + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public SkepticBreakerBean(FailureInterpreter fi) { + super(fi); + } + + /** + * Creates a {@link SkepticBreakerBean} with the specified + * {@link FailureInterpreter} and the default "tripped" exception + * behavior (throwing a {@link org.fishwife.jrugged.BreakerException}). + * @param name the name for the {@link SkepticBreakerBean} + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + */ + public SkepticBreakerBean(String name, FailureInterpreter fi) { + super(name, fi); + } + + /** + * Creates a {@link CircuitBreaker} with a {@link + * DefaultFailureInterpreter} and using the supplied {@link + * BreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * @param name the name for the {@link SkepticBreakerBean} + * @param mapper helper used to translate a {@link + * org.fishwife.jrugged.BreakerException} into an application-specific one + */ + public SkepticBreakerBean(String name, BreakerExceptionMapper mapper) { + super(name, mapper); + } + + /** + * Creates a {@link CircuitBreaker} with the provided {@link + * FailureInterpreter} and using the provided {@link + * BreakerExceptionMapper} when client calls are made + * while the breaker is tripped. + * + * @param name the name for the {@link SkepticBreakerBean} + * @param fi the FailureInterpreter to use when + * determining whether a specific failure ought to cause the + * breaker to trip + * @param mapper helper used to translate a {@link + * org.fishwife.jrugged.BreakerException} into an application-specific one + */ + public SkepticBreakerBean(String name, FailureInterpreter fi, + BreakerExceptionMapper mapper) { + super(name, fi, mapper); + } + + /** + * Manually trips the CircuitBreaker until {@link #reset()} is invoked. + */ + @ManagedOperation + @Override + public void tripHard() { + super.tripHard(); + } + + /** + * Manually trips the CircuitBreaker until {@link #reset()} is invoked. + */ + @ManagedOperation + @Override + public void trip() { + super.trip(); + } + + /** + * Returns the last time the breaker tripped OPEN, measured in + * milliseconds since the Epoch. + * @return long the last failure time + */ + @ManagedAttribute + @Override + public long getLastTripTime() { + return super.getLastTripTime(); + } + + /** + * Returns the number of times the breaker has tripped OPEN during + * its lifetime. + * @return long the number of times the circuit breaker tripped + */ + @ManagedAttribute + @Override + public long getTripCount() { + return super.getTripCount(); + } + + /** + * When called with true - causes the {@link CircuitBreaker} to byPass + * its functionality allowing requests to be executed unmolested + * until the CircuitBreaker is reset or the byPass + * is manually set to false. + */ + @ManagedAttribute + @Override + public void setByPassState(boolean b) { + super.setByPassState(b); + } + + /** + * Get the current state of the {@link CircuitBreaker} byPass + * + * @return boolean the byPass flag's current value + */ + @ManagedAttribute + @Override + public boolean getByPassState() { + return super.getByPassState(); + } + + /** + * 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!!! + */ + @ManagedOperation + @Override + public void reset() { + super.reset(); + } + + + @ManagedAttribute + @Override + public long getWaitMult() { + return super.getWaitMult(); + } + + @ManagedAttribute + @Override + public void setWaitMult(long l) { + super.setWaitMult(l); + } + + @ManagedAttribute + @Override + public long getGoodMult() { + return super.getGoodMult(); + } + + @ManagedAttribute + @Override + public void setGoodMult(long l) { + super.setGoodMult(l); + } + + @ManagedAttribute + @Override + public long getWaitBase() { + return super.getWaitBase(); + } + + @ManagedAttribute + @Override + public void setWaitBase(long l) { + super.setWaitBase(l); + } + + @ManagedAttribute + @Override + public long getGoodBase() { + return super.getGoodBase(); + } + + @ManagedAttribute + @Override + public void setGoodBase(long l) { + super.setGoodBase(l); + } + + @ManagedAttribute + @Override + public long getMaxLevel() { + return super.getMaxLevel(); + } + + @ManagedAttribute + @Override + public void setMaxLevel(long l) { + super.setMaxLevel(l); + } + + /** + * Returns a {@link String} representation of the breaker's + * status; potentially useful for exposing to monitoring software. + * + * @return String which is "GREEN" if + * the breaker is CLOSED; "YELLOW" if the breaker + * is HALF_CLOSED; and "RED" if the breaker is + * OPEN (tripped). + */ + @ManagedAttribute + @Override + public String getHealthCheck() { return super.getHealthCheck(); } + + /** + * Gets the failure tolerance limit for the {@link DefaultFailureInterpreter} that + * comes with a {@link CircuitBreaker} by default. + * + * @see DefaultFailureInterpreter + * + * @return the number of tolerated failures in a window + */ + @ManagedAttribute + public int getLimit() { + return ((DefaultFailureInterpreter) super.getFailureInterpreter()).getLimit(); + } + + /** + * 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 + */ + @ManagedAttribute + @Override + public void setLimit(int limit) { + ((DefaultFailureInterpreter) super.getFailureInterpreter()).setLimit(limit); + } + + /** + * Gets the tolerance window in milliseconds for the {@link DefaultFailureInterpreter} + * that comes with a {@link CircuitBreaker} by default. + * + * @see DefaultFailureInterpreter + * + * @return length of the window in milliseconds + */ + @ManagedAttribute + public long getWindowMillis() { + return ((DefaultFailureInterpreter) super.getFailureInterpreter()).getWindowMillis(); + } + + /** + * 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 + */ + @ManagedAttribute + @Override + public void setWindowMillis(long windowMillis) { + super.setWindowMillis(windowMillis); + } + + /** + * returns a {@link String} representation of the breaker's + * last known exception that caused it to OPEN (i.e. when the breaker + * opens, it will record the specific exception that caused it to open) + * + * @return String which is the full stack trace. + */ + @ManagedAttribute + @Override + public String getTripExceptionAsString() { + return super.getTripExceptionAsString(); + } + + /** + * Specifies whether the associated CircuitBreaker should be tripped + * at startup time. + * + * @param b true if the CircuitBreaker should start + * open (tripped); false if the CircuitBreaker should start + * closed (not tripped). + */ + public void setDisabled(boolean b) { + disabledAtStart = b; + } +} diff --git a/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/SkepticBreakerBeanFactory.java b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/SkepticBreakerBeanFactory.java new file mode 100644 index 00000000..2af8ce49 --- /dev/null +++ b/jrugged-spring/src/main/java/org/fishwife/jrugged/spring/SkepticBreakerBeanFactory.java @@ -0,0 +1,149 @@ +/* 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.spring; + +import java.lang.reflect.Method; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.fishwife.jrugged.SkepticBreaker; +import org.fishwife.jrugged.SkepticBreakerConfig; +import org.fishwife.jrugged.BreakerFactory; +import org.fishwife.jrugged.DefaultFailureInterpreter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jmx.export.MBeanExportOperations; +import org.springframework.jmx.export.MBeanExporter; + +/** + * Factory to create new {@link SkepticBreakerBean} instances and keep track of + * them. If a {@link MBeanExportOperations} is set, then the SkepticBreakerBean will be + * automatically exported as a JMX MBean. + */ +public class SkepticBreakerBeanFactory extends BreakerFactory implements InitializingBean { + + @Autowired(required = false) + private MBeanExportOperations mBeanExportOperations; + + private String packageScanBase; + + /** + * {@inheritDoc} + */ + public void afterPropertiesSet() { + buildAnnotatedSkepticBreakers(); + } + + /** + * Set the {@link MBeanExporter} to use to export {@link SkepticBreakerBean} + * instances as JMX MBeans. + * @param mBeanExporter the {@link MBeanExporter} to set. + */ + @Deprecated + public void setMBeanExporter(MBeanExporter mBeanExporter) { + setMBeanExportOperations(mBeanExporter); + } + + /** + * Set the {@link MBeanExportOperations} to use to export {@link SkepticBreakerBean} + * instances as JMX MBeans. + * @param mBeanExportOperations the {@link MBeanExportOperations} to set. + */ + public void setMBeanExportOperations(MBeanExportOperations mBeanExportOperations) { + this.mBeanExportOperations = mBeanExportOperations; + } + + /** + * If specified, SkepticBreakerBeanFactory will scan all classes + * under packageScanBase for methods with the + * {@link org.fishwife.jrugged.aspects.SkepticBreaker} annotation + * and initialize skepticbreakers for them. + * + * @param packageScanBase Where should the scan for annotations begin + */ + public void setPackageScanBase(String packageScanBase) { + this.packageScanBase = packageScanBase; + } + + /** + * If packageScanBase is defined will search packages for {@link org.fishwife.jrugged.aspects.SkepticBreaker} + * annotations and create skepticbreakers for them. + */ + public void buildAnnotatedSkepticBreakers() { + if (packageScanBase != null) { + AnnotatedMethodScanner methodScanner = new AnnotatedMethodScanner(); + for (Method m : methodScanner.findAnnotatedMethods(packageScanBase, org.fishwife.jrugged.aspects.SkepticBreaker.class)) { + org.fishwife.jrugged.aspects.SkepticBreaker skepticBreakerAnnotation = m.getAnnotation(org.fishwife.jrugged.aspects.SkepticBreaker.class); + DefaultFailureInterpreter dfi = new DefaultFailureInterpreter(skepticBreakerAnnotation.ignore(), skepticBreakerAnnotation.limit(), skepticBreakerAnnotation.windowMillis()); + SkepticBreakerConfig config = new SkepticBreakerConfig(skepticBreakerAnnotation.goodBase(), skepticBreakerAnnotation.goodMult(), skepticBreakerAnnotation.waitBase(), + skepticBreakerAnnotation.waitMult(), skepticBreakerAnnotation.maxLevel(), dfi); + createSkepticBreaker(skepticBreakerAnnotation.name(), config); + } + } + + } + + /** + * Create a new {@link SkepticBreakerBean} and map it to the provided value. + * If the {@link MBeanExportOperations} is set, then the SkepticBreakerBean will be + * exported as a JMX MBean. + * If the SkepticBreaker already exists, then the existing instance is + * returned. + * @param name the value for the {@link org.fishwife.jrugged.SkepticBreaker} + * @param config the {@link org.fishwife.jrugged.SkepticBreakerConfig} + */ + public synchronized SkepticBreaker createSkepticBreaker(String name, SkepticBreakerConfig config) { + + SkepticBreaker skepticBreaker = findSkepticBreaker(name); + + if (skepticBreaker == null) { + skepticBreaker = new SkepticBreakerBean(name); + + configureSkepticBreaker(name, skepticBreaker, config); + + if (mBeanExportOperations != null) { + ObjectName objectName; + + try { + objectName = new ObjectName("org.fishwife.jrugged.spring:type=SkepticBreakerBean," + "name=" + name); + } catch (MalformedObjectNameException e) { + throw new IllegalArgumentException("Invalid MBean Name " + name, e); + + } + + mBeanExportOperations.registerManagedResource(skepticBreaker, objectName); + } + + addSkepticBreakerToMap(name, skepticBreaker); + } + + return skepticBreaker; + } + + /** + * Find an existing {@link SkepticBreakerBean} + * @param name the value for the {@link SkepticBreakerBean} + * @return the found {@link SkepticBreakerBean}, or null if it is not found. + */ + public SkepticBreakerBean findSkepticBreakerBean(String name) { + SkepticBreaker skepticBreaker = findSkepticBreaker(name); + + if (skepticBreaker instanceof SkepticBreakerBean) { + return (SkepticBreakerBean) skepticBreaker; + } + return null; + } +} diff --git a/jrugged-spring/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBean.java b/jrugged-spring/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBean.java index 98b0038c..72b81027 100644 --- a/jrugged-spring/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBean.java +++ b/jrugged-spring/src/test/java/org/fishwife/jrugged/spring/TestCircuitBreakerBean.java @@ -20,7 +20,7 @@ import java.util.concurrent.Callable; -import org.fishwife.jrugged.CircuitBreakerException; +import org.fishwife.jrugged.BreakerException; import org.junit.Before; import org.junit.Test; @@ -59,7 +59,7 @@ public void canBeDisabled() throws Exception { try { impl.invoke(call); fail("Should have thrown CircuitBreakerException"); - } catch (CircuitBreakerException cbe) { + } catch (BreakerException cbe) { } } }