Showing posts with label bucket4j. Show all posts
Showing posts with label bucket4j. Show all posts

Tuesday, 5 July 2022

Rate limit a spring boot API using Bucket4j

In this post, I am going to explain how to rate limit a spring boot API using Bucket4j.

 

Bucket4j is a Java rate-limiting library based on token-bucket algorithm.

 

What is Token bucket algorithm

A token bucket add or generate tokens at some fixed rate. For example, I can create a token bucket of size 10 and a refill rate of 1 per 10 seconds. This refill happen only when the number of token in the bucket are < 10.

 

Let us try to understand this with below example. 

Time      Requests    AvailableTokens
5:00:00     0          10 (we have 10 tokens initially)
5:00:10     0          10 (refill attempt failed cause Bucket is full)
5:00:20     0          10 (refill attempt failed cause Bucket is full)
5:00:30     0          10 (refill attempt failed cause Bucket is full)
5:00:40     0          10 (refill attempt failed cause Bucket is full)
5:00:50     0          10 (refill attempt failed cause Bucket is full)
5:00:58     8          2 (10 - 8) 
5:01:00     0          3 (refill 1 token)
5:01:10     0          4 (refill 1 token)
5:01:20     0          5 (refill 1 token)
5:01:30     0          6 (refill 1 token)
5:01:49     4          2 (6 - 4) 
5:01:50     0          3 (refill 1 token)

 

When a client send request to the API, application remove the token from bucket if it is available and accept the request. Request is rejected when the bucket do not have any tokens.

 

Define a bucket

Bandwidth limit = Bandwidth.classic(5, Refill.greedy(2, Duration.ofSeconds(30)));
Bucket bucket = Bucket.builder().addLimit(limit).build();

 

Above definition set the bucket capacity to 5 and refill the tokens in a greedy manner (2 tokens for every 30 seconds). For example refill "10 tokens per 1 second" will add 1 token per each 100 millisecond, in other words refill will not wait 1 second to regenerate whole bunch of 10 tokens.

 

Apply the token to REST API

@RequestMapping("/hello")
public ResponseEntity<String> home() {

	System.out.println("Availabel tokens : " + bucket.getAvailableTokens());

	if (bucket.tryConsume(1)) {
		return ResponseEntity.ok("Hello!!!!");
	}

	return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).header("Retry-After", "10").build();

}

 

'tryConsume(long numTokens)' tries to consume a specified number of tokens from this bucket and return true if the tokens were consumed, false otherwise.

 

Find the below working application.

 

Step 1: Create new maven project ‘bucket4j-hello-world’.

 

Step 2: Update pom.xml with maven dependencies.

 

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.sample.app</groupId>
	<artifactId>bucket4j-hello-world</artifactId>
	<version>1</version>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.6</version>
	</parent>

	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.github.vladimir-bukhtoyarov</groupId>
			<artifactId>bucket4j-core</artifactId>
			<version>7.5.0</version>
		</dependency>


	</dependencies>
</project>

 

Step 3: Define HelloController class.

 

HelloController.java

package com.sample.app.controller;

import java.time.Duration;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;

@RestController
public class HelloController {

	private final Bucket bucket;

	public HelloController() {
		Bandwidth limit = Bandwidth.classic(5, Refill.greedy(2, Duration.ofSeconds(30)));
		bucket = Bucket.builder().addLimit(limit).build();
	}

	@RequestMapping("/hello")
	public ResponseEntity<String> home() {

		System.out.println("Availabel tokens : " + bucket.getAvailableTokens());

		if (bucket.tryConsume(1)) {
			return ResponseEntity.ok("Hello!!!!");
		}

		return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).header("Retry-After", "10").build();

	}
}

Step 4: Define main application class.

 

App.java

package com.sample.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
	public static void main(String[] args) {

		SpringApplication.run(App.class, args);

	}
}

Total project structure looks like below.





Run App.java.

 

Open the url ‘http://localhost:8080/hello’ in browser.

 

 


 

When the bucket has no token left, you will get http response code 429.

 


 

You can download complete working application from this link.


Previous                                                 Next                                                 Home

Bucket4j: Rate limit by pricing plan

In this post, I am going to explain how can we use Bucket4j to rate limit the API based on the pricing plan.

 

Suppose, I have a product P1, which support following price plans.

a.   FREE – 5 requests per hour

b.   BASIC -  400 requests per hour

c.    STANDARD – 1000 requests per hour

d.   PREMIUM – 10000 requests per hour.

 

When a client subscribe to my product P1, we will assign an apiKey to the client based on the price plan. Whenever client send request to the api, he should pass this apiKey. This would help us identify the pricing plan linked with the API client.

 

Step 1: Define the Buckets for each pricing plan.

 

