Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for a Bucket4jRateLimiter in server webflux #2955

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ The default implementation of `KeyResolver` is the `PrincipalNameKeyResolver`, w
By default, if the `KeyResolver` does not find a key, requests are denied.
You can adjust this behavior by setting the `spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key` (`true` or `false`) and `spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code` properties.

The following example configures a `KeyResolver` in Java:

.Config.java
[source,java]
----
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
----

[NOTE]
=====
The `RequestRateLimiter` is not configurable with the "shortcut" notation. The following example below is _invalid_:
Expand Down Expand Up @@ -81,6 +92,7 @@ The following listing configures a `redis-rate-limiter`:

Rate limits below `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`.
For example, setting `replenishRate=1`, `requestedTokens=60`, and `burstCapacity=60` results in a limit of `1 request/min`.

.application.yml
[source,yaml]
----
Expand All @@ -99,21 +111,87 @@ spring:

----

The following example configures a `KeyResolver` in Java:
This defines a request rate limit of 10 per user. A burst of 20 is allowed, but, in the next second, only 10 requests are available.
The `KeyResolver` is a simple one that gets the `user` request parameter
NOTE: This is not recommended for production

[[bucket4j-ratelimiter]]
== Bucket4j `RateLimiter`

This implementation is based on the https://bucket4j.com/[Bucket4j] Java library.
It requires the use of the `com.bucket4j:bucket4j_jdk17-core` dependency as well as one of the https://github.com/bucket4j/bucket4j?tab=readme-ov-file#bucket4j-distributed-features[distributed persistence options].

In this example, we will use the Caffeine integration, which is a local cache. This can be added by including the `com.github.ben-manes.caffeine:caffeine` artifact in your dependency management. The `com.bucket4j:bucket4j_jdk17-caffeine` artifact will need to be imported as well.

.pom.xml
[source,xml]
----
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j_jdk17-caffeine</artifactId>
<version>${bucket4j.version}</version>
</dependency>
----

First a bean of type `io.github.bucket4j.distributed.proxy.AsyncProxyMananger<String>` needs to be created.

.Config.java
[source,java]
----
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
AsyncProxyManager<String> caffeineProxyManager() {
Caffeine<String, RemoteBucketState> builder = (Caffeine) Caffeine.newBuilder().maximumSize(100);
return new CaffeineProxyManager<>(builder, Duration.ofMinutes(1)).asAsync();
}
----

This defines a request rate limit of 10 per user. A burst of 20 is allowed, but, in the next second, only 10 requests are available.
The `KeyResolver` is a simple one that gets the `user` request parameter
The `bucket4j-rate-limiter.capacity` property is the maximum number of requests a user is allowed in a single second (without any dropped requests).
This is the number of tokens the token bucket can hold.
Must be greater than zero.

The `bucket4j-rate-limiter.refillPeriod` property defines the refill period. The bucket refills at a rate of `refillTokens` per `refillPeriod`. This is a required property and uses the https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config.typesafe-configuration-properties.conversion.periods[Spring Boot Period format].

The `bucket4j-rate-limiter.refillTokens` property defines how many tokens are added to the bucket in during `refillPeriod`.
This defaults to `capacity` and must be greater than or equal to zero.

The `bucket4j-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`. Must be greater than zero.

The `bucket4j-rate-limiter.refillStyle` property defines how the bucket is refilled. The 3 options are `GREEDY` (default), `INTERVALLY` and `INTERVALLY_ALIGNED`.
`GREEDY` tries to add the tokens to the bucket as soon as possible. `INTERVALLY`, in opposite to greedy, waits until the whole `refillPeriod` has elapsed before refilling tokens. `INTERVALLY_ALIGNED` is like `INTERVALLY`, but with a specified `timeOfFirstRefill`.

The `bucket4j-rate-limiter.timeOfFirstRefill` property is an `Instant` only used when `refillStyle` is set to `INTERVALLY_ALIGNED`.

The following example defines a request rate limit of 10 per user. A burst of 20 is allowed, but, in the next second, only 10 requests are available.
NOTE: This is not recommended for production

.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
bucket4j-rate-limiter.capacity: 20
bucket4j-rate-limiter.refillTokens: 10
bucket4j-rate-limiter.refillPeriod: 1s
bucket4j-rate-limiter.requestedTokens: 1

----

[[custom-ratelimiter]]
== Custom `RateLimiter`

You can also define a rate limiter as a bean that implements the `RateLimiter` interface.
In configuration, you can reference the bean by name using SpEL.
`#{@myRateLimiter}` is a SpEL expression that references a bean with named `myRateLimiter`.
Expand Down
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<bucket4j.version>8.10.1</bucket4j.version>
<bucket4j.version>8.14.0</bucket4j.version>
<blockhound.version>1.0.8.RELEASE</blockhound.version>
<java.version>17</java.version>
<junit-pioneer.version>2.3.0</junit-pioneer.version>
Expand Down Expand Up @@ -99,12 +99,12 @@
</dependency>
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<artifactId>bucket4j_jdk17-core</artifactId>
<version>${bucket4j.version}</version>
</dependency>
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-caffeine</artifactId>
<artifactId>bucket4j_jdk17-caffeine</artifactId>
<version>${bucket4j.version}</version>
</dependency>
<dependency>
Expand Down
4 changes: 2 additions & 2 deletions spring-cloud-gateway-server-mvc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<!-- Third party dependencies -->
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<artifactId>bucket4j_jdk17-core</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring test dependencies -->
Expand All @@ -92,7 +92,7 @@
<!-- Third party test dependencies -->
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-caffeine</artifactId>
<artifactId>bucket4j_jdk17-caffeine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
11 changes: 11 additions & 0 deletions spring-cloud-gateway-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<properties>
<main.basedir>${basedir}/..</main.basedir>
<grpc.version>1.68.1</grpc.version>
<context-propagation.version>1.0.0</context-propagation.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -135,6 +136,16 @@
<artifactId>caffeine</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j_jdk17-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j_jdk17-caffeine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import javax.net.ssl.TrustManagerFactory;

import io.github.bucket4j.distributed.proxy.AsyncProxyManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
Expand Down Expand Up @@ -123,6 +124,7 @@
import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.TransferEncodingNormalizationHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter;
import org.springframework.cloud.gateway.filter.ratelimit.Bucket4jRateLimiter;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.PrincipalNameKeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
Expand Down Expand Up @@ -736,6 +738,20 @@ static ConfigurableHintsRegistrationProcessor configurableHintsRegistrationProce
return new ConfigurableHintsRegistrationProcessor();
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AsyncProxyManager.class)
protected static class Bucket4jConfiguration {

@Bean
@ConditionalOnBean(AsyncProxyManager.class)
@ConditionalOnEnabledFilter(RequestRateLimiterGatewayFilterFactory.class)
public Bucket4jRateLimiter bucket4jRateLimiter(AsyncProxyManager<String> proxyManager,
ConfigurationService configurationService) {
return new Bucket4jRateLimiter(proxyManager, configurationService);
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HttpClient.class)
protected static class NettyConfiguration {
Expand Down
Loading
Loading