diff --git a/sentinel-cluster/pom.xml b/sentinel-cluster/pom.xml index c58ee305..3f1f61ed 100644 --- a/sentinel-cluster/pom.xml +++ b/sentinel-cluster/pom.xml @@ -23,6 +23,7 @@ sentinel-cluster-client-default sentinel-cluster-server-default sentinel-cluster-common-default + sentinel-cluster-server-envoy-rls diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/README.md b/sentinel-cluster/sentinel-cluster-server-envoy-rls/README.md new file mode 100644 index 00000000..88b337e0 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/README.md @@ -0,0 +1,15 @@ +# Sentinel Token Server (Envoy RLS implementation) + +This module provides the [Envoy rate limiting gRPC service](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting#arch-overview-rate-limit) implementation +with Sentinel token server. + +> Note: the gRPC stub classes for Envoy RLS service is generated via `protobuf-maven-plugin` during the `compile` goal. +> The generated classes is located in the directory: `target/generated-sources/protobuf`. + +## Build + +Build the executable jar: + +```bash +mvn clean package -P prod +``` diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml b/sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml new file mode 100644 index 00000000..5f78ab90 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml @@ -0,0 +1,157 @@ + + + + sentinel-cluster + com.alibaba.csp + 1.7.0-SNAPSHOT + + 4.0.0 + + sentinel-cluster-server-envoy-rls + 1.7.0-SNAPSHOT + + + 1.8 + 1.8 + + 3.10.0 + 1.24.0 + + 3.2.1 + + + + + com.alibaba.csp + sentinel-cluster-server-default + ${project.version} + + + com.alibaba.csp + sentinel-datasource-extension + + + com.alibaba.csp + sentinel-transport-simple-http + + + + io.grpc + grpc-netty + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + org.yaml + snakeyaml + 1.25 + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + + + + kr.motd.maven + os-maven-plugin + 1.6.2 + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + + compile + compile-custom + + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${maven.pmd.version} + + + target/generated-sources + + + + + + + + + prod + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven.shade.version} + + + package + + shade + + + sentinel-envoy-rls-token-server + + + + com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsServer + + + + + + + + + + + + + \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsConstants.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsConstants.java new file mode 100644 index 00000000..369fe3e0 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsConstants.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls; + +/** + * @author Eric Zhao + */ +public final class SentinelEnvoyRlsConstants { + + public static final int DEFAULT_GRPC_PORT = 10245; + public static final String SERVER_APP_NAME = "sentinel-rls-token-server"; + + public static final String GRPC_PORT_ENV_KEY = "SENTINEL_RLS_GRPC_PORT"; + public static final String GRPC_PORT_PROPERTY_KEY = "csp.sentinel.grpc.server.port"; + public static final String RULE_FILE_PATH_ENV_KEY = "SENTINEL_RLS_RULE_FILE_PATH"; + public static final String RULE_FILE_PATH_PROPERTY_KEY = "csp.sentinel.rls.rule.file"; + + public static final String ENABLE_ACCESS_LOG_ENV_KEY = "SENTINEL_RLS_ACCESS_LOG"; + + private SentinelEnvoyRlsConstants() {} +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServer.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServer.java new file mode 100644 index 00000000..102add2d --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServer.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls; + +import java.util.Optional; + +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.datasource.EnvoyRlsRuleDataSourceService; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.init.InitExecutor; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + */ +public class SentinelEnvoyRlsServer { + + public static void main(String[] args) throws Exception { + System.setProperty("project.name", SentinelEnvoyRlsConstants.SERVER_APP_NAME); + + EnvoyRlsRuleDataSourceService dataSourceService = new EnvoyRlsRuleDataSourceService(); + dataSourceService.init(); + + int port = resolvePort(); + SentinelRlsGrpcServer server = new SentinelRlsGrpcServer(port); + server.start(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.err.println("[SentinelEnvoyRlsServer] Shutting down gRPC RLS server since JVM is shutting down"); + server.shutdown(); + dataSourceService.onShutdown(); + System.err.println("[SentinelEnvoyRlsServer] Server has been shut down"); + })); + InitExecutor.doInit(); + + server.blockUntilShutdown(); + } + + private static int resolvePort() { + final int defaultPort = SentinelEnvoyRlsConstants.DEFAULT_GRPC_PORT; + // Order: system env > property + String portStr = Optional.ofNullable(System.getenv(SentinelEnvoyRlsConstants.GRPC_PORT_ENV_KEY)) + .orElse(SentinelConfig.getConfig(SentinelEnvoyRlsConstants.GRPC_PORT_PROPERTY_KEY)); + if (StringUtil.isBlank(portStr)) { + return defaultPort; + } + try { + int port = Integer.parseInt(portStr); + if (port <= 0 || port > 65535) { + RecordLog.warn("[SentinelEnvoyRlsServer] Invalid port <" + portStr + ">, using default" + defaultPort); + return defaultPort; + } + return port; + } catch (Exception ex) { + RecordLog.warn("[SentinelEnvoyRlsServer] Failed to resolve port, using default " + defaultPort); + System.err.println("[SentinelEnvoyRlsServer] Failed to resolve port, using default " + defaultPort); + return defaultPort; + } + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImpl.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImpl.java new file mode 100644 index 00000000..7e0bc7b9 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImpl.java @@ -0,0 +1,135 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.csp.sentinel.cluster.TokenResult; +import com.alibaba.csp.sentinel.cluster.TokenResultStatus; +import com.alibaba.csp.sentinel.cluster.flow.rule.ClusterFlowRuleManager; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.flow.SimpleClusterFlowChecker; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.log.RlsAccessLogger; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.function.Tuple2; + +import com.google.protobuf.TextFormat; +import io.envoyproxy.envoy.api.v2.ratelimit.RateLimitDescriptor; +import io.envoyproxy.envoy.api.v2.ratelimit.RateLimitDescriptor.Entry; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitRequest; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.Code; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.DescriptorStatus; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.RateLimit; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.RateLimit.Unit; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitServiceGrpc; +import io.grpc.stub.StreamObserver; + +import static com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter.SEPARATOR; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public class SentinelEnvoyRlsServiceImpl extends RateLimitServiceGrpc.RateLimitServiceImplBase { + + @Override + public void shouldRateLimit(RateLimitRequest request, StreamObserver responseObserver) { + int acquireCount = request.getHitsAddend(); + if (acquireCount < 0) { + responseObserver.onError(new IllegalArgumentException( + "acquireCount should be positive, but actual: " + acquireCount)); + return; + } + if (acquireCount == 0) { + // Not present, use the default "1" by default. + acquireCount = 1; + } + + String domain = request.getDomain(); + boolean blocked = false; + List statusList = new ArrayList<>(request.getDescriptorsCount()); + for (RateLimitDescriptor descriptor : request.getDescriptorsList()) { + Tuple2 t = checkToken(domain, descriptor, acquireCount); + TokenResult r = t.r2; + + printAccessLogIfNecessary(domain, descriptor, r); + + if (r.getStatus() == TokenResultStatus.NO_RULE_EXISTS) { + // If the rule of the descriptor is absent, the request will pass directly. + r.setStatus(TokenResultStatus.OK); + } + + if (!blocked && r.getStatus() != TokenResultStatus.OK) { + blocked = true; + } + + Code statusCode = r.getStatus() == TokenResultStatus.OK ? Code.OK : Code.OVER_LIMIT; + DescriptorStatus.Builder descriptorStatusBuilder = DescriptorStatus.newBuilder() + .setCode(statusCode); + if (t.r1 != null) { + descriptorStatusBuilder + .setCurrentLimit(RateLimit.newBuilder().setUnit(Unit.SECOND) + .setRequestsPerUnit((int)t.r1.getCount()) + .build()) + .setLimitRemaining(r.getRemaining()); + } + statusList.add(descriptorStatusBuilder.build()); + } + + Code overallStatus = blocked ? Code.OVER_LIMIT : Code.OK; + RateLimitResponse response = RateLimitResponse.newBuilder() + .setOverallCode(overallStatus) + .addAllStatuses(statusList) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + private void printAccessLogIfNecessary(String domain, RateLimitDescriptor descriptor, TokenResult result) { + if (!RlsAccessLogger.isEnabled()) { + return; + } + String message = new StringBuilder("[RlsAccessLog] domain=").append(domain) + .append(", descriptor=").append(TextFormat.shortDebugString(descriptor)) + .append(", checkStatus=").append(result.getStatus()) + .append(", remaining=").append(result.getRemaining()) + .toString(); + RlsAccessLogger.log(message); + } + + protected Tuple2 checkToken(String domain, RateLimitDescriptor descriptor, int acquireCount) { + long ruleId = EnvoySentinelRuleConverter.generateFlowId(generateKey(domain, descriptor)); + + FlowRule rule = ClusterFlowRuleManager.getFlowRuleById(ruleId); + if (rule == null) { + // Pass if the target rule is absent. + return Tuple2.of(null, new TokenResult(TokenResultStatus.NO_RULE_EXISTS)); + } + // If the rule is present, it should be valid. + return Tuple2.of(rule, SimpleClusterFlowChecker.acquireClusterToken(rule, acquireCount)); + } + + private String generateKey(String domain, RateLimitDescriptor descriptor) { + StringBuilder sb = new StringBuilder(domain); + for (Entry resource : descriptor.getEntriesList()) { + sb.append(SEPARATOR).append(resource.getKey()).append(SEPARATOR).append(resource.getValue()); + } + return sb.toString(); + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java new file mode 100644 index 00000000..459540ad --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java @@ -0,0 +1,59 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls; + +import java.io.IOException; + +import com.alibaba.csp.sentinel.log.RecordLog; + +import io.grpc.Server; +import io.grpc.ServerBuilder; + +/** + * @author Eric Zhao + */ +public class SentinelRlsGrpcServer { + + private final Server server; + + public SentinelRlsGrpcServer(int port) { + ServerBuilder builder = ServerBuilder.forPort(port) + .addService(new SentinelEnvoyRlsServiceImpl()); + server = builder.build(); + } + + public void start() throws IOException { + // The gRPC server has already checked the start status, so we don't check here. + server.start(); + String message = "[SentinelRlsGrpcServer] RLS server is running at port " + server.getPort(); + RecordLog.info(message); + System.out.println(message); + } + + public void shutdown() { + server.shutdownNow(); + } + + public boolean isShutdown() { + return server.isShutdown(); + } + + public void blockUntilShutdown() throws InterruptedException { + if (server != null) { + server.awaitTermination(); + } + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/datasource/EnvoyRlsRuleDataSourceService.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/datasource/EnvoyRlsRuleDataSourceService.java new file mode 100644 index 00000000..4cf0f07e --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/datasource/EnvoyRlsRuleDataSourceService.java @@ -0,0 +1,80 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls.datasource; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsConstants; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRule; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRuleManager; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource; +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.util.StringUtil; + +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.representer.Representer; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public class EnvoyRlsRuleDataSourceService { + + private final Yaml yaml; + private ReadableDataSource> ds; + + public EnvoyRlsRuleDataSourceService() { + this.yaml = createYamlParser(); + } + + private Yaml createYamlParser() { + Representer representer = new Representer(); + representer.getPropertyUtils().setSkipMissingProperties(true); + return new Yaml(representer); + } + + public synchronized void init() throws Exception { + if (ds != null) { + return; + } + String configPath = getRuleConfigPath(); + if (StringUtil.isBlank(configPath)) { + throw new IllegalStateException("Empty rule config path, please set the file path in the env: " + + SentinelEnvoyRlsConstants.RULE_FILE_PATH_ENV_KEY); + } + + this.ds = new FileRefreshableDataSource<>(configPath, s -> Arrays.asList(yaml.loadAs(s, EnvoyRlsRule.class))); + EnvoyRlsRuleManager.register2Property(ds.getProperty()); + } + + public synchronized void onShutdown() { + if (ds != null) { + try { + ds.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private String getRuleConfigPath() { + return Optional.ofNullable(System.getenv(SentinelEnvoyRlsConstants.RULE_FILE_PATH_ENV_KEY)) + .orElse(SentinelConfig.getConfig(SentinelEnvoyRlsConstants.RULE_FILE_PATH_PROPERTY_KEY)); + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/flow/SimpleClusterFlowChecker.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/flow/SimpleClusterFlowChecker.java new file mode 100644 index 00000000..efa307cd --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/flow/SimpleClusterFlowChecker.java @@ -0,0 +1,74 @@ +/* + * 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.cluster.server.envoy.rls.flow; + +import com.alibaba.csp.sentinel.cluster.TokenResult; +import com.alibaba.csp.sentinel.cluster.TokenResultStatus; +import com.alibaba.csp.sentinel.cluster.flow.statistic.ClusterMetricStatistics; +import com.alibaba.csp.sentinel.cluster.flow.statistic.data.ClusterFlowEvent; +import com.alibaba.csp.sentinel.cluster.flow.statistic.metric.ClusterMetric; +import com.alibaba.csp.sentinel.cluster.server.config.ClusterServerConfigManager; +import com.alibaba.csp.sentinel.cluster.server.log.ClusterServerStatLogUtil; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public final class SimpleClusterFlowChecker { + + public static TokenResult acquireClusterToken(/*@Valid*/ FlowRule rule, int acquireCount) { + Long id = rule.getClusterConfig().getFlowId(); + + ClusterMetric metric = ClusterMetricStatistics.getMetric(id); + if (metric == null) { + return new TokenResult(TokenResultStatus.FAIL); + } + + double latestQps = metric.getAvg(ClusterFlowEvent.PASS); + double globalThreshold = rule.getCount() * ClusterServerConfigManager.getExceedCount(); + double nextRemaining = globalThreshold - latestQps - acquireCount; + + if (nextRemaining >= 0) { + metric.add(ClusterFlowEvent.PASS, acquireCount); + metric.add(ClusterFlowEvent.PASS_REQUEST, 1); + + ClusterServerStatLogUtil.log("flow|pass|" + id, acquireCount); + ClusterServerStatLogUtil.log("flow|pass_request|" + id, 1); + + // Remaining count is cut down to a smaller integer. + return new TokenResult(TokenResultStatus.OK) + .setRemaining((int) nextRemaining) + .setWaitInMs(0); + } else { + // Blocked. + metric.add(ClusterFlowEvent.BLOCK, acquireCount); + metric.add(ClusterFlowEvent.BLOCK_REQUEST, 1); + ClusterServerStatLogUtil.log("flow|block|" + id, acquireCount); + ClusterServerStatLogUtil.log("flow|block_request|" + id, 1); + + return blockedResult(); + } + } + + private static TokenResult blockedResult() { + return new TokenResult(TokenResultStatus.BLOCKED) + .setRemaining(0) + .setWaitInMs(0); + } + + private SimpleClusterFlowChecker() {} +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/log/RlsAccessLogger.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/log/RlsAccessLogger.java new file mode 100644 index 00000000..7dc23b46 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/log/RlsAccessLogger.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.cluster.server.envoy.rls.log; + +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsConstants; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + */ +public final class RlsAccessLogger { + + private static boolean enabled = false; + + static { + try { + enabled = "on".equalsIgnoreCase(System.getenv(SentinelEnvoyRlsConstants.ENABLE_ACCESS_LOG_ENV_KEY)); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + public static boolean isEnabled() { + return enabled; + } + + public static void log(String info) { + if (enabled && StringUtil.isNotEmpty(info)) { + System.out.println(info); + } + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRule.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRule.java new file mode 100644 index 00000000..fe66d4fd --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRule.java @@ -0,0 +1,147 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import com.alibaba.csp.sentinel.util.AssertUtil; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public class EnvoyRlsRule { + + private String domain; + private List descriptors; + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public List getDescriptors() { + return descriptors; + } + + public void setDescriptors(List descriptors) { + this.descriptors = descriptors; + } + + @Override + public String toString() { + return "EnvoyRlsRule{" + + "domain='" + domain + '\'' + + ", descriptors=" + descriptors + + '}'; + } + + public static class ResourceDescriptor { + + private Set resources; + + private Double count; + + public ResourceDescriptor() {} + + public ResourceDescriptor(Set resources, Double count) { + this.resources = resources; + this.count = count; + } + + public Set getResources() { + return resources; + } + + public void setResources(Set resources) { + this.resources = resources; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + @Override + public String toString() { + return "ResourceDescriptor{" + + "resources=" + resources + + ", count=" + count + + '}'; + } + } + + public static class KeyValueResource { + + private String key; + private String value; + + public KeyValueResource() {} + + public KeyValueResource(String key, String value) { + AssertUtil.assertNotBlank(key, "key cannot be blank"); + AssertUtil.assertNotBlank(value, "value cannot be blank"); + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + KeyValueResource that = (KeyValueResource)o; + return Objects.equals(key, that.key) && + Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + return "KeyValueResource{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRuleManager.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRuleManager.java new file mode 100644 index 00000000..1acca600 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRuleManager.java @@ -0,0 +1,153 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule; + +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 java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import com.alibaba.csp.sentinel.cluster.flow.rule.ClusterFlowRuleManager; +import com.alibaba.csp.sentinel.cluster.server.ServerConstants; +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.property.SimplePropertyListener; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public final class EnvoyRlsRuleManager { + + private static final ConcurrentMap RULE_MAP = new ConcurrentHashMap<>(); + + private static final PropertyListener> PROPERTY_LISTENER = new EnvoyRlsRulePropertyListener(); + private static SentinelProperty> currentProperty = new DynamicSentinelProperty<>(); + + static { + currentProperty.addListener(PROPERTY_LISTENER); + } + + /** + * Listen to the {@link SentinelProperty} for Envoy RLS rules. The property is the source of {@link EnvoyRlsRule}. + * + * @param property the property to listen + */ + public static void register2Property(SentinelProperty> property) { + AssertUtil.notNull(property, "property cannot be null"); + synchronized (PROPERTY_LISTENER) { + RecordLog.info("[EnvoyRlsRuleManager] Registering new property to Envoy rate limit service rule manager"); + currentProperty.removeListener(PROPERTY_LISTENER); + property.addListener(PROPERTY_LISTENER); + currentProperty = property; + } + } + + /** + * Load Envoy RLS rules, while former rules will be replaced. + * + * @param rules new rules to load + * @return true if there are actual changes, otherwise false + */ + public static boolean loadRules(List rules) { + return currentProperty.updateValue(rules); + } + + public static List getRules() { + return new ArrayList<>(RULE_MAP.values()); + } + + static final class EnvoyRlsRulePropertyListener extends SimplePropertyListener> { + + @Override + public synchronized void configUpdate(List conf) { + Map ruleMap = generateRuleMap(conf); + + List flowRules = ruleMap.values().stream() + .flatMap(e -> EnvoySentinelRuleConverter.toSentinelFlowRules(e).stream()) + .collect(Collectors.toList()); + + RULE_MAP.clear(); + RULE_MAP.putAll(ruleMap); + RecordLog.info("[EnvoyRlsRuleManager] Envoy RLS rules loaded: " + flowRules); + + // Use the "default" namespace. + ClusterFlowRuleManager.loadRules(ServerConstants.DEFAULT_NAMESPACE, flowRules); + } + + Map generateRuleMap(List conf) { + if (conf == null || conf.isEmpty()) { + return new HashMap<>(2); + } + Map map = new HashMap<>(conf.size()); + for (EnvoyRlsRule rule : conf) { + if (!isValidRule(rule)) { + RecordLog.warn("[EnvoyRlsRuleManager] Ignoring invalid rule when loading new RLS rules: " + rule); + continue; + } + if (map.containsKey(rule.getDomain())) { + RecordLog.warn("[EnvoyRlsRuleManager] Ignoring duplicate RLS rule for specific domain: " + rule); + continue; + } + map.put(rule.getDomain(), rule); + } + return map; + } + } + + /** + * Check whether the given Envoy RLS rule is valid. + * + * @param rule the rule to check + * @return true if the rule is valid, otherwise false + */ + public static boolean isValidRule(EnvoyRlsRule rule) { + if (rule == null || StringUtil.isBlank(rule.getDomain())) { + return false; + } + List descriptors = rule.getDescriptors(); + if (descriptors == null || descriptors.isEmpty()) { + return false; + } + for (EnvoyRlsRule.ResourceDescriptor descriptor : descriptors) { + if (descriptor == null || descriptor.getCount() == null || descriptor.getCount() < 0) { + return false; + } + Set resources = descriptor.getResources(); + if (resources == null || resources.isEmpty()) { + return false; + } + for (EnvoyRlsRule.KeyValueResource resource : resources) { + if (resource == null || + StringUtil.isBlank(resource.getKey()) || StringUtil.isBlank(resource.getValue())) { + return false; + } + } + } + return true; + } + + private EnvoyRlsRuleManager() {} +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverter.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverter.java new file mode 100644 index 00000000..a7e65b17 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverter.java @@ -0,0 +1,88 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule; + +import java.util.List; +import java.util.stream.Collectors; + +import com.alibaba.csp.sentinel.slots.block.ClusterRuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.ClusterFlowConfig; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + * @since 1.7.0 + */ +public final class EnvoySentinelRuleConverter { + + /** + * Currently we use "|" to separate each key/value entries. + */ + public static final String SEPARATOR = "|"; + + /** + * Convert the {@link EnvoyRlsRule} to a list of Sentinel flow rules. + * + * @param rule a valid Envoy RLS rule + * @return converted rules + */ + public static List toSentinelFlowRules(EnvoyRlsRule rule) { + if (!EnvoyRlsRuleManager.isValidRule(rule)) { + throw new IllegalArgumentException("Not a valid RLS rule"); + } + return rule.getDescriptors().stream() + .map(e -> toSentinelFlowRule(rule.getDomain(), e)) + .collect(Collectors.toList()); + } + + public static FlowRule toSentinelFlowRule(String domain, EnvoyRlsRule.ResourceDescriptor descriptor) { + // One descriptor could have only one rule. + String identifier = generateKey(domain, descriptor); + long flowId = generateFlowId(identifier); + return new FlowRule(identifier) + .setCount(descriptor.getCount()) + .setClusterMode(true) + .setClusterConfig(new ClusterFlowConfig() + .setFlowId(flowId) + .setThresholdType(ClusterRuleConstant.FLOW_THRESHOLD_GLOBAL) + .setSampleCount(1) + .setFallbackToLocalWhenFail(false)); + } + + public static long generateFlowId(String key) { + if (StringUtil.isBlank(key)) { + return -1L; + } + // Add offset to avoid negative ID. + return Integer.MAX_VALUE + key.hashCode(); + } + + public static String generateKey(String domain, EnvoyRlsRule.ResourceDescriptor descriptor) { + AssertUtil.assertNotBlank(domain, "domain cannot be blank"); + AssertUtil.notNull(descriptor, "EnvoyRlsRule.ResourceDescriptor cannot be null"); + AssertUtil.assertNotEmpty(descriptor.getResources(), "resources in descriptor cannot be null"); + + StringBuilder sb = new StringBuilder(domain); + for (EnvoyRlsRule.KeyValueResource resource : descriptor.getResources()) { + sb.append(SEPARATOR).append(resource.getKey()).append(SEPARATOR).append(resource.getValue()); + } + return sb.toString(); + } + + private EnvoySentinelRuleConverter() {} +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/core/base.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/core/base.proto new file mode 100644 index 00000000..4903df18 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/core/base.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package envoy.api.v2.core; + +option java_outer_classname = "BaseProto"; +option java_multiple_files = true; +option java_package = "io.envoyproxy.envoy.api.v2.core"; + +import "google/protobuf/any.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +import "validate/validate.proto"; + +// Header name/value pair. +message HeaderValue { + // Header name. + string key = 1 [(validate.rules).string = {min_bytes: 1 max_bytes: 16384}]; + + // Header value. + // + // The same :ref:`format specifier ` as used for + // :ref:`HTTP access logging ` applies here, however + // unknown header values are replaced with the empty string instead of `-`. + string value = 2 [(validate.rules).string = {max_bytes: 16384}]; +} \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/ratelimit/ratelimit.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/ratelimit/ratelimit.proto new file mode 100644 index 00000000..dfcd4174 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/ratelimit/ratelimit.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package envoy.api.v2.ratelimit; + +option java_outer_classname = "RatelimitProto"; +option java_multiple_files = true; +option java_package = "io.envoyproxy.envoy.api.v2.ratelimit"; + +import "validate/validate.proto"; + +// [#protodoc-title: Common rate limit components] + +// A RateLimitDescriptor is a list of hierarchical entries that are used by the service to +// determine the final rate limit key and overall allowed limit. Here are some examples of how +// they might be used for the domain "envoy". +// +// .. code-block:: cpp +// +// ["authenticated": "false"], ["remote_address": "10.0.0.1"] +// +// What it does: Limits all unauthenticated traffic for the IP address 10.0.0.1. The +// configuration supplies a default limit for the *remote_address* key. If there is a desire to +// raise the limit for 10.0.0.1 or block it entirely it can be specified directly in the +// configuration. +// +// .. code-block:: cpp +// +// ["authenticated": "false"], ["path": "/foo/bar"] +// +// What it does: Limits all unauthenticated traffic globally for a specific path (or prefix if +// configured that way in the service). +// +// .. code-block:: cpp +// +// ["authenticated": "false"], ["path": "/foo/bar"], ["remote_address": "10.0.0.1"] +// +// What it does: Limits unauthenticated traffic to a specific path for a specific IP address. +// Like (1) we can raise/block specific IP addresses if we want with an override configuration. +// +// .. code-block:: cpp +// +// ["authenticated": "true"], ["client_id": "foo"] +// +// What it does: Limits all traffic for an authenticated client "foo" +// +// .. code-block:: cpp +// +// ["authenticated": "true"], ["client_id": "foo"], ["path": "/foo/bar"] +// +// What it does: Limits traffic to a specific path for an authenticated client "foo" +// +// The idea behind the API is that (1)/(2)/(3) and (4)/(5) can be sent in 1 request if desired. +// This enables building complex application scenarios with a generic backend. +message RateLimitDescriptor { + message Entry { + // Descriptor key. + string key = 1 [(validate.rules).string = {min_bytes: 1}]; + + // Descriptor value. + string value = 2 [(validate.rules).string = {min_bytes: 1}]; + } + + // Descriptor entries. + repeated Entry entries = 1 [(validate.rules).repeated = {min_items: 1}]; +} \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v2/rls.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v2/rls.proto new file mode 100644 index 00000000..a737a51e --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v2/rls.proto @@ -0,0 +1,109 @@ +syntax = "proto3"; + +package envoy.service.ratelimit.v2; + +option java_outer_classname = "RlsProto"; +option java_multiple_files = true; +option java_package = "io.envoyproxy.envoy.service.ratelimit.v2"; +option java_generic_services = true; + +import "envoy/api/v2/core/base.proto"; +import "envoy/api/v2/ratelimit/ratelimit.proto"; + +import "validate/validate.proto"; + +// [#protodoc-title: Rate Limit Service (RLS)] + +service RateLimitService { + // Determine whether rate limiting should take place. + rpc ShouldRateLimit(RateLimitRequest) returns (RateLimitResponse) { + } +} + +// Main message for a rate limit request. The rate limit service is designed to be fully generic +// in the sense that it can operate on arbitrary hierarchical key/value pairs. The loaded +// configuration will parse the request and find the most specific limit to apply. In addition, +// a RateLimitRequest can contain multiple "descriptors" to limit on. When multiple descriptors +// are provided, the server will limit on *ALL* of them and return an OVER_LIMIT response if any +// of them are over limit. This enables more complex application level rate limiting scenarios +// if desired. +message RateLimitRequest { + // All rate limit requests must specify a domain. This enables the configuration to be per + // application without fear of overlap. E.g., "envoy". + string domain = 1; + + // All rate limit requests must specify at least one RateLimitDescriptor. Each descriptor is + // processed by the service (see below). If any of the descriptors are over limit, the entire + // request is considered to be over limit. + repeated api.v2.ratelimit.RateLimitDescriptor descriptors = 2; + + // Rate limit requests can optionally specify the number of hits a request adds to the matched + // limit. If the value is not set in the message, a request increases the matched limit by 1. + uint32 hits_addend = 3; +} + +// A response from a ShouldRateLimit call. +message RateLimitResponse { + enum Code { + // The response code is not known. + UNKNOWN = 0; + + // The response code to notify that the number of requests are under limit. + OK = 1; + + // The response code to notify that the number of requests are over limit. + OVER_LIMIT = 2; + } + + // Defines an actual rate limit in terms of requests per unit of time and the unit itself. + message RateLimit { + enum Unit { + // The time unit is not known. + UNKNOWN = 0; + + // The time unit representing a second. + SECOND = 1; + + // The time unit representing a minute. + MINUTE = 2; + + // The time unit representing an hour. + HOUR = 3; + + // The time unit representing a day. + DAY = 4; + } + + // The number of requests per unit of time. + uint32 requests_per_unit = 1; + + // The unit of time. + Unit unit = 2; + } + + message DescriptorStatus { + // The response code for an individual descriptor. + Code code = 1; + + // The current limit as configured by the server. Useful for debugging, etc. + RateLimit current_limit = 2; + + // The limit remaining in the current time unit. + uint32 limit_remaining = 3; + } + + // The overall response code which takes into account all of the descriptors that were passed + // in the RateLimitRequest message. + Code overall_code = 1; + + // A list of DescriptorStatus messages which matches the length of the descriptor list passed + // in the RateLimitRequest. This can be used by the caller to determine which individual + // descriptors failed and/or what the currently configured limits are for all of them. + repeated DescriptorStatus statuses = 2; + + // [#next-major-version: rename to response_headers_to_add] + repeated api.v2.core.HeaderValue headers = 3; + + // A list of headers to add to the request when forwarded + repeated api.v2.core.HeaderValue request_headers_to_add = 4; +} \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto new file mode 100644 index 00000000..b6625513 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto @@ -0,0 +1,763 @@ +syntax = "proto2"; +package validate; + +option go_package = "github.com/lyft/protoc-gen-validate/validate"; +option java_package = "com.lyft.pgv.validate"; + +import "google/protobuf/descriptor.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; + +// Validation rules applied at the message level +extend google.protobuf.MessageOptions { + // Disabled nullifies any validation rules for this message, including any + // message fields associated with it that do support validation. + optional bool disabled = 919191; +} + +// Validation rules applied at the oneof level +extend google.protobuf.OneofOptions { + // Required ensures that exactly one the field options in a oneof is set; + // validation fails if no fields in the oneof are set. + optional bool required = 919191; +} + +// Validation rules applied at the field level +extend google.protobuf.FieldOptions { + // Rules specify the validations to be performed on this field. By default, + // no validation is performed against a field. + optional FieldRules rules = 919191; +} + +// FieldRules encapsulates the rules for each type of field. Depending on the +// field, the correct set should be used to ensure proper validations. +message FieldRules { + oneof type { + // Scalar Field Types + FloatRules float = 1; + DoubleRules double = 2; + Int32Rules int32 = 3; + Int64Rules int64 = 4; + UInt32Rules uint32 = 5; + UInt64Rules uint64 = 6; + SInt32Rules sint32 = 7; + SInt64Rules sint64 = 8; + Fixed32Rules fixed32 = 9; + Fixed64Rules fixed64 = 10; + SFixed32Rules sfixed32 = 11; + SFixed64Rules sfixed64 = 12; + BoolRules bool = 13; + StringRules string = 14; + BytesRules bytes = 15; + + // Complex Field Types + EnumRules enum = 16; + MessageRules message = 17; + RepeatedRules repeated = 18; + MapRules map = 19; + + // Well-Known Field Types + AnyRules any = 20; + DurationRules duration = 21; + TimestampRules timestamp = 22; + } +} + +// FloatRules describes the constraints applied to `float` values +message FloatRules { + // Const specifies that this field must be exactly the specified value + optional float const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional float lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional float lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional float gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional float gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated float in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated float not_in = 7; +} + +// DoubleRules describes the constraints applied to `double` values +message DoubleRules { + // Const specifies that this field must be exactly the specified value + optional double const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional double lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional double lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional double gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional double gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated double in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated double not_in = 7; +} + +// Int32Rules describes the constraints applied to `int32` values +message Int32Rules { + // Const specifies that this field must be exactly the specified value + optional int32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional int32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional int32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional int32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional int32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated int32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated int32 not_in = 7; +} + +// Int64Rules describes the constraints applied to `int64` values +message Int64Rules { + // Const specifies that this field must be exactly the specified value + optional int64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional int64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional int64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional int64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional int64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated int64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated int64 not_in = 7; +} + +// UInt32Rules describes the constraints applied to `uint32` values +message UInt32Rules { + // Const specifies that this field must be exactly the specified value + optional uint32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional uint32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional uint32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional uint32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional uint32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated uint32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated uint32 not_in = 7; +} + +// UInt64Rules describes the constraints applied to `uint64` values +message UInt64Rules { + // Const specifies that this field must be exactly the specified value + optional uint64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional uint64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional uint64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional uint64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional uint64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated uint64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated uint64 not_in = 7; +} + +// SInt32Rules describes the constraints applied to `sint32` values +message SInt32Rules { + // Const specifies that this field must be exactly the specified value + optional sint32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sint32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sint32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sint32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sint32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sint32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sint32 not_in = 7; +} + +// SInt64Rules describes the constraints applied to `sint64` values +message SInt64Rules { + // Const specifies that this field must be exactly the specified value + optional sint64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sint64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sint64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sint64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sint64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sint64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sint64 not_in = 7; +} + +// Fixed32Rules describes the constraints applied to `fixed32` values +message Fixed32Rules { + // Const specifies that this field must be exactly the specified value + optional fixed32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional fixed32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional fixed32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional fixed32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional fixed32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated fixed32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated fixed32 not_in = 7; +} + +// Fixed64Rules describes the constraints applied to `fixed64` values +message Fixed64Rules { + // Const specifies that this field must be exactly the specified value + optional fixed64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional fixed64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional fixed64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional fixed64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional fixed64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated fixed64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated fixed64 not_in = 7; +} + +// SFixed32Rules describes the constraints applied to `sfixed32` values +message SFixed32Rules { + // Const specifies that this field must be exactly the specified value + optional sfixed32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sfixed32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sfixed32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sfixed32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sfixed32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sfixed32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sfixed32 not_in = 7; +} + +// SFixed64Rules describes the constraints applied to `sfixed64` values +message SFixed64Rules { + // Const specifies that this field must be exactly the specified value + optional sfixed64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sfixed64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sfixed64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sfixed64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sfixed64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sfixed64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sfixed64 not_in = 7; +} + +// BoolRules describes the constraints applied to `bool` values +message BoolRules { + // Const specifies that this field must be exactly the specified value + optional bool const = 1; +} + +// StringRules describe the constraints applied to `string` values +message StringRules { + // Const specifies that this field must be exactly the specified value + optional string const = 1; + + // Len specifies that this field must be the specified number of + // characters (Unicode code points). Note that the number of + // characters may differ from the number of bytes in the string. + optional uint64 len = 19; + + // MinLen specifies that this field must be the specified number of + // characters (Unicode code points) at a minimum. Note that the number of + // characters may differ from the number of bytes in the string. + optional uint64 min_len = 2; + + // MaxLen specifies that this field must be the specified number of + // characters (Unicode code points) at a maximum. Note that the number of + // characters may differ from the number of bytes in the string. + optional uint64 max_len = 3; + + // LenBytes specifies that this field must be the specified number of bytes + // at a minimum + optional uint64 len_bytes = 20; + + // MinBytes specifies that this field must be the specified number of bytes + // at a minimum + optional uint64 min_bytes = 4; + + // MaxBytes specifies that this field must be the specified number of bytes + // at a maximum + optional uint64 max_bytes = 5; + + // Pattern specifes that this field must match against the specified + // regular expression (RE2 syntax). The included expression should elide + // any delimiters. + optional string pattern = 6; + + // Prefix specifies that this field must have the specified substring at + // the beginning of the string. + optional string prefix = 7; + + // Suffix specifies that this field must have the specified substring at + // the end of the string. + optional string suffix = 8; + + // Contains specifies that this field must have the specified substring + // anywhere in the string. + optional string contains = 9; + + // In specifies that this field must be equal to one of the specified + // values + repeated string in = 10; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated string not_in = 11; + + // WellKnown rules provide advanced constraints against common string + // patterns + oneof well_known { + // Email specifies that the field must be a valid email address as + // defined by RFC 5322 + bool email = 12; + + // Hostname specifies that the field must be a valid hostname as + // defined by RFC 1034. This constraint does not support + // internationalized domain names (IDNs). + bool hostname = 13; + + // Ip specifies that the field must be a valid IP (v4 or v6) address. + // Valid IPv6 addresses should not include surrounding square brackets. + bool ip = 14; + + // Ipv4 specifies that the field must be a valid IPv4 address. + bool ipv4 = 15; + + // Ipv6 specifies that the field must be a valid IPv6 address. Valid + // IPv6 addresses should not include surrounding square brackets. + bool ipv6 = 16; + + // Uri specifies that the field must be a valid, absolute URI as defined + // by RFC 3986 + bool uri = 17; + + // UriRef specifies that the field must be a valid URI as defined by RFC + // 3986 and may be relative or absolute. + bool uri_ref = 18; + } +} + +// BytesRules describe the constraints applied to `bytes` values +message BytesRules { + // Const specifies that this field must be exactly the specified value + optional bytes const = 1; + + // Len specifies that this field must be the specified number of bytes + optional uint64 len = 13; + + // MinLen specifies that this field must be the specified number of bytes + // at a minimum + optional uint64 min_len = 2; + + // MaxLen specifies that this field must be the specified number of bytes + // at a maximum + optional uint64 max_len = 3; + + // Pattern specifes that this field must match against the specified + // regular expression (RE2 syntax). The included expression should elide + // any delimiters. + optional string pattern = 4; + + // Prefix specifies that this field must have the specified bytes at the + // beginning of the string. + optional bytes prefix = 5; + + // Suffix specifies that this field must have the specified bytes at the + // end of the string. + optional bytes suffix = 6; + + // Contains specifies that this field must have the specified bytes + // anywhere in the string. + optional bytes contains = 7; + + // In specifies that this field must be equal to one of the specified + // values + repeated bytes in = 8; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated bytes not_in = 9; + + // WellKnown rules provide advanced constraints against common byte + // patterns + oneof well_known { + // Ip specifies that the field must be a valid IP (v4 or v6) address in + // byte format + bool ip = 10; + + // Ipv4 specifies that the field must be a valid IPv4 address in byte + // format + bool ipv4 = 11; + + // Ipv6 specifies that the field must be a valid IPv6 address in byte + // format + bool ipv6 = 12; + } +} + +// EnumRules describe the constraints applied to enum values +message EnumRules { + // Const specifies that this field must be exactly the specified value + optional int32 const = 1; + + // DefinedOnly specifies that this field must be only one of the defined + // values for this enum, failing on any undefined value. + optional bool defined_only = 2; + + // In specifies that this field must be equal to one of the specified + // values + repeated int32 in = 3; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated int32 not_in = 4; +} + +// MessageRules describe the constraints applied to embedded message values. +// For message-type fields, validation is performed recursively. +message MessageRules { + // Skip specifies that the validation rules of this field should not be + // evaluated + optional bool skip = 1; + + // Required specifies that this field must be set + optional bool required = 2; +} + +// RepeatedRules describe the constraints applied to `repeated` values +message RepeatedRules { + // MinItems specifies that this field must have the specified number of + // items at a minimum + optional uint64 min_items = 1; + + // MaxItems specifies that this field must have the specified number of + // items at a maximum + optional uint64 max_items = 2; + + // Unique specifies that all elements in this field must be unique. This + // contraint is only applicable to scalar and enum types (messages are not + // supported). + optional bool unique = 3; + + // Items specifies the contraints to be applied to each item in the field. + // Repeated message fields will still execute validation against each item + // unless skip is specified here. + optional FieldRules items = 4; +} + +// MapRules describe the constraints applied to `map` values +message MapRules { + // MinPairs specifies that this field must have the specified number of + // KVs at a minimum + optional uint64 min_pairs = 1; + + // MaxPairs specifies that this field must have the specified number of + // KVs at a maximum + optional uint64 max_pairs = 2; + + // NoSparse specifies values in this field cannot be unset. This only + // applies to map's with message value types. + optional bool no_sparse = 3; + + // Keys specifies the constraints to be applied to each key in the field. + optional FieldRules keys = 4; + + // Values specifies the constraints to be applied to the value of each key + // in the field. Message values will still have their validations evaluated + // unless skip is specified here. + optional FieldRules values = 5; +} + +// AnyRules describe constraints applied exclusively to the +// `google.protobuf.Any` well-known type +message AnyRules { + // Required specifies that this field must be set + optional bool required = 1; + + // In specifies that this field's `type_url` must be equal to one of the + // specified values. + repeated string in = 2; + + // NotIn specifies that this field's `type_url` must not be equal to any of + // the specified values. + repeated string not_in = 3; +} + +// DurationRules describe the constraints applied exclusively to the +// `google.protobuf.Duration` well-known type +message DurationRules { + // Required specifies that this field must be set + optional bool required = 1; + + // Const specifies that this field must be exactly the specified value + optional google.protobuf.Duration const = 2; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional google.protobuf.Duration lt = 3; + + // Lt specifies that this field must be less than the specified value, + // inclusive + optional google.protobuf.Duration lte = 4; + + // Gt specifies that this field must be greater than the specified value, + // exclusive + optional google.protobuf.Duration gt = 5; + + // Gte specifies that this field must be greater than the specified value, + // inclusive + optional google.protobuf.Duration gte = 6; + + // In specifies that this field must be equal to one of the specified + // values + repeated google.protobuf.Duration in = 7; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated google.protobuf.Duration not_in = 8; +} + +// TimestampRules describe the constraints applied exclusively to the +// `google.protobuf.Timestamp` well-known type +message TimestampRules { + // Required specifies that this field must be set + optional bool required = 1; + + // Const specifies that this field must be exactly the specified value + optional google.protobuf.Timestamp const = 2; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional google.protobuf.Timestamp lt = 3; + + // Lte specifies that this field must be less than the specified value, + // inclusive + optional google.protobuf.Timestamp lte = 4; + + // Gt specifies that this field must be greater than the specified value, + // exclusive + optional google.protobuf.Timestamp gt = 5; + + // Gte specifies that this field must be greater than the specified value, + // inclusive + optional google.protobuf.Timestamp gte = 6; + + // LtNow specifies that this must be less than the current time. LtNow + // can only be used with the Within rule. + optional bool lt_now = 7; + + // GtNow specifies that this must be greater than the current time. GtNow + // can only be used with the Within rule. + optional bool gt_now = 8; + + // Within specifies that this field must be within this duration of the + // current time. This constraint can be used alone or with the LtNow and + // GtNow rules. + optional google.protobuf.Duration within = 9; +} \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImplTest.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImplTest.java new file mode 100644 index 00000000..d7261896 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImplTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls; + +import com.alibaba.csp.sentinel.cluster.TokenResult; +import com.alibaba.csp.sentinel.cluster.TokenResultStatus; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.function.Tuple2; + +import io.envoyproxy.envoy.api.v2.ratelimit.RateLimitDescriptor; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitRequest; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse; +import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.Code; +import io.grpc.stub.StreamObserver; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Eric Zhao + */ +public class SentinelEnvoyRlsServiceImplTest { + + @Test + public void testShouldRateLimitPass() { + SentinelEnvoyRlsServiceImpl rlsService = mock(SentinelEnvoyRlsServiceImpl.class); + StreamObserver streamObserver = mock(StreamObserver.class); + String domain = "testShouldRateLimitPass"; + int acquireCount = 1; + + RateLimitDescriptor descriptor1 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a1").setValue("b1").build()) + .build(); + RateLimitDescriptor descriptor2 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a2").setValue("b2").build()) + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a3").setValue("b3").build()) + .build(); + + ArgumentCaptor responseCapture = ArgumentCaptor.forClass(RateLimitResponse.class); + doNothing().when(streamObserver) + .onNext(responseCapture.capture()); + + doCallRealMethod().when(rlsService).shouldRateLimit(any(), any()); + when(rlsService.checkToken(eq(domain), same(descriptor1), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.OK))); + when(rlsService.checkToken(eq(domain), same(descriptor2), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.OK))); + + RateLimitRequest rateLimitRequest = RateLimitRequest.newBuilder() + .addDescriptors(descriptor1) + .addDescriptors(descriptor2) + .setDomain(domain) + .setHitsAddend(acquireCount) + .build(); + rlsService.shouldRateLimit(rateLimitRequest, streamObserver); + + RateLimitResponse response = responseCapture.getValue(); + assertEquals(Code.OK, response.getOverallCode()); + response.getStatusesList() + .forEach(e -> assertEquals(Code.OK, e.getCode())); + } + + @Test + public void testShouldRatePartialBlock() { + SentinelEnvoyRlsServiceImpl rlsService = mock(SentinelEnvoyRlsServiceImpl.class); + StreamObserver streamObserver = mock(StreamObserver.class); + String domain = "testShouldRatePartialBlock"; + int acquireCount = 1; + + RateLimitDescriptor descriptor1 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a1").setValue("b1").build()) + .build(); + RateLimitDescriptor descriptor2 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a2").setValue("b2").build()) + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a3").setValue("b3").build()) + .build(); + + ArgumentCaptor responseCapture = ArgumentCaptor.forClass(RateLimitResponse.class); + doNothing().when(streamObserver) + .onNext(responseCapture.capture()); + + doCallRealMethod().when(rlsService).shouldRateLimit(any(), any()); + when(rlsService.checkToken(eq(domain), same(descriptor1), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.BLOCKED))); + when(rlsService.checkToken(eq(domain), same(descriptor2), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.OK))); + + RateLimitRequest rateLimitRequest = RateLimitRequest.newBuilder() + .addDescriptors(descriptor1) + .addDescriptors(descriptor2) + .setDomain(domain) + .setHitsAddend(acquireCount) + .build(); + rlsService.shouldRateLimit(rateLimitRequest, streamObserver); + + RateLimitResponse response = responseCapture.getValue(); + assertEquals(Code.OVER_LIMIT, response.getOverallCode()); + assertEquals(2, response.getStatusesCount()); + assertTrue(response.getStatusesList().stream() + .anyMatch(e -> e.getCode().equals(Code.OVER_LIMIT))); + assertFalse(response.getStatusesList().stream() + .allMatch(e -> e.getCode().equals(Code.OVER_LIMIT))); + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverterTest.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverterTest.java new file mode 100644 index 00000000..554b38c7 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverterTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRule.KeyValueResource; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRule.ResourceDescriptor; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; + +import org.junit.Test; + +import static com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter.SEPARATOR; +import static org.junit.Assert.*; + +/** + * @author Eric Zhao + */ +public class EnvoySentinelRuleConverterTest { + + @Test + public void testConvertToSentinelFlowRules() { + String domain = "testConvertToSentinelFlowRules"; + EnvoyRlsRule rlsRule = new EnvoyRlsRule(); + rlsRule.setDomain(domain); + List descriptors = new ArrayList<>(); + ResourceDescriptor d1 = new ResourceDescriptor(); + d1.setCount(10d); + d1.setResources(Collections.singleton(new KeyValueResource("k1", "v1"))); + descriptors.add(d1); + ResourceDescriptor d2 = new ResourceDescriptor(); + d2.setCount(20d); + d2.setResources(new HashSet<>(Arrays.asList( + new KeyValueResource("k2", "v2"), + new KeyValueResource("k3", "v3") + ))); + descriptors.add(d2); + rlsRule.setDescriptors(descriptors); + + List rules = EnvoySentinelRuleConverter.toSentinelFlowRules(rlsRule); + final String expectedK1 = domain + SEPARATOR + "k1" + SEPARATOR + "v1"; + FlowRule r1 = rules.stream() + .filter(e -> e.getResource().equals(expectedK1)) + .findAny() + .orElseThrow(() -> new AssertionError("the converted rule does not exist, expected key: " + expectedK1)); + assertEquals(10d, r1.getCount(), 0.01); + + final String expectedK2 = domain + SEPARATOR + "k2" + SEPARATOR + "v2" + SEPARATOR + "k3" + SEPARATOR + "v3"; + FlowRule r2 = rules.stream() + .filter(e -> e.getResource().equals(expectedK2)) + .findAny() + .orElseThrow(() -> new AssertionError("the converted rule does not exist, expected key: " + expectedK2)); + assertEquals(20d, r2.getCount(), 0.01); + } +}