/*
* Copyright 2010 Nabil Ben Said.
*
* 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 net.greencoding.thysdrus.circuitbreaker.aspect;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import net.greencoding.thysdrus.circuitbreaker.annotation.MonitoredByCircuitBreaker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Nabil Ben Said (nabil.bensaid@gmail.com)
*
*/
public class CircuitBreakerMethodRegistry {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private ReentrantLock exclusiveHalfOpenLock = new ReentrantLock();
// needed for checking the CLOSED cycle
private ThreadLocal<Map<String, Long>> threadLocalMap = new ThreadLocal<Map<String, Long>>();
// map with global state of CircuitBreaker entries
private final Map<String, CircuitBreakerRegistryEntry> globalMap = new ConcurrentHashMap<String, CircuitBreakerRegistryEntry>();
// this method is call before every CircuitBreaker annotated method is
// called.
public void registerMehtodIfnecessary(String method, MonitoredByCircuitBreaker annotation) {
Map<String, Long> localMap = threadLocalMap.get();
if (localMap == null) {
localMap = new HashMap<String, Long>();
threadLocalMap.set(localMap);
}
// this shall be null - recursion is not supported
assert threadLocalMap.get().get(method) == null;
CircuitBreakerRegistryEntry entry = globalMap.get(method);
if (entry == null) {
entry = createCircuitBreakerRegistryEntry(method, annotation);
globalMap.put(method, entry);
}
localMap.put(method, Long.valueOf(entry.getClosedCycleCounter()));
}
/**
* method changes the status from HALF_OPEN to CLOSED then unlock the
* exclusiveHalfOpenLock. the caller must make sure that he has the half
* open exclusive lock before invoking this method.
*
* @see getStatusWithHalfOpenExclusiveLockTry()
* @param method
*/
public void closeAndUnlock(String method) {
assert exclusiveHalfOpenLock.isHeldByCurrentThread();
CircuitBreakerRegistryEntry entry = globalMap.get(method);
assert entry.getStatus().equals(CircuitBreakerStatus.HALF_OPEN);
entry.getFailures().clear();
entry.setStatus(CircuitBreakerStatus.CLOSED);
entry.increaseClosedCycleCounter();
exclusiveHalfOpenLock.unlock();
}
/**
* method changes the status from HALF_OPEN to OPEN then unlock the
* exclusiveHalfOpenLock. the caller must make sure that he has the half
* open exclusive lock before invoking this method.
*
* @see getStatusWithHalfOpenExclusiveLockTry()
* @param method
*/
public void keepOpenAndUnlock(String method) {
assert exclusiveHalfOpenLock.isHeldByCurrentThread();
CircuitBreakerRegistryEntry entry = globalMap.get(method);
assert entry.getStatus().equals(CircuitBreakerStatus.HALF_OPEN);
entry.getFailures().clear();
entry.setStatus(CircuitBreakerStatus.OPEN);
entry.setLastOpenedTime(System.currentTimeMillis());
exclusiveHalfOpenLock.unlock();
}
/**
* method returns true if closed cycle at the beginning of the method
* invocation is the same as at the end of the invocation, otherwise returns
* false.
*
* @param method
* @return
*/
public boolean sameClosedCycleInLocalAndGlobaleContext(String method) {
if (threadLocalMap.get().get(method) == globalMap.get(method).getClosedCycleCounter()) {
return true;
}
return false;
}
/**
* method returns the {@link CircuitBreakerStatus} and tries to get the
* exclusive lock for the {@link CircuitBreakerStatus}.HALF_OPEN status in
* case the condition for the half open status is statisfied. If the lock
* succeed it saves HALF_OPEN state into the registry and returns
* HALF_OPEN_EXCLUSIVE to signal to the caller, that the current thread got
* the exclusive lock. It returns {@link CircuitBreakerStatus}.
*
* Method may returns also one of the status CLOSED, OPEN or HALF_OPEN.
* HALF_OPEN is returned only if one of the concurrent threads has already
* the exclusive lock.
*
*
* @param method
* @return
*/
public CircuitBreakerStatus getStatusWithHalfOpenExclusiveLockTry(String method) {
CircuitBreakerRegistryEntry entry = globalMap.get(method);
if (entry.getStatus().equals(CircuitBreakerStatus.OPEN) && System.currentTimeMillis() - entry.getLastOpenedTime() >= entry.getRetryAfterMs()) {
// condition for half open is statisfied, try to get the halfopene
// exclusive lock.
if (exclusiveHalfOpenLock.tryLock()) {
// the lock will be release in the cleanUp method.
entry.setStatus(CircuitBreakerStatus.HALF_OPEN);
return CircuitBreakerStatus.HALF_OPEN_EXCLUSIVE;
}
}
return entry.getStatus();
}
private CircuitBreakerRegistryEntry createCircuitBreakerRegistryEntry(String method, MonitoredByCircuitBreaker circuitBreaker) {
int failureThreshold = circuitBreaker.failureThreshold();
long failureThresholdTimeFrameMs = circuitBreaker.failureThresholdTimeFrameMs();
long retryAfterMs = circuitBreaker.retryAfterMs();
List<Class<? extends Throwable>> faultIndications = null;
if (circuitBreaker.failureIndications() != null && circuitBreaker.failureIndications().length > 0) {
faultIndications = Arrays.asList(circuitBreaker.failureIndications());
} else {
faultIndications = Collections.emptyList();
}
return new CircuitBreakerRegistryEntry(method, failureThreshold, failureThresholdTimeFrameMs, retryAfterMs, faultIndications);
}
/**
* method adds failure for the given method name. Checks if failure
* threshold is achieved, in case of set status to OPEN and return true,
* otherwise return false;
*
* @param method
* @return true if status is changed from CLOSED to OPEN otherwise false.
*/
public synchronized boolean addFailureAndOpenCircuitIfThresholdAchived(String method) {
CircuitBreakerRegistryEntry entry = globalMap.get(method);
if (entry.getFailures().size() == entry.getFailureThreshold()) {
entry.getFailures().remove(0);
}
long now = System.currentTimeMillis();
entry.getFailures().add(Long.valueOf(now));
if (!entry.getStatus().equals(CircuitBreakerStatus.OPEN) && entry.getFailures().size() == entry.getFailureThreshold()
&& now - entry.getFailures().get(0) <= entry.getFailureThresholdTimeFrameMs()) {
// open condition is full filled
entry.setStatus(CircuitBreakerStatus.OPEN);
entry.setLastOpenedTime(now);
entry.getFailures().clear();
return true;
}
return false;
}
public void cleanUp(String method) {
// a Lock can be hold multiple times in a thread that's why we use an while loop here.
while (exclusiveHalfOpenLock.isHeldByCurrentThread()){
exclusiveHalfOpenLock.unlock();
logger.error("exclusiveHalfOpenLock was not properly unlocked");
}
threadLocalMap.get().remove(method);
}
public List<Class<? extends Throwable>> getfailureIndications(String method) {
return globalMap.get(method).getFailureIndications();
}
// needed only for unit tests
CircuitBreakerRegistryEntry getEntry(String method) {
return globalMap.get(method);
}
}