* Add `CircuitBreaker` abstraction (with half-open state) and add circuit breaker state change event observer support. * Improve circuit breaking strategy (avg RT → slow request ratio) and make statistics of each rule dependent (to support arbitrary statistic interval). * Add simple "trial" mechanism (aka. half-open). * Refactor mechanism of metric recording and state change handling for circuit breakers: record RT and error when requests have completed (i.e. `onExit`, based on #1420). Signed-off-by: Eric Zhao <sczyh16@gmail.com>master
@@ -15,19 +15,12 @@ | |||||
*/ | */ | ||||
package com.alibaba.csp.sentinel.slots.block.degrade; | package com.alibaba.csp.sentinel.slots.block.degrade; | ||||
import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory; | |||||
import com.alibaba.csp.sentinel.context.Context; | import com.alibaba.csp.sentinel.context.Context; | ||||
import com.alibaba.csp.sentinel.node.ClusterNode; | |||||
import com.alibaba.csp.sentinel.node.DefaultNode; | import com.alibaba.csp.sentinel.node.DefaultNode; | ||||
import com.alibaba.csp.sentinel.slots.block.AbstractRule; | import com.alibaba.csp.sentinel.slots.block.AbstractRule; | ||||
import com.alibaba.csp.sentinel.slots.block.RuleConstant; | import com.alibaba.csp.sentinel.slots.block.RuleConstant; | ||||
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; | |||||
import java.util.concurrent.Executors; | |||||
import java.util.concurrent.ScheduledExecutorService; | |||||
import java.util.concurrent.TimeUnit; | |||||
import java.util.concurrent.atomic.AtomicBoolean; | |||||
import java.util.concurrent.atomic.AtomicLong; | |||||
import java.util.Objects; | |||||
/** | /** | ||||
* <p> | * <p> | ||||
@@ -52,13 +45,10 @@ import java.util.concurrent.atomic.AtomicLong; | |||||
* </ul> | * </ul> | ||||
* | * | ||||
* @author jialiang.linjl | * @author jialiang.linjl | ||||
* @author Eric Zhao | |||||
*/ | */ | ||||
public class DegradeRule extends AbstractRule { | public class DegradeRule extends AbstractRule { | ||||
@SuppressWarnings("PMD.ThreadPoolCreationRule") | |||||
private static ScheduledExecutorService pool = Executors.newScheduledThreadPool( | |||||
Runtime.getRuntime().availableProcessors(), new NamedThreadFactory("sentinel-degrade-reset-task", true)); | |||||
public DegradeRule() {} | public DegradeRule() {} | ||||
public DegradeRule(String resourceName) { | public DegradeRule(String resourceName) { | ||||
@@ -66,33 +56,34 @@ public class DegradeRule extends AbstractRule { | |||||
} | } | ||||
/** | /** | ||||
* RT threshold or exception ratio threshold count. | |||||
* Circuit breaking strategy (0: average RT, 1: exception ratio, 2: exception count). | |||||
*/ | */ | ||||
private double count; | |||||
private int grade = RuleConstant.DEGRADE_GRADE_RT; | |||||
/** | /** | ||||
* Degrade recover timeout (in seconds) when degradation occurs. | |||||
* Threshold count. | |||||
*/ | */ | ||||
private int timeWindow; | |||||
private double count; | |||||
/** | /** | ||||
* Degrade strategy (0: average RT, 1: exception ratio, 2: exception count). | |||||
* Recovery timeout (in seconds) when circuit breaker opens. After the timeout, the circuit breaker will | |||||
* transform to half-open state for trying a few requests. | |||||
*/ | */ | ||||
private int grade = RuleConstant.DEGRADE_GRADE_RT; | |||||
private int timeWindow; | |||||
/** | /** | ||||
* Minimum number of consecutive slow requests that can trigger RT circuit breaking. | |||||
* Minimum number of requests (in an active statistic time span) that can trigger circuit breaking. | |||||
* | * | ||||
* @since 1.7.0 | * @since 1.7.0 | ||||
*/ | */ | ||||
private int rtSlowRequestAmount = RuleConstant.DEGRADE_DEFAULT_SLOW_REQUEST_AMOUNT; | |||||
private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT; | |||||
/** | /** | ||||
* Minimum number of requests (in an active statistic time span) that can trigger circuit breaking. | |||||
* | |||||
* @since 1.7.0 | |||||
* The threshold of slow request ratio in RT mode. | |||||
*/ | */ | ||||
private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT; | |||||
private double slowRatioThreshold = 1.0d; | |||||
private int statIntervalMs = 1000; | |||||
public int getGrade() { | public int getGrade() { | ||||
return grade; | return grade; | ||||
@@ -121,21 +112,30 @@ public class DegradeRule extends AbstractRule { | |||||
return this; | return this; | ||||
} | } | ||||
public int getRtSlowRequestAmount() { | |||||
return rtSlowRequestAmount; | |||||
public int getMinRequestAmount() { | |||||
return minRequestAmount; | |||||
} | } | ||||
public DegradeRule setRtSlowRequestAmount(int rtSlowRequestAmount) { | |||||
this.rtSlowRequestAmount = rtSlowRequestAmount; | |||||
public DegradeRule setMinRequestAmount(int minRequestAmount) { | |||||
this.minRequestAmount = minRequestAmount; | |||||
return this; | return this; | ||||
} | } | ||||
public int getMinRequestAmount() { | |||||
return minRequestAmount; | |||||
public double getSlowRatioThreshold() { | |||||
return slowRatioThreshold; | |||||
} | } | ||||
public DegradeRule setMinRequestAmount(int minRequestAmount) { | |||||
this.minRequestAmount = minRequestAmount; | |||||
public DegradeRule setSlowRatioThreshold(double slowRatioThreshold) { | |||||
this.slowRatioThreshold = slowRatioThreshold; | |||||
return this; | |||||
} | |||||
public int getStatIntervalMs() { | |||||
return statIntervalMs; | |||||
} | |||||
public DegradeRule setStatIntervalMs(int statIntervalMs) { | |||||
this.statIntervalMs = statIntervalMs; | |||||
return this; | return this; | ||||
} | } | ||||
@@ -144,23 +144,19 @@ public class DegradeRule extends AbstractRule { | |||||
if (this == o) { return true; } | if (this == o) { return true; } | ||||
if (o == null || getClass() != o.getClass()) { return false; } | if (o == null || getClass() != o.getClass()) { return false; } | ||||
if (!super.equals(o)) { return false; } | if (!super.equals(o)) { return false; } | ||||
DegradeRule that = (DegradeRule) o; | |||||
return Double.compare(that.count, count) == 0 && | |||||
timeWindow == that.timeWindow && | |||||
grade == that.grade && | |||||
rtSlowRequestAmount == that.rtSlowRequestAmount && | |||||
minRequestAmount == that.minRequestAmount; | |||||
DegradeRule rule = (DegradeRule)o; | |||||
return Double.compare(rule.count, count) == 0 && | |||||
timeWindow == rule.timeWindow && | |||||
grade == rule.grade && | |||||
minRequestAmount == rule.minRequestAmount && | |||||
Double.compare(rule.slowRatioThreshold, slowRatioThreshold) == 0 && | |||||
statIntervalMs == rule.statIntervalMs; | |||||
} | } | ||||
@Override | @Override | ||||
public int hashCode() { | public int hashCode() { | ||||
int result = super.hashCode(); | |||||
result = 31 * result + new Double(count).hashCode(); | |||||
result = 31 * result + timeWindow; | |||||
result = 31 * result + grade; | |||||
result = 31 * result + rtSlowRequestAmount; | |||||
result = 31 * result + minRequestAmount; | |||||
return result; | |||||
return Objects.hash(super.hashCode(), count, timeWindow, grade, minRequestAmount, | |||||
slowRatioThreshold, statIntervalMs); | |||||
} | } | ||||
@Override | @Override | ||||
@@ -171,84 +167,15 @@ public class DegradeRule extends AbstractRule { | |||||
", count=" + count + | ", count=" + count + | ||||
", limitApp=" + getLimitApp() + | ", limitApp=" + getLimitApp() + | ||||
", timeWindow=" + timeWindow + | ", timeWindow=" + timeWindow + | ||||
", rtSlowRequestAmount=" + rtSlowRequestAmount + | |||||
", minRequestAmount=" + minRequestAmount + | ", minRequestAmount=" + minRequestAmount + | ||||
"}"; | |||||
", slowRatioThreshold=" + slowRatioThreshold + | |||||
", statIntervalMs=" + statIntervalMs + | |||||
'}'; | |||||
} | } | ||||
// Internal implementation (will be deprecated and moved outside). | |||||
private AtomicLong passCount = new AtomicLong(0); | |||||
private final AtomicBoolean cut = new AtomicBoolean(false); | |||||
@Override | @Override | ||||
public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) { | |||||
if (cut.get()) { | |||||
return false; | |||||
} | |||||
ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource()); | |||||
if (clusterNode == null) { | |||||
return true; | |||||
} | |||||
if (grade == RuleConstant.DEGRADE_GRADE_RT) { | |||||
double rt = clusterNode.avgRt(); | |||||
if (rt < this.count) { | |||||
passCount.set(0); | |||||
return true; | |||||
} | |||||
// Sentinel will degrade the service only if count exceeds. | |||||
if (passCount.incrementAndGet() < rtSlowRequestAmount) { | |||||
return true; | |||||
} | |||||
} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { | |||||
double exception = clusterNode.exceptionQps(); | |||||
double success = clusterNode.successQps(); | |||||
double total = clusterNode.totalQps(); | |||||
// If total amount is less than minRequestAmount, the request will pass. | |||||
if (total < minRequestAmount) { | |||||
return true; | |||||
} | |||||
// In the same aligned statistic time window, | |||||
// "success" (aka. completed count) = exception count + non-exception count (realSuccess) | |||||
double realSuccess = success - exception; | |||||
if (realSuccess <= 0 && exception < minRequestAmount) { | |||||
return true; | |||||
} | |||||
if (exception / success < count) { | |||||
return true; | |||||
} | |||||
} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) { | |||||
double exception = clusterNode.totalException(); | |||||
if (exception < count) { | |||||
return true; | |||||
} | |||||
} | |||||
if (cut.compareAndSet(false, true)) { | |||||
ResetTask resetTask = new ResetTask(this); | |||||
pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS); | |||||
} | |||||
@Deprecated | |||||
public boolean passCheck(Context context, DefaultNode node, int count, Object... args) { | |||||
return false; | return false; | ||||
} | } | ||||
private static final class ResetTask implements Runnable { | |||||
private DegradeRule rule; | |||||
ResetTask(DegradeRule rule) { | |||||
this.rule = rule; | |||||
} | |||||
@Override | |||||
public void run() { | |||||
rule.passCount.set(0); | |||||
rule.cut.set(false); | |||||
} | |||||
} | |||||
} | } |
@@ -21,29 +21,29 @@ import java.util.HashSet; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.concurrent.ConcurrentHashMap; | |||||
import com.alibaba.csp.sentinel.config.SentinelConfig; | |||||
import com.alibaba.csp.sentinel.context.Context; | |||||
import com.alibaba.csp.sentinel.log.RecordLog; | import com.alibaba.csp.sentinel.log.RecordLog; | ||||
import com.alibaba.csp.sentinel.node.DefaultNode; | |||||
import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; | import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; | ||||
import com.alibaba.csp.sentinel.property.PropertyListener; | import com.alibaba.csp.sentinel.property.PropertyListener; | ||||
import com.alibaba.csp.sentinel.property.SentinelProperty; | import com.alibaba.csp.sentinel.property.SentinelProperty; | ||||
import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; | |||||
import com.alibaba.csp.sentinel.slots.block.BlockException; | |||||
import com.alibaba.csp.sentinel.slots.block.RuleConstant; | import com.alibaba.csp.sentinel.slots.block.RuleConstant; | ||||
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreaker; | |||||
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.ExceptionCircuitBreaker; | |||||
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.ResponseTimeCircuitBreaker; | |||||
import com.alibaba.csp.sentinel.util.AssertUtil; | import com.alibaba.csp.sentinel.util.AssertUtil; | ||||
import com.alibaba.csp.sentinel.util.StringUtil; | import com.alibaba.csp.sentinel.util.StringUtil; | ||||
/** | /** | ||||
* The rule manager for circuit breaking rules ({@link DegradeRule}). | |||||
* | |||||
* @author youji.zj | * @author youji.zj | ||||
* @author jialiang.linjl | * @author jialiang.linjl | ||||
* @author Eric Zhao | * @author Eric Zhao | ||||
*/ | */ | ||||
public final class DegradeRuleManager { | public final class DegradeRuleManager { | ||||
private static final Map<String, Set<DegradeRule>> degradeRules = new ConcurrentHashMap<>(); | |||||
private static volatile Map<String, List<CircuitBreaker>> circuitBreakers = new HashMap<>(); | |||||
private static volatile Map<String, Set<DegradeRule>> ruleMap = new HashMap<>(); | |||||
private static final RulePropertyListener LISTENER = new RulePropertyListener(); | private static final RulePropertyListener LISTENER = new RulePropertyListener(); | ||||
private static SentinelProperty<List<DegradeRule>> currentProperty | private static SentinelProperty<List<DegradeRule>> currentProperty | ||||
@@ -69,41 +69,37 @@ public final class DegradeRuleManager { | |||||
} | } | ||||
} | } | ||||
public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count) | |||||
throws BlockException { | |||||
Set<DegradeRule> rules = degradeRules.get(resource.getName()); | |||||
if (rules == null) { | |||||
return; | |||||
} | |||||
for (DegradeRule rule : rules) { | |||||
if (!rule.passCheck(context, node, count)) { | |||||
throw new DegradeException(rule.getLimitApp(), rule); | |||||
} | |||||
} | |||||
static List<CircuitBreaker> getCircuitBreakers(String resourceName) { | |||||
return circuitBreakers.get(resourceName); | |||||
} | } | ||||
public static boolean hasConfig(String resource) { | public static boolean hasConfig(String resource) { | ||||
if (resource == null) { | if (resource == null) { | ||||
return false; | return false; | ||||
} | } | ||||
return degradeRules.containsKey(resource); | |||||
return circuitBreakers.containsKey(resource); | |||||
} | } | ||||
/** | /** | ||||
* Get a copy of the rules. | |||||
* <p>Get existing circuit breaking rules.</p> | |||||
* <p>Note: DO NOT modify the rules from the returned list directly. | |||||
* The behavior is <strong>undefined</strong>.</p> | |||||
* | * | ||||
* @return a new copy of the rules. | |||||
* @return list of existing circuit breaking rules, or empty list if no rules were loaded | |||||
*/ | */ | ||||
public static List<DegradeRule> getRules() { | public static List<DegradeRule> getRules() { | ||||
List<DegradeRule> rules = new ArrayList<>(); | List<DegradeRule> rules = new ArrayList<>(); | ||||
for (Map.Entry<String, Set<DegradeRule>> entry : degradeRules.entrySet()) { | |||||
for (Map.Entry<String, Set<DegradeRule>> entry : ruleMap.entrySet()) { | |||||
rules.addAll(entry.getValue()); | rules.addAll(entry.getValue()); | ||||
} | } | ||||
return rules; | return rules; | ||||
} | } | ||||
public static Set<DegradeRule> getRulesOfResource(String resource) { | |||||
AssertUtil.assertNotBlank(resource, "resource name cannot be blank"); | |||||
return ruleMap.get(resource); | |||||
} | |||||
/** | /** | ||||
* Load {@link DegradeRule}s, former rules will be replaced. | * Load {@link DegradeRule}s, former rules will be replaced. | ||||
* | * | ||||
@@ -113,7 +109,7 @@ public final class DegradeRuleManager { | |||||
try { | try { | ||||
currentProperty.updateValue(rules); | currentProperty.updateValue(rules); | ||||
} catch (Throwable e) { | } catch (Throwable e) { | ||||
RecordLog.warn("[DegradeRuleManager] Unexpected error when loading degrade rules", e); | |||||
RecordLog.error("[DegradeRuleManager] Unexpected error when loading degrade rules", e); | |||||
} | } | ||||
} | } | ||||
@@ -128,7 +124,7 @@ public final class DegradeRuleManager { | |||||
public static boolean setRulesForResource(String resourceName, Set<DegradeRule> rules) { | public static boolean setRulesForResource(String resourceName, Set<DegradeRule> rules) { | ||||
AssertUtil.notEmpty(resourceName, "resourceName cannot be empty"); | AssertUtil.notEmpty(resourceName, "resourceName cannot be empty"); | ||||
try { | try { | ||||
Map<String, Set<DegradeRule>> newRuleMap = new HashMap<>(degradeRules); | |||||
Map<String, Set<DegradeRule>> newRuleMap = new HashMap<>(ruleMap); | |||||
if (rules == null) { | if (rules == null) { | ||||
newRuleMap.remove(resourceName); | newRuleMap.remove(resourceName); | ||||
} else { | } else { | ||||
@@ -146,88 +142,127 @@ public final class DegradeRuleManager { | |||||
} | } | ||||
return currentProperty.updateValue(allRules); | return currentProperty.updateValue(allRules); | ||||
} catch (Throwable e) { | } catch (Throwable e) { | ||||
RecordLog.warn( | |||||
"[DegradeRuleManager] Unexpected error when setting degrade rules for resource: " + resourceName, e); | |||||
RecordLog.error("[DegradeRuleManager] Unexpected error when setting circuit breaking" | |||||
+ " rules for resource: " + resourceName, e); | |||||
return false; | |||||
} | |||||
} | |||||
private static CircuitBreaker getExistingSameCbOrNew(/*@Valid*/ DegradeRule rule) { | |||||
List<CircuitBreaker> cbs = getCircuitBreakers(rule.getResource()); | |||||
if (cbs == null || cbs.isEmpty()) { | |||||
return newCircuitBreakerFrom(rule); | |||||
} | |||||
for (CircuitBreaker cb : cbs) { | |||||
if (rule.equals(cb.getRule())) { | |||||
// Reuse the circuit breaker if the rule remains unchanged. | |||||
return cb; | |||||
} | |||||
} | |||||
return newCircuitBreakerFrom(rule); | |||||
} | |||||
/** | |||||
* Create a circuit breaker instance from provided circuit breaking rule. | |||||
* | |||||
* @param rule a valid circuit breaking rule | |||||
* @return new circuit breaker based on provided rule; null if rule is invalid or unsupported type | |||||
*/ | |||||
private static CircuitBreaker newCircuitBreakerFrom(/*@Valid*/ DegradeRule rule) { | |||||
switch (rule.getGrade()) { | |||||
case RuleConstant.DEGRADE_GRADE_RT: | |||||
return new ResponseTimeCircuitBreaker(rule); | |||||
case RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO: | |||||
case RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT: | |||||
return new ExceptionCircuitBreaker(rule); | |||||
default: | |||||
return null; | |||||
} | |||||
} | |||||
public static boolean isValidRule(DegradeRule rule) { | |||||
boolean baseValid = rule != null && !StringUtil.isBlank(rule.getResource()) | |||||
&& rule.getCount() >= 0 && rule.getTimeWindow() > 0; | |||||
if (!baseValid) { | |||||
return false; | |||||
} | |||||
if (rule.getMinRequestAmount() <= 0 || rule.getStatIntervalMs() <= 0) { | |||||
return false; | return false; | ||||
} | } | ||||
switch (rule.getGrade()) { | |||||
case RuleConstant.DEGRADE_GRADE_RT: | |||||
return rule.getSlowRatioThreshold() >= 0 && rule.getSlowRatioThreshold() <= 1; | |||||
case RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO: | |||||
return rule.getCount() <= 1; | |||||
case RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT: | |||||
return true; | |||||
default: | |||||
return false; | |||||
} | |||||
} | } | ||||
private static class RulePropertyListener implements PropertyListener<List<DegradeRule>> { | private static class RulePropertyListener implements PropertyListener<List<DegradeRule>> { | ||||
private synchronized void reloadFrom(List<DegradeRule> list) { | |||||
Map<String, List<CircuitBreaker>> cbs = buildCircuitBreakers(list); | |||||
Map<String, Set<DegradeRule>> rm = new HashMap<>(cbs.size()); | |||||
for (Map.Entry<String, List<CircuitBreaker>> e : cbs.entrySet()) { | |||||
assert e.getValue() != null && !e.getValue().isEmpty(); | |||||
Set<DegradeRule> rules = new HashSet<>(e.getValue().size()); | |||||
for (CircuitBreaker cb : e.getValue()) { | |||||
rules.add(cb.getRule()); | |||||
} | |||||
rm.put(e.getKey(), rules); | |||||
} | |||||
DegradeRuleManager.circuitBreakers = cbs; | |||||
DegradeRuleManager.ruleMap = rm; | |||||
} | |||||
@Override | @Override | ||||
public void configUpdate(List<DegradeRule> conf) { | public void configUpdate(List<DegradeRule> conf) { | ||||
Map<String, Set<DegradeRule>> rules = loadDegradeConf(conf); | |||||
if (rules != null) { | |||||
degradeRules.clear(); | |||||
degradeRules.putAll(rules); | |||||
} | |||||
RecordLog.info("[DegradeRuleManager] Degrade rules received: " + degradeRules); | |||||
reloadFrom(conf); | |||||
RecordLog.info("[DegradeRuleManager] Degrade rules has been updated to: " + ruleMap); | |||||
} | } | ||||
@Override | @Override | ||||
public void configLoad(List<DegradeRule> conf) { | public void configLoad(List<DegradeRule> conf) { | ||||
Map<String, Set<DegradeRule>> rules = loadDegradeConf(conf); | |||||
if (rules != null) { | |||||
degradeRules.clear(); | |||||
degradeRules.putAll(rules); | |||||
} | |||||
RecordLog.info("[DegradeRuleManager] Degrade rules loaded: " + degradeRules); | |||||
reloadFrom(conf); | |||||
RecordLog.info("[DegradeRuleManager] Degrade rules loaded: " + ruleMap); | |||||
} | } | ||||
private Map<String, Set<DegradeRule>> loadDegradeConf(List<DegradeRule> list) { | |||||
Map<String, Set<DegradeRule>> newRuleMap = new ConcurrentHashMap<>(); | |||||
private Map<String, List<CircuitBreaker>> buildCircuitBreakers(List<DegradeRule> list) { | |||||
Map<String, List<CircuitBreaker>> cbMap = new HashMap<>(8); | |||||
if (list == null || list.isEmpty()) { | if (list == null || list.isEmpty()) { | ||||
return newRuleMap; | |||||
return cbMap; | |||||
} | } | ||||
for (DegradeRule rule : list) { | for (DegradeRule rule : list) { | ||||
if (!isValidRule(rule)) { | if (!isValidRule(rule)) { | ||||
RecordLog.warn( | |||||
"[DegradeRuleManager] Ignoring invalid degrade rule when loading new rules: " + rule); | |||||
RecordLog.warn("[DegradeRuleManager] Ignoring invalid rule when loading new rules: " + rule); | |||||
continue; | continue; | ||||
} | } | ||||
if (StringUtil.isBlank(rule.getLimitApp())) { | if (StringUtil.isBlank(rule.getLimitApp())) { | ||||
rule.setLimitApp(RuleConstant.LIMIT_APP_DEFAULT); | rule.setLimitApp(RuleConstant.LIMIT_APP_DEFAULT); | ||||
} | } | ||||
String identity = rule.getResource(); | |||||
Set<DegradeRule> ruleSet = newRuleMap.get(identity); | |||||
if (ruleSet == null) { | |||||
ruleSet = new HashSet<>(); | |||||
newRuleMap.put(identity, ruleSet); | |||||
CircuitBreaker cb = getExistingSameCbOrNew(rule); | |||||
if (cb == null) { | |||||
RecordLog.warn("[DegradeRuleManager] Unknown circuit breaking strategy, ignoring: " + rule); | |||||
continue; | |||||
} | } | ||||
ruleSet.add(rule); | |||||
} | |||||
return newRuleMap; | |||||
} | |||||
} | |||||
String resourceName = rule.getResource(); | |||||
public static boolean isValidRule(DegradeRule rule) { | |||||
boolean baseValid = rule != null && !StringUtil.isBlank(rule.getResource()) | |||||
&& rule.getCount() >= 0 && rule.getTimeWindow() > 0; | |||||
if (!baseValid) { | |||||
return false; | |||||
} | |||||
int maxAllowedRt = SentinelConfig.statisticMaxRt(); | |||||
if (rule.getGrade() == RuleConstant.DEGRADE_GRADE_RT) { | |||||
if (rule.getRtSlowRequestAmount() <= 0) { | |||||
return false; | |||||
} | |||||
// Warn for RT mode that exceeds the {@code TIME_DROP_VALVE}. | |||||
if (rule.getCount() > maxAllowedRt) { | |||||
RecordLog.warn(String.format("[DegradeRuleManager] WARN: setting large RT threshold (%.1f ms)" | |||||
+ " in RT mode will not take effect since it exceeds the max allowed value (%d ms)", | |||||
rule.getCount(), maxAllowedRt)); | |||||
List<CircuitBreaker> cbList = cbMap.get(resourceName); | |||||
if (cbList == null) { | |||||
cbList = new ArrayList<>(); | |||||
cbMap.put(resourceName, cbList); | |||||
} | |||||
cbList.add(cb); | |||||
} | } | ||||
return cbMap; | |||||
} | } | ||||
// Check exception ratio mode. | |||||
if (rule.getGrade() == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { | |||||
return rule.getCount() <= 1 && rule.getMinRequestAmount() > 0; | |||||
} | |||||
return true; | |||||
} | } | ||||
} | } |
@@ -15,30 +15,73 @@ | |||||
*/ | */ | ||||
package com.alibaba.csp.sentinel.slots.block.degrade; | package com.alibaba.csp.sentinel.slots.block.degrade; | ||||
import java.util.List; | |||||
import com.alibaba.csp.sentinel.Entry; | |||||
import com.alibaba.csp.sentinel.context.Context; | import com.alibaba.csp.sentinel.context.Context; | ||||
import com.alibaba.csp.sentinel.node.DefaultNode; | import com.alibaba.csp.sentinel.node.DefaultNode; | ||||
import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; | import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; | ||||
import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; | import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; | ||||
import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; | import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; | ||||
import com.alibaba.csp.sentinel.slots.block.BlockException; | |||||
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreaker; | |||||
import com.alibaba.csp.sentinel.spi.SpiOrder; | import com.alibaba.csp.sentinel.spi.SpiOrder; | ||||
import com.alibaba.csp.sentinel.util.TimeUtil; | |||||
/** | /** | ||||
* A {@link ProcessorSlot} dedicates to {@link DegradeRule} checking. | |||||
* A {@link ProcessorSlot} dedicates to circuit breaking. | |||||
* | * | ||||
* @author leyou | |||||
* @author Carpenter Lee | |||||
* @author Eric Zhao | |||||
*/ | */ | ||||
@SpiOrder(-1000) | @SpiOrder(-1000) | ||||
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> { | public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> { | ||||
@Override | @Override | ||||
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) | |||||
throws Throwable { | |||||
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count); | |||||
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, | |||||
boolean prioritized, Object... args) throws Throwable { | |||||
performChecking(resourceWrapper); | |||||
fireEntry(context, resourceWrapper, node, count, prioritized, args); | fireEntry(context, resourceWrapper, node, count, prioritized, args); | ||||
} | } | ||||
void performChecking(ResourceWrapper r) throws BlockException { | |||||
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName()); | |||||
if (circuitBreakers == null || circuitBreakers.isEmpty()) { | |||||
return; | |||||
} | |||||
for (CircuitBreaker cb : circuitBreakers) { | |||||
if (!cb.tryPass()) { | |||||
throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule()); | |||||
} | |||||
} | |||||
} | |||||
@Override | @Override | ||||
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { | |||||
fireExit(context, resourceWrapper, count, args); | |||||
public void exit(Context context, ResourceWrapper r, int count, Object... args) { | |||||
Entry curEntry = context.getCurEntry(); | |||||
if (curEntry.getBlockError() != null) { | |||||
fireExit(context, r, count, args); | |||||
return; | |||||
} | |||||
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName()); | |||||
if (circuitBreakers == null || circuitBreakers.isEmpty()) { | |||||
fireExit(context, r, count, args); | |||||
return; | |||||
} | |||||
if (curEntry.getBlockError() == null) { | |||||
long completeTime = curEntry.getCompleteTimestamp(); | |||||
if (completeTime <= 0) { | |||||
completeTime = TimeUtil.currentTimeMillis(); | |||||
} | |||||
long rt = completeTime - curEntry.getCreateTimestamp(); | |||||
Throwable error = curEntry.getError(); | |||||
for (CircuitBreaker circuitBreaker : circuitBreakers) { | |||||
circuitBreaker.onRequestComplete(rt, error); | |||||
} | |||||
} | |||||
fireExit(context, r, count, args); | |||||
} | } | ||||
} | } |
@@ -0,0 +1,147 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* 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 com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker; | |||||
import java.util.concurrent.atomic.AtomicReference; | |||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; | |||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; | |||||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||||
import com.alibaba.csp.sentinel.util.TimeUtil; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.8.0 | |||||
*/ | |||||
public abstract class AbstractCircuitBreaker implements CircuitBreaker { | |||||
protected final DegradeRule rule; | |||||
protected final int recoveryTimeoutMs; | |||||
private final EventObserverRegistry observerRegistry; | |||||
protected final AtomicReference<State> currentState = new AtomicReference<>(State.CLOSED); | |||||
protected volatile long nextRetryTimestamp; | |||||
public AbstractCircuitBreaker(DegradeRule rule) { | |||||
this(rule, EventObserverRegistry.getInstance()); | |||||
} | |||||
AbstractCircuitBreaker(DegradeRule rule, EventObserverRegistry observerRegistry) { | |||||
AssertUtil.notNull(observerRegistry, "observerRegistry cannot be null"); | |||||
if (!DegradeRuleManager.isValidRule(rule)) { | |||||
throw new IllegalArgumentException("Invalid DegradeRule: " + rule); | |||||
} | |||||
this.observerRegistry = observerRegistry; | |||||
this.rule = rule; | |||||
this.recoveryTimeoutMs = rule.getTimeWindow() * 1000; | |||||
} | |||||
@Override | |||||
public DegradeRule getRule() { | |||||
return rule; | |||||
} | |||||
@Override | |||||
public State currentState() { | |||||
return currentState.get(); | |||||
} | |||||
@Override | |||||
public boolean tryPass() { | |||||
// Template implementation. | |||||
if (currentState.get() == State.CLOSED) { | |||||
return true; | |||||
} | |||||
if (currentState.get() == State.OPEN) { | |||||
// For half-open state we allow a request for trial. | |||||
return retryTimeoutArrived() && fromOpenToHalfOpen(); | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* Reset the statistic data. | |||||
*/ | |||||
abstract void resetStat(); | |||||
protected boolean retryTimeoutArrived() { | |||||
return TimeUtil.currentTimeMillis() >= nextRetryTimestamp; | |||||
} | |||||
protected void updateNextRetryTimestamp() { | |||||
this.nextRetryTimestamp = TimeUtil.currentTimeMillis() + recoveryTimeoutMs; | |||||
} | |||||
protected boolean fromCloseToOpen(double snapshotValue) { | |||||
State prev = State.CLOSED; | |||||
if (currentState.compareAndSet(prev, State.OPEN)) { | |||||
updateNextRetryTimestamp(); | |||||
for (CircuitBreakerStateChangeObserver observer : observerRegistry.getStateChangeObservers()) { | |||||
observer.onStateChange(prev, State.OPEN, rule, snapshotValue); | |||||
} | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
protected boolean fromOpenToHalfOpen() { | |||||
if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) { | |||||
for (CircuitBreakerStateChangeObserver observer : observerRegistry.getStateChangeObservers()) { | |||||
observer.onStateChange(State.OPEN, State.HALF_OPEN, rule, null); | |||||
} | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
protected boolean fromHalfOpenToOpen(double snapshotValue) { | |||||
if (currentState.compareAndSet(State.HALF_OPEN, State.OPEN)) { | |||||
updateNextRetryTimestamp(); | |||||
for (CircuitBreakerStateChangeObserver observer : observerRegistry.getStateChangeObservers()) { | |||||
observer.onStateChange(State.HALF_OPEN, State.OPEN, rule, snapshotValue); | |||||
} | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
protected boolean fromHalfOpenToClose() { | |||||
if (currentState.compareAndSet(State.HALF_OPEN, State.CLOSED)) { | |||||
resetStat(); | |||||
for (CircuitBreakerStateChangeObserver observer : observerRegistry.getStateChangeObservers()) { | |||||
observer.onStateChange(State.HALF_OPEN, State.CLOSED, rule, null); | |||||
} | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
protected void transformToOpen(double triggerValue) { | |||||
State cs = currentState.get(); | |||||
switch (cs) { | |||||
case CLOSED: | |||||
fromCloseToOpen(triggerValue); | |||||
break; | |||||
case HALF_OPEN: | |||||
fromHalfOpenToOpen(triggerValue); | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,79 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* 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 | |||||
* | |||||
* https://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 com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker; | |||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; | |||||
/** | |||||
* <p>Basic <a href="https://martinfowler.com/bliki/CircuitBreaker.html">circuit breaker</a> interface.</p> | |||||
* | |||||
* @author Eric Zhao | |||||
*/ | |||||
public interface CircuitBreaker { | |||||
/** | |||||
* Get the associated circuit breaking rule. | |||||
* | |||||
* @return associated circuit breaking rule | |||||
*/ | |||||
DegradeRule getRule(); | |||||
/** | |||||
* Acquires permission of an invocation only if it is available at the time of invocation. | |||||
* | |||||
* @return {@code true} if permission was acquired and {@code false} otherwise | |||||
*/ | |||||
boolean tryPass(); | |||||
/** | |||||
* Get current state of the circuit breaker. | |||||
* | |||||
* @return current state of the circuit breaker | |||||
*/ | |||||
State currentState(); | |||||
/** | |||||
* Record a completed request with the given response time and error (if present) and | |||||
* handle state transformation of the circuit breaker. | |||||
* | |||||
* @param rt the response time of this entry | |||||
* @param error the error of this entry (if present) | |||||
*/ | |||||
void onRequestComplete(long rt, Throwable error); | |||||
/** | |||||
* Circuit breaker state. | |||||
*/ | |||||
enum State { | |||||
/** | |||||
* In {@code OPEN} state, all requests will be rejected until the next recovery time point. | |||||
*/ | |||||
OPEN, | |||||
/** | |||||
* In {@code HALF_OPEN} state, the circuit breaker will allow a "probe" invocation. | |||||
* If the invocation is abnormal according to the strategy (e.g. it's slow), the circuit breaker | |||||
* will re-transform to the {@code OPEN} state and wait for the next recovery time point; | |||||
* otherwise the resource will be regarded as "recovered" and the circuit breaker | |||||
* will cease cutting off requests and transform to {@code CLOSED} state. | |||||
*/ | |||||
HALF_OPEN, | |||||
/** | |||||
* In {@code CLOSED} state, all requests are permitted. When current metric value exceeds the threshold, | |||||
* the circuit breaker will transform to {@code OPEN} state. | |||||
*/ | |||||
CLOSED | |||||
} | |||||
} |
@@ -0,0 +1,42 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* 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 com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker; | |||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.8.0 | |||||
*/ | |||||
public interface CircuitBreakerStateChangeObserver { | |||||
/** | |||||
* <p>Observer method triggered when circuit breaker state changed. The transformation could be:</p> | |||||
* <ul> | |||||
* <li>From {@code CLOSED} to {@code OPEN} (with the triggered metric)</li> | |||||
* <li>From {@code OPEN} to {@code HALF_OPEN}</li> | |||||
* <li>From {@code OPEN} to {@code CLOSED}</li> | |||||
* <li>From {@code HALF_OPEN} to {@code OPEN} (with the triggered metric)</li> | |||||
* </ul> | |||||
* | |||||
* @param prevState previous state of the circuit breaker | |||||
* @param newState new state of the circuit breaker | |||||
* @param rule associated rule | |||||
* @param snapshotValue triggered value on circuit breaker opens (null if the new state is CLOSED or HALF_OPEN) | |||||
*/ | |||||
void onStateChange(CircuitBreaker.State prevState, CircuitBreaker.State newState, DegradeRule rule, | |||||
Double snapshotValue); | |||||
} |
@@ -0,0 +1,46 @@ | |||||
/* | |||||
* Copyright 1999-2020 Alibaba Group Holding Ltd. | |||||
* | |||||
* 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 | |||||
* | |||||
* https://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 com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.8.0 | |||||
*/ | |||||
public enum CircuitBreakerStrategy { | |||||
/** | |||||
* Circuit breaker opens (cuts off) when slow request ratio exceeds the threshold. | |||||
*/ | |||||
SLOW_REQUEST_RATIO(0), | |||||
/** | |||||
* Circuit breaker opens (cuts off) when error ratio exceeds the threshold. | |||||
*/ | |||||
ERROR_RATIO(1), | |||||
/** | |||||
* Circuit breaker opens (cuts off) when error count exceeds the threshold. | |||||
*/ | |||||
ERROR_COUNT(2); | |||||
private int type; | |||||
CircuitBreakerStrategy(int type) { | |||||
this.type = type; | |||||
} | |||||
public int getType() { | |||||
return type; | |||||
} | |||||
} |
@@ -0,0 +1,70 @@ | |||||
/* | |||||
* Copyright 1999-2020 Alibaba Group Holding Ltd. | |||||
* | |||||
* 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 | |||||
* | |||||
* https://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 com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker; | |||||
import java.util.ArrayList; | |||||
import java.util.HashMap; | |||||
import java.util.List; | |||||
import java.util.Map; | |||||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||||
/** | |||||
* <p>Registry for circuit breaker event observers.</p> | |||||
* | |||||
* @author Eric Zhao | |||||
* @since 1.8.0 | |||||
*/ | |||||
public class EventObserverRegistry { | |||||
private final Map<String, CircuitBreakerStateChangeObserver> stateChangeObserverMap = new HashMap<>(); | |||||
/** | |||||
* Register a circuit breaker state change observer. | |||||
* | |||||
* @param name observer name | |||||
* @param observer a valid observer | |||||
*/ | |||||
public void addStateChangeObserver(String name, CircuitBreakerStateChangeObserver observer) { | |||||
AssertUtil.notNull(name, "name cannot be null"); | |||||
AssertUtil.notNull(observer, "observer cannot be null"); | |||||
stateChangeObserverMap.put(name, observer); | |||||
} | |||||
public boolean removeStateChangeObserver(String name) { | |||||
AssertUtil.notNull(name, "name cannot be null"); | |||||
return stateChangeObserverMap.remove(name) != null; | |||||
} | |||||
/** | |||||
* Get all registered state chane observers. | |||||
* | |||||
* @return all registered state chane observers | |||||
*/ | |||||
public List<CircuitBreakerStateChangeObserver> getStateChangeObservers() { | |||||
return new ArrayList<>(stateChangeObserverMap.values()); | |||||
} | |||||
public static EventObserverRegistry getInstance() { | |||||
return InstanceHolder.instance; | |||||
} | |||||
private static class InstanceHolder { | |||||
private static EventObserverRegistry instance = new EventObserverRegistry(); | |||||
} | |||||
EventObserverRegistry() {} | |||||
} |
@@ -0,0 +1,156 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* 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 com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker; | |||||
import java.util.List; | |||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; | |||||
import com.alibaba.csp.sentinel.slots.statistic.base.LeapArray; | |||||
import com.alibaba.csp.sentinel.slots.statistic.base.LongAdder; | |||||
import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; | |||||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||||
import static com.alibaba.csp.sentinel.slots.block.RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO; | |||||
import static com.alibaba.csp.sentinel.slots.block.RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.8.0 | |||||
*/ | |||||
public class ExceptionCircuitBreaker extends AbstractCircuitBreaker { | |||||
private final int strategy; | |||||
private final int minRequestAmount; | |||||
private final double threshold; | |||||
private final LeapArray<SimpleErrorCounter> stat; | |||||
public ExceptionCircuitBreaker(DegradeRule rule) { | |||||
this(rule, new SimpleErrorCounterLeapArray(1, rule.getStatIntervalMs())); | |||||
} | |||||
ExceptionCircuitBreaker(DegradeRule rule, LeapArray<SimpleErrorCounter> stat) { | |||||
super(rule); | |||||
this.strategy = rule.getGrade(); | |||||
boolean modeOk = strategy == DEGRADE_GRADE_EXCEPTION_RATIO || strategy == DEGRADE_GRADE_EXCEPTION_COUNT; | |||||
AssertUtil.isTrue(modeOk, "rule strategy should be error-ratio or error-count"); | |||||
AssertUtil.notNull(stat, "stat cannot be null"); | |||||
this.minRequestAmount = rule.getMinRequestAmount(); | |||||
this.threshold = rule.getCount(); | |||||
this.stat = stat; | |||||
} | |||||
@Override | |||||
protected void resetStat() { | |||||
// Reset current bucket (bucket count = 1). | |||||
stat.currentWindow().value().reset(); | |||||
} | |||||
@Override | |||||
public void onRequestComplete(long rt, Throwable error) { | |||||
SimpleErrorCounter counter = stat.currentWindow().value(); | |||||
if (error != null) { | |||||
counter.getErrorCount().add(1); | |||||
} | |||||
counter.getTotalCount().add(1); | |||||
handleStateChangeWhenThresholdExceeded(error); | |||||
} | |||||
private void handleStateChangeWhenThresholdExceeded(Throwable error) { | |||||
if (currentState.get() == State.OPEN) { | |||||
return; | |||||
} | |||||
if (currentState.get() == State.HALF_OPEN) { | |||||
if (error == null) { | |||||
fromHalfOpenToClose(); | |||||
} else { | |||||
fromHalfOpenToOpen(1.0d); | |||||
} | |||||
return; | |||||
} | |||||
List<SimpleErrorCounter> counters = stat.values(); | |||||
long errCount = 0; | |||||
long totalCount = 0; | |||||
for (SimpleErrorCounter counter : counters) { | |||||
errCount += counter.errorCount.sum(); | |||||
totalCount += counter.totalCount.sum(); | |||||
} | |||||
if (totalCount < minRequestAmount) { | |||||
return; | |||||
} | |||||
double curCount = errCount; | |||||
if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) { | |||||
// Use errorRatio | |||||
curCount = errCount * 1.0d / totalCount; | |||||
} | |||||
if (curCount > threshold) { | |||||
transformToOpen(curCount); | |||||
} | |||||
} | |||||
static class SimpleErrorCounter { | |||||
private LongAdder errorCount; | |||||
private LongAdder totalCount; | |||||
public SimpleErrorCounter() { | |||||
this.errorCount = new LongAdder(); | |||||
this.totalCount = new LongAdder(); | |||||
} | |||||
public LongAdder getErrorCount() { | |||||
return errorCount; | |||||
} | |||||
public LongAdder getTotalCount() { | |||||
return totalCount; | |||||
} | |||||
public SimpleErrorCounter reset() { | |||||
errorCount.reset(); | |||||
totalCount.reset(); | |||||
return this; | |||||
} | |||||
@Override | |||||
public String toString() { | |||||
return "SimpleErrorCounter{" + | |||||
"errorCount=" + errorCount + | |||||
", totalCount=" + totalCount + | |||||
'}'; | |||||
} | |||||
} | |||||
static class SimpleErrorCounterLeapArray extends LeapArray<SimpleErrorCounter> { | |||||
public SimpleErrorCounterLeapArray(int sampleCount, int intervalInMs) { | |||||
super(sampleCount, intervalInMs); | |||||
} | |||||
@Override | |||||
public SimpleErrorCounter newEmptyBucket(long timeMillis) { | |||||
return new SimpleErrorCounter(); | |||||
} | |||||
@Override | |||||
protected WindowWrap<SimpleErrorCounter> resetWindowTo(WindowWrap<SimpleErrorCounter> w, long startTime) { | |||||
// Update the start time and reset value. | |||||
w.resetTo(startTime); | |||||
w.value().reset(); | |||||
return w; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,150 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* 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 com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker; | |||||
import java.util.List; | |||||
import com.alibaba.csp.sentinel.slots.block.RuleConstant; | |||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; | |||||
import com.alibaba.csp.sentinel.slots.statistic.base.LeapArray; | |||||
import com.alibaba.csp.sentinel.slots.statistic.base.LongAdder; | |||||
import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; | |||||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.8.0 | |||||
*/ | |||||
public class ResponseTimeCircuitBreaker extends AbstractCircuitBreaker { | |||||
private final long maxAllowedRt; | |||||
private final double maxSlowRequestRatio; | |||||
private final int minRequestAmount; | |||||
private final LeapArray<SlowRequestCounter> slidingCounter; | |||||
public ResponseTimeCircuitBreaker(DegradeRule rule) { | |||||
this(rule, new SlowRequestLeapArray(1, rule.getStatIntervalMs())); | |||||
} | |||||
ResponseTimeCircuitBreaker(DegradeRule rule, LeapArray<SlowRequestCounter> stat) { | |||||
super(rule); | |||||
AssertUtil.isTrue(rule.getGrade() == RuleConstant.DEGRADE_GRADE_RT, "rule metric type should be RT"); | |||||
AssertUtil.notNull(stat, "stat cannot be null"); | |||||
this.maxAllowedRt = Math.round(rule.getCount()); | |||||
this.maxSlowRequestRatio = rule.getSlowRatioThreshold(); | |||||
this.minRequestAmount = rule.getMinRequestAmount(); | |||||
this.slidingCounter = stat; | |||||
} | |||||
@Override | |||||
public void resetStat() { | |||||
// Reset current bucket (bucket count = 1). | |||||
slidingCounter.currentWindow().value().reset(); | |||||
} | |||||
@Override | |||||
public void onRequestComplete(long rt, Throwable error) { | |||||
SlowRequestCounter counter = slidingCounter.currentWindow().value(); | |||||
if (rt > maxAllowedRt) { | |||||
counter.slowCount.add(1); | |||||
} | |||||
counter.totalCount.add(1); | |||||
handleStateChangeWhenThresholdExceeded(rt); | |||||
} | |||||
private void handleStateChangeWhenThresholdExceeded(long rt) { | |||||
if (currentState.get() == State.OPEN) { | |||||
return; | |||||
} | |||||
if (currentState.get() == State.HALF_OPEN) { | |||||
// TODO: improve logic for half-open recovery | |||||
if (rt > maxAllowedRt) { | |||||
fromHalfOpenToOpen(1.0d); | |||||
} else { | |||||
fromHalfOpenToClose(); | |||||
} | |||||
return; | |||||
} | |||||
List<SlowRequestCounter> counters = slidingCounter.values(); | |||||
long slowCount = 0; | |||||
long totalCount = 0; | |||||
for (SlowRequestCounter counter : counters) { | |||||
slowCount += counter.slowCount.sum(); | |||||
totalCount += counter.totalCount.sum(); | |||||
} | |||||
if (totalCount < minRequestAmount) { | |||||
return; | |||||
} | |||||
double currentRatio = slowCount * 1.0d / totalCount; | |||||
if (currentRatio > maxSlowRequestRatio) { | |||||
transformToOpen(currentRatio); | |||||
} | |||||
} | |||||
static class SlowRequestCounter { | |||||
private LongAdder slowCount; | |||||
private LongAdder totalCount; | |||||
public SlowRequestCounter() { | |||||
this.slowCount = new LongAdder(); | |||||
this.totalCount = new LongAdder(); | |||||
} | |||||
public LongAdder getSlowCount() { | |||||
return slowCount; | |||||
} | |||||
public LongAdder getTotalCount() { | |||||
return totalCount; | |||||
} | |||||
public SlowRequestCounter reset() { | |||||
slowCount.reset(); | |||||
totalCount.reset(); | |||||
return this; | |||||
} | |||||
@Override | |||||
public String toString() { | |||||
return "SlowRequestCounter{" + | |||||
"slowCount=" + slowCount + | |||||
", totalCount=" + totalCount + | |||||
'}'; | |||||
} | |||||
} | |||||
static class SlowRequestLeapArray extends LeapArray<SlowRequestCounter> { | |||||
public SlowRequestLeapArray(int sampleCount, int intervalInMs) { | |||||
super(sampleCount, intervalInMs); | |||||
} | |||||
@Override | |||||
public SlowRequestCounter newEmptyBucket(long timeMillis) { | |||||
return new SlowRequestCounter(); | |||||
} | |||||
@Override | |||||
protected WindowWrap<SlowRequestCounter> resetWindowTo(WindowWrap<SlowRequestCounter> w, long startTime) { | |||||
w.resetTo(startTime); | |||||
w.value().reset(); | |||||
return w; | |||||
} | |||||
} | |||||
} |