From 39fbc162e6941a2789908a949ab2fd6dfaa1dd58 Mon Sep 17 00:00:00 2001
From: Eric Zhao <sczyh16@gmail.com>
Date: Tue, 5 Nov 2019 17:55:57 +0800
Subject: [PATCH 1/2] Add token server module for Envoy RLS service

Signed-off-by: Eric Zhao <sczyh16@gmail.com>
---
 sentinel-cluster/pom.xml                      |   1 +
 .../README.md                                 |  15 +
 .../sentinel-cluster-server-envoy-rls/pom.xml | 157 ++++
 .../envoy/rls/SentinelEnvoyRlsConstants.java  |  34 +
 .../envoy/rls/SentinelEnvoyRlsServer.java     |  73 ++
 .../rls/SentinelEnvoyRlsServiceImpl.java      | 135 ++++
 .../envoy/rls/SentinelRlsGrpcServer.java      |  59 ++
 .../EnvoyRlsRuleDataSourceService.java        |  80 ++
 .../rls/flow/SimpleClusterFlowChecker.java    |  74 ++
 .../server/envoy/rls/log/RlsAccessLogger.java |  45 ++
 .../server/envoy/rls/rule/EnvoyRlsRule.java   | 147 ++++
 .../envoy/rls/rule/EnvoyRlsRuleManager.java   | 153 ++++
 .../rls/rule/EnvoySentinelRuleConverter.java  |  88 ++
 .../main/proto/envoy/api/v2/core/base.proto   |  26 +
 .../envoy/api/v2/ratelimit/ratelimit.proto    |  65 ++
 .../envoy/service/ratelimit/v2/rls.proto      | 109 +++
 .../src/main/proto/validate/validate.proto    | 763 ++++++++++++++++++
 17 files changed, 2024 insertions(+)
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/README.md
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsConstants.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServer.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImpl.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/datasource/EnvoyRlsRuleDataSourceService.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/flow/SimpleClusterFlowChecker.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/log/RlsAccessLogger.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRule.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRuleManager.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverter.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/core/base.proto
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/ratelimit/ratelimit.proto
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v2/rls.proto
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto

diff --git a/sentinel-cluster/pom.xml b/sentinel-cluster/pom.xml
index c58ee305bc..3f1f61edc5 100644
--- a/sentinel-cluster/pom.xml
+++ b/sentinel-cluster/pom.xml
@@ -23,6 +23,7 @@
         <module>sentinel-cluster-client-default</module>
         <module>sentinel-cluster-server-default</module>
         <module>sentinel-cluster-common-default</module>