PricingPlan.java

package com.sample.app.enums;

import java.time.Duration;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;

public enum PricingPlan {

	FREE(5), BASIC(400), STANDARD(1000), PREMIUM(10000);

	private int bucketCapacity;

	private PricingPlan(int bucketCapacity) {
		this.bucketCapacity = bucketCapacity;
	}

	public static Bucket resolveBucketApiKey(String apiKey) {
		if (apiKey == null || apiKey.isEmpty()) {
			Bandwidth bandwidth = Bandwidth.classic(PricingPlan.FREE.bucketCapacity,
					Refill.intervally(PricingPlan.FREE.bucketCapacity, Duration.ofHours(1)));
			return Bucket.builder().addLimit(bandwidth).build();

		}

		if (apiKey.startsWith("BASIC-")) {
			Bandwidth bandwidth = Bandwidth.classic(PricingPlan.BASIC.bucketCapacity,
					Refill.intervally(PricingPlan.BASIC.bucketCapacity, Duration.ofHours(1)));
			return Bucket.builder().addLimit(bandwidth).build();
		}

		if (apiKey.startsWith("STANDARD-")) {
			Bandwidth bandwidth = Bandwidth.classic(PricingPlan.STANDARD.bucketCapacity,
					Refill.intervally(PricingPlan.STANDARD.bucketCapacity, Duration.ofHours(1)));
			return Bucket.builder().addLimit(bandwidth).build();
		}

		if (apiKey.startsWith("PREMIUM-")) {
			Bandwidth bandwidth = Bandwidth.classic(PricingPlan.PREMIUM.bucketCapacity,
					Refill.intervally(PricingPlan.PREMIUM.bucketCapacity, Duration.ofHours(1)));
			return Bucket.builder().addLimit(bandwidth).build();
		}

		Bandwidth bandwidth = Bandwidth.classic(PricingPlan.FREE.bucketCapacity,
				Refill.intervally(PricingPlan.FREE.bucketCapacity, Duration.ofHours(1)));
		return Bucket.builder().addLimit(bandwidth).build();

	}
}

Step 2: Define PricePlanDemo class.

 

PricePlanDemo.java

package com.sample.app;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import com.sample.app.enums.PricingPlan;

import io.github.bucket4j.Bucket;

public class PricePlanDemo {

	private static final Map<String, Bucket> BUCKETS_MAP = new ConcurrentHashMap<>();

	private static void getResource(String apiKey) {
		Bucket bucket = null;

		if (BUCKETS_MAP.containsKey(apiKey)) {
			bucket = BUCKETS_MAP.get(apiKey);
		} else {
			bucket = PricingPlan.resolveBucketApiKey(apiKey);
			BUCKETS_MAP.put(apiKey, bucket);
		}

		if (bucket.tryConsume(1)) {
			System.out.println("Resource accessed....");
			return;
		}

		System.err.println("Too Many Requests, all the tokens consumed");
	}

	public static void main(String[] args) throws InterruptedException {
		while (true) {
			TimeUnit.SECONDS.sleep(1);
			getResource("FREE-12easdfgft");
		}

	}

}

Sample Output



 

 

Previous                                                 Next                                                 Home

Bucket4j: refill the tokens in greedy manner

Refill#greedy method Creates the Refill that does refill of tokens in greedy manner, it will try to add the tokens to bucket as soon as possible.

 

For example refill "10 tokens per 1 second" will add 1 token per each 100 millisecond, in other words refill will not wait 1 second to regenerate whole bunch of 10 tokens.

 

Signature

public static Refill greedy(long tokens, Duration period)

Argument 'tokens' specifies amount of tokens and 'period' specifies the period within tokens will be fully regenerated.

 

Note

The three refills bellow do refill of tokens with same speed:

a.   Refill.greedy(600, Duration.ofMinutes(1));

b.   Refill.greedy(10, Duration.ofSeconds(1));

c.    Refill.greedy(1, Duration.ofMillis(100));

 

Find the below working application.

 

HelloWorld.java

package com.sample.app;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;

public class HelloWorld {

	private static Bucket bucket;

	private static void getResource() {

		if (bucket.tryConsume(1)) {
			System.out.println("Resource accessed....");
			return;
		}

		System.err.println("Too Many Requests, all the tokens consumed");
	}

	public static void main(String[] args) throws InterruptedException {
		Bandwidth limit = Bandwidth.classic(5, Refill.greedy(10, Duration.ofSeconds(30)));
		bucket = Bucket.builder().addLimit(limit).build();

		while (true) {
			TimeUnit.SECONDS.sleep(1);
			getResource();
		}
	}

}

 

Sample output


 

  

Previous                                                 Next                                                 Home

