Skip to content

Commit ab5e61d

Browse files
authored
Adds support for a Bucket4jRateLimiter in server webflux (#2955)
* Adds support for a Bucket4jRateLimiter * Makes bucket4j-core optional and caffeine integration test scoped * Adds Bucket4jRateLimiter auto-configuration * Updates bucket4j to 8.14.0 * Adds customizable header and adapts to async build * Adds configuration for alternative RefillStyles * Adds documentation and refillTokens property
1 parent d5f0f5c commit ab5e61d

File tree

8 files changed

+540
-10
lines changed

8 files changed

+540
-10
lines changed

docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/requestratelimiter-factory.adoc

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ The default implementation of `KeyResolver` is the `PrincipalNameKeyResolver`, w
2727
By default, if the `KeyResolver` does not find a key, requests are denied.
2828
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.
2929

30+
The following example configures a `KeyResolver` in Java:
31+
32+
.Config.java
33+
[source,java]
34+
----
35+
@Bean
36+
KeyResolver userKeyResolver() {
37+
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
38+
}
39+
----
40+
3041
[NOTE]
3142
=====
3243
The `RequestRateLimiter` is not configurable with the "shortcut" notation. The following example below is _invalid_:
@@ -81,6 +92,7 @@ The following listing configures a `redis-rate-limiter`:
8192

8293
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`.
8394
For example, setting `replenishRate=1`, `requestedTokens=60`, and `burstCapacity=60` results in a limit of `1 request/min`.
95+
8496
.application.yml
8597
[source,yaml]
8698
----
@@ -99,21 +111,87 @@ spring:
99111
100112
----
101113

102-
The following example configures a `KeyResolver` in Java:
114+
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.
115+
The `KeyResolver` is a simple one that gets the `user` request parameter
116+
NOTE: This is not recommended for production
117+
118+
[[bucket4j-ratelimiter]]
119+
== Bucket4j `RateLimiter`
120+
121+
This implementation is based on the https://bucket4j.com/[Bucket4j] Java library.
122+
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].
123+
124+
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.
125+
126+
.pom.xml
127+
[source,xml]
128+
----
129+
<dependency>
130+
<groupId>com.github.ben-manes.caffeine</groupId>
131+
<artifactId>caffeine</artifactId>
132+
<version>${caffeine.version}</version>
133+
</dependency>
134+
<dependency>
135+
<groupId>com.bucket4j</groupId>
136+
<artifactId>bucket4j_jdk17-caffeine</artifactId>
137+
<version>${bucket4j.version}</version>
138+
</dependency>
139+
----
140+
141+
First a bean of type `io.github.bucket4j.distributed.proxy.AsyncProxyMananger<String>` needs to be created.
103142

104143
.Config.java
105144
[source,java]
106145
----
107146
@Bean
108-
KeyResolver userKeyResolver() {
109-
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
147+
AsyncProxyManager<String> caffeineProxyManager() {
148+
Caffeine<String, RemoteBucketState> builder = (Caffeine) Caffeine.newBuilder().maximumSize(100);
149+
return new CaffeineProxyManager<>(builder, Duration.ofMinutes(1)).asAsync();
110150
}
111151
----
112152

113-
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.
114-
The `KeyResolver` is a simple one that gets the `user` request parameter
153+
The `bucket4j-rate-limiter.capacity` property is the maximum number of requests a user is allowed in a single second (without any dropped requests).
154+
This is the number of tokens the token bucket can hold.
155+
Must be greater than zero.
156+
157+
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].
158+
159+
The `bucket4j-rate-limiter.refillTokens` property defines how many tokens are added to the bucket in during `refillPeriod`.
160+
This defaults to `capacity` and must be greater than or equal to zero.
161+
162+
The `bucket4j-rate-limiter.requestedTokens` property is how many tokens a request costs.
163+
This is the number of tokens taken from the bucket for each request and defaults to `1`. Must be greater than zero.
164+
165+
The `bucket4j-rate-limiter.refillStyle` property defines how the bucket is refilled. The 3 options are `GREEDY` (default), `INTERVALLY` and `INTERVALLY_ALIGNED`.
166+
`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`.
167+
168+
The `bucket4j-rate-limiter.timeOfFirstRefill` property is an `Instant` only used when `refillStyle` is set to `INTERVALLY_ALIGNED`.
169+
170+
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.
115171
NOTE: This is not recommended for production
116172

173+
.application.yml
174+
[source,yaml]
175+
----
176+
spring:
177+
cloud:
178+
gateway:
179+
routes:
180+
- id: requestratelimiter_route
181+
uri: https://example.org
182+
filters:
183+
- name: RequestRateLimiter
184+
args:
185+
bucket4j-rate-limiter.capacity: 20
186+
bucket4j-rate-limiter.refillTokens: 10
187+
bucket4j-rate-limiter.refillPeriod: 1s
188+
bucket4j-rate-limiter.requestedTokens: 1
189+
190+
----
191+
192+
[[custom-ratelimiter]]
193+
== Custom `RateLimiter`
194+
117195
You can also define a rate limiter as a bean that implements the `RateLimiter` interface.
118196
In configuration, you can reference the bean by name using SpEL.
119197
`#{@myRateLimiter}` is a SpEL expression that references a bean with named `myRateLimiter`.

pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
<properties>
5252
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
5353
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
54-
<bucket4j.version>8.10.1</bucket4j.version>
54+
<bucket4j.version>8.14.0</bucket4j.version>
5555
<blockhound.version>1.0.8.RELEASE</blockhound.version>
5656
<java.version>17</java.version>
5757
<junit-pioneer.version>2.3.0</junit-pioneer.version>
@@ -99,12 +99,12 @@
9999
</dependency>
100100
<dependency>
101101
<groupId>com.bucket4j</groupId>
102-
<artifactId>bucket4j-core</artifactId>
102+
<artifactId>bucket4j_jdk17-core</artifactId>
103103
<version>${bucket4j.version}</version>
104104
</dependency>
105105
<dependency>
106106
<groupId>com.bucket4j</groupId>
107-
<artifactId>bucket4j-caffeine</artifactId>
107+
<artifactId>bucket4j_jdk17-caffeine</artifactId>
108108
<version>${bucket4j.version}</version>
109109
</dependency>
110110
<dependency>

spring-cloud-gateway-server-mvc/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
<!-- Third party dependencies -->
7676
<dependency>
7777
<groupId>com.bucket4j</groupId>
78-
<artifactId>bucket4j-core</artifactId>
78+
<artifactId>bucket4j_jdk17-core</artifactId>
7979
<optional>true</optional>
8080
</dependency>
8181
<!-- Spring test dependencies -->
@@ -92,7 +92,7 @@
9292
<!-- Third party test dependencies -->
9393
<dependency>
9494
<groupId>com.bucket4j</groupId>
95-
<artifactId>bucket4j-caffeine</artifactId>
95+
<artifactId>bucket4j_jdk17-caffeine</artifactId>
9696
<scope>test</scope>
9797
</dependency>
9898
<dependency>

spring-cloud-gateway-server/pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<properties>
1818
<main.basedir>${basedir}/..</main.basedir>
1919
<grpc.version>1.68.1</grpc.version>
20+
<context-propagation.version>1.0.0</context-propagation.version>
2021
</properties>
2122

2223
<dependencies>
@@ -135,6 +136,16 @@
135136
<artifactId>caffeine</artifactId>
136137
<optional>true</optional>
137138
</dependency>
139+
<dependency>
140+
<groupId>com.bucket4j</groupId>
141+
<artifactId>bucket4j_jdk17-core</artifactId>
142+
<optional>true</optional>
143+
</dependency>
144+
<dependency>
145+
<groupId>com.bucket4j</groupId>
146+
<artifactId>bucket4j_jdk17-caffeine</artifactId>
147+
<scope>test</scope>
148+
</dependency>
138149
<dependency>
139150
<groupId>io.micrometer</groupId>
140151
<artifactId>micrometer-observation-test</artifactId>

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import javax.net.ssl.TrustManagerFactory;
2727

28+
import io.github.bucket4j.distributed.proxy.AsyncProxyManager;
2829
import org.apache.commons.logging.Log;
2930
import org.apache.commons.logging.LogFactory;
3031
import reactor.core.publisher.Flux;
@@ -123,6 +124,7 @@
123124
import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter;
124125
import org.springframework.cloud.gateway.filter.headers.TransferEncodingNormalizationHeadersFilter;
125126
import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter;
127+
import org.springframework.cloud.gateway.filter.ratelimit.Bucket4jRateLimiter;
126128
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
127129
import org.springframework.cloud.gateway.filter.ratelimit.PrincipalNameKeyResolver;
128130
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
@@ -736,6 +738,20 @@ static ConfigurableHintsRegistrationProcessor configurableHintsRegistrationProce
736738
return new ConfigurableHintsRegistrationProcessor();
737739
}
738740

741+
@Configuration(proxyBeanMethods = false)
742+
@ConditionalOnClass(AsyncProxyManager.class)
743+
protected static class Bucket4jConfiguration {
744+
745+
@Bean
746+
@ConditionalOnBean(AsyncProxyManager.class)
747+
@ConditionalOnEnabledFilter(RequestRateLimiterGatewayFilterFactory.class)
748+
public Bucket4jRateLimiter bucket4jRateLimiter(AsyncProxyManager<String> proxyManager,
749+
ConfigurationService configurationService) {
750+
return new Bucket4jRateLimiter(proxyManager, configurationService);
751+
}
752+
753+
}
754+
739755
@Configuration(proxyBeanMethods = false)
740756
@ConditionalOnClass(HttpClient.class)
741757
protected static class NettyConfiguration {

0 commit comments

Comments
 (0)