Skip to content

Commit

Permalink
Adds support for a Bucket4jRateLimiter in server webflux (#2955)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
spencergibb authored Jan 17, 2025
1 parent d5f0f5c commit ab5e61d
Show file tree
Hide file tree
Showing 8 changed files with 540 additions and 10 deletions.
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

0 comments on commit ab5e61d

Please sign in to comment.