Rate Limiting in Minitab

Here’s the translation of the Go rate limiting example to Java, along with explanations in Markdown format suitable for Hugo:

Rate limiting is an important mechanism for controlling resource utilization and maintaining quality of service. Java supports rate limiting with threads, ExecutorService, and ScheduledExecutorService.

First, we’ll look at basic rate limiting. Suppose we want to limit our handling of incoming requests. We’ll serve these requests off a BlockingQueue of the same name.

import java.util.concurrent.*;
import java.time.LocalDateTime;

public class RateLimiting {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> requests = new ArrayBlockingQueue<>(5);
        for (int i = 1; i <= 5; i++) {
            requests.offer(i);
        }

        // This ScheduledExecutorService will execute a task every 200 milliseconds.
        // This is the regulator in our rate limiting scheme.
        ScheduledExecutorService limiter = Executors.newScheduledThreadPool(1);
        limiter.scheduleAtFixedRate(() -> {
            try {
                Integer req = requests.take();
                System.out.println("request " + req + " " + LocalDateTime.now());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 0, 200, TimeUnit.MILLISECONDS);

        // Allow the program to run for a while
        Thread.sleep(1500);
        limiter.shutdown();

We may want to allow short bursts of requests in our rate limiting scheme while preserving the overall rate limit. We can accomplish this by using a semaphore. This burstyLimiter will allow bursts of up to 3 events.

        // Create a semaphore with 3 permits for bursty traffic
        Semaphore burstyLimiter = new Semaphore(3);

        // Fill up the semaphore to represent allowed bursting
        burstyLimiter.acquire(3);

        // Every 200 milliseconds we'll try to add a new permit to burstyLimiter, up to its limit of 3
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(() -> burstyLimiter.release(), 200, 200, TimeUnit.MILLISECONDS);

        // Now simulate 5 more incoming requests. The first 3 of these will benefit from the burst capability of burstyLimiter.
        BlockingQueue<Integer> burstyRequests = new ArrayBlockingQueue<>(5);
        for (int i = 1; i <= 5; i++) {
            burstyRequests.offer(i);
        }

        ExecutorService executor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                try {
                    burstyLimiter.acquire();
                    Integer req = burstyRequests.take();
                    System.out.println("request " + req + " " + LocalDateTime.now());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // Allow the program to run for a while
        Thread.sleep(1500);
        scheduler.shutdown();
        executor.shutdown();
    }
}

Running our program we see the first batch of requests handled once every ~200 milliseconds as desired.

request 1 2023-06-14T10:15:00.123
request 2 2023-06-14T10:15:00.323
request 3 2023-06-14T10:15:00.523
request 4 2023-06-14T10:15:00.723
request 5 2023-06-14T10:15:00.923

For the second batch of requests we serve the first 3 immediately because of the burstable rate limiting, then serve the remaining 2 with ~200ms delays each.

request 1 2023-06-14T10:15:01.123
request 2 2023-06-14T10:15:01.123
request 3 2023-06-14T10:15:01.123
request 4 2023-06-14T10:15:01.323
request 5 2023-06-14T10:15:01.523

This Java implementation uses ScheduledExecutorService for periodic tasks, BlockingQueue for managing requests, and Semaphore for implementing bursty rate limiting. While the structure is different from the Go version, it achieves the same rate limiting behavior.