Problem 1:
I want this rateLimiter can use to all route in gateway,but i find in follow class, code
Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig); get null,
because getConfig() return map key is defaultFilters, value is a RequestRateLimiter we config above,but the code get by key(current route's id) current route is't config filter RequestRateLimiter, so
it will do default logic,but the defaultConfig is also null。
and i try config as follow:
i expect it can work but it error when run :
Problem 2:
Failed to bind properties under 'spring.cloud.gateway.redis-rate-limiter.config.replenishrate' to org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter$Config:
Property: spring.cloud.gateway.redis-rate-limiter.config.replenishrate
Value: 2
Origin: class path resource [application.yml]:18:26
Reason: No converter found capable of converting from type [java.lang.Integer] to type [org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter$Config]
Action:
Update your application's configuration
I think your code' get default logic is wrong,should be:
public Mono<Response> isAllowed(String routeId, String id) {
...
// this is get the RequestRateLimiter Filter config under current route
Config routeConfig = getConfig().get(routeId);
if(routeConfig == null) {
//get default RequestRateLimiter Filter conifg under spring.cloud.gateway.default-filters node or config under spring.cloud.gateway.redis-rate-limiter node
routeConfig=getConfig().getOrDefault("defaultFilters", defaultConfig);
}
if (routeConfig == null) {
throw new IllegalArgumentException("No Configuration found for route " + routeId);
}
...
}
If the code is like what I said,Problem 2 is also should be fix;
@ConfigurationProperties("spring.cloud.gateway.redis-rate-limiter")
public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Config> implements ApplicationContextAware {
@Deprecated
public static final String REPLENISH_RATE_KEY = "replenishRate";
@Deprecated
public static final String BURST_CAPACITY_KEY = "burstCapacity";
public static final String CONFIGURATION_PROPERTY_NAME = "redis-rate-limiter";
public static final String REDIS_SCRIPT_NAME = "redisRequestRateLimiterScript";
public static final String REMAINING_HEADER = "X-RateLimit-Remaining";
public static final String REPLENISH_RATE_HEADER = "X-RateLimit-Replenish-Rate";
public static final String BURST_CAPACITY_HEADER = "X-RateLimit-Burst-Capacity";
private Log log = LogFactory.getLog(getClass());
private ReactiveRedisTemplate<String, String> redisTemplate;
private RedisScript<List<Long>> script;
private AtomicBoolean initialized = new AtomicBoolean(false);
private Config defaultConfig;
// configuration properties
/** Whether or not to include headers containing rate limiter information, defaults to true. */
private boolean includeHeaders = true;
/** The name of the header that returns number of remaining requests during the current second. */
private String remainingHeader = REMAINING_HEADER;
/** The name of the header that returns the replenish rate configuration. */
private String replenishRateHeader = REPLENISH_RATE_HEADER;
/** The name of the header that returns the burst capacity configuration. */
private String burstCapacityHeader = BURST_CAPACITY_HEADER;
public RedisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate,
RedisScript<List<Long>> script, Validator validator) {
super(Config.class, CONFIGURATION_PROPERTY_NAME, validator);
this.redisTemplate = redisTemplate;
this.script = script;
initialized.compareAndSet(false, true);
}
public RedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) {
super(Config.class, CONFIGURATION_PROPERTY_NAME, null);
this.defaultConfig = new Config()
.setReplenishRate(defaultReplenishRate)
.setBurstCapacity(defaultBurstCapacity);
}
public boolean isIncludeHeaders() {
return includeHeaders;
}
public void setIncludeHeaders(boolean includeHeaders) {
this.includeHeaders = includeHeaders;
}
public String getRemainingHeader() {
return remainingHeader;
}
public void setRemainingHeader(String remainingHeader) {
this.remainingHeader = remainingHeader;
}
public String getReplenishRateHeader() {
return replenishRateHeader;
}
public void setReplenishRateHeader(String replenishRateHeader) {
this.replenishRateHeader = replenishRateHeader;
}
public String getBurstCapacityHeader() {
return burstCapacityHeader;
}
public void setBurstCapacityHeader(String burstCapacityHeader) {
this.burstCapacityHeader = burstCapacityHeader;
}
@Override
@SuppressWarnings("unchecked")
public void setApplicationContext(ApplicationContext context) throws BeansException {
if (initialized.compareAndSet(false, true)) {
this.redisTemplate = context.getBean("stringReactiveRedisTemplate", ReactiveRedisTemplate.class);
this.script = context.getBean(REDIS_SCRIPT_NAME, RedisScript.class);
if (context.getBeanNamesForType(Validator.class).length > 0) {
this.setValidator(context.getBean(Validator.class));
}
}
}
/* for testing */ Config getDefaultConfig() {
return defaultConfig;
}
/**
* This uses a basic token bucket algorithm and relies on the fact that Redis scripts
* execute atomically. No other operations can run between fetching the count and
* writing the new count.
*/
@Override
@SuppressWarnings("unchecked")
public Mono<Response> isAllowed(String routeId, String id) {
if (!this.initialized.get()) {
throw new IllegalStateException("RedisRateLimiter is not initialized");
}
Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);
if (routeConfig == null) {
throw new IllegalArgumentException("No Configuration found for route " + routeId);
}
// How many requests per second do you want a user to be allowed to do?
int replenishRate = routeConfig.getReplenishRate();
// How much bursting do you want to allow?
int burstCapacity = routeConfig.getBurstCapacity();
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");
// allowed, tokens_left = redis.eval(SCRIPT, keys, args)
Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
// .log("redisratelimiter", Level.FINER);
return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
.reduce(new ArrayList<Long>(), (longs, l) -> {
longs.addAll(l);
return longs;
}) .map(results -> {
boolean allowed = results.get(0) == 1L;
Long tokensLeft = results.get(1);
Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));
if (log.isDebugEnabled()) {
log.debug("response: " + response);
}
return response;
});
}
catch (Exception e) {
/*
* We don't want a hard dependency on Redis to allow traffic. Make sure to set
* an alert so you know if this is happening too much. Stripe's observed
* failure rate is 0.01%.
*/
log.error("Error determining if user allowed from redis", e);
}
return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
}
@NotNull
public HashMap<String, String> getHeaders(Config config, Long tokensLeft) {
HashMap<String, String> headers = new HashMap<>();
headers.put(this.remainingHeader, tokensLeft.toString());
headers.put(this.replenishRateHeader, String.valueOf(config.getReplenishRate()));
headers.put(this.burstCapacityHeader, String.valueOf(config.getBurstCapacity()));
return headers;
}
static List<String> getKeys(String id) {
// use `{}` around keys to use Redis Key hash tags
// this allows for using redis cluster
// Make a unique key per user.
String prefix = "request_rate_limiter.{" + id;
// You need two Redis keys for Token Bucket.
String tokenKey = prefix + "}.tokens";
String timestampKey = prefix + "}.timestamp";
return Arrays.asList(tokenKey, timestampKey);
}
@Validated
public static class Config {
@Min(1)
private int replenishRate;
@Min(1)
private int burstCapacity = 1;
public int getReplenishRate() {
return replenishRate;
}
public Config setReplenishRate(int replenishRate) {
this.replenishRate = replenishRate;
return this;
}
public int getBurstCapacity() {
return burstCapacity;
}
public Config setBurstCapacity(int burstCapacity) {
this.burstCapacity = burstCapacity;
return this;
}
@Override
public String toString() {
return "Config{" +
"replenishRate=" + replenishRate +
", burstCapacity=" + burstCapacity +
'}';
}
}
}
When my route config is:
Problem 1: I want this rateLimiter can use to all route in gateway,but i find in follow class, code
Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);
get null, because getConfig() return map key is defaultFilters, value is a RequestRateLimiter we config above,but the code get by key(current route's id) current route is't config filter RequestRateLimiter, so it will do default logic,but the defaultConfig is also null。 and i try config as follow:i expect it can work but it error when run : Problem 2:
I think your code' get default logic is wrong,should be:
If the code is like what I said,Problem 2 is also should be fix;