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);
+ }
+}