From 60f9f68f69eb7e51669399cf73b26c58ed1be45c Mon Sep 17 00:00:00 2001 From: Subodh Date: Tue, 15 Sep 2026 23:02:33 +0530 Subject: [PATCH] resetting attempts after 1 hour --- .../java/com/memcache/gateway/api/KvController.java | 5 ++++- .../com/memcache/gateway/api/QuotaController.java | 11 ++++------- .../com/memcache/gateway/kv/WriteRateLimiter.java | 8 ++++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/beedb-gateway/src/main/java/com/memcache/gateway/api/KvController.java b/beedb-gateway/src/main/java/com/memcache/gateway/api/KvController.java index 47bd89e..b460e97 100644 --- a/beedb-gateway/src/main/java/com/memcache/gateway/api/KvController.java +++ b/beedb-gateway/src/main/java/com/memcache/gateway/api/KvController.java @@ -69,8 +69,11 @@ private ResponseEntity rateLimited(HttpServletRequest request) { return null; } LOGGER.warn("rate limit hit by {} ({} writes/hour)", client, rateLimiter.maxWritesPerHour()); + // Retry-After is when this client's window really ends, not a flat hour: a visitor who + // hit the limit 50 minutes into their window only has 10 minutes to wait. + long retryAfter = Math.max(1, rateLimiter.resetInSeconds(client)); return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) - .header("Retry-After", "3600") + .header("Retry-After", String.valueOf(retryAfter)) .body("write limit reached: " + rateLimiter.maxWritesPerHour() + " writes per hour. This is a shared demo."); } diff --git a/beedb-gateway/src/main/java/com/memcache/gateway/api/QuotaController.java b/beedb-gateway/src/main/java/com/memcache/gateway/api/QuotaController.java index 01968c3..a357c88 100644 --- a/beedb-gateway/src/main/java/com/memcache/gateway/api/QuotaController.java +++ b/beedb-gateway/src/main/java/com/memcache/gateway/api/QuotaController.java @@ -10,10 +10,6 @@ import com.memcache.gateway.kv.WriteRateLimiter; -/** - * How many writes this client has left this hour, so the page shows the server's real count - * instead of guessing - */ @RestController @RequestMapping("/api") public class QuotaController { @@ -25,9 +21,10 @@ public QuotaController(WriteRateLimiter rateLimiter) { } @GetMapping("/quota") - public Map quota(HttpServletRequest request) { + public Map quota(HttpServletRequest request) { String client = request.getRemoteAddr(); - return Map.of("remaining", rateLimiter.remaining(client), - "limit", rateLimiter.maxWritesPerHour()); + return Map.of("remaining", (long) rateLimiter.remaining(client), + "limit", (long) rateLimiter.maxWritesPerHour(), + "resetInSeconds", rateLimiter.resetInSeconds(client)); } } diff --git a/beedb-gateway/src/main/java/com/memcache/gateway/kv/WriteRateLimiter.java b/beedb-gateway/src/main/java/com/memcache/gateway/kv/WriteRateLimiter.java index 0ed0c73..9601f63 100644 --- a/beedb-gateway/src/main/java/com/memcache/gateway/kv/WriteRateLimiter.java +++ b/beedb-gateway/src/main/java/com/memcache/gateway/kv/WriteRateLimiter.java @@ -83,6 +83,14 @@ public int remaining(String clientId) { return Math.max(0, maxWritesPerHour - used); } + public long resetInSeconds(String clientId) { + return writesPerClient.policy().expireAfterWrite() + .flatMap(window -> window.ageOf(clientId) + .map(age -> window.getExpiresAfter().minus(age))) + .map(left -> Math.max(0L, (left.toMillis() + 999) / 1000)) + .orElse(0L); + } + public int maxWritesPerHour() { return maxWritesPerHour; }