/**
* Dianping.com Inc.
* Copyright (c) 2003-2013 All Rights Reserved.
*/
package com.dianping.pigeon.remoting.invoker.process;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import com.dianping.pigeon.monitor.MonitorTransaction;
import com.dianping.pigeon.remoting.common.util.InvocationUtils;
import org.springframework.util.CollectionUtils;
import com.dianping.pigeon.config.ConfigChangeListener;
import com.dianping.pigeon.config.ConfigManager;
import com.dianping.pigeon.config.ConfigManagerLoader;
import com.dianping.pigeon.log.Logger;
import com.dianping.pigeon.log.LoggerLoader;
import com.dianping.pigeon.monitor.Monitor;
import com.dianping.pigeon.monitor.MonitorLoader;
import com.dianping.pigeon.remoting.common.exception.RejectedException;
import com.dianping.pigeon.remoting.invoker.domain.InvokerContext;
import com.dianping.pigeon.remoting.invoker.exception.RemoteInvocationException;
import com.dianping.pigeon.remoting.invoker.exception.RequestTimeoutException;
import com.dianping.pigeon.remoting.invoker.exception.ServiceDegradedException;
import com.dianping.pigeon.remoting.invoker.exception.ServiceUnavailableException;
import com.dianping.pigeon.remoting.invoker.process.filter.DegradationFilter;
import com.dianping.pigeon.remoting.invoker.route.quality.RequestQualityManager;
import com.dianping.pigeon.remoting.invoker.route.quality.RequestQualityManager.Quality;
import com.dianping.pigeon.threadpool.DefaultThreadFactory;
/**
* @author xiangwu
*
*/
public enum DegradationManager {
INSTANCE;
private static final Logger logger = LoggerLoader.getLogger(DegradationManager.class);
private static final ConfigManager configManager = ConfigManagerLoader.getConfigManager();
private static ConcurrentHashMap<String, ConcurrentHashMap<Integer, Count>> requestSecondCountMap = new ConcurrentHashMap<String, ConcurrentHashMap<Integer, Count>>();
private static volatile Map<String, Count> requestCountMap = null;
private static final String KEY_DEGRADE_FORCE = "pigeon.invoker.degrade.force";
private static final String KEY_DEGRADE_FAILURE = "pigeon.invoker.degrade.failure";
private static final String KEY_DEGRADE_AUTO = "pigeon.invoker.degrade.auto";
private static final String KEY_DEGRADE_RECOVER_PERCENT = "pigeon.invoker.degrade.recover.percent";
private static final String KEY_DEGRADE_RECOVER_INTERVAL = "pigeon.invoker.degrade.recover.interval";
private static final String KEY_DEGRADE_THRESHOLD_INVOKE = "pigeon.invoker.degrade.threshold.invoke";
private static final String KEY_DEGRADE_THRESHOLD_TOTAL = "pigeon.invoker.degrade.threshold.total";
private static final String KEY_DEGRADE_PERCENT_MAX = "pigeon.invoker.degrade.percent.max";
private static final String KEY_DEGRADE_CHECK_SECONDS = "pigeon.invoker.degrade.check.seconds";
private static final String KEY_DEGRADE_CHECK_INTERVAL = "pigeon.invoker.degrade.check.interval";
private static final String KEY_DEGRADE_LOG_ENABLE = "pigeon.invoker.degrade.log.enable";
private static final ExecutorService checkThreadPool = Executors.newFixedThreadPool(1,
new DefaultThreadFactory("Pigeon-Client-Degrade-Checker"));
private static final Random random = new Random();
private final Monitor monitor = MonitorLoader.getMonitor();
private static volatile boolean isForceDegrade = configManager.getBooleanValue(KEY_DEGRADE_FORCE, false);
private static volatile boolean isAutoDegrade = configManager.getBooleanValue(KEY_DEGRADE_AUTO, false);
private static volatile boolean isFailureDegrade = configManager.getBooleanValue(KEY_DEGRADE_FAILURE, false);
private static volatile int degradeTotalThreshold = configManager.getIntValue(KEY_DEGRADE_THRESHOLD_TOTAL, 100);
private static volatile int degradeInvokeThreshold = configManager.getIntValue(KEY_DEGRADE_THRESHOLD_INVOKE, 2);
private static volatile float degradeRecoverPercent = configManager.getFloatValue(KEY_DEGRADE_RECOVER_PERCENT, 1);
private static volatile int degradeRecoverInterval = configManager.getIntValue(KEY_DEGRADE_RECOVER_INTERVAL, 10);
private static volatile float degradePercentMax = configManager.getFloatValue(KEY_DEGRADE_PERCENT_MAX, 99.90f);
private static volatile int degradeCheckSeconds = configManager.getIntValue(KEY_DEGRADE_CHECK_SECONDS, 10);
private static volatile int degradeCheckInterval = configManager.getIntValue(KEY_DEGRADE_CHECK_INTERVAL, 2);
private static volatile boolean isLogDegrade = configManager.getBooleanValue(KEY_DEGRADE_LOG_ENABLE, false);
private DegradationManager() {
ConfigManagerLoader.getConfigManager().registerConfigChangeListener(new InnerConfigChangeListener());
}
private static class InnerConfigChangeListener implements ConfigChangeListener {
@Override
public void onKeyUpdated(String key, String value) {
try {
if (key.endsWith(KEY_DEGRADE_FORCE)) {
isForceDegrade = Boolean.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_AUTO)) {
isAutoDegrade = Boolean.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_FAILURE)) {
isFailureDegrade = Boolean.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_THRESHOLD_TOTAL)) {
degradeTotalThreshold = Integer.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_THRESHOLD_INVOKE)) {
degradeInvokeThreshold = Integer.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_RECOVER_PERCENT)) {
degradeRecoverPercent = Float.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_RECOVER_INTERVAL)) {
degradeRecoverInterval = Integer.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_PERCENT_MAX)) {
degradePercentMax = Float.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_CHECK_SECONDS)) {
degradeCheckSeconds = Integer.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_CHECK_INTERVAL)) {
degradeCheckInterval = Integer.valueOf(value);
} else if (key.endsWith(KEY_DEGRADE_LOG_ENABLE)) {
isLogDegrade = Boolean.valueOf(value);
}
} catch (RuntimeException e) {
logger.warn("invalid value for key " + key, e);
}
}
@Override
public void onKeyAdded(String key, String value) {
}
@Override
public void onKeyRemoved(String key) {
}
}
static {
checkThreadPool.execute(new Checker());
}
public String getRequestUrl(InvokerContext context) {
return context.getInvokerConfig().getUrl() + "#" + context.getMethodName();
}
public boolean needDegrade(InvokerContext context) {
if (degradationIsEnable(context)) {
if (isForceDegrade) {
return true;
}
if (isAutoDegrade) {
if (!CollectionUtils.isEmpty(requestCountMap)) {
String requestUrl = getRequestUrl(context);
Count count = requestCountMap.get(requestUrl);
if (count != null) {
if (count.getTotalValue() >= degradeTotalThreshold) {
if ((count.getTotalValue() - count.getDegradedValue()) > degradeInvokeThreshold
&& count.getFailedPercent() < degradeRecoverPercent) {
return random(count.getDegradedPercent() - degradeRecoverInterval);
} else if (count.getFailedPercent() >= degradeRecoverPercent) {
return random(degradePercentMax);
}
}
}
}
}
if (isFailureDegrade) {
return false;
}
}
return false;
}
public boolean needFailureDegrade(InvokerContext context) {
return degradationIsEnable(context)
&& (isAutoDegrade || isFailureDegrade);
}
private boolean degradationIsEnable(InvokerContext context) {
DegradationFilter.DegradeAction action = DegradationFilter.getDegradeMethodActions()
.get(getRequestUrl(context));
return action != null && action.getEnable();
}
private boolean random(float percent) {
return random.nextInt(10000) < percent * 100;
}
public void addFailedRequest(InvokerContext context, Throwable t) {
if (t instanceof ServiceUnavailableException || t instanceof RequestTimeoutException
|| t instanceof RemoteInvocationException || t instanceof RejectedException) {
addRequest(context, t, false);
}
}
public void addDegradedRequest(InvokerContext context, Throwable t) {
addRequest(context, null, true);
if (isLogDegrade && !(t instanceof ServiceDegradedException)) {
ServiceDegradedException ex = new ServiceDegradedException(getRequestUrl(context), t);
ex.setStackTrace(new StackTraceElement[] {});
monitor.logError(ex);
}
}
private void addRequest(InvokerContext context, Throwable t, boolean degraded) {
if (isAutoDegrade || isForceDegrade) {
int currentSecond = Calendar.getInstance().get(Calendar.SECOND);
String requestUrl = getRequestUrl(context);
ConcurrentHashMap<Integer, Count> secondCount = requestSecondCountMap.get(requestUrl);
if (secondCount == null) {
secondCount = new ConcurrentHashMap<Integer, Count>();
ConcurrentHashMap<Integer, Count> last = requestSecondCountMap.putIfAbsent(requestUrl, secondCount);
if (last != null) {
secondCount = last;
}
}
Count count = secondCount.get(currentSecond);
if (count == null) {
count = new Count(0, 0, 0);
Count last = secondCount.putIfAbsent(currentSecond, count);
if (last != null) {
count = last;
}
}
count.total.incrementAndGet();
if (t != null) {
count.failed.incrementAndGet();
}
if (degraded) {
count.degraded.incrementAndGet();
}
}
}
public void monitorDegrade(InvokerContext context, MonitorTransaction transaction) {
String callInterface = InvocationUtils.getRemoteCallFullName(context.getInvokerConfig().getUrl(),
context.getMethodName(), context.getParameterTypes());
if (context.getDegradeInfo().isDegrade()
&& !context.getDegradeInfo().isFailureDegrade()) {
transaction.logEvent("PigeonCall.degrade", callInterface, "");
} else if (context.getDegradeInfo().isFailureDegrade()) {
transaction.logEvent("PigeonCall.failureDegrade", callInterface,
context.getDegradeInfo().getCause() == null ?
"" : context.getDegradeInfo().getCause().toString());
}
}
public static class DegradeActionConfig implements Serializable {
private static final long serialVersionUID = 1L;
private String returnClass;
private String componentClass;
private String keyClass;
private String valueClass;
private String content;
private boolean throwException = false;
private boolean useMockClass = false;
private boolean useGroovyScript = false;
private boolean enable = true;
public boolean getEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
public boolean getUseGroovyScript() {
return useGroovyScript;
}
public void setUseGroovyScript(boolean useGroovyScript) {
this.useGroovyScript = useGroovyScript;
}
public boolean getUseMockClass() {
return useMockClass;
}
public void setUseMockClass(boolean useMockClass) {
this.useMockClass = useMockClass;
}
public boolean getThrowException() {
return throwException;
}
public void setThrowException(boolean throwException) {
this.throwException = throwException;
}
public DegradeActionConfig() {
}
public String getReturnClass() {
return returnClass;
}
public void setReturnClass(String returnClass) {
this.returnClass = returnClass;
}
public String getComponentClass() {
return componentClass;
}
public void setComponentClass(String componentClass) {
this.componentClass = componentClass;
}
public String getKeyClass() {
return keyClass;
}
public void setKeyClass(String keyClass) {
this.keyClass = keyClass;
}
public String getValueClass() {
return valueClass;
}
public void setValueClass(String valueClass) {
this.valueClass = valueClass;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
private static class Count {
private AtomicInteger failed = new AtomicInteger();
private AtomicInteger total = new AtomicInteger();
private AtomicInteger degraded = new AtomicInteger();
public Count() {
}
public Count(int total, int failed, int degraded) {
this.total.set(total);
this.failed.set(failed);
this.degraded.set(degraded);
}
public AtomicInteger getFailed() {
return failed;
}
public int getFailedValue() {
return failed.get();
}
public void setFailed(int failed) {
this.failed.set(failed);
}
public AtomicInteger getDegraded() {
return degraded;
}
public int getDegradedValue() {
return degraded.get();
}
public void setDegraded(int degraded) {
this.degraded.set(degraded);
}
public AtomicInteger getTotal() {
return total;
}
public int getTotalValue() {
return total.get();
}
public void setTotal(int total) {
this.total.set(total);
}
public Count merge(Count count) {
Count n = new Count();
n.total.set(this.total.get() + count.total.get());
n.failed.set(this.failed.get() + count.failed.get());
n.degraded.set(this.degraded.get() + count.degraded.get());
return n;
}
public float getFailedPercent() {
int m = (total.get() - degraded.get());
if (total.get() > 0 && m > 0) {
return failed.get() * 100 / m;
} else {
return 0;
}
}
public float getDegradedPercent() {
if (total.get() > 0) {
return degraded.get() * 100 / total.get();
} else {
return 0;
}
}
public void clear() {
total.set(0);
failed.set(0);
degraded.set(0);
}
}
static class Checker implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000 * degradeCheckInterval);
checkRequestSecondCount();
} catch (Exception e) {
logger.error("", e);
}
}
}
private void checkRequestSecondCount() {
Map<String, Count> countMap = new ConcurrentHashMap<String, Count>();
final int recentSeconds = degradeCheckSeconds;
final int currentSecond = Calendar.getInstance().get(Calendar.SECOND);
for (String url : requestSecondCountMap.keySet()) {
Map<Integer, Count> secondCount = requestSecondCountMap.get(url);
int total = 0, failed = 0, degraded = 0;
for (int i = 1; i <= recentSeconds; i++) {
int prevSec = currentSecond - i;
prevSec = prevSec >= 0 ? prevSec : prevSec + 60;
Count ct = secondCount.get(prevSec);
if (ct != null) {
total += ct.getTotalValue();
failed += ct.getFailedValue();
degraded += ct.getDegradedValue();
}
}
countMap.put(url, new Count(total, failed, degraded));
// clear previous seconds
for (int i = recentSeconds + 1; i <= recentSeconds + 20; i++) {
int prevSec = currentSecond - i;
prevSec = prevSec >= 0 ? prevSec : prevSec + 60;
Count ct = secondCount.get(prevSec);
if (ct != null) {
ct.clear();
}
}
}
Map<String, Count> old = requestCountMap;
requestCountMap = countMap;
if (old != null) {
old.clear();
old = null;
}
// 复用降级统计和清空的线程,用于服务质量统计和清空(窗口默认为10秒)
ConcurrentMap<String, ConcurrentMap<String, ConcurrentMap<Integer, Quality>>> addrReqUrlSecondQualities = RequestQualityManager.INSTANCE
.getAddrReqUrlSecondQualities();
ConcurrentMap<String, ConcurrentMap<String, Quality>> addrReqUrlQualities = new ConcurrentHashMap<String, ConcurrentMap<String, Quality>>();
for (String address : addrReqUrlSecondQualities.keySet()) {
ConcurrentMap<String, ConcurrentMap<Integer, Quality>> reqUrlSecondQualities = addrReqUrlSecondQualities
.get(address);
ConcurrentHashMap<String, Quality> reqUrlQualities = new ConcurrentHashMap<String, Quality>();
for (String requestUrl : reqUrlSecondQualities.keySet()) {
ConcurrentMap<Integer, Quality> secondQualities = reqUrlSecondQualities.get(requestUrl);
int total = 0, failed = 0;
for (int i = 1; i <= recentSeconds; i++) {
int prevSec = currentSecond - i;
prevSec = prevSec >= 0 ? prevSec : prevSec + 60;
Quality quality = secondQualities.get(prevSec);
if (quality != null) {
total += quality.getTotalValue();
failed += quality.getFailedValue();
}
}
reqUrlQualities.put(requestUrl, new Quality(total, failed));
// clear previous seconds
for (int i = recentSeconds + 1; i <= recentSeconds + 20; i++) {
int prevSec = currentSecond - i;
prevSec = prevSec >= 0 ? prevSec : prevSec + 60;
Quality quality = secondQualities.get(prevSec);
if (quality != null) {
quality.clear();
}
}
}
addrReqUrlQualities.put(address, reqUrlQualities);
}
RequestQualityManager.INSTANCE.setAddrReqUrlQualities(addrReqUrlQualities);
}
}
}