* 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
@@ -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> | |||
@@ -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 | |||
``` |
@@ -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> |
@@ -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() {} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
} |
@@ -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() {} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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 + '\'' + | |||
'}'; | |||
} | |||
} | |||
} |
@@ -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() {} | |||
} |
@@ -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() {} | |||
} |
@@ -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}]; | |||
} |
@@ -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}]; | |||
} |
@@ -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; | |||
} |
@@ -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; | |||
} |
@@ -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))); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |