Adds support for rate limits bellow 1 req/s
Adds requestedTokens configuration option. Previously, this was hardcoded to 1. Make the 'requested tokens' redis template argument configurable in order to allow defining rate limits lower than 1 req/s, e.g. 1 req/m. This is accomplished by setting: - replenishRate = requestRate - burstRate = requestRate * timeSpanInSeconds - requestedTokens = timeSpanInSeconds For 1 req/m this would be accomplished by: - replenishRate = 1 - burstRate = 60 - requestedTokens = 60 fixes gh-1327
This commit is contained in:
@@ -994,18 +994,23 @@ It requires the use of the `spring-boot-starter-data-redis-reactive` Spring Boot
|
||||
|
||||
The algorithm used is the https://en.wikipedia.org/wiki/Token_bucket[Token Bucket Algorithm].
|
||||
|
||||
The `redis-rate-limiter.replenishRate` is how many requests per second you want a user to be allowed to do, without any dropped requests.
|
||||
The `redis-rate-limiter.replenishRate` property is how many requests per second you want a user to be allowed to do, without any dropped requests.
|
||||
This is the rate at which the token bucket is filled.
|
||||
|
||||
The `redis-rate-limiter.burstCapacity` is the maximum number of requests a user is allowed to do in a single second.
|
||||
The `redis-rate-limiter.burstCapacity` property is the maximum number of requests a user is allowed to do in a single second.
|
||||
This is the number of tokens the token bucket can hold.
|
||||
Setting this value to zero blocks all requests.
|
||||
|
||||
The `redis-rate-limiter.requestedTokens` property is how many tokens a request costs.
|
||||
This is the number of tokens taken from the bucket for each request and defaults to `1`.
|
||||
|
||||
A steady rate is accomplished by setting the same value in `replenishRate` and `burstCapacity`.
|
||||
Temporary bursts can be allowed by setting `burstCapacity` higher than `replenishRate`.
|
||||
In this case, the rate limiter needs to be allowed some time between bursts (according to `replenishRate`), as two consecutive bursts will result in dropped requests (`HTTP 429 - Too Many Requests`).
|
||||
The following listing configures a `redis-rate-limiter`:
|
||||
|
||||
Rate limits bellow `1 request/s` are accomplished by setting `replenishRate` to the wanted number of requests, `requestedTokens` to the timespan in seconds and `burstCapacity` to the product of `replenishRate` and `requestedTokens`, e.g. setting `replenishRate=1`, `requestedTokens=60` and `burstCapacity=60` will result in a limit of `1 request/min`.
|
||||
|
||||
.application.yml
|
||||
====
|
||||
[source,yaml]
|
||||
@@ -1021,6 +1026,7 @@ spring:
|
||||
args:
|
||||
redis-rate-limiter.replenishRate: 10
|
||||
redis-rate-limiter.burstCapacity: 20
|
||||
redis-rate-limiter.requestedTokens: 1
|
||||
|
||||
----
|
||||
====
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator;
|
||||
import org.springframework.cloud.gateway.support.ConfigurationService;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.validation.Validator;
|
||||
@@ -49,6 +50,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Ronny Bräunlich
|
||||
* @author Denis Cutic
|
||||
*/
|
||||
@ConfigurationProperties("spring.cloud.gateway.redis-rate-limiter")
|
||||
public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Config>
|
||||
@@ -87,10 +89,15 @@ public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Confi
|
||||
public static final String REPLENISH_RATE_HEADER = "X-RateLimit-Replenish-Rate";
|
||||
|
||||
/**
|
||||
* Burst Capacity Header name.
|
||||
* Burst Capacity header name.
|
||||
*/
|
||||
public static final String BURST_CAPACITY_HEADER = "X-RateLimit-Burst-Capacity";
|
||||
|
||||
/**
|
||||
* Requested Tokens header name.
|
||||
*/
|
||||
public static final String REQUESTED_TOKENS_HEADER = "X-RateLimit-Requested-Tokens";
|
||||
|
||||
private Log log = LogFactory.getLog(getClass());
|
||||
|
||||
private ReactiveStringRedisTemplate redisTemplate;
|
||||
@@ -120,6 +127,9 @@ public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Confi
|
||||
/** The name of the header that returns the burst capacity configuration. */
|
||||
private String burstCapacityHeader = BURST_CAPACITY_HEADER;
|
||||
|
||||
/** The name of the header that returns the requested tokens configuration. */
|
||||
private String requestedTokensHeader = REQUESTED_TOKENS_HEADER;
|
||||
|
||||
public RedisRateLimiter(ReactiveStringRedisTemplate redisTemplate,
|
||||
RedisScript<List<Long>> script, ConfigurationService configurationService) {
|
||||
super(Config.class, CONFIGURATION_PROPERTY_NAME, configurationService);
|
||||
@@ -141,7 +151,7 @@ public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Confi
|
||||
* This creates an instance with default static configuration, useful in Java DSL.
|
||||
* @param defaultReplenishRate how many tokens per second in token-bucket algorithm.
|
||||
* @param defaultBurstCapacity how many tokens the bucket can hold in token-bucket
|
||||
* alogritm.
|
||||
* algorithm.
|
||||
*/
|
||||
public RedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) {
|
||||
super(Config.class, CONFIGURATION_PROPERTY_NAME, (ConfigurationService) null);
|
||||
@@ -149,6 +159,19 @@ public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Confi
|
||||
.setBurstCapacity(defaultBurstCapacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates an instance with default static configuration, useful in Java DSL.
|
||||
* @param defaultReplenishRate how many tokens per second in token-bucket algorithm.
|
||||
* @param defaultBurstCapacity how many tokens the bucket can hold in token-bucket
|
||||
* algorithm.
|
||||
* @param defaultRequestedTokens how many tokens are requested per request.
|
||||
*/
|
||||
public RedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity,
|
||||
int defaultRequestedTokens) {
|
||||
this(defaultReplenishRate, defaultBurstCapacity);
|
||||
this.defaultConfig.setRequestedTokens(defaultRequestedTokens);
|
||||
}
|
||||
|
||||
static List<String> getKeys(String id) {
|
||||
// use `{}` around keys to use Redis Key hash tags
|
||||
// this allows for using redis cluster
|
||||
@@ -194,6 +217,14 @@ public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Confi
|
||||
this.burstCapacityHeader = burstCapacityHeader;
|
||||
}
|
||||
|
||||
public String getRequestedTokensHeader() {
|
||||
return requestedTokensHeader;
|
||||
}
|
||||
|
||||
public void setRequestedTokensHeader(String requestedTokensHeader) {
|
||||
this.requestedTokensHeader = requestedTokensHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when setting default configuration in constructor.
|
||||
* @param context the ApplicationContext object to be used by this object
|
||||
@@ -237,12 +268,16 @@ public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Confi
|
||||
// How much bursting do you want to allow?
|
||||
int burstCapacity = routeConfig.getBurstCapacity();
|
||||
|
||||
// How many tokens are requested per request?
|
||||
int requestedTokens = routeConfig.getRequestedTokens();
|
||||
|
||||
try {
|
||||
List<String> keys = getKeys(id);
|
||||
|
||||
// The arguments to the LUA script. time() returns unixtime in seconds.
|
||||
List<String> scriptArgs = Arrays.asList(replenishRate + "",
|
||||
burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
|
||||
burstCapacity + "", Instant.now().getEpochSecond() + "",
|
||||
requestedTokens + "");
|
||||
// allowed, tokens_left = redis.eval(SCRIPT, keys, args)
|
||||
Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys,
|
||||
scriptArgs);
|
||||
@@ -298,6 +333,8 @@ public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Confi
|
||||
String.valueOf(config.getReplenishRate()));
|
||||
headers.put(this.burstCapacityHeader,
|
||||
String.valueOf(config.getBurstCapacity()));
|
||||
headers.put(this.requestedTokensHeader,
|
||||
String.valueOf(config.getRequestedTokens()));
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -311,6 +348,9 @@ public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Confi
|
||||
@Min(1)
|
||||
private int burstCapacity = 1;
|
||||
|
||||
@Min(1)
|
||||
private int requestedTokens = 1;
|
||||
|
||||
public int getReplenishRate() {
|
||||
return replenishRate;
|
||||
}
|
||||
@@ -329,10 +369,21 @@ public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Confi
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getRequestedTokens() {
|
||||
return requestedTokens;
|
||||
}
|
||||
|
||||
public Config setRequestedTokens(int requestedTokens) {
|
||||
this.requestedTokens = requestedTokens;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Config{" + "replenishRate=" + replenishRate + ", burstCapacity="
|
||||
+ burstCapacity + '}';
|
||||
return new ToStringCreator(this).append("replenishRate", replenishRate)
|
||||
.append("burstCapacity", burstCapacity)
|
||||
.append("requestedTokens", requestedTokens).toString();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,27 +51,32 @@ public class RedisRateLimiterConfigTests {
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
routeLocator.getRoutes().collectList().block(); // prime routes since getRoutes()
|
||||
// no longer blocks
|
||||
// prime routes since getRoutes() no longer blocks
|
||||
routeLocator.getRoutes().collectList().block();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void redisRateConfiguredFromEnvironment() {
|
||||
assertFilter("redis_rate_limiter_config_test", 10, 20, false);
|
||||
assertFilter("redis_rate_limiter_config_test", 10, 20, 1, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void redisRateConfiguredFromEnvironmentMinimal() {
|
||||
assertFilter("redis_rate_limiter_minimal_config_test", 2, 1, 1, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void redisRateConfiguredFromJavaAPI() {
|
||||
assertFilter("custom_redis_rate_limiter", 20, 40, false);
|
||||
assertFilter("custom_redis_rate_limiter", 20, 40, 10, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void redisRateConfiguredFromJavaAPIDirectBean() {
|
||||
assertFilter("alt_custom_redis_rate_limiter", 30, 60, true);
|
||||
assertFilter("alt_custom_redis_rate_limiter", 30, 60, 20, true);
|
||||
}
|
||||
|
||||
private void assertFilter(String key, int replenishRate, int burstCapacity,
|
||||
boolean useDefaultConfig) {
|
||||
int requestedTokens, boolean useDefaultConfig) {
|
||||
RedisRateLimiter.Config config;
|
||||
|
||||
if (useDefaultConfig) {
|
||||
@@ -84,6 +89,7 @@ public class RedisRateLimiterConfigTests {
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getReplenishRate()).isEqualTo(replenishRate);
|
||||
assertThat(config.getBurstCapacity()).isEqualTo(burstCapacity);
|
||||
assertThat(config.getRequestedTokens()).isEqualTo(requestedTokens);
|
||||
|
||||
Route route = routeLocator.getRoutes().filter(r -> r.getId().equals(key)).next()
|
||||
.block();
|
||||
@@ -100,7 +106,8 @@ public class RedisRateLimiterConfigTests {
|
||||
return builder.routes().route("custom_redis_rate_limiter",
|
||||
r -> r.path("/custom").filters(f -> f.requestRateLimiter()
|
||||
.rateLimiter(RedisRateLimiter.class,
|
||||
rl -> rl.setBurstCapacity(40).setReplenishRate(20))
|
||||
rl -> rl.setBurstCapacity(40).setReplenishRate(20)
|
||||
.setRequestedTokens(10))
|
||||
.and()).uri("http://localhost"))
|
||||
.route("alt_custom_redis_rate_limiter",
|
||||
r -> r.path("/custom")
|
||||
@@ -113,7 +120,7 @@ public class RedisRateLimiterConfigTests {
|
||||
|
||||
@Bean
|
||||
public RedisRateLimiter myRateLimiter() {
|
||||
return new RedisRateLimiter(30, 60);
|
||||
return new RedisRateLimiter(30, 60, 20);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Denis Cutic
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
@@ -49,22 +50,23 @@ public class RedisRateLimiterDefaultFilterConfigTests {
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
routeLocator.getRoutes().collectList().block(); // prime routes since getRoutes()
|
||||
// no longer blocks
|
||||
// prime routes since getRoutes() no longer blocks
|
||||
routeLocator.getRoutes().collectList().block();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void redisRateConfiguredFromEnvironmentDefaultFilters() {
|
||||
String routeId = "redis_rate_limiter_config_default_test";
|
||||
RedisRateLimiter.Config config = rateLimiter.loadConfiguration(routeId);
|
||||
assertConfigAndRoute(routeId, 70, 80, config);
|
||||
assertConfigAndRoute(routeId, 70, 80, 10, config);
|
||||
}
|
||||
|
||||
private void assertConfigAndRoute(String key, int replenishRate, int burstCapacity,
|
||||
RedisRateLimiter.Config config) {
|
||||
int requestedTokens, RedisRateLimiter.Config config) {
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getReplenishRate()).isEqualTo(replenishRate);
|
||||
assertThat(config.getBurstCapacity()).isEqualTo(burstCapacity);
|
||||
assertThat(config.getRequestedTokens()).isEqualTo(requestedTokens);
|
||||
|
||||
Route route = routeLocator.getRoutes().filter(r -> r.getId().equals(key)).next()
|
||||
.block();
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.cloud.gateway.filter.ratelimit;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -45,6 +47,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Ronny Bräunlich
|
||||
* @author Denis Cutic
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
||||
@@ -57,52 +60,48 @@ public class RedisRateLimiterTests extends BaseWebClientTests {
|
||||
@Autowired
|
||||
private RedisRateLimiter rateLimiter;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
assumeThat("Ignore on Circle", System.getenv("CIRCLECI"), is(nullValue()));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
rateLimiter.setIncludeHeaders(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void redisRateLimiterWorks() throws Exception {
|
||||
assumeThat("Ignore on Circle", System.getenv("CIRCLECI"), is(nullValue()));
|
||||
|
||||
String id = UUID.randomUUID().toString();
|
||||
|
||||
int replenishRate = 10;
|
||||
int burstCapacity = 2 * replenishRate;
|
||||
int requestedTokens = 1;
|
||||
|
||||
String routeId = "myroute";
|
||||
rateLimiter.getConfig().put(routeId, new RedisRateLimiter.Config()
|
||||
.setBurstCapacity(burstCapacity).setReplenishRate(replenishRate));
|
||||
rateLimiter.getConfig().put(routeId,
|
||||
new RedisRateLimiter.Config().setBurstCapacity(burstCapacity)
|
||||
.setReplenishRate(replenishRate)
|
||||
.setRequestedTokens(requestedTokens));
|
||||
|
||||
// Bursts work
|
||||
for (int i = 0; i < burstCapacity; i++) {
|
||||
Response response = rateLimiter.isAllowed(routeId, id).block();
|
||||
assertThat(response.isAllowed()).as("Burst # %s is allowed", i).isTrue();
|
||||
assertThat(response.getHeaders())
|
||||
.containsKey(RedisRateLimiter.REMAINING_HEADER);
|
||||
assertThat(response.getHeaders()).containsEntry(
|
||||
RedisRateLimiter.REPLENISH_RATE_HEADER,
|
||||
String.valueOf(replenishRate));
|
||||
assertThat(response.getHeaders()).containsEntry(
|
||||
RedisRateLimiter.BURST_CAPACITY_HEADER,
|
||||
String.valueOf(burstCapacity));
|
||||
}
|
||||
checkLimitEnforced(id, replenishRate, burstCapacity, requestedTokens, routeId);
|
||||
}
|
||||
|
||||
Response response = rateLimiter.isAllowed(routeId, id).block();
|
||||
if (response.isAllowed()) { // TODO: sometimes there is an off by one error
|
||||
response = rateLimiter.isAllowed(routeId, id).block();
|
||||
}
|
||||
assertThat(response.isAllowed()).as("Burst # %s is not allowed", burstCapacity)
|
||||
.isFalse();
|
||||
@Test
|
||||
public void redisRateLimiterWorksForLowRates() throws Exception {
|
||||
String id = UUID.randomUUID().toString();
|
||||
|
||||
Thread.sleep(1000);
|
||||
int replenishRate = 1;
|
||||
int burstCapacity = 3;
|
||||
int requestedTokens = 3;
|
||||
|
||||
// # After the burst is done, check the steady state
|
||||
for (int i = 0; i < replenishRate; i++) {
|
||||
response = rateLimiter.isAllowed(routeId, id).block();
|
||||
assertThat(response.isAllowed()).as("steady state # %s is allowed", i)
|
||||
.isTrue();
|
||||
}
|
||||
String routeId = "low_rate_route";
|
||||
rateLimiter.getConfig().put(routeId,
|
||||
new RedisRateLimiter.Config().setBurstCapacity(burstCapacity)
|
||||
.setReplenishRate(replenishRate)
|
||||
.setRequestedTokens(requestedTokens));
|
||||
|
||||
response = rateLimiter.isAllowed(routeId, id).block();
|
||||
assertThat(response.isAllowed()).as("steady state # %s is allowed", replenishRate)
|
||||
.isFalse();
|
||||
checkLimitEnforced(id, replenishRate, burstCapacity, requestedTokens, routeId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -113,8 +112,6 @@ public class RedisRateLimiterTests extends BaseWebClientTests {
|
||||
|
||||
@Test
|
||||
public void redisRateLimiterDoesNotSendHeadersIfDeactivated() throws Exception {
|
||||
assumeThat("Ignore on Circle", System.getenv("CIRCLECI"), is(nullValue()));
|
||||
|
||||
String id = UUID.randomUUID().toString();
|
||||
String routeId = "myroute";
|
||||
|
||||
@@ -128,6 +125,62 @@ public class RedisRateLimiterTests extends BaseWebClientTests {
|
||||
.doesNotContainKey(RedisRateLimiter.REPLENISH_RATE_HEADER);
|
||||
assertThat(response.getHeaders())
|
||||
.doesNotContainKey(RedisRateLimiter.BURST_CAPACITY_HEADER);
|
||||
assertThat(response.getHeaders())
|
||||
.doesNotContainKey(RedisRateLimiter.REQUESTED_TOKENS_HEADER);
|
||||
}
|
||||
|
||||
private void checkLimitEnforced(String id, int replenishRate, int burstCapacity,
|
||||
int requestedTokens, String routeId) throws InterruptedException {
|
||||
// Bursts work
|
||||
simulateBurst(id, replenishRate, burstCapacity, requestedTokens, routeId);
|
||||
|
||||
checkLimitReached(id, burstCapacity, routeId);
|
||||
|
||||
Thread.sleep(Math.max(1, requestedTokens / replenishRate) * 1000);
|
||||
|
||||
// # After the burst is done, check the steady state
|
||||
checkSteadyState(id, replenishRate, routeId);
|
||||
}
|
||||
|
||||
private void simulateBurst(String id, int replenishRate, int burstCapacity,
|
||||
int requestedTokens, String routeId) {
|
||||
for (int i = 0; i < burstCapacity / requestedTokens; i++) {
|
||||
Response response = rateLimiter.isAllowed(routeId, id).block();
|
||||
assertThat(response.isAllowed()).as("Burst # %s is allowed", i).isTrue();
|
||||
assertThat(response.getHeaders())
|
||||
.containsKey(RedisRateLimiter.REMAINING_HEADER);
|
||||
assertThat(response.getHeaders()).containsEntry(
|
||||
RedisRateLimiter.REPLENISH_RATE_HEADER,
|
||||
String.valueOf(replenishRate));
|
||||
assertThat(response.getHeaders()).containsEntry(
|
||||
RedisRateLimiter.BURST_CAPACITY_HEADER,
|
||||
String.valueOf(burstCapacity));
|
||||
assertThat(response.getHeaders()).containsEntry(
|
||||
RedisRateLimiter.REQUESTED_TOKENS_HEADER,
|
||||
String.valueOf(requestedTokens));
|
||||
}
|
||||
}
|
||||
|
||||
private void checkLimitReached(String id, int burstCapacity, String routeId) {
|
||||
Response response = rateLimiter.isAllowed(routeId, id).block();
|
||||
if (response.isAllowed()) { // TODO: sometimes there is an off by one error
|
||||
response = rateLimiter.isAllowed(routeId, id).block();
|
||||
}
|
||||
assertThat(response.isAllowed()).as("Burst # %s is not allowed", burstCapacity)
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
private void checkSteadyState(String id, int replenishRate, String routeId) {
|
||||
Response response;
|
||||
for (int i = 0; i < replenishRate; i++) {
|
||||
response = rateLimiter.isAllowed(routeId, id).block();
|
||||
assertThat(response.isAllowed()).as("steady state # %s is allowed", i)
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
response = rateLimiter.isAllowed(routeId, id).block();
|
||||
assertThat(response.isAllowed()).as("steady state # %s is allowed", replenishRate)
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.cloud.gateway.filter.ratelimit;
|
||||
|
||||
import io.lettuce.core.RedisException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.gateway.support.ConfigurationService;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.MapEntry.entry;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Denis Cutic
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class RedisRateLimiterUnitTests {
|
||||
|
||||
private static final int DEFAULT_REPLENISH_RATE = 1;
|
||||
|
||||
private static final int DEFAULT_BURST_CAPACITY = 1;
|
||||
|
||||
public static final String ROUTE_ID = "routeId";
|
||||
|
||||
public static final String REQUEST_ID = "id";
|
||||
|
||||
public static final String[] CONFIGURATION_SERVICE_BEANS = new String[0];
|
||||
|
||||
public static final RedisException REDIS_EXCEPTION = new RedisException(
|
||||
"Mocked problem");
|
||||
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
private ReactiveStringRedisTemplate redisTemplate;
|
||||
|
||||
private RedisRateLimiter redisRateLimiter;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
when(applicationContext.getBean(ReactiveStringRedisTemplate.class))
|
||||
.thenReturn(redisTemplate);
|
||||
when(applicationContext.getBeanNamesForType(ConfigurationService.class))
|
||||
.thenReturn(CONFIGURATION_SERVICE_BEANS);
|
||||
redisRateLimiter = new RedisRateLimiter(DEFAULT_REPLENISH_RATE,
|
||||
DEFAULT_BURST_CAPACITY);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
Mockito.reset(applicationContext);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void shouldThrowWhenNotInitialized() {
|
||||
redisRateLimiter.isAllowed(ROUTE_ID, REQUEST_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowRequestWhenRedisIssueOccurs() {
|
||||
when(redisTemplate.execute(any(), anyList(), anyList()))
|
||||
.thenThrow(REDIS_EXCEPTION);
|
||||
redisRateLimiter.setApplicationContext(applicationContext);
|
||||
Mono<RateLimiter.Response> response = redisRateLimiter.isAllowed(ROUTE_ID,
|
||||
REQUEST_ID);
|
||||
assertThat(response.block()).extracting(RateLimiter.Response::isAllowed)
|
||||
.isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnHeadersWhenRedisIssueOccurs() {
|
||||
when(redisTemplate.execute(any(), anyList(), anyList()))
|
||||
.thenThrow(REDIS_EXCEPTION);
|
||||
redisRateLimiter.setApplicationContext(applicationContext);
|
||||
Mono<RateLimiter.Response> response = redisRateLimiter.isAllowed(ROUTE_ID,
|
||||
REQUEST_ID);
|
||||
assertThat(response.block().getHeaders()).containsOnly(
|
||||
entry(redisRateLimiter.getRemainingHeader(), "-1"),
|
||||
entry(redisRateLimiter.getBurstCapacityHeader(),
|
||||
DEFAULT_BURST_CAPACITY + ""),
|
||||
entry(redisRateLimiter.getReplenishRateHeader(),
|
||||
DEFAULT_REPLENISH_RATE + ""),
|
||||
entry(redisRateLimiter.getRequestedTokensHeader(), "1"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,4 +14,13 @@ spring:
|
||||
redis-rate-limiter:
|
||||
replenish-rate: 10
|
||||
burst-capacity: 20
|
||||
- id: redis_rate_limiter_minimal_config_test
|
||||
uri: ${test.uri}
|
||||
predicates:
|
||||
- Path=/
|
||||
filters:
|
||||
- name: RequestRateLimiter
|
||||
args:
|
||||
redis-rate-limiter:
|
||||
replenish-rate: 2
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ spring:
|
||||
redis-rate-limiter:
|
||||
replenish-rate: 70
|
||||
burst-capacity: 80
|
||||
requested-tokens: 10
|
||||
routes:
|
||||
# =====================================
|
||||
- id: redis_rate_limiter_config_default_test
|
||||
|
||||
Reference in New Issue
Block a user