Bucket4j: refill the tokens in interval manner

Refill#intervally method refill the tokens in intervally manner. This refill strategy will wait until whole period will be elapsed before regenerate tokens.

 

Signature

public static Refill intervally(long tokens, Duration period)

'tokens' specifies the amount of tokens and 'period' specifies the period within tokens will be fully regenerated.

 

Example

Bandwidth limit = Bandwidth.classic(5, Refill.intervally(5, Duration.ofSeconds(10)));

Unlike ‘Refill.greedy’ method, Refill.intervally method refills the bucket at the beginning of the time window, in this example, 5 tokens at the start of the 10 seconds.

 

Find the below working application.

 

RefillTokensIntervally.java

package com.sample.app;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;

public class RefillTokensIntervally {

	private static Bucket bucket;

	private static void getResource() {

		final long noOfTokensLeft = bucket.getAvailableTokens();

		if (noOfTokensLeft == 0L) {
			System.err.println("Number of tokens left : " + bucket.getAvailableTokens());
		} else {
			System.out.println("Number of tokens left : " + bucket.getAvailableTokens());
		}

		if (bucket.tryConsume(1)) {
			System.out.println("Resource accessed....\n");
			return;
		}

		System.err.println("Too Many Requests, all the tokens consumed\n");
	}

	public static void main(String[] args) throws InterruptedException {
		Bandwidth limit = Bandwidth.classic(5, Refill.intervally(5, Duration.ofSeconds(10)));
		bucket = Bucket.builder().addLimit(limit).build();

		while (true) {
			TimeUnit.SECONDS.sleep(1);
			getResource();
		}
	}

}

Output

Number of tokens left : 5
Resource accessed....

Number of tokens left : 4
Resource accessed....

Number of tokens left : 3
Resource accessed....

Number of tokens left : 2
Resource accessed....

Number of tokens left : 1
Resource accessed....

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 5
Resource accessed....

Number of tokens left : 4
Resource accessed....

Number of tokens left : 3
Resource accessed....

Number of tokens left : 2
Resource accessed....

Number of tokens left : 1
Resource accessed....

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 5
Resource accessed....

Number of tokens left : 4
Resource accessed....

Number of tokens left : 3
Resource accessed....

Number of tokens left : 2
Resource accessed....

Number of tokens left : 1
Resource accessed....

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 0
Too Many Requests, all the tokens consumed

Number of tokens left : 5
Resource accessed....

Number of tokens left : 4
Resource accessed....

Number of tokens left : 3
Resource accessed....

Number of tokens left : 2
Resource accessed....

Number of tokens left : 1
Resource accessed....

Number of tokens left : 0
Too Many Requests, all the tokens consumed




 

Previous                                                 Next                                                 Home

Bucket4j: Hello World application

Step 1: Define a Bucket instance.

Bandwidth limit = Bandwidth.classic(5, Refill.greedy(10, Duration.ofSeconds(30)));
Bucket bucket = Bucket.builder().addLimit(limit).build();

 

Above snippet define a bucket with capacity 5 (It can store 5 tokens maximum), and refill the tokens in greedy manner (10 tokens in 30 seconds duration). For example refill "10 tokens per 1 second" will add 1 token per each 100 millisecond, in other words refill will not wait 1 second to regenerate whole bunch of 10 tokens.

 

The three refills bellow do refill of tokens with same speed:

a.   Refill.greedy(600, Duration.ofMinutes(1));

b.   Refill.greedy(10, Duration.ofSeconds(1));

c.    Refill.greedy(1, Duration.ofMillis(100));

 

Step 2: Apply rate limiting on the methods that you are interested.

private static void getResource() {

	if (bucket.tryConsume(1)) {
		System.out.println("Resource accessed....");
		return;
	}

	System.err.println("Too Many Requests, all the tokens consumed");
}

 

‘getResource’ method try to get a token before permitting access to the resource. if it got the token then the resource will be accessed successfully, else some error message is printed to the console.

 

Find the below working application.

 

HelloWorld.java

package com.sample.app;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;

public class HelloWorld {

	private static Bucket bucket;

	private static void getResource() {

		if (bucket.tryConsume(1)) {
			System.out.println("Resource accessed....");
			return;
		}

		System.err.println("Too Many Requests, all the tokens consumed");
	}

	public static void main(String[] args) throws InterruptedException {
		Bandwidth limit = Bandwidth.classic(5, Refill.greedy(10, Duration.ofSeconds(30)));
		bucket = Bucket.builder().addLimit(limit).build();

		while (true) {
			TimeUnit.SECONDS.sleep(1);
			getResource();
		}
	}

}

Sample output



 

 

 

 

 

 

Previous                                                 Next                                                 Home