+        <module>sentinel-cluster-server-envoy-rls</module>
     </modules>
 
     <dependencyManagement>
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 0000000000..88b337e092
--- /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 0000000000..5f78ab9016
--- /dev/null
+++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>sentinel-cluster</artifactId>
+        <groupId>com.alibaba.csp</groupId>
+        <version>1.7.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>sentinel-cluster-server-envoy-rls</artifactId>
+    <version>1.7.0-SNAPSHOT</version>
+
+    <properties>
+        <java.source.version>1.8</java.source.version>
+        <java.target.version>1.8</java.target.version>
+
+        <protobuf.version>3.10.0</protobuf.version>
+        <grpc.version>1.24.0</grpc.version>
+
+        <maven.shade.version>3.2.1</maven.shade.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.alibaba.csp</groupId>
+            <artifactId>sentinel-cluster-server-default</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba.csp</groupId>
+            <artifactId>sentinel-datasource-extension</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba.csp</groupId>
+            <artifactId>sentinel-transport-simple-http</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.grpc</groupId>
+            <artifactId>grpc-netty</artifactId>
+            <version>${grpc.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.grpc</groupId>
+            <artifactId>grpc-protobuf</artifactId>
+            <version>${grpc.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.grpc</groupId>
+            <artifactId>grpc-stub</artifactId>
+            <version>${grpc.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+            <version>${protobuf.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+            <version>1.25</version>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <extensions>
+            <extension>
+                <groupId>kr.motd.maven</groupId>
+                <artifactId>os-maven-plugin</artifactId>
+                <version>1.6.2</version>
+            </extension>
+        </extensions>
+        <plugins>
+            <plugin>
+                <groupId>org.xolstice.maven.plugins</groupId>
+                <artifactId>protobuf-maven-plugin</artifactId>
+                <version>0.6.1</version>
+                <configuration>
+                    <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
+                    </protocArtifact>
+                    <pluginId>grpc-java</pluginId>
+                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
+                    </pluginArtifact>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>compile</goal>
+                            <goal>compile-custom</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-pmd-plugin</artifactId>
+                <version>${maven.pmd.version}</version>
+                <configuration>
+                    <excludeRoots>
+                        <excludeRoot>target/generated-sources</excludeRoot>
+                    </excludeRoots>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>prod</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-shade-plugin</artifactId>
+                        <version>${maven.shade.version}</version>
+                        <executions>
+                            <execution>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>shade</goal>
+                                </goals>
+                                <configuration>
+                                    <finalName>sentinel-envoy-rls-token-server</finalName>
+                                    <transformers>
+                                        <transformer
+                                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                            <mainClass>
+                                                com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsServer
+                                            </mainClass>
+                                        </transformer>
+                                        <transformer
+                                                implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+                                    </transformers>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>
\ 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 0000000000..369fe3e05f
--- /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 0000000000..102add2d89
--- /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 0000000000..7e0bc7b95b
--- /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<RateLimitResponse> 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<DescriptorStatus> statusList = new ArrayList<>(request.getDescriptorsCount());
+        for (RateLimitDescriptor descriptor : request.getDescriptorsList()) {
+            Tuple2<FlowRule, TokenResult> 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<FlowRule, TokenResult> 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 0000000000..459540ad69
--- /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 0000000000..4cf0f07e72
--- /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<String, List<EnvoyRlsRule>> 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 0000000000..efa307cd34
--- /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 0000000000..7dc23b46f5
--- /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 0000000000..fe66d4fd98
--- /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<ResourceDescriptor> descriptors;
+
+    public String getDomain() {
+        return domain;
+    }
+
+    public void setDomain(String domain) {
+        this.domain = domain;
+    }
+
+    public List<ResourceDescriptor> getDescriptors() {
+        return descriptors;
+    }
+
+    public void setDescriptors(List<ResourceDescriptor> descriptors) {
+        this.descriptors = descriptors;
+    }
+
+    @Override
+    public String toString() {
+        return "EnvoyRlsRule{" +
+            "domain='" + domain + '\'' +
+            ", descriptors=" + descriptors +
+            '}';
+    }
+
+    public static class ResourceDescriptor {
+
+        private Set<KeyValueResource> resources;
+
+        private Double count;
+
+        public ResourceDescriptor() {}
+
+        public ResourceDescriptor(Set<KeyValueResource> resources, Double count) {
+            this.resources = resources;
+            this.count = count;
+        }
+
+        public Set<KeyValueResource> getResources() {
+            return resources;
+        }
+
+        public void setResources(Set<KeyValueResource> 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 0000000000..1acca60040
--- /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<String, EnvoyRlsRule> RULE_MAP = new ConcurrentHashMap<>();
+
+    private static final PropertyListener<List<EnvoyRlsRule>> PROPERTY_LISTENER = new EnvoyRlsRulePropertyListener();
+    private static SentinelProperty<List<EnvoyRlsRule>> 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<List<EnvoyRlsRule>> 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<EnvoyRlsRule> rules) {
+        return currentProperty.updateValue(rules);
+    }
+
+    public static List<EnvoyRlsRule> getRules() {
+        return new ArrayList<>(RULE_MAP.values());
+    }
+
+    static final class EnvoyRlsRulePropertyListener extends SimplePropertyListener<List<EnvoyRlsRule>> {
+
+        @Override
+        public synchronized void configUpdate(List<EnvoyRlsRule> conf) {
+            Map<String, EnvoyRlsRule> ruleMap = generateRuleMap(conf);
+
+            List<FlowRule> 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<String, EnvoyRlsRule> generateRuleMap(List<EnvoyRlsRule> conf) {
+            if (conf == null || conf.isEmpty()) {
+                return new HashMap<>(2);
+            }
+            Map<String, EnvoyRlsRule> 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<EnvoyRlsRule.ResourceDescriptor> 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<EnvoyRlsRule.KeyValueResource> 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 0000000000..a7e65b17a3
--- /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<FlowRule> 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 0000000000..4903df1858
--- /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 <config_access_log_format>` as used for
+    // :ref:`HTTP access logging <config_access_log>` 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 0000000000..dfcd4174eb
--- /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 0000000000..a737a51e07
--- /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 0000000000..b662551352
--- /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

From bbe3a6ca56fa9ce457d7b5f4dc57f6e0fa124104 Mon Sep 17 00:00:00 2001
From: Eric Zhao <sczyh16@gmail.com>
Date: Tue, 5 Nov 2019 20:53:35 +0800
Subject: [PATCH 2/2] Add basic test cases for Sentinel Envoy RLS server

Signed-off-by: Eric Zhao <sczyh16@gmail.com>
---
 .../rls/SentinelEnvoyRlsServiceImplTest.java  | 119 ++++++++++++++++++
 .../rule/EnvoySentinelRuleConverterTest.java  |  72 +++++++++++
 2 files changed, 191 insertions(+)
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImplTest.java
 create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverterTest.java

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 0000000000..d72618961d
--- /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<RateLimitResponse> 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<RateLimitResponse> 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<RateLimitResponse> 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<RateLimitResponse> 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 0000000000..554b38c717
--- /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<ResourceDescriptor> 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<FlowRule> 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);
+    }
+}