diff --git a/pom.xml b/pom.xml index d4463d49..a3922ec1 100755 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,11 @@ sentinel-annotation-aspectj ${project.version} + + com.alibaba.csp + sentinel-parameter-flow-control + ${project.version} + com.alibaba.csp sentinel-datasource-extension @@ -112,6 +117,11 @@ sentinel-transport-simple-http ${project.version} + + com.alibaba.csp + sentinel-transport-common + ${project.version} + com.alibaba.csp sentinel-adapter diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlot.java index d4f0af5b..dfe5449d 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlot.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlot.java @@ -34,7 +34,7 @@ public interface ProcessorSlot { * @param param Generics parameter, usually is a {@link com.alibaba.csp.sentinel.node.Node} * @param count tokens needed * @param args parameters of the original call - * @throws Throwable + * @throws Throwable blocked exception or unexpected error */ void entry(Context context, ResourceWrapper resourceWrapper, T param, int count, Object... args) throws Throwable; @@ -44,10 +44,10 @@ public interface ProcessorSlot { * * @param context current {@link Context} * @param resourceWrapper current resource - * @param obj + * @param obj relevant object (e.g. Node) * @param count tokens needed * @param args parameters of the original call - * @throws Throwable + * @throws Throwable blocked exception or unexpected error */ void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) throws Throwable; diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotEntryCallback.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotEntryCallback.java new file mode 100644 index 00000000..a29be37a --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotEntryCallback.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2018 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.slotchain; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * Callback for entering {@link com.alibaba.csp.sentinel.slots.statistic.StatisticSlot} (passed and blocked). + * + * @author Eric Zhao + * @since 0.2.0 + */ +public interface ProcessorSlotEntryCallback { + + void onPass(Context context, ResourceWrapper resourceWrapper, T param, int count, Object... args) throws Exception; + + void onBlocked(BlockException ex, Context context, ResourceWrapper resourceWrapper, T param, int count, Object... args); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotExitCallback.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotExitCallback.java new file mode 100644 index 00000000..b8349dd4 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotExitCallback.java @@ -0,0 +1,29 @@ +/* + * Copyright 1999-2018 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.slotchain; + +import com.alibaba.csp.sentinel.context.Context; + +/** + * Callback for exiting {@link com.alibaba.csp.sentinel.slots.statistic.StatisticSlot} (passed and blocked). + * + * @author Eric Zhao + * @since 0.2.0 + */ +public interface ProcessorSlotExitCallback { + + void onExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java index c4e4d8f6..093adbe3 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java @@ -15,6 +15,10 @@ */ package com.alibaba.csp.sentinel.slots.statistic; +import java.util.Collection; + +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotEntryCallback; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotExitCallback; import com.alibaba.csp.sentinel.util.TimeUtil; import com.alibaba.csp.sentinel.Constants; import com.alibaba.csp.sentinel.EntryType; @@ -39,13 +43,13 @@ import com.alibaba.csp.sentinel.slots.block.BlockException; *

* * @author jialiang.linjl + * @author Eric Zhao */ public class StatisticSlot extends AbstractLinkedProcessorSlot { @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable { - try { fireEntry(context, resourceWrapper, node, count, args); node.increaseThreadNum(); @@ -61,6 +65,9 @@ public class StatisticSlot extends AbstractLinkedProcessorSlot { Constants.ENTRY_NODE.addPassRequest(); } + for (ProcessorSlotEntryCallback handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) { + handler.onPass(context, resourceWrapper, node, count, args); + } } catch (BlockException e) { context.getCurEntry().setError(e); @@ -74,6 +81,10 @@ public class StatisticSlot extends AbstractLinkedProcessorSlot { Constants.ENTRY_NODE.increaseBlockedQps(); } + for (ProcessorSlotEntryCallback handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) { + handler.onBlocked(e, context, resourceWrapper, node, count, args); + } + throw e; } catch (Throwable e) { context.getCurEntry().setError(e); @@ -117,11 +128,14 @@ public class StatisticSlot extends AbstractLinkedProcessorSlot { Constants.ENTRY_NODE.decreaseThreadNum(); } } else { - // error may happen - // node.rt(-2); + // Error may happen. + } + + Collection exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks(); + for (ProcessorSlotExitCallback handler : exitCallbacks) { + handler.onExit(context, resourceWrapper, count, args); } fireExit(context, resourceWrapper, count); } - } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlotCallbackRegistry.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlotCallbackRegistry.java new file mode 100644 index 00000000..a06fc984 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlotCallbackRegistry.java @@ -0,0 +1,85 @@ +/* + * Copyright 1999-2018 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.statistic; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotEntryCallback; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotExitCallback; + +/** + *

+ * Callback registry for {@link StatisticSlot}. Now two kind of callbacks are supported: + *

    + *
  • {@link ProcessorSlotEntryCallback}: callback for entry (passed and blocked)
  • + *
  • {@link ProcessorSlotExitCallback}: callback for exiting {@link StatisticSlot}
  • + *
+ *

+ * + * @author Eric Zhao + * @since 0.2.0 + */ +public final class StatisticSlotCallbackRegistry { + + private static final Map> entryCallbackMap + = new ConcurrentHashMap>(); + + private static final Map exitCallbackMap + = new ConcurrentHashMap(); + + public static void clearEntryCallback() { + entryCallbackMap.clear(); + } + + public static void clearExitCallback() { + exitCallbackMap.clear(); + } + + public static void addEntryCallback(String key, ProcessorSlotEntryCallback callback) { + entryCallbackMap.put(key, callback); + } + + public static void addExitCallback(String key, ProcessorSlotExitCallback callback) { + exitCallbackMap.put(key, callback); + } + + public static ProcessorSlotEntryCallback removeEntryCallback(String key) { + if (key == null) { + return null; + } + return entryCallbackMap.remove(key); + } + + public static ProcessorSlotExitCallback removeExitCallback(String key) { + if (key == null) { + return null; + } + return exitCallbackMap.remove(key); + } + + public static Collection> getEntryCallbacks() { + return entryCallbackMap.values(); + } + + public static Collection getExitCallbacks() { + return exitCallbackMap.values(); + } + + private StatisticSlotCallbackRegistry() {} +} diff --git a/sentinel-demo/pom.xml b/sentinel-demo/pom.xml index 1ca9b159..1dc8a6bb 100755 --- a/sentinel-demo/pom.xml +++ b/sentinel-demo/pom.xml @@ -26,6 +26,7 @@ sentinel-demo-zookeeper-datasource sentinel-demo-apollo-datasource sentinel-demo-annotation-spring-aop + sentinel-demo-parameter-flow-control diff --git a/sentinel-demo/sentinel-demo-parameter-flow-control/pom.xml b/sentinel-demo/sentinel-demo-parameter-flow-control/pom.xml new file mode 100644 index 00000000..b86e8aed --- /dev/null +++ b/sentinel-demo/sentinel-demo-parameter-flow-control/pom.xml @@ -0,0 +1,30 @@ + + + + sentinel-demo + com.alibaba.csp + 0.2.0-SNAPSHOT + + 4.0.0 + + sentinel-demo-parameter-flow-control + + + 1.8 + 1.8 + + + + + com.alibaba.csp + sentinel-parameter-flow-control + + + + com.alibaba.csp + sentinel-transport-simple-http + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/demo/flow/param/ParamFlowQpsDemo.java b/sentinel-demo/sentinel-demo-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/demo/flow/param/ParamFlowQpsDemo.java new file mode 100644 index 00000000..84cba516 --- /dev/null +++ b/sentinel-demo/sentinel-demo-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/demo/flow/param/ParamFlowQpsDemo.java @@ -0,0 +1,69 @@ +/* + * Copyright 1999-2018 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.demo.flow.param; + +import java.util.Collections; + +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowItem; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager; + +/** + * This demo demonstrates flow control by frequent ("hot spot") parameters. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamFlowQpsDemo { + + private static final int PARAM_A = 1; + private static final int PARAM_B = 2; + private static final int PARAM_C = 3; + private static final int PARAM_D = 4; + + /** + * Here we prepare different parameters to validate flow control by parameters. + */ + private static final Integer[] PARAMS = new Integer[] {PARAM_A, PARAM_B, PARAM_C, PARAM_D}; + + private static final String RESOURCE_KEY = "resA"; + + public static void main(String[] args) { + initHotParamFlowRules(); + + final int threadCount = 8; + ParamFlowQpsRunner runner = new ParamFlowQpsRunner<>(PARAMS, RESOURCE_KEY, threadCount, 120); + runner.simulateTraffic(); + runner.tick(); + } + + private static void initHotParamFlowRules() { + // QPS mode, threshold is 5 for every frequent "hot spot" parameter in index 0 (the first arg). + ParamFlowRule rule = new ParamFlowRule(RESOURCE_KEY) + .setParamIdx(0) + .setBlockGrade(RuleConstant.FLOW_GRADE_QPS) + .setCount(5); + // We can set threshold count for specific parameter value individually. + // Here we add an exception item. That means: QPS threshold of entries with parameter `PARAM_B` (type: int) + // in index 0 will be 10, rather than the global threshold (5). + ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B)) + .setClassType(int.class.getName()) + .setCount(10); + rule.setParamFlowItemList(Collections.singletonList(item)); + ParamFlowRuleManager.loadRules(Collections.singletonList(rule)); + } +} diff --git a/sentinel-demo/sentinel-demo-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/demo/flow/param/ParamFlowQpsRunner.java b/sentinel-demo/sentinel-demo-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/demo/flow/param/ParamFlowQpsRunner.java new file mode 100644 index 00000000..c79c3d98 --- /dev/null +++ b/sentinel-demo/sentinel-demo-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/demo/flow/param/ParamFlowQpsRunner.java @@ -0,0 +1,167 @@ +/* + * Copyright 1999-2018 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.demo.flow.param; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.util.TimeUtil; + +/** + * A traffic runner to simulate flow for different parameters. + * + * @author Eric Zhao + * @since 0.2.0 + */ +class ParamFlowQpsRunner { + + private final T[] params; + private final String resourceName; + private int seconds; + private final int threadCount; + + private final Map passCountMap = new ConcurrentHashMap<>(); + + private volatile boolean stop = false; + + public ParamFlowQpsRunner(T[] params, String resourceName, int threadCount, int seconds) { + assertTrue(params != null && params.length > 0, "Parameter array should not be empty"); + assertTrue(StringUtil.isNotBlank(resourceName), "Resource name cannot be empty"); + assertTrue(seconds > 0, "Time period should be positive"); + assertTrue(threadCount > 0 && threadCount <= 1000, "Invalid thread count"); + this.params = params; + this.resourceName = resourceName; + this.seconds = seconds; + this.threadCount = threadCount; + + for (T param : params) { + assertTrue(param != null, "Parameters should not be null"); + passCountMap.putIfAbsent(param, new AtomicLong()); + } + } + + private void assertTrue(boolean b, String message) { + if (!b) { + throw new IllegalArgumentException(message); + } + } + + /** + * Pick one of provided parameters randomly. + * + * @return picked parameter + */ + private T generateParam() { + int i = ThreadLocalRandom.current().nextInt(0, params.length); + return params[i]; + } + + void simulateTraffic() { + for (int i = 0; i < threadCount; i++) { + Thread t = new Thread(new RunTask()); + t.setName("sentinel-simulate-traffic-task-" + i); + t.start(); + } + } + + void tick() { + Thread timer = new Thread(new TimerTask()); + timer.setName("sentinel-timer-task"); + timer.start(); + } + + private void passFor(T param) { + passCountMap.get(param).incrementAndGet(); + } + + final class RunTask implements Runnable { + @Override + public void run() { + while (!stop) { + Entry entry = null; + + try { + T param = generateParam(); + entry = SphU.entry(resourceName, EntryType.IN, 1, param); + // Add pass for parameter. + passFor(param); + } catch (BlockException e1) { + // block.incrementAndGet(); + } catch (Exception e2) { + // biz exception + } finally { + // total.incrementAndGet(); + if (entry != null) { + entry.exit(); + } + } + + try { + TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextInt(0, 10)); + } catch (InterruptedException e) { + // ignore + } + } + } + } + + final class TimerTask implements Runnable { + @Override + public void run() { + long start = System.currentTimeMillis(); + System.out.println("Begin to run! Go go go!"); + System.out.println("See corresponding metrics.log for accurate statistic data"); + + Map map = new HashMap<>(params.length); + for (T param : params) { + map.putIfAbsent(param, 0L); + } + while (!stop) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + } + // There may be a mismatch for time window of internal sliding window. + // See corresponding `metrics.log` for accurate statistic log. + for (T param : params) { + long globalPass = passCountMap.get(param).get(); + long oldPass = map.get(param); + long oneSecondPass = globalPass - oldPass; + map.put(param, globalPass); + System.out.println(String.format("[%d][%d] Hot param metrics for resource %s: " + + "pass count for param <%s> is %d", + seconds, TimeUtil.currentTimeMillis(), resourceName, param, oneSecondPass)); + } + if (seconds-- <= 0) { + stop = true; + } + } + + long cost = System.currentTimeMillis() - start; + System.out.println("Time cost: " + cost + " ms"); + System.exit(0); + } + } +} diff --git a/sentinel-extension/pom.xml b/sentinel-extension/pom.xml index 876121b7..a472bf62 100755 --- a/sentinel-extension/pom.xml +++ b/sentinel-extension/pom.xml @@ -18,6 +18,7 @@ sentinel-datasource-apollo sentinel-datasource-redis sentinel-annotation-aspectj + sentinel-parameter-flow-control diff --git a/sentinel-extension/sentinel-parameter-flow-control/README.md b/sentinel-extension/sentinel-parameter-flow-control/README.md new file mode 100644 index 00000000..5fe44e0d --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/README.md @@ -0,0 +1,61 @@ +# Sentinel Parameter Flow Control + +This component provides functionality of flow control by frequent ("hot spot") parameters. + +## Usage + +To use Sentinel Parameter Flow Control, you need to add the following dependency to `pom.xml`: + +```xml + + com.alibaba.csp + sentinel-parameter-flow-control + x.y.z + +``` + +First you need to pass parameters with the following `SphU.entry` overloaded methods: + +```java +public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException + +public static Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException +``` + +For example, if there are two parameters to provide, you can: + +```java +// paramA in index 0, paramB in index 1. +SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB); +``` + +Then you can configure parameter flow control rules via `loadRules` method in `ParamFlowRuleManager`: + +```java +// QPS mode, threshold is 5 for every frequent "hot spot" parameter in index 0 (the first arg). +ParamFlowRule rule = new ParamFlowRule(RESOURCE_KEY) + .setParamIdx(0) + .setCount(5); +// We can set threshold count for specific parameter value individually. +// Here we add an exception item. That means: QPS threshold of entries with parameter `PARAM_B` (type: int) +// in index 0 will be 10, rather than the global threshold (5). +ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B)) + .setClassType(int.class.getName()) + .setCount(10); +rule.setParamFlowItemList(Collections.singletonList(item)); +ParamFlowRuleManager.loadRules(Collections.singletonList(rule)); +``` + +The description for fields of `ParamFlowRule`: + +| Field | Description | Default | +| :----: | :----| :----| +| resource| resource name (**required**) || +| count | flow control threshold (**required**) || +| blockGrade | flow control mode (only QPS mode is supported) | QPS mode | +| paramIdx | the index of provided parameter in `SphU.entry(xxx, args)` (**required**) || +| paramFlowItemList | the exception items of parameter; you can set threshold to a specific parameter value || + + +Now the parameter flow control rules will take effect. + diff --git a/sentinel-extension/sentinel-parameter-flow-control/pom.xml b/sentinel-extension/sentinel-parameter-flow-control/pom.xml new file mode 100644 index 00000000..9511a743 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/pom.xml @@ -0,0 +1,43 @@ + + + + sentinel-extension + com.alibaba.csp + 0.2.0-SNAPSHOT + + 4.0.0 + + sentinel-parameter-flow-control + jar + + + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-transport-common + provided + + + + com.googlecode.concurrentlinkedhashmap + concurrentlinkedhashmap-lru + 1.4.2 + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchTopParamsCommandHandler.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchTopParamsCommandHandler.java new file mode 100644 index 00000000..ddebeaf6 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchTopParamsCommandHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright 1999-2018 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.command.handler; + +import java.util.Map; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParameterMetric; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.fastjson.JSON; + +/** + * @author Eric Zhao + * @since 0.2.0 + */ +@CommandMapping(name = "topParams") +public class FetchTopParamsCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + String resourceName = request.getParam("res"); + if (StringUtil.isBlank(resourceName)) { + return CommandResponse.ofFailure(new IllegalArgumentException("Invalid parameter: res")); + } + String idx = request.getParam("idx"); + int index; + try { + index = Integer.valueOf(idx); + } catch (Exception ex) { + return CommandResponse.ofFailure(ex, "Invalid parameter: idx"); + } + String n = request.getParam("n"); + int amount; + try { + amount = Integer.valueOf(n); + } catch (Exception ex) { + return CommandResponse.ofFailure(ex, "Invalid parameter: n"); + } + ParameterMetric metric = ParamFlowSlot.getHotParamMetricForName(resourceName); + if (metric == null) { + return CommandResponse.ofSuccess("{}"); + } + Map values = metric.getTopPassParamCount(index, amount); + + return CommandResponse.ofSuccess(JSON.toJSONString(values)); + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/command/handler/GetParamFlowRulesCommandHandler.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/command/handler/GetParamFlowRulesCommandHandler.java new file mode 100644 index 00000000..dd1bb721 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/command/handler/GetParamFlowRulesCommandHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2018 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.command.handler; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager; +import com.alibaba.fastjson.JSON; + +/** + * @author Eric Zhao + * @since 0.2.0 + */ +@CommandMapping(name = "getParamFlowRules") +public class GetParamFlowRulesCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + return CommandResponse.ofSuccess(JSON.toJSONString(ParamFlowRuleManager.getRules())); + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyParamFlowRulesCommandHandler.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyParamFlowRulesCommandHandler.java new file mode 100644 index 00000000..ffd9073a --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyParamFlowRulesCommandHandler.java @@ -0,0 +1,95 @@ +/* + * Copyright 1999-2018 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.command.handler; + +import java.net.URLDecoder; +import java.util.List; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.datasource.WritableDataSource; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.fastjson.JSONArray; + +/** + * @author Eric Zhao + * @since 0.2.0 + */ +@CommandMapping(name = "setParamFlowRules") +public class ModifyParamFlowRulesCommandHandler implements CommandHandler { + + private static WritableDataSource> paramFlowWds = null; + + @Override + public CommandResponse handle(CommandRequest request) { + String data = request.getParam("data"); + if (StringUtil.isBlank(data)) { + return CommandResponse.ofFailure(new IllegalArgumentException("Bad data")); + } + try { + data = URLDecoder.decode(data, "utf-8"); + } catch (Exception e) { + RecordLog.info("Decode rule data error", e); + return CommandResponse.ofFailure(e, "decode rule data error"); + } + + RecordLog.info(String.format("[API Server] Receiving rule change (type:parameter flow rule): %s", data)); + + String result = SUCCESS_MSG; + List flowRules = JSONArray.parseArray(data, ParamFlowRule.class); + ParamFlowRuleManager.loadRules(flowRules); + if (!writeToDataSource(paramFlowWds, flowRules)) { + result = WRITE_DS_FAILURE_MSG; + } + return CommandResponse.ofSuccess(result); + } + + /** + * Write target value to given data source. + * + * @param dataSource writable data source + * @param value target value to save + * @param value type + * @return true if write successful or data source is empty; false if error occurs + */ + private boolean writeToDataSource(WritableDataSource dataSource, T value) { + if (dataSource != null) { + try { + dataSource.write(value); + } catch (Exception e) { + RecordLog.warn("Write data source failed", e); + return false; + } + } + return true; + } + + public synchronized static WritableDataSource> getWritableDataSource() { + return paramFlowWds; + } + + public synchronized static void setWritableDataSource(WritableDataSource> hotParamWds) { + ModifyParamFlowRulesCommandHandler.paramFlowWds = hotParamWds; + } + + private static final String SUCCESS_MSG = "success"; + private static final String WRITE_DS_FAILURE_MSG = "partial success (write data source failed)"; +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/init/ParamFlowStatisticSlotCallbackInit.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/init/ParamFlowStatisticSlotCallbackInit.java new file mode 100644 index 00000000..56d6bd28 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/init/ParamFlowStatisticSlotCallbackInit.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 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.init; + +import com.alibaba.csp.sentinel.slots.statistic.ParamFlowStatisticEntryCallback; +import com.alibaba.csp.sentinel.slots.statistic.StatisticSlotCallbackRegistry; + +/** + * Init function for adding callbacks to {@link StatisticSlotCallbackRegistry} to record metrics + * for frequent parameters in {@link com.alibaba.csp.sentinel.slots.statistic.StatisticSlot}. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamFlowStatisticSlotCallbackInit implements InitFunc { + + @Override + public void init() { + StatisticSlotCallbackRegistry.addEntryCallback(ParamFlowStatisticEntryCallback.class.getName(), + new ParamFlowStatisticEntryCallback()); + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/HotParamSlotChainBuilder.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/HotParamSlotChainBuilder.java new file mode 100644 index 00000000..61705fe5 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/HotParamSlotChainBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2018 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; + +import com.alibaba.csp.sentinel.slotchain.DefaultProcessorSlotChain; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotChain; +import com.alibaba.csp.sentinel.slotchain.SlotChainBuilder; +import com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot; +import com.alibaba.csp.sentinel.slots.block.flow.FlowSlot; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.slots.logger.LogSlot; +import com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot; +import com.alibaba.csp.sentinel.slots.statistic.StatisticSlot; +import com.alibaba.csp.sentinel.slots.system.SystemSlot; + +/** + * @author Eric Zhao + * @since 0.2.0 + */ +public class HotParamSlotChainBuilder implements SlotChainBuilder { + + @Override + public ProcessorSlotChain build() { + ProcessorSlotChain chain = new DefaultProcessorSlotChain(); + chain.addLast(new NodeSelectorSlot()); + chain.addLast(new ClusterBuilderSlot()); + chain.addLast(new LogSlot()); + chain.addLast(new StatisticSlot()); + chain.addLast(new ParamFlowSlot()); + chain.addLast(new SystemSlot()); + chain.addLast(new AuthoritySlot()); + chain.addLast(new FlowSlot()); + chain.addLast(new DegradeSlot()); + + return chain; + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowChecker.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowChecker.java new file mode 100644 index 00000000..65228c44 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowChecker.java @@ -0,0 +1,100 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Set; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; + +/** + * @author Eric Zhao + * @since 0.2.0 + */ +final class ParamFlowChecker { + + static boolean passCheck(ResourceWrapper resourceWrapper, /*@Valid*/ ParamFlowRule rule, /*@Valid*/ int count, + Object... args) { + if (args == null) { + return true; + } + + int paramIdx = rule.getParamIdx(); + if (args.length <= paramIdx) { + return true; + } + + Object value = args[paramIdx]; + + return passLocalCheck(resourceWrapper, rule, count, value); + } + + private static ParameterMetric getHotParameters(ResourceWrapper resourceWrapper) { + // Should not be null. + return ParamFlowSlot.getParamMetric(resourceWrapper); + } + + private static boolean passLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count, Object value) { + try { + if (Collection.class.isAssignableFrom(value.getClass())) { + for (Object param : ((Collection)value)) { + if (!passSingleValueCheck(resourceWrapper, rule, count, param)) { + return false; + } + } + } else if (value.getClass().isArray()) { + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + Object param = Array.get(value, i); + if (!passSingleValueCheck(resourceWrapper, rule, count, param)) { + return false; + } + } + } else { + return passSingleValueCheck(resourceWrapper, rule, count, value); + } + } catch (Throwable e) { + RecordLog.info("[ParamFlowChecker] Unexpected error", e); + } + + return true; + } + + static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count, Object value) { + Set exclusionItems = rule.getParsedHotItems().keySet(); + if (rule.getBlockGrade() == RuleConstant.FLOW_GRADE_QPS) { + double curCount = getHotParameters(resourceWrapper).getPassParamQps(rule.getParamIdx(), value); + + if (exclusionItems.contains(value)) { + // Pass check for exclusion items. + int itemQps = rule.getParsedHotItems().get(value); + return curCount + count <= itemQps; + } else if (curCount + count > rule.getCount()) { + if ((curCount - rule.getCount()) < 1 && (curCount - rule.getCount()) > 0) { + return true; + } + return false; + } + } + + return true; + } + + private ParamFlowChecker() {} +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowException.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowException.java new file mode 100644 index 00000000..a4a659c0 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowException.java @@ -0,0 +1,48 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * Block exception for frequent ("hot-spot") parameter flow control. + * + * @author jialiang.linjl + * @since 0.2.0 + */ +public class ParamFlowException extends BlockException { + + private final String resourceName; + + public ParamFlowException(String resourceName, String message, Throwable cause) { + super(message, cause); + this.resourceName = resourceName; + } + + public ParamFlowException(String resourceName, String message) { + super(message, message); + this.resourceName = resourceName; + } + + public String getResourceName() { + return resourceName; + } + + @Override + public Throwable fillInStackTrace() { + return this; + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowItem.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowItem.java new file mode 100644 index 00000000..68b494ce --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowItem.java @@ -0,0 +1,101 @@ +/* + * Copyright 1999-2018 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.flow.param; + +/** + * A flow control item for a specific parameter value. + * + * @author jialiang.linjl + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamFlowItem { + + private String object; + private Integer count; + private String classType; + + public ParamFlowItem() {} + + public ParamFlowItem(String object, Integer count, String classType) { + this.object = object; + this.count = count; + this.classType = classType; + } + + public static ParamFlowItem newItem(T object, Integer count) { + if (object == null) { + throw new IllegalArgumentException("Invalid object: null"); + } + return new ParamFlowItem(object.toString(), count, object.getClass().getName()); + } + + public String getObject() { + return object; + } + + public ParamFlowItem setObject(String object) { + this.object = object; + return this; + } + + public Integer getCount() { + return count; + } + + public ParamFlowItem setCount(Integer count) { + this.count = count; + return this; + } + + public String getClassType() { + return classType; + } + + public ParamFlowItem setClassType(String classType) { + this.classType = classType; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + + ParamFlowItem item = (ParamFlowItem)o; + + if (object != null ? !object.equals(item.object) : item.object != null) { return false; } + if (count != null ? !count.equals(item.count) : item.count != null) { return false; } + return classType != null ? classType.equals(item.classType) : item.classType == null; + } + + @Override + public int hashCode() { + int result = object != null ? object.hashCode() : 0; + result = 31 * result + (count != null ? count.hashCode() : 0); + result = 31 * result + (classType != null ? classType.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ParamFlowItem{" + + "object=" + object + + ", count=" + count + + ", classType='" + classType + '\'' + + '}'; + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRule.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRule.java new file mode 100644 index 00000000..dec8d9e6 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRule.java @@ -0,0 +1,156 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; + +/** + * Rules for "hot-spot" frequent parameter flow control. + * + * @author jialiang.linjl + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamFlowRule extends AbstractRule { + + public ParamFlowRule() {} + + public ParamFlowRule(String resourceName) { + setResource(resourceName); + } + + /** + * The threshold type of flow control (1: QPS). + */ + private int blockGrade = RuleConstant.FLOW_GRADE_QPS; + + /** + * Parameter index. + */ + private Integer paramIdx; + + /** + * The threshold count. + */ + private double count; + + /** + * Original exclusion items of parameters. + */ + private List paramFlowItemList = new ArrayList(); + + /** + * Parsed exclusion items of parameters. Only for internal use. + */ + private Map hotItems = new HashMap(); + + public int getBlockGrade() { + return blockGrade; + } + + public ParamFlowRule setBlockGrade(int blockGrade) { + this.blockGrade = blockGrade; + return this; + } + + public Integer getParamIdx() { + return paramIdx; + } + + public ParamFlowRule setParamIdx(Integer paramIdx) { + this.paramIdx = paramIdx; + return this; + } + + public double getCount() { + return count; + } + + public ParamFlowRule setCount(double count) { + this.count = count; + return this; + } + + public List getParamFlowItemList() { + return paramFlowItemList; + } + + public ParamFlowRule setParamFlowItemList(List paramFlowItemList) { + this.paramFlowItemList = paramFlowItemList; + return this; + } + + Map getParsedHotItems() { + return hotItems; + } + + ParamFlowRule setParsedHotItems(Map hotItems) { + this.hotItems = hotItems; + return this; + } + + @Override + @Deprecated + public boolean passCheck(Context context, DefaultNode node, int count, Object... args) { + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + if (!super.equals(o)) { return false; } + + ParamFlowRule rule = (ParamFlowRule)o; + + if (blockGrade != rule.blockGrade) { return false; } + if (Double.compare(rule.count, count) != 0) { return false; } + if (paramIdx != null ? !paramIdx.equals(rule.paramIdx) : rule.paramIdx != null) { return false; } + return paramFlowItemList != null ? paramFlowItemList.equals(rule.paramFlowItemList) : rule.paramFlowItemList == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + long temp; + result = 31 * result + blockGrade; + result = 31 * result + (paramIdx != null ? paramIdx.hashCode() : 0); + temp = Double.doubleToLongBits(count); + result = 31 * result + (int)(temp ^ (temp >>> 32)); + result = 31 * result + (paramFlowItemList != null ? paramFlowItemList.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ParamFlowRule{" + + "resource=" + getResource() + + ", limitApp=" + getLimitApp() + + ", blockGrade=" + blockGrade + + ", paramIdx=" + paramIdx + + ", count=" + count + + ", paramFlowItemList=" + paramFlowItemList + + '}'; + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRuleManager.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRuleManager.java new file mode 100644 index 00000000..4700ddd7 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRuleManager.java @@ -0,0 +1,233 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Manager for frequent ("hot-spot") parameter flow rules. + * + * @author jialiang.linjl + * @author Eric Zhao + * @since 0.2.0 + */ +public final class ParamFlowRuleManager { + + private static final Map> paramFlowRules + = new ConcurrentHashMap>(); + + private final static RulePropertyListener PROPERTY_LISTENER = new RulePropertyListener(); + private static SentinelProperty> currentProperty + = new DynamicSentinelProperty>(); + + static { + currentProperty.addListener(PROPERTY_LISTENER); + } + + /** + * Load parameter flow rules. Former rules will be replaced. + * + * @param rules new rules to load. + */ + public static void loadRules(List rules) { + try { + currentProperty.updateValue(rules); + } catch (Throwable e) { + RecordLog.info("[ParamFlowRuleManager] Failed to load rules", e); + } + } + + /** + * Listen to the {@link SentinelProperty} for {@link ParamFlowRule}s. The property is the source + * of {@link ParamFlowRule}s. Parameter flow rules can also be set by {@link #loadRules(List)} directly. + * + * @param property the property to listen + */ + public static void register2Property(SentinelProperty> property) { + synchronized (PROPERTY_LISTENER) { + currentProperty.removeListener(PROPERTY_LISTENER); + property.addListener(PROPERTY_LISTENER); + currentProperty = property; + RecordLog.info("[ParamFlowRuleManager] New property has been registered to hot param rule manager"); + } + } + + public static List getRulesOfResource(String resourceName) { + return paramFlowRules.get(resourceName); + } + + public static boolean hasRules(String resourceName) { + List rules = paramFlowRules.get(resourceName); + return rules != null && !rules.isEmpty(); + } + + /** + * Get a copy of the rules. + * + * @return a new copy of the rules. + */ + public static List getRules() { + List rules = new ArrayList(); + for (Map.Entry> entry : paramFlowRules.entrySet()) { + rules.addAll(entry.getValue()); + } + return rules; + } + + private static Object parseValue(String value, String classType) { + if (value == null) { + throw new IllegalArgumentException("Null value"); + } + if (StringUtil.isBlank(classType)) { + // If the class type is not provided, then treat it as string. + return value; + } + // Handle primitive type. + if (int.class.toString().equals(classType) || Integer.class.getName().equals(classType)) { + return Integer.parseInt(value); + } else if (boolean.class.toString().equals(classType) || Boolean.class.getName().equals(classType)) { + return Boolean.parseBoolean(value); + } else if (long.class.toString().equals(classType) || Long.class.getName().equals(classType)) { + return Long.parseLong(value); + } else if (double.class.toString().equals(classType) || Double.class.getName().equals(classType)) { + return Double.parseDouble(value); + } else if (float.class.toString().equals(classType) || Float.class.getName().equals(classType)) { + return Float.parseFloat(value); + } else if (byte.class.toString().equals(classType) || Byte.class.getName().equals(classType)) { + return Byte.parseByte(value); + } else if (short.class.toString().equals(classType) || Short.class.getName().equals(classType)) { + return Short.parseShort(value); + } else if (char.class.toString().equals(classType)) { + char[] array = value.toCharArray(); + return array.length > 0 ? array[0] : null; + } + + return value; + } + + static class RulePropertyListener implements PropertyListener> { + + @Override + public void configUpdate(List list) { + Map> rules = aggregateHotParamRules(list); + if (rules != null) { + paramFlowRules.clear(); + paramFlowRules.putAll(rules); + } + RecordLog.info("[ParamFlowRuleManager] Hot spot parameter flow rules received: " + paramFlowRules); + } + + @Override + public void configLoad(List list) { + Map> rules = aggregateHotParamRules(list); + if (rules != null) { + paramFlowRules.clear(); + paramFlowRules.putAll(rules); + } + RecordLog.info("[ParamFlowRuleManager] Hot spot parameter flow rules received: " + paramFlowRules); + } + + private Map> aggregateHotParamRules(List list) { + Map> newRuleMap = new ConcurrentHashMap>(); + + if (list == null || list.isEmpty()) { + // No parameter flow rules, so clear all the metrics. + ParamFlowSlot.getMetricsMap().clear(); + RecordLog.info("[ParamFlowRuleManager] No parameter flow rules, clearing all parameter metrics"); + return newRuleMap; + } + + for (ParamFlowRule rule : list) { + if (!isValidRule(rule)) { + RecordLog.warn("[ParamFlowRuleManager] Ignoring invalid rule when loading new rules: " + rule); + continue; + } + + if (StringUtil.isBlank(rule.getLimitApp())) { + rule.setLimitApp(FlowRule.LIMIT_APP_DEFAULT); + } + + if (rule.getParamFlowItemList() == null) { + rule.setParamFlowItemList(new ArrayList()); + } + + Map itemMap = parseHotItems(rule.getParamFlowItemList()); + rule.setParsedHotItems(itemMap); + + String resourceName = rule.getResource(); + List ruleList = newRuleMap.get(resourceName); + if (ruleList == null) { + ruleList = new ArrayList(); + newRuleMap.put(resourceName, ruleList); + } + ruleList.add(rule); + } + + // Clear unused hot param metrics. + Set previousResources = paramFlowRules.keySet(); + for (String resource : previousResources) { + if (!newRuleMap.containsKey(resource)) { + ParamFlowSlot.clearHotParamMetricForName(resource); + } + } + + return newRuleMap; + } + } + + static Map parseHotItems(List items) { + Map itemMap = new HashMap(); + if (items == null || items.isEmpty()) { + return itemMap; + } + for (ParamFlowItem item : items) { + // Value should not be null. + Object value; + try { + value = parseValue(item.getObject(), item.getClassType()); + } catch (Exception ex) { + RecordLog.warn("[ParamFlowRuleManager] Failed to parse value for item: " + item, ex); + continue; + } + if (item.getCount() == null || item.getCount() < 0 || value == null) { + RecordLog.warn("[ParamFlowRuleManager] Ignoring invalid exclusion parameter item: " + item); + continue; + } + itemMap.put(value, item.getCount()); + } + return itemMap; + } + + static boolean isValidRule(ParamFlowRule rule) { + return rule != null && !StringUtil.isBlank(rule.getResource()) && rule.getCount() >= 0 + && rule.getParamIdx() != null && rule.getParamIdx() >= 0; + } + + private ParamFlowRuleManager() {} +} + diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlot.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlot.java new file mode 100644 index 00000000..c95d328d --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlot.java @@ -0,0 +1,158 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * A processor slot that is responsible for flow control by frequent ("hot spot") parameters. + * + * @author jialiang.linjl + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamFlowSlot extends AbstractLinkedProcessorSlot { + + private static final Map metricsMap + = new ConcurrentHashMap(); + + /** + * Lock for a specific resource. + */ + private final Object LOCK = new Object(); + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) + throws Throwable { + + if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) { + fireEntry(context, resourceWrapper, node, count, args); + return; + } + + checkFlow(resourceWrapper, count, args); + fireEntry(context, resourceWrapper, node, count, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + fireExit(context, resourceWrapper, count, args); + } + + void checkFlow(ResourceWrapper resourceWrapper, int count, Object... args) + throws BlockException { + if (ParamFlowRuleManager.hasRules(resourceWrapper.getName())) { + List rules = ParamFlowRuleManager.getRulesOfResource(resourceWrapper.getName()); + if (rules == null) { + return; + } + + for (ParamFlowRule rule : rules) { + // Initialize the parameter metrics. + initHotParamMetricsFor(resourceWrapper, rule.getParamIdx()); + + if (!ParamFlowChecker.passCheck(resourceWrapper, rule, count, args)) { + + // Here we add the block count. + addBlockCount(resourceWrapper, count, args); + + String message = ""; + if (args.length > rule.getParamIdx()) { + Object value = args[rule.getParamIdx()]; + message = String.valueOf(value); + } + throw new ParamFlowException(resourceWrapper.getName(), message); + } + } + } + } + + private void addBlockCount(ResourceWrapper resourceWrapper, int count, Object... args) { + ParameterMetric parameterMetric = ParamFlowSlot.getParamMetric(resourceWrapper); + + if (parameterMetric != null) { + parameterMetric.addBlock(count, args); + } + } + + /** + * Init the parameter metric and index map for given resource. + * Package-private for test. + * + * @param resourceWrapper resource to init + * @param index index to initialize, which must be valid + */ + void initHotParamMetricsFor(ResourceWrapper resourceWrapper, /*@Valid*/ int index) { + ParameterMetric metric; + // Assume that the resource is valid. + if ((metric = metricsMap.get(resourceWrapper)) == null) { + synchronized (LOCK) { + if ((metric = metricsMap.get(resourceWrapper)) == null) { + metric = new ParameterMetric(); + metricsMap.put(resourceWrapper, metric); + RecordLog.info("[ParamFlowSlot] Creating parameter metric for: " + resourceWrapper.getName()); + } + } + } + metric.initializeForIndex(index); + } + + public static ParameterMetric getParamMetric(ResourceWrapper resourceWrapper) { + if (resourceWrapper == null || resourceWrapper.getName() == null) { + return null; + } + return metricsMap.get(resourceWrapper); + } + + public static ParameterMetric getHotParamMetricForName(String resourceName) { + if (StringUtil.isBlank(resourceName)) { + return null; + } + for (EntryType nodeType : EntryType.values()) { + ParameterMetric metric = metricsMap.get(new StringResourceWrapper(resourceName, nodeType)); + if (metric != null) { + return metric; + } + } + return null; + } + + static void clearHotParamMetricForName(String resourceName) { + if (StringUtil.isBlank(resourceName)) { + return; + } + for (EntryType nodeType : EntryType.values()) { + metricsMap.remove(new StringResourceWrapper(resourceName, nodeType)); + } + RecordLog.info("[ParamFlowSlot] Clearing parameter metric for: " + resourceName); + } + + public static Map getMetricsMap() { + return metricsMap; + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetric.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetric.java new file mode 100644 index 00000000..b0a4530e --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetric.java @@ -0,0 +1,147 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.node.IntervalProperty; +import com.alibaba.csp.sentinel.node.SampleCountProperty; +import com.alibaba.csp.sentinel.slots.statistic.metric.HotParameterLeapArray; + +/** + * Metrics for frequent ("hot spot") parameters. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParameterMetric { + + private Map rollingParameters = + new ConcurrentHashMap(); + + public Map getRollingParameters() { + return rollingParameters; + } + + public synchronized void clear() { + rollingParameters.clear(); + } + + public void initializeForIndex(int index) { + if (!rollingParameters.containsKey(index)) { + synchronized (this) { + // putIfAbsent + if (rollingParameters.get(index) == null) { + rollingParameters.put(index, new HotParameterLeapArray( + 1000 / 2, IntervalProperty.INTERVAL)); + } + } + } + } + + public void addPass(int count, Object... args) { + add(RollingParamEvent.REQUEST_PASSED, count, args); + } + + public void addBlock(int count, Object... args) { + add(RollingParamEvent.REQUEST_BLOCKED, count, args); + } + + @SuppressWarnings("rawtypes") + private void add(RollingParamEvent event, int count, Object... args) { + if (args == null) { + return; + } + try { + for (int index = 0; index < args.length; index++) { + HotParameterLeapArray param = rollingParameters.get(index); + if (param == null) { + continue; + } + + Object arg = args[index]; + if (arg == null) { + continue; + } + if (Collection.class.isAssignableFrom(arg.getClass())) { + for (Object value : ((Collection)arg)) { + param.addValue(event, count, value); + } + } else if (arg.getClass().isArray()) { + int length = Array.getLength(arg); + for (int i = 0; i < length; i++) { + Object value = Array.get(arg, i); + param.addValue(event, count, value); + } + } else { + param.addValue(event, count, arg); + } + + } + } catch (Throwable e) { + RecordLog.warn("[ParameterMetric] Param exception", e); + } + } + + public double getPassParamQps(int index, Object value) { + try { + HotParameterLeapArray parameter = rollingParameters.get(index); + if (parameter == null || value == null) { + return -1; + } + return parameter.getRollingAvg(RollingParamEvent.REQUEST_PASSED, value); + } catch (Throwable e) { + RecordLog.info(e.getMessage(), e); + } + + return -1; + } + + public long getBlockParamQps(int index, Object value) { + try { + HotParameterLeapArray parameter = rollingParameters.get(index); + if (parameter == null || value == null) { + return -1; + } + + return (long)rollingParameters.get(index).getRollingAvg(RollingParamEvent.REQUEST_BLOCKED, value); + } catch (Throwable e) { + RecordLog.info(e.getMessage(), e); + } + + return -1; + } + + public Map getTopPassParamCount(int index, int number) { + try { + HotParameterLeapArray parameter = rollingParameters.get(index); + if (parameter == null) { + return new HashMap(); + } + + return parameter.getTopValues(RollingParamEvent.REQUEST_PASSED, number); + } catch (Throwable e) { + RecordLog.info(e.getMessage(), e); + } + + return new HashMap(); + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/RollingParamEvent.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/RollingParamEvent.java new file mode 100644 index 00000000..d60cb687 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/RollingParamEvent.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2018 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.flow.param; + +/** + * @author Eric Zhao + * @since 0.2.0 + */ +public enum RollingParamEvent { + /** + * Indicates that the request successfully passed the slot chain (entry). + */ + REQUEST_PASSED, + /** + * Indicates that the request is blocked by a specific slot. + */ + REQUEST_BLOCKED +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/ParamFlowStatisticEntryCallback.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/ParamFlowStatisticEntryCallback.java new file mode 100644 index 00000000..df99cfcf --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/ParamFlowStatisticEntryCallback.java @@ -0,0 +1,49 @@ +/* + * Copyright 1999-2018 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.statistic; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotEntryCallback; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParameterMetric; + +/** + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamFlowStatisticEntryCallback implements ProcessorSlotEntryCallback { + + @Override + public void onPass(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) + throws Exception { + // The "hot spot" parameter metric is present only if parameter flow rules for the resource exist. + ParameterMetric parameterMetric = ParamFlowSlot.getParamMetric(resourceWrapper); + + if (parameterMetric != null) { + parameterMetric.addPass(count, args); + } + } + + @Override + public void onBlocked(BlockException ex, Context context, ResourceWrapper resourceWrapper, DefaultNode param, + int count, Object... args) { + // Here we don't add block count here because checking the type of block exception can affect performance. + // We add the block count when throwing the ParamFlowException instead. + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/CacheMap.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/CacheMap.java new file mode 100644 index 00000000..f014f1e5 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/CacheMap.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2018 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.statistic.cache; + +import java.util.Set; + +/** + * A common cache map interface. + * + * @param type of the key + * @param type of the value + * @author Eric Zhao + * @since 0.2.0 + */ +public interface CacheMap { + + boolean containsKey(K key); + + V get(K key); + + V remove(K key); + + V put(K key, V value); + + V putIfAbsent(K key, V value); + + long size(); + + void clear(); + + Set ascendingKeySet(); +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/ConcurrentLinkedHashMapWrapper.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/ConcurrentLinkedHashMapWrapper.java new file mode 100644 index 00000000..cda53766 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/ConcurrentLinkedHashMapWrapper.java @@ -0,0 +1,92 @@ +/* + * Copyright 1999-2018 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.statistic.cache; + +import java.util.Set; + +import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; +import com.googlecode.concurrentlinkedhashmap.Weighers; + +/** + * A {@link ConcurrentLinkedHashMap} wrapper for the universal {@link CacheMap}. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class ConcurrentLinkedHashMapWrapper implements CacheMap { + + private static final int DEFAULT_CONCURRENCY_LEVEL = 16; + + private final ConcurrentLinkedHashMap map; + + public ConcurrentLinkedHashMapWrapper(long size) { + if (size <= 0) { + throw new IllegalArgumentException("Cache max capacity should be positive: " + size); + } + this.map = new ConcurrentLinkedHashMap.Builder() + .concurrencyLevel(DEFAULT_CONCURRENCY_LEVEL) + .maximumWeightedCapacity(size) + .weigher(Weighers.singleton()) + .build(); + } + + public ConcurrentLinkedHashMapWrapper(ConcurrentLinkedHashMap map) { + if (map == null) { + throw new IllegalArgumentException("Invalid map instance"); + } + this.map = map; + } + + @Override + public boolean containsKey(T key) { + return map.containsKey(key); + } + + @Override + public R get(T key) { + return map.get(key); + } + + @Override + public R remove(T key) { + return map.remove(key); + } + + @Override + public R put(T key, R value) { + return map.put(key, value); + } + + @Override + public R putIfAbsent(T key, R value) { + return map.putIfAbsent(key, value); + } + + @Override + public long size() { + return map.weightedSize(); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Set ascendingKeySet() { + return map.ascendingKeySet(); + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucket.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucket.java new file mode 100644 index 00000000..9691436b --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucket.java @@ -0,0 +1,67 @@ +/* + * Copyright 1999-2018 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.statistic.data; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.slots.block.flow.param.RollingParamEvent; +import com.alibaba.csp.sentinel.slots.statistic.cache.CacheMap; +import com.alibaba.csp.sentinel.slots.statistic.cache.ConcurrentLinkedHashMapWrapper; + +/** + * Represents metric bucket of frequent parameters in a period of time window. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamMapBucket { + + private final CacheMap[] data; + + @SuppressWarnings("unchecked") + public ParamMapBucket() { + RollingParamEvent[] events = RollingParamEvent.values(); + this.data = new CacheMap[events.length]; + for (RollingParamEvent event : events) { + data[event.ordinal()] = new ConcurrentLinkedHashMapWrapper(DEFAULT_MAX_CAPACITY); + } + } + + public void reset() { + for (RollingParamEvent event : RollingParamEvent.values()) { + data[event.ordinal()].clear(); + } + } + + public int get(RollingParamEvent event, Object value) { + AtomicInteger counter = data[event.ordinal()].get(value); + return counter == null ? 0 : counter.intValue(); + } + + public ParamMapBucket add(RollingParamEvent event, int count, Object value) { + data[event.ordinal()].putIfAbsent(value, new AtomicInteger()); + AtomicInteger counter = data[event.ordinal()].get(value); + counter.addAndGet(count); + return this; + } + + public Set ascendingKeySet(RollingParamEvent type) { + return data[type.ordinal()].ascendingKeySet(); + } + + public static final int DEFAULT_MAX_CAPACITY = 200; +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArray.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArray.java new file mode 100644 index 00000000..e3262d7b --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArray.java @@ -0,0 +1,127 @@ +/* + * Copyright 1999-2018 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.statistic.metric; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.alibaba.csp.sentinel.slots.block.flow.param.RollingParamEvent; +import com.alibaba.csp.sentinel.slots.statistic.base.LeapArray; +import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; +import com.alibaba.csp.sentinel.slots.statistic.data.ParamMapBucket; + +/** + * The fundamental data structure for frequent parameters statistics in a time window. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class HotParameterLeapArray extends LeapArray { + + private int intervalInSec; + + public HotParameterLeapArray(int windowLengthInMs, int intervalInSec) { + super(windowLengthInMs, intervalInSec); + this.intervalInSec = intervalInSec; + } + + public int getIntervalInSec() { + return intervalInSec; + } + + @Override + public ParamMapBucket newEmptyBucket() { + return new ParamMapBucket(); + } + + @Override + protected WindowWrap resetWindowTo(WindowWrap w, long startTime) { + w.resetTo(startTime); + w.value().reset(); + return w; + } + + public void addValue(RollingParamEvent event, int count, Object value) { + currentWindow().value().add(event, count, value); + } + + public Map getTopValues(RollingParamEvent event, int number) { + currentWindow(); + List buckets = this.values(); + + Map result = new HashMap(); + + for (ParamMapBucket b : buckets) { + Set subSet = b.ascendingKeySet(event); + for (Object o : subSet) { + Integer count = result.get(o); + if (count == null) { + count = b.get(event, o); + } else { + count += b.get(event, o); + } + result.put(o, count); + } + } + + // After merge, get the top set one. + Set> set = result.entrySet(); + List> list = new ArrayList>(set); + Collections.sort(list, new Comparator>() { + @Override + public int compare(Entry a, + Entry b) { + return (b.getValue() == null ? 0 : b.getValue()) - (a.getValue() == null ? 0 : a.getValue()); + } + }); + + Map doubleResult = new HashMap(); + + int size = list.size() > number ? number : list.size(); + for (int i = 0; i < size; i++) { + Map.Entry x = list.get(i); + if (x.getValue() == 0) { + break; + } + doubleResult.put(x.getKey(), ((double)x.getValue()) / getIntervalInSec()); + } + + return doubleResult; + } + + public long getRollingSum(RollingParamEvent event, Object value) { + currentWindow(); + + long sum = 0; + + List buckets = this.values(); + for (ParamMapBucket b : buckets) { + sum += b.get(event, value); + } + + return sum; + } + + public double getRollingAvg(RollingParamEvent event, Object value) { + return ((double) getRollingSum(event, value)) / getIntervalInSec(); + } +} diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler b/sentinel-extension/sentinel-parameter-flow-control/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler new file mode 100755 index 00000000..4c8e6938 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler @@ -0,0 +1,3 @@ +com.alibaba.csp.sentinel.command.handler.GetParamFlowRulesCommandHandler +com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler +com.alibaba.csp.sentinel.command.handler.FetchTopParamsCommandHandler \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc b/sentinel-extension/sentinel-parameter-flow-control/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc new file mode 100755 index 00000000..99c2bae6 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc @@ -0,0 +1 @@ +com.alibaba.csp.sentinel.init.ParamFlowStatisticSlotCallbackInit \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.slotchain.SlotChainBuilder b/sentinel-extension/sentinel-parameter-flow-control/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.slotchain.SlotChainBuilder new file mode 100644 index 00000000..a0354c1e --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.slotchain.SlotChainBuilder @@ -0,0 +1 @@ +com.alibaba.csp.sentinel.slots.HotParamSlotChainBuilder \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowCheckerTest.java b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowCheckerTest.java new file mode 100644 index 00000000..0b9e481e --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowCheckerTest.java @@ -0,0 +1,193 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test cases for {@link ParamFlowChecker}. + * + * @author Eric Zhao + */ +public class ParamFlowCheckerTest { + + @Test + public void testHotParamCheckerPassCheckExceedArgs() { + final String resourceName = "testHotParamCheckerPassCheckExceedArgs"; + final ResourceWrapper resourceWrapper = new StringResourceWrapper(resourceName, EntryType.IN); + int paramIdx = 1; + + ParamFlowRule rule = new ParamFlowRule(); + rule.setResource(resourceName); + rule.setCount(10); + rule.setParamIdx(paramIdx); + + assertTrue("The rule will pass if the paramIdx exceeds provided args", + ParamFlowChecker.passCheck(resourceWrapper, rule, 1, "abc")); + } + + @Test + public void testSingleValueCheckQpsWithoutExceptionItems() { + final String resourceName = "testSingleValueCheckQpsWithoutExceptionItems"; + final ResourceWrapper resourceWrapper = new StringResourceWrapper(resourceName, EntryType.IN); + int paramIdx = 0; + + long threshold = 5L; + + ParamFlowRule rule = new ParamFlowRule(); + rule.setResource(resourceName); + rule.setCount(threshold); + rule.setParamIdx(paramIdx); + + String valueA = "valueA"; + String valueB = "valueB"; + ParameterMetric metric = mock(ParameterMetric.class); + when(metric.getPassParamQps(paramIdx, valueA)).thenReturn((double)threshold - 1); + when(metric.getPassParamQps(paramIdx, valueB)).thenReturn((double)threshold + 1); + ParamFlowSlot.getMetricsMap().put(resourceWrapper, metric); + + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueA)); + assertFalse(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueB)); + } + + @Test + public void testSingleValueCheckQpsWithExceptionItems() { + final String resourceName = "testSingleValueCheckQpsWithExceptionItems"; + final ResourceWrapper resourceWrapper = new StringResourceWrapper(resourceName, EntryType.IN); + int paramIdx = 0; + + long globalThreshold = 5L; + int thresholdB = 3; + int thresholdD = 7; + + ParamFlowRule rule = new ParamFlowRule(); + rule.setResource(resourceName); + rule.setCount(globalThreshold); + rule.setParamIdx(paramIdx); + + String valueA = "valueA"; + String valueB = "valueB"; + String valueC = "valueC"; + String valueD = "valueD"; + + // Directly set parsed map for test. + Map map = new HashMap(); + map.put(valueB, thresholdB); + map.put(valueD, thresholdD); + rule.setParsedHotItems(map); + + ParameterMetric metric = mock(ParameterMetric.class); + when(metric.getPassParamQps(paramIdx, valueA)).thenReturn((double)globalThreshold - 1); + when(metric.getPassParamQps(paramIdx, valueB)).thenReturn((double)globalThreshold - 1); + when(metric.getPassParamQps(paramIdx, valueC)).thenReturn((double)globalThreshold - 1); + when(metric.getPassParamQps(paramIdx, valueD)).thenReturn((double)globalThreshold + 1); + ParamFlowSlot.getMetricsMap().put(resourceWrapper, metric); + + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueA)); + assertFalse(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueB)); + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueC)); + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueD)); + + when(metric.getPassParamQps(paramIdx, valueA)).thenReturn((double)globalThreshold); + when(metric.getPassParamQps(paramIdx, valueB)).thenReturn((double)thresholdB - 1L); + when(metric.getPassParamQps(paramIdx, valueC)).thenReturn((double)globalThreshold + 1); + when(metric.getPassParamQps(paramIdx, valueD)).thenReturn((double)globalThreshold - 1) + .thenReturn((double)thresholdD); + + assertFalse(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueA)); + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueB)); + assertFalse(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueC)); + assertTrue(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueD)); + assertFalse(ParamFlowChecker.passSingleValueCheck(resourceWrapper, rule, 1, valueD)); + } + + @Test + public void testPassLocalCheckForCollection() { + final String resourceName = "testPassLocalCheckForCollection"; + final ResourceWrapper resourceWrapper = new StringResourceWrapper(resourceName, EntryType.IN); + int paramIdx = 0; + double globalThreshold = 10; + + ParamFlowRule rule = new ParamFlowRule(resourceName) + .setParamIdx(paramIdx) + .setCount(globalThreshold); + + String v1 = "a", v2 = "B", v3 = "Cc"; + List list = Arrays.asList(v1, v2, v3); + ParameterMetric metric = mock(ParameterMetric.class); + when(metric.getPassParamQps(paramIdx, v1)).thenReturn(globalThreshold - 2) + .thenReturn(globalThreshold - 1); + when(metric.getPassParamQps(paramIdx, v2)).thenReturn(globalThreshold - 2) + .thenReturn(globalThreshold - 1); + when(metric.getPassParamQps(paramIdx, v3)).thenReturn(globalThreshold - 1) + .thenReturn(globalThreshold); + ParamFlowSlot.getMetricsMap().put(resourceWrapper, metric); + + assertTrue(ParamFlowChecker.passCheck(resourceWrapper, rule, 1, list)); + assertFalse(ParamFlowChecker.passCheck(resourceWrapper, rule, 1, list)); + } + + @Test + public void testPassLocalCheckForArray() { + final String resourceName = "testPassLocalCheckForArray"; + final ResourceWrapper resourceWrapper = new StringResourceWrapper(resourceName, EntryType.IN); + int paramIdx = 0; + double globalThreshold = 10; + + ParamFlowRule rule = new ParamFlowRule(resourceName) + .setParamIdx(paramIdx) + .setCount(globalThreshold); + + String v1 = "a", v2 = "B", v3 = "Cc"; + Object arr = new String[] {v1, v2, v3}; + ParameterMetric metric = mock(ParameterMetric.class); + when(metric.getPassParamQps(paramIdx, v1)).thenReturn(globalThreshold - 2) + .thenReturn(globalThreshold - 1); + when(metric.getPassParamQps(paramIdx, v2)).thenReturn(globalThreshold - 2) + .thenReturn(globalThreshold - 1); + when(metric.getPassParamQps(paramIdx, v3)).thenReturn(globalThreshold - 1) + .thenReturn(globalThreshold); + ParamFlowSlot.getMetricsMap().put(resourceWrapper, metric); + + assertTrue(ParamFlowChecker.passCheck(resourceWrapper, rule, 1, arr)); + assertFalse(ParamFlowChecker.passCheck(resourceWrapper, rule, 1, arr)); + } + + @Before + public void setUp() throws Exception { + ParamFlowSlot.getMetricsMap().clear(); + } + + @After + public void tearDown() throws Exception { + ParamFlowSlot.getMetricsMap().clear(); + } +} \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRuleManagerTest.java b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRuleManagerTest.java new file mode 100644 index 00000000..a0953eda --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRuleManagerTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test cases for {@link ParamFlowRuleManager}. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamFlowRuleManagerTest { + + @Before + public void setUp() { + ParamFlowRuleManager.loadRules(null); + } + + @After + public void tearDown() { + ParamFlowRuleManager.loadRules(null); + } + + @Test + public void testLoadHotParamRulesClearingUnusedMetrics() { + final String resA = "resA"; + ParamFlowRule ruleA = new ParamFlowRule(resA) + .setCount(1) + .setParamIdx(0); + ParamFlowRuleManager.loadRules(Collections.singletonList(ruleA)); + ParamFlowSlot.getMetricsMap().put(new StringResourceWrapper(resA, EntryType.IN), new ParameterMetric()); + assertNotNull(ParamFlowSlot.getHotParamMetricForName(resA)); + + final String resB = "resB"; + ParamFlowRule ruleB = new ParamFlowRule(resB) + .setCount(2) + .setParamIdx(1); + ParamFlowRuleManager.loadRules(Collections.singletonList(ruleB)); + assertNull("The unused hot param metric should be cleared", ParamFlowSlot.getHotParamMetricForName(resA)); + } + + @Test + public void testLoadHotParamRulesAndGet() { + final String resA = "abc"; + final String resB = "foo"; + final String resC = "baz"; + // Rule A to C is for resource A. + // Rule A is invalid. + ParamFlowRule ruleA = new ParamFlowRule(resA).setCount(10); + ParamFlowRule ruleB = new ParamFlowRule(resA) + .setCount(28) + .setParamIdx(1); + ParamFlowRule ruleC = new ParamFlowRule(resA) + .setCount(8) + .setParamIdx(1) + .setBlockGrade(RuleConstant.FLOW_GRADE_QPS); + // Rule D is for resource B. + ParamFlowRule ruleD = new ParamFlowRule(resB) + .setCount(9) + .setParamIdx(0) + .setParamFlowItemList(Arrays.asList(ParamFlowItem.newItem(7L, 6), ParamFlowItem.newItem(9L, 4))); + ParamFlowRuleManager.loadRules(Arrays.asList(ruleA, ruleB, ruleC, ruleD)); + + // Test for ParamFlowRuleManager#hasRules + assertTrue(ParamFlowRuleManager.hasRules(resA)); + assertTrue(ParamFlowRuleManager.hasRules(resB)); + assertFalse(ParamFlowRuleManager.hasRules(resC)); + // Test for ParamFlowRuleManager#getRulesOfResource + List rulesForResA = ParamFlowRuleManager.getRulesOfResource(resA); + assertEquals(2, rulesForResA.size()); + assertFalse(rulesForResA.contains(ruleA)); + assertTrue(rulesForResA.contains(ruleB)); + assertTrue(rulesForResA.contains(ruleC)); + List rulesForResB = ParamFlowRuleManager.getRulesOfResource(resB); + assertEquals(1, rulesForResB.size()); + assertEquals(ruleD, rulesForResB.get(0)); + // Test for ParamFlowRuleManager#getRules + List allRules = ParamFlowRuleManager.getRules(); + assertFalse(allRules.contains(ruleA)); + assertTrue(allRules.contains(ruleB)); + assertTrue(allRules.contains(ruleC)); + assertTrue(allRules.contains(ruleD)); + } + + @Test + public void testParseHotParamExceptionItemsFailure() { + String valueB = "Sentinel"; + Integer valueC = 6; + char valueD = 6; + float valueE = 11.11f; + // Null object will not be parsed. + ParamFlowItem itemA = new ParamFlowItem(null, 1, double.class.getName()); + // Hot item with empty class type will be treated as string. + ParamFlowItem itemB = new ParamFlowItem(valueB, 3, null); + ParamFlowItem itemE = new ParamFlowItem(String.valueOf(valueE), 3, ""); + // Bad count will not be parsed. + ParamFlowItem itemC = ParamFlowItem.newItem(valueC, -5); + ParamFlowItem itemD = new ParamFlowItem(String.valueOf(valueD), null, char.class.getName()); + + List badItems = Arrays.asList(itemA, itemB, itemC, itemD, itemE); + Map parsedItems = ParamFlowRuleManager.parseHotItems(badItems); + + // Value B and E will be parsed, but ignoring the type. + assertEquals(2, parsedItems.size()); + assertEquals(itemB.getCount(), parsedItems.get(valueB)); + assertFalse(parsedItems.containsKey(valueE)); + assertEquals(itemE.getCount(), parsedItems.get(String.valueOf(valueE))); + } + + @Test + public void testParseHotParamExceptionItemsSuccess() { + // Test for empty list. + assertEquals(0, ParamFlowRuleManager.parseHotItems(null).size()); + assertEquals(0, ParamFlowRuleManager.parseHotItems(new ArrayList()).size()); + + // Test for boxing objects and primitive types. + Double valueA = 1.1d; + String valueB = "Sentinel"; + Integer valueC = 6; + char valueD = 'c'; + ParamFlowItem itemA = ParamFlowItem.newItem(valueA, 1); + ParamFlowItem itemB = ParamFlowItem.newItem(valueB, 3); + ParamFlowItem itemC = ParamFlowItem.newItem(valueC, 5); + ParamFlowItem itemD = new ParamFlowItem().setObject(String.valueOf(valueD)) + .setClassType(char.class.getName()) + .setCount(7); + List items = Arrays.asList(itemA, itemB, itemC, itemD); + Map parsedItems = ParamFlowRuleManager.parseHotItems(items); + assertEquals(itemA.getCount(), parsedItems.get(valueA)); + assertEquals(itemB.getCount(), parsedItems.get(valueB)); + assertEquals(itemC.getCount(), parsedItems.get(valueC)); + assertEquals(itemD.getCount(), parsedItems.get(valueD)); + } + + @Test + public void testCheckValidHotParamRule() { + // Null or empty resource; + ParamFlowRule rule1 = new ParamFlowRule(); + ParamFlowRule rule2 = new ParamFlowRule(""); + assertFalse(ParamFlowRuleManager.isValidRule(null)); + assertFalse(ParamFlowRuleManager.isValidRule(rule1)); + assertFalse(ParamFlowRuleManager.isValidRule(rule2)); + + // Invalid threshold count. + ParamFlowRule rule3 = new ParamFlowRule("abc") + .setCount(-1) + .setParamIdx(1); + assertFalse(ParamFlowRuleManager.isValidRule(rule3)); + + // Parameter index not set or invalid. + ParamFlowRule rule4 = new ParamFlowRule("abc") + .setCount(1); + ParamFlowRule rule5 = new ParamFlowRule("abc") + .setCount(1) + .setParamIdx(-1); + assertFalse(ParamFlowRuleManager.isValidRule(rule4)); + assertFalse(ParamFlowRuleManager.isValidRule(rule5)); + + ParamFlowRule goodRule = new ParamFlowRule("abc") + .setCount(10) + .setParamIdx(1); + assertTrue(ParamFlowRuleManager.isValidRule(goodRule)); + } +} \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlotTest.java b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlotTest.java new file mode 100644 index 00000000..7b03922c --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowSlotTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import java.util.Collections; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test cases for {@link ParamFlowSlot}. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamFlowSlotTest { + + private final ParamFlowSlot paramFlowSlot = new ParamFlowSlot(); + + @Test + public void testEntryWhenParamFlowRuleNotExists() throws Throwable { + String resourceName = "testEntryWhenParamFlowRuleNotExists"; + ResourceWrapper resourceWrapper = new StringResourceWrapper(resourceName, EntryType.IN); + paramFlowSlot.entry(null, resourceWrapper, null, 1, "abc"); + // The parameter metric instance will not be created. + assertNull(ParamFlowSlot.getParamMetric(resourceWrapper)); + } + + @Test + public void testEntryWhenParamFlowExists() throws Throwable { + String resourceName = "testEntryWhenParamFlowExists"; + ResourceWrapper resourceWrapper = new StringResourceWrapper(resourceName, EntryType.IN); + long argToGo = 1L; + double count = 10; + ParamFlowRule rule = new ParamFlowRule(resourceName) + .setCount(count) + .setParamIdx(0); + ParamFlowRuleManager.loadRules(Collections.singletonList(rule)); + + ParameterMetric metric = mock(ParameterMetric.class); + // First pass, then blocked. + when(metric.getPassParamQps(rule.getParamIdx(), argToGo)) + .thenReturn(count - 1) + .thenReturn(count); + // Insert the mock metric to control pass or block. + ParamFlowSlot.getMetricsMap().put(resourceWrapper, metric); + + // The first entry will pass. + paramFlowSlot.entry(null, resourceWrapper, null, 1, argToGo); + // The second entry will be blocked. + try { + paramFlowSlot.entry(null, resourceWrapper, null, 1, argToGo); + } catch (ParamFlowException ex) { + assertEquals(String.valueOf(argToGo), ex.getMessage()); + assertEquals(resourceName, ex.getResourceName()); + return; + } + fail("The second entry should be blocked"); + } + + @Test + public void testGetNullParamMetric() { + assertNull(ParamFlowSlot.getParamMetric(null)); + } + + @Test + public void testInitParamMetrics() { + int index = 1; + String resourceName = "res-" + System.currentTimeMillis(); + ResourceWrapper resourceWrapper = new StringResourceWrapper(resourceName, EntryType.IN); + + assertNull(ParamFlowSlot.getParamMetric(resourceWrapper)); + + paramFlowSlot.initHotParamMetricsFor(resourceWrapper, index); + ParameterMetric metric = ParamFlowSlot.getParamMetric(resourceWrapper); + assertNotNull(metric); + assertNotNull(metric.getRollingParameters().get(index)); + + // Duplicate init. + paramFlowSlot.initHotParamMetricsFor(resourceWrapper, index); + assertSame(metric, ParamFlowSlot.getParamMetric(resourceWrapper)); + } + + @Before + public void setUp() throws Exception { + ParamFlowRuleManager.loadRules(null); + ParamFlowSlot.getMetricsMap().clear(); + } + + @After + public void tearDown() throws Exception { + // Clean the metrics map. + ParamFlowSlot.getMetricsMap().clear(); + ParamFlowRuleManager.loadRules(null); + } +} \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetricTest.java b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetricTest.java new file mode 100644 index 00000000..e7789e84 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParameterMetricTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 1999-2018 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.flow.param; + +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.csp.sentinel.slots.statistic.metric.HotParameterLeapArray; + +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test cases for {@link ParameterMetric}. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParameterMetricTest { + + @Test + public void testGetTopParamCount() { + ParameterMetric metric = new ParameterMetric(); + int index = 1; + int n = 10; + RollingParamEvent event = RollingParamEvent.REQUEST_PASSED; + HotParameterLeapArray leapArray = mock(HotParameterLeapArray.class); + Map topValues = new HashMap() {{ + put("a", 3d); + put("b", 7d); + }}; + when(leapArray.getTopValues(event, n)).thenReturn(topValues); + + // Get when not initialized. + assertEquals(0, metric.getTopPassParamCount(index, n).size()); + + metric.getRollingParameters().put(index, leapArray); + assertEquals(topValues, metric.getTopPassParamCount(index, n)); + } + + @Test + public void testInitAndClearHotParameterMetric() { + ParameterMetric metric = new ParameterMetric(); + int index = 1; + metric.initializeForIndex(index); + HotParameterLeapArray leapArray = metric.getRollingParameters().get(index); + assertNotNull(leapArray); + + metric.initializeForIndex(index); + assertSame(leapArray, metric.getRollingParameters().get(index)); + + metric.clear(); + assertEquals(0, metric.getRollingParameters().size()); + } + + private static final int PARAM_TYPE_NORMAL = 0; + private static final int PARAM_TYPE_ARRAY = 1; + private static final int PARAM_TYPE_COLLECTION = 2; +} \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucketTest.java b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucketTest.java new file mode 100644 index 00000000..5935b819 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucketTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 1999-2018 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.statistic.data; + +import com.alibaba.csp.sentinel.slots.block.flow.param.RollingParamEvent; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test cases for {@link ParamMapBucket}. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class ParamMapBucketTest { + + @Test + public void testAddEviction() { + ParamMapBucket bucket = new ParamMapBucket(); + for (int i = 0; i < ParamMapBucket.DEFAULT_MAX_CAPACITY; i++) { + bucket.add(RollingParamEvent.REQUEST_PASSED, 1, "param-" + i); + } + String lastParam = "param-end"; + bucket.add(RollingParamEvent.REQUEST_PASSED, 1, lastParam); + assertEquals(0, bucket.get(RollingParamEvent.REQUEST_PASSED, "param-0")); + assertEquals(1, bucket.get(RollingParamEvent.REQUEST_PASSED, "param-1")); + assertEquals(1, bucket.get(RollingParamEvent.REQUEST_PASSED, lastParam)); + } + + @Test + public void testAddGetResetCommon() { + ParamMapBucket bucket = new ParamMapBucket(); + double paramA = 1.1d; + double paramB = 2.2d; + double paramC = -19.7d; + // Block: A 5 | B 1 | C 6 + // Pass: A 0 | B 1 | C 7 + bucket.add(RollingParamEvent.REQUEST_BLOCKED, 3, paramA); + bucket.add(RollingParamEvent.REQUEST_PASSED, 1, paramB); + bucket.add(RollingParamEvent.REQUEST_BLOCKED, 1, paramB); + bucket.add(RollingParamEvent.REQUEST_BLOCKED, 2, paramA); + bucket.add(RollingParamEvent.REQUEST_PASSED, 6, paramC); + bucket.add(RollingParamEvent.REQUEST_BLOCKED, 4, paramC); + bucket.add(RollingParamEvent.REQUEST_PASSED, 1, paramC); + bucket.add(RollingParamEvent.REQUEST_BLOCKED, 2, paramC); + + assertEquals(5, bucket.get(RollingParamEvent.REQUEST_BLOCKED, paramA)); + assertEquals(1, bucket.get(RollingParamEvent.REQUEST_BLOCKED, paramB)); + assertEquals(6, bucket.get(RollingParamEvent.REQUEST_BLOCKED, paramC)); + assertEquals(0, bucket.get(RollingParamEvent.REQUEST_PASSED, paramA)); + assertEquals(1, bucket.get(RollingParamEvent.REQUEST_PASSED, paramB)); + assertEquals(7, bucket.get(RollingParamEvent.REQUEST_PASSED, paramC)); + + bucket.reset(); + assertEquals(0, bucket.get(RollingParamEvent.REQUEST_BLOCKED, paramA)); + assertEquals(0, bucket.get(RollingParamEvent.REQUEST_BLOCKED, paramB)); + assertEquals(0, bucket.get(RollingParamEvent.REQUEST_BLOCKED, paramC)); + assertEquals(0, bucket.get(RollingParamEvent.REQUEST_PASSED, paramA)); + assertEquals(0, bucket.get(RollingParamEvent.REQUEST_PASSED, paramB)); + assertEquals(0, bucket.get(RollingParamEvent.REQUEST_PASSED, paramC)); + } +} \ No newline at end of file diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArrayTest.java b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArrayTest.java new file mode 100644 index 00000000..b4056576 --- /dev/null +++ b/sentinel-extension/sentinel-parameter-flow-control/src/test/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArrayTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 1999-2018 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.statistic.metric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.alibaba.csp.sentinel.slots.block.flow.param.RollingParamEvent; +import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; +import com.alibaba.csp.sentinel.slots.statistic.data.ParamMapBucket; + +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Test cases for {@link HotParameterLeapArray}. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class HotParameterLeapArrayTest { + + @Test + public void testAddValueToBucket() { + HotParameterLeapArray leapArray = mock(HotParameterLeapArray.class); + String paramA = "paramA"; + int initialCountA = 3; + RollingParamEvent passEvent = RollingParamEvent.REQUEST_PASSED; + final ParamMapBucket bucket = new ParamMapBucket(); + bucket.add(passEvent, initialCountA, paramA); + + doCallRealMethod().when(leapArray).addValue(any(RollingParamEvent.class), anyInt(), any(Object.class)); + when(leapArray.currentWindow()).thenReturn(new WindowWrap(0, 0, bucket)); + assertEquals(initialCountA, leapArray.currentWindow().value().get(passEvent, paramA)); + + int delta = 2; + leapArray.addValue(passEvent, delta, paramA); + assertEquals(initialCountA + delta, leapArray.currentWindow().value().get(passEvent, paramA)); + } + + @Test + public void testGetTopValues() { + int intervalInSec = 2; + int a1 = 3, a2 = 5; + String paramPrefix = "param-"; + HotParameterLeapArray leapArray = mock(HotParameterLeapArray.class); + when(leapArray.getIntervalInSec()).thenReturn(intervalInSec); + + final ParamMapBucket b1 = generateBucket(a1, paramPrefix); + final ParamMapBucket b2 = generateBucket(a2, paramPrefix); + List buckets = new ArrayList() {{ + add(b1); + add(b2); + }}; + when(leapArray.values()).thenReturn(buckets); + when(leapArray.getTopValues(any(RollingParamEvent.class), any(int.class))).thenCallRealMethod(); + + Map top2Values = leapArray.getTopValues(RollingParamEvent.REQUEST_PASSED, a1 - 1); + // Top 2 should be 5 and 3 + assertEquals((double)5 * 10 / intervalInSec, top2Values.get(paramPrefix + 5), 0.01); + assertEquals((double)3 * 20 / intervalInSec, top2Values.get(paramPrefix + 3), 0.01); + + Map top4Values = leapArray.getTopValues(RollingParamEvent.REQUEST_PASSED, a2 - 1); + assertEquals(a2 - 1, top4Values.size()); + assertFalse(top4Values.containsKey(paramPrefix + 1)); + + Map topMoreValues = leapArray.getTopValues(RollingParamEvent.REQUEST_PASSED, a2 + 1); + assertEquals("This should contain all parameters but no more than " + a2, a2, topMoreValues.size()); + } + + private ParamMapBucket generateBucket(int amount, String prefix) { + if (amount <= 0) { + throw new IllegalArgumentException("Bad amount"); + } + ParamMapBucket bucket = new ParamMapBucket(); + for (int i = 1; i <= amount; i++) { + bucket.add(RollingParamEvent.REQUEST_PASSED, i * 10, prefix + i); + bucket.add(RollingParamEvent.REQUEST_BLOCKED, i, prefix + i); + } + return bucket; + } + + @Test + public void testGetRollingSum() { + HotParameterLeapArray leapArray = mock(HotParameterLeapArray.class); + String v1 = "a", v2 = "B", v3 = "Cc"; + int p1a = 19, p1b = 3; + int p2a = 6, p2c = 17; + RollingParamEvent passEvent = RollingParamEvent.REQUEST_PASSED; + final ParamMapBucket b1 = new ParamMapBucket() + .add(passEvent, p1a, v1) + .add(passEvent, p1b, v2); + final ParamMapBucket b2 = new ParamMapBucket() + .add(passEvent, p2a, v1) + .add(passEvent, p2c, v3); + List buckets = new ArrayList() {{ add(b1); add(b2); }}; + when(leapArray.values()).thenReturn(buckets); + when(leapArray.getRollingSum(any(RollingParamEvent.class), any(Object.class))).thenCallRealMethod(); + + assertEquals(p1a + p2a, leapArray.getRollingSum(passEvent, v1)); + assertEquals(p1b, leapArray.getRollingSum(passEvent, v2)); + assertEquals(p2c, leapArray.getRollingSum(passEvent, v3)); + } + + @Test + public void testGetRollingAvg() { + HotParameterLeapArray leapArray = mock(HotParameterLeapArray.class); + when(leapArray.getRollingSum(any(RollingParamEvent.class), any(Object.class))).thenReturn(15L); + when(leapArray.getIntervalInSec()).thenReturn(1) + .thenReturn(2); + when(leapArray.getRollingAvg(any(RollingParamEvent.class), any(Object.class))).thenCallRealMethod(); + + assertEquals(15.0d, leapArray.getRollingAvg(RollingParamEvent.REQUEST_PASSED, "abc"), 0.001); + assertEquals(15.0d / 2, leapArray.getRollingAvg(RollingParamEvent.REQUEST_PASSED, "abc"), 0.001); + } +} \ No newline at end of file