Browse Source

Add Sentinel Envoy RLS server implementation (#1139)

* Add the sentinel-cluster-server-envoy-rls module, a Envoy RLS server implementation using Sentinel token server.

Signed-off-by: Eric Zhao <sczyh16@gmail.com>
master
Eric Zhao GitHub 5 years ago
parent
commit
b66680be87
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2215 additions and 0 deletions
  1. +1
    -0
      sentinel-cluster/pom.xml
  2. +15
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/README.md
  3. +157
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml
  4. +34
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsConstants.java
  5. +73
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServer.java
  6. +135
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImpl.java
  7. +59
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java
  8. +80
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/datasource/EnvoyRlsRuleDataSourceService.java
  9. +74
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/flow/SimpleClusterFlowChecker.java
  10. +45
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/log/RlsAccessLogger.java
  11. +147
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRule.java
  12. +153
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRuleManager.java
  13. +88
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverter.java
  14. +26
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/core/base.proto
  15. +65
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/ratelimit/ratelimit.proto
  16. +109
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v2/rls.proto
  17. +763
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto
  18. +119
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImplTest.java
  19. +72
    -0
      sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverterTest.java

+ 1
- 0
sentinel-cluster/pom.xml View File

@@ -23,6 +23,7 @@
<module>sentinel-cluster-client-default</module> <module>sentinel-cluster-client-default</module>
<module>sentinel-cluster-server-default</module> <module>sentinel-cluster-server-default</module>
<module>sentinel-cluster-common-default</module> <module>sentinel-cluster-common-default</module>
<module>sentinel-cluster-server-envoy-rls</module>
</modules> </modules>


<dependencyManagement> <dependencyManagement>


+ 15
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/README.md View File

@@ -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
```

+ 157
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/pom.xml View File

@@ -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>

+ 34
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsConstants.java View File

@@ -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() {}
}

+ 73
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServer.java View File

@@ -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;
}
}
}

+ 135
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImpl.java View File

@@ -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();
}
}

+ 59
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java View File

@@ -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();
}
}
}

+ 80
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/datasource/EnvoyRlsRuleDataSourceService.java View File

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

+ 74
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/flow/SimpleClusterFlowChecker.java View File

@@ -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() {}
}

+ 45
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/log/RlsAccessLogger.java View File

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

+ 147
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRule.java View File

@@ -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 + '\'' +
'}';
}
}
}

+ 153
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoyRlsRuleManager.java View File

@@ -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() {}
}

+ 88
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverter.java View File

@@ -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() {}
}

+ 26
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/core/base.proto View File

@@ -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}];
}

+ 65
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/api/v2/ratelimit/ratelimit.proto View File

@@ -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}];
}

+ 109
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v2/rls.proto View File

@@ -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;
}

+ 763
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto View File

@@ -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;
}

+ 119
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelEnvoyRlsServiceImplTest.java View File

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

+ 72
- 0
sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/rule/EnvoySentinelRuleConverterTest.java View File

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

Loading…
Cancel
Save