Skip to content

Commit

Permalink
🚧 work in propress for bucket4j rate limit
Browse files Browse the repository at this point in the history
  • Loading branch information
sanshengshui committed Oct 14, 2021
1 parent 9575e13 commit 1d42fb0
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
package iot.technology.ratelimiting;

import iot.technology.ratelimiting.bucket4j.interceptor.RateLimitInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* @author mushuwei
*/
@SpringBootApplication(scanBasePackages = {"iot.technology.ratelimiting"})
public class RateLimitApplication {
public class RateLimitApplication implements WebMvcConfigurer {

@Autowired
@Lazy
private RateLimitInterceptor interceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor)
.addPathPatterns("/api/v1/area/**");
}

public static void main(String[] args) {
SpringApplication.run(RateLimitApplication.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package iot.technology.ratelimiting.bucket4j;

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

import java.time.Duration;

/**
* @author mushuwei
*/
public enum PricingPlan {

FREE(20),

BASIC(40),

PROFESSIONAL(100);

private int bucketCapacity;

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

Bandwidth getLimit() {
return Bandwidth.classic(bucketCapacity, Refill.intervally(bucketCapacity, Duration.ofHours(1)));
}

public int bucketCapacity() {
return bucketCapacity;
}

static PricingPlan resolvePlanFromApiKey(String apiKey) {
if (apiKey == null || apiKey.isEmpty()) {
return FREE;
} else if (apiKey.startsWith("PX001-")) {
return PROFESSIONAL;
} else if (apiKey.startsWith("BX001-")) {
return BASIC;
}
return FREE;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package iot.technology.ratelimiting.bucket4j;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import org.springframework.stereotype.Service;

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

/**
* @author mushuwei
*/
@Service
public class PricingPlanService {

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

public Bucket resolveBucket(String apiKey) {
return cache.computeIfAbsent(apiKey, this::newBucket);
}

private Bucket newBucket(String apiKey) {
PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey);
return bucket(pricingPlan.getLimit());
}

private Bucket bucket(Bandwidth limit) {
return Bucket4j.builder()
.addLimit(limit)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package iot.technology.ratelimiting.bucket4j.interceptor;

import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe;
import iot.technology.ratelimiting.bucket4j.PricingPlanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author mushuwei
*/
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

private static final String HEADER_API_KEY = "X-api-key";
public static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining";
public static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Seconds";

@Autowired
private PricingPlanService pricingPlanService;

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String apiKey = request.getHeader(HEADER_API_KEY);

if (apiKey == null || apiKey.isEmpty()) {
response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: " + HEADER_API_KEY);
}
Bucket tokenBucket = pricingPlanService.resolveBucket(apiKey);
ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1);

if (probe.isConsumed()) {
response.addHeader(HEADER_LIMIT_REMAINING, String.valueOf(probe.getRemainingTokens()));
return true;
} else {
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefill));
response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota");
return false;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package iot.technology.ratelimiting.config;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* @author mushuwei
*/
public class AppConfig implements WebMvcConfigurer {


}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### 1.计算长方形面积
POST localhost:8080/api/v1/area/rectangle
Content-Type: application/json
X-api-key: FX001-99999

{
"length": "10",
Expand Down

0 comments on commit 1d42fb0

Please sign in to comment.