package com.kendelong.util.circuitbreaker; import java.text.DateFormat; import java.util.Date; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; import com.kendelong.util.monitoring.graphite.GraphiteClient; /** * This is the main Circuit Breaker class. It is a stateful object, the state variable * refers to the current state re the GoF State Pattern. This is modeled after the * CB in "Release It" by Michael Nygard. * * The CB allows all calls to go through as long as they are succeeding. However, if the remote * service starts to fail, the breaker will trip and go open, and not allow any calls. After * a bit, it will try the remote service again, and if it has recovered it will start sending * work again. Otherwise, it goes back to Open state. * * Note that a unique instance is created for each proxied service. * * Configuration is like * <pre> * {@code <bean class="com.kendelong.util.circuitbreaker.CircuitBreakerAspect" scope="prototype"> <property name="graphiteClient" ref="graphiteClient"/> </bean> <bean id="aspectJmxExporter" class="com.kendelong.util.spring.JmxExportingAspectPostProcessor" lazy-init="false"> <property name="mbeanExporter" ref="mbeanExporter"/> <property name="annotationToServiceNames"> <map> <entry key="com.kendelong.util.circuitbreaker.CircuitBreakerAspect" value="circuitbreaker"/> </map> </property> <property name="jmxDomain" value="app.mystuff"/> </bean> } </pre> * * See the javadoc for the individual states for deeper explanation. * * {@link OpenState} * {@link ClosedState} * {@link HalfOpenState} * * @author kdelong */ @Aspect @ManagedResource(description="Circuit Breaker for protecting ourselves against badly behaving remote services") @Order(100) public class CircuitBreakerAspect implements Ordered { private final AtomicReference<ICircuitBreakerState> state = new AtomicReference<ICircuitBreakerState>(); private final int DEFAULT_FAILURE_THRESHOLD = 3; private final ClosedState CLOSED_STATE = new ClosedState(); private final OpenState OPEN_STATE = new OpenState(); private final AtomicInteger totalNumberOfTrips = new AtomicInteger(); private final AtomicReference<Date> timeOfLastTrip = new AtomicReference<Date>(); private final Logger logger = LoggerFactory.getLogger(this.getClass()); private GraphiteClient graphiteClient; private final ThreadLocal<String> keys = new ThreadLocal<String>(); private int order = 0; public CircuitBreakerAspect() { CLOSED_STATE.setFailureThreshold(DEFAULT_FAILURE_THRESHOLD); state.set(CLOSED_STATE); } @Around("@annotation(com.kendelong.util.circuitbreaker.CircuitBreakable)") public Object applyCircuitBreaker(ProceedingJoinPoint pjp) throws Throwable { if(graphiteClient != null) { String methodKey = "circuitbreaker." + getMethodKey(pjp); keys.set(methodKey); graphiteClient.increment(methodKey + ".accesses"); } Object result = null; try { getState().preInvoke(this); result = pjp.proceed(); getState().postInvoke(this); } catch(Throwable t) { getState().onError(this, t); throw t; } finally { keys.remove(); } return result; } private String getMethodKey(ProceedingJoinPoint pjp) { String classKey = StringUtils.substringAfterLast(pjp.getSignature().getDeclaringTypeName(), "."); String methodName = pjp.getSignature().getName(); String methodKey = classKey + "." + methodName; return methodKey; } private ICircuitBreakerState getState() { return state.get(); } @ManagedAttribute() public void setFailureThreshold(int threshold) { CLOSED_STATE.setFailureThreshold(threshold); } @ManagedAttribute(description="Number of sucessive failure before we trip the breaker") public int getFailureThreshold() { return CLOSED_STATE.getFailureThreshold(); } @ManagedOperation(description="Open the circuit breaker (disallow calls to remote service)") public void tripBreaker() { OPEN_STATE.trip(); state.set(OPEN_STATE); timeOfLastTrip.set(new Date()); logger.warn("Circuit breaker tripped; going to OpenState"); totalNumberOfTrips.incrementAndGet(); if(graphiteClient != null) graphiteClient.increment(keys.get() + ".trips"); } @ManagedAttribute() public void setRecoveryTimeout(int timeout) { OPEN_STATE.setRecoveryTimeout(timeout); } @ManagedAttribute(description="Number of milliseconds to wait before we try the remote service again") public int getRecoveryTimeout() { return OPEN_STATE.getRecoveryTimeout(); } @ManagedOperation(description="Move to half-open state; try the remote service tentatively") public void attemptReset() { // there's no state to maintain in this one, so just create a new object // there's not going to be that many of them state.set(new HalfOpenState()); logger.info("Attempting reset; going HalfOpen"); } @ManagedOperation(description="Reset the breaker and go closed (start using the remote service again)") public void reset() { CLOSED_STATE.resetFailureCount(); state.set(CLOSED_STATE); logger.info("Circuit breaker reset; all is happy again"); if(graphiteClient != null) graphiteClient.increment(keys.get() + ".resets"); } @ManagedAttribute(description="Number of current failures in the closed state") public int getCurrentFailureCount() { return CLOSED_STATE.getCurrentFailureCount(); } @ManagedAttribute(description="When in open state, the number of milliseconds until we try sending another request to the remote service") public long getTimeToNextRetry() { if(state.get() == OPEN_STATE) return OPEN_STATE.getTimeToNextRetry(); else return 0; } @ManagedAttribute(description="Total times the circuit breaker has tripped") public int getTotalNumberOfTrips() { return totalNumberOfTrips.get(); } @ManagedAttribute(description="Current state of the circuit breaker. Closed State is normal, Open means errors.") public String getCurrentState() { return state.get().getClass().getSimpleName(); } @ManagedOperation(description="Reset the number of trips on this breaker to zero") public void resetStatistics() { totalNumberOfTrips.set(0); timeOfLastTrip.set(null); } @ManagedAttribute(description="Time of last trip") public String getTimeOfLastTrip() { Date time = timeOfLastTrip.get(); if(time != null) { DateFormat df = DateFormat.getTimeInstance(DateFormat.FULL); return df.format(time); } else { return ""; } } @ManagedAttribute(description="Time since the last trip, in seconds") public long getTimeSinceLastTripInSeconds() { Date time = timeOfLastTrip.get(); if(time != null) { Date now = new Date(); return (now.getTime() - time.getTime())/1000; } else { return -1; } } public GraphiteClient getGraphiteClient() { return graphiteClient; } public void setGraphiteClient(GraphiteClient graphiteClient) { this.graphiteClient = graphiteClient; } @Override public int getOrder() { return order; } public void setOrder(int theOrder) { order = theOrder; } }