001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.guard; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.time.*; 022import java.util.*; 023import java.util.concurrent.*; 024import java.util.function.*; 025 026import org.apache.juneau.http.response.*; 027import org.apache.juneau.rest.server.*; 028 029/** 030 * Token-bucket {@link RestGuard} that throttles requests on a configurable per-request key. 031 * 032 * <p> 033 * On each request the guard atomically attempts to acquire one token from a per-key bucket. When tokens are 034 * available the request proceeds and three advisory response headers are populated 035 * ({@code X-RateLimit-Limit}, {@code X-RateLimit-Remaining}, {@code X-RateLimit-Reset}). When the bucket is 036 * empty a {@link TooManyRequests} exception is thrown with a {@code Retry-After} header set to the integer 037 * number of seconds until the next token will be available. 038 * 039 * <h5 class='topic'>Example usage</h5> 040 * 041 * <p class='bjava'> 042 * <ja>@Rest</ja>(path=<js>"/api"</js>) 043 * <jk>public class</jk> ApiResource <jk>extends</jk> BasicRestServlet { 044 * 045 * <jc>// Per-IP rate limit, 100 req/min, 200-burst.</jc> 046 * <ja>@Bean</ja>(name=<js>"guards"</js>) 047 * <jk>public</jk> RestGuardList rateLimits(BeanStore <jv>bs</jv>) { 048 * <jk>return</jk> RestGuardList.<jsm>create</jsm>(<jv>bs</jv>) 049 * .append(RateLimitGuard.<jsm>create</jsm>() 050 * .permitsPerMinute(100) 051 * .burst(200) 052 * .build()) 053 * .build(); 054 * } 055 * } 056 * </p> 057 * 058 * <h5 class='topic'>Keying</h5> 059 * 060 * <p> 061 * The default key is the request's {@linkplain RestRequest#getRemoteAddr() remote address}. Set 062 * {@link Builder#xForwardedForAware(boolean) xForwardedForAware(true)} to honor the first hop of the 063 * {@code X-Forwarded-For} header — required when running behind a trusted reverse proxy. 064 * 065 * <p> 066 * <b>Trusted-proxy assumption.</b> When {@code xForwardedForAware} is enabled without a trusted proxy in front 067 * of the application, attackers can spoof the {@code X-Forwarded-For} header and defeat the IP-based key. Only 068 * opt in when the request edge enforces the header. 069 * 070 * <h5 class='topic'>Storage</h5> 071 * 072 * <p> 073 * Bucket state is held by a pluggable {@link Storage} SPI. The default {@link Storage#inMemory()} implementation 074 * uses a {@link ConcurrentHashMap} with an LRU-style eviction policy (capped at 100 000 keys by default), suitable 075 * for single-pod deployments. Distributed deployments should provide a substrate-backed implementation 076 * (e.g. Redis) so per-pod buckets do not split the shared rate envelope. 077 * 078 * <h5 class='topic'>Probe paths</h5> 079 * 080 * <p> 081 * Built-in probe paths ({@code /healthz}, {@code /readyz}, {@code /livez}) are exempted from throttling by 082 * default so a misconfigured limit cannot mask a healthy pod from the orchestrator. Override via 083 * {@link Builder#exemptPaths(String...)}. 084 * 085 * <h5 class='topic'>Time source</h5> 086 * 087 * <p> 088 * Refill math uses {@link System#nanoTime()}. This is monotonic and safe across wall-clock jumps but does not 089 * map to a calendar instant, which slightly complicates debugging when the bucket is in an unexpected state. The 090 * nanosecond source is pluggable on {@link InMemoryStorage} (package-private constructor overload) so tests can 091 * pin the refill window with a deterministic clock instead of racing real wall-clock time; production code always 092 * goes through the single-arg constructor, which defaults to {@code System::nanoTime}. The wall-clock 093 * {@link BucketState#lastRequest()} instant reported by {@link Storage#snapshot()} is read from a separate 094 * {@link Clock} (default {@link Clock#systemUTC()}), likewise overridable through a package-private constructor. 095 * 096 * <h5 class='section'>See Also:</h5><ul> 097 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerRateLimitAndRequestId">Rate-Limiting and Request-Id Propagation</a> 098 * </ul> 099 * 100 * @since 10.0.0 101 */ 102@SuppressWarnings({ 103 "java:S1192", // Duplicate string literals are HTTP header names and rate-limit response field names; intentional 104 "java:S3516" // guard() and isRequestAllowed() always return true by contract; denial is signalled by throwing tooManyRequests 105}) 106public class RateLimitGuard extends RestGuard { 107 108 /** Response header name for the advisory ceiling (tokens per refill window). */ 109 public static final String HEADER_LIMIT = "X-RateLimit-Limit"; 110 111 /** Response header name for the advisory tokens remaining after this call. */ 112 public static final String HEADER_REMAINING = "X-RateLimit-Remaining"; 113 114 /** Response header name for the advisory seconds until the bucket is fully refilled. */ 115 public static final String HEADER_RESET = "X-RateLimit-Reset"; 116 117 /** Standard HTTP {@code Retry-After} header name. */ 118 public static final String HEADER_RETRY_AFTER = "Retry-After"; 119 120 private final int capacity; 121 private final double permitsPerSecond; 122 private final Function<RestRequest,String> keyResolver; 123 private final boolean xForwardedForAware; 124 private final Set<String> exemptPaths; 125 private final BiConsumer<RestRequest,RateLimitInfo> onLimitExceeded; 126 private final Storage storage; 127 128 /** 129 * Constructor. 130 * 131 * @param b The builder configuring this guard. Must not be <jk>null</jk>. 132 */ 133 protected RateLimitGuard(Builder b) { 134 reqnn("builder", b); 135 if (b.permitsPerSecond <= 0.0) 136 throw new IllegalArgumentException("Argument 'permitsPerSecond' must be > 0."); 137 if (b.burst <= 0) 138 throw new IllegalArgumentException("Argument 'burst' must be > 0."); 139 this.capacity = b.burst; 140 this.permitsPerSecond = b.permitsPerSecond; 141 this.keyResolver = b.keyResolver != null ? b.keyResolver : RestRequest::getRemoteAddr; 142 this.xForwardedForAware = b.xForwardedForAware; 143 this.exemptPaths = Set.copyOf(b.exemptPaths); 144 this.onLimitExceeded = b.onLimitExceeded; 145 this.storage = b.storage != null ? b.storage : Storage.inMemory(); 146 } 147 148 /** 149 * Creates a new builder. 150 * 151 * @return A new builder. 152 */ 153 public static Builder create() { 154 return new Builder(); 155 } 156 157 @Override /* Overridden from RestGuard */ 158 public boolean guard(RestRequest req, RestResponse res) { 159 if (isExempt(req)) 160 return true; 161 var key = resolveKey(req); 162 var result = storage.tryAcquire(key, capacity, permitsPerSecond); 163 var info = new RateLimitInfo(key, capacity, result.remaining(), result.secondsUntilReset(), result.allowed()); 164 if (! result.allowed()) { 165 setAdvisoryHeaders(res, info); 166 var retry = Math.max(1L, info.secondsUntilReset()); 167 res.setHeader(HEADER_RETRY_AFTER, Long.toString(retry)); 168 if (onLimitExceeded != null) 169 onLimitExceeded.accept(req, info); 170 throw tooManyRequests(info); 171 } 172 setAdvisoryHeaders(res, info); 173 return true; 174 } 175 176 @Override /* Overridden from RestGuard */ 177 public boolean isRequestAllowed(RestRequest req) { 178 return true; 179 } 180 181 private boolean isExempt(RestRequest req) { 182 if (exemptPaths.isEmpty()) 183 return false; 184 var pi = Objects.toString(req.getPathInfo(), ""); 185 var sp = Objects.toString(req.getServletPath(), ""); 186 return exemptPaths.contains(pi) || exemptPaths.contains(sp); 187 } 188 189 private String resolveKey(RestRequest req) { 190 if (xForwardedForAware) { 191 var xff = req.getHeader("X-Forwarded-For"); 192 if (inb(xff)) { 193 var comma = xff.indexOf(','); 194 return (comma < 0 ? xff : xff.substring(0, comma)).trim(); 195 } 196 } 197 var k = keyResolver.apply(req); 198 return or(k, ""); 199 } 200 201 private static TooManyRequests tooManyRequests(RateLimitInfo info) { 202 var retry = Math.max(1L, info.secondsUntilReset()); 203 var ex = new TooManyRequests("Rate limit exceeded for key '%s'. Retry after %ss.", info.key(), retry); 204 ex.setHeader(HEADER_LIMIT, Integer.toString(info.limit())); 205 ex.setHeader(HEADER_REMAINING, "0"); 206 ex.setHeader(HEADER_RESET, Long.toString(info.secondsUntilReset())); 207 ex.setHeader(HEADER_RETRY_AFTER, Long.toString(retry)); 208 return ex; 209 } 210 211 private static void setAdvisoryHeaders(RestResponse res, RateLimitInfo info) { 212 res.setHeader(HEADER_LIMIT, Integer.toString(info.limit())); 213 res.setHeader(HEADER_REMAINING, Integer.toString(info.remaining())); 214 res.setHeader(HEADER_RESET, Long.toString(info.secondsUntilReset())); 215 } 216 217 /** 218 * Builder for {@link RateLimitGuard}. 219 */ 220 public static class Builder { 221 222 private static final Set<String> DEFAULT_EXEMPT_PATHS = Set.of("/healthz", "/readyz", "/livez"); 223 224 double permitsPerSecond = 10.0; 225 int burst = 10; 226 Function<RestRequest,String> keyResolver; 227 boolean xForwardedForAware; 228 Set<String> exemptPaths = new LinkedHashSet<>(DEFAULT_EXEMPT_PATHS); 229 BiConsumer<RestRequest,RateLimitInfo> onLimitExceeded; 230 Storage storage; 231 232 /** 233 * Constructor. 234 */ 235 protected Builder() {} 236 237 /** 238 * Sets the steady-state refill rate in permits per second. 239 * 240 * @param value Permits per second. Must be {@code > 0}. 241 * @return This object. 242 */ 243 public Builder permitsPerSecond(int value) { 244 permitsPerSecond = value; 245 return this; 246 } 247 248 /** 249 * Sets the steady-state refill rate in permits per minute. 250 * 251 * <p> 252 * Equivalent to {@code permitsPerSecond(value / 60.0)}. 253 * 254 * @param value Permits per minute. Must be {@code > 0}. 255 * @return This object. 256 */ 257 public Builder permitsPerMinute(int value) { 258 permitsPerSecond = value / 60.0; 259 return this; 260 } 261 262 /** 263 * Sets the steady-state refill rate in permits per hour. 264 * 265 * <p> 266 * Equivalent to {@code permitsPerSecond(value / 3600.0)}. 267 * 268 * @param value Permits per hour. Must be {@code > 0}. 269 * @return This object. 270 */ 271 public Builder permitsPerHour(int value) { 272 permitsPerSecond = value / 3600.0; 273 return this; 274 } 275 276 /** 277 * Sets the burst capacity (maximum tokens a bucket can hold). 278 * 279 * @param value Burst capacity. Must be {@code > 0}. 280 * @return This object. 281 */ 282 public Builder burst(int value) { 283 burst = value; 284 return this; 285 } 286 287 /** 288 * Sets the per-request key resolver. 289 * 290 * <p> 291 * Default is {@link RestRequest#getRemoteAddr()}. Common alternatives include the user-principal name, 292 * an API-key header, or a tenant id. When the resolver returns <jk>null</jk> the empty string is used. 293 * 294 * @param value Resolver function. Must not be <jk>null</jk>. 295 * @return This object. 296 */ 297 public Builder keyBy(Function<RestRequest,String> value) { 298 reqnn("value", value); 299 keyResolver = value; 300 return this; 301 } 302 303 /** 304 * Enables {@code X-Forwarded-For}-aware key resolution. 305 * 306 * <p> 307 * When enabled, the first comma-separated hop of the {@code X-Forwarded-For} header takes precedence over 308 * the configured {@link #keyBy(Function)} resolver. Only opt in when a trusted reverse proxy strips and 309 * rewrites the header; otherwise clients can spoof it to defeat IP-based throttling. 310 * 311 * @param value <jk>true</jk> to honor {@code X-Forwarded-For}. 312 * @return This object. 313 */ 314 public Builder xForwardedForAware(boolean value) { 315 xForwardedForAware = value; 316 return this; 317 } 318 319 /** 320 * Sets the set of request paths that bypass throttling. 321 * 322 * <p> 323 * Matched against {@link RestRequest#getPathInfo()} and {@link RestRequest#getServletPath()} (exact 324 * match). Defaults to {@code /healthz}, {@code /readyz}, {@code /livez} — the built-in probe paths. 325 * Pass an empty array to clear the defaults. 326 * 327 * @param values Exempt paths. Must not be <jk>null</jk>. 328 * @return This object. 329 */ 330 public Builder exemptPaths(String...values) { 331 reqnn("values", values); 332 exemptPaths = new LinkedHashSet<>(Arrays.asList(values)); 333 return this; 334 } 335 336 /** 337 * Sets a callback invoked when a request is rejected due to an empty bucket. 338 * 339 * <p> 340 * Useful for structured logging or metrics emission. The callback runs before {@link TooManyRequests} is 341 * thrown and must not block. Exceptions raised inside the callback propagate to the caller. 342 * 343 * @param value Callback. Must not be <jk>null</jk>. 344 * @return This object. 345 */ 346 public Builder whenLimitExceeded(BiConsumer<RestRequest,RateLimitInfo> value) { 347 reqnn("value", value); 348 onLimitExceeded = value; 349 return this; 350 } 351 352 /** 353 * Sets the bucket-state storage backend. 354 * 355 * <p> 356 * Default is {@link Storage#inMemory()}. Substitute a distributed backend (Redis, DynamoDB, etc.) when 357 * running multiple pods that share a single rate envelope. 358 * 359 * @param value Storage backend. Must not be <jk>null</jk>. 360 * @return This object. 361 */ 362 public Builder storage(Storage value) { 363 reqnn("value", value); 364 storage = value; 365 return this; 366 } 367 368 /** 369 * Builds the guard. 370 * 371 * @return A new {@link RateLimitGuard}. 372 */ 373 public RateLimitGuard build() { 374 return new RateLimitGuard(this); 375 } 376 } 377 378 /** 379 * Snapshot of rate-limit state passed to the {@link Builder#whenLimitExceeded(BiConsumer) onLimitExceeded} 380 * callback and used to populate advisory response headers. 381 * 382 * @param key The per-request key (e.g. remote address, principal name). 383 * @param limit The bucket capacity (also reported as {@code X-RateLimit-Limit}). 384 * @param remaining Tokens remaining after this acquisition attempt. Zero when the request was rejected. 385 * @param secondsUntilReset Seconds until the bucket is full again (also reported as {@code X-RateLimit-Reset}). 386 * @param allowed <jk>true</jk> if the request was admitted; <jk>false</jk> if the bucket was empty. 387 */ 388 public record RateLimitInfo(String key, int limit, int remaining, long secondsUntilReset, boolean allowed) {} 389 390 /** 391 * Point-in-time view of a single token bucket as exposed by {@link Storage#snapshot()}. 392 * 393 * <p> 394 * Used by operator-facing tooling (e.g. {@code AdminMixin}'s {@code /admin/ratelimit} endpoint) to 395 * surface live per-key bucket state alongside the static configuration. Token-bucket vocabulary 396 * intentionally — there is no discrete refill window because the bucket refills continuously at 397 * {@code permitsPerSecond}, so this record reports the current fill level ({@link #tokens()}), the integer 398 * tokens currently available ({@link #remaining()}), a convenience throttled flag, and the wall-clock instant 399 * of the bucket's last activity. 400 * 401 * @param key The per-request key (e.g. remote address, principal name). 402 * @param tokens The current fractional token count in the bucket. Continuously refilled at the 403 * configured {@code permitsPerSecond} rate up to the configured capacity. 404 * @param remaining The integer number of tokens currently available — {@code floor(tokens)}. 405 * Mirrors the {@code X-RateLimit-Remaining} advisory header. 406 * @param throttled <jk>true</jk> when the bucket is empty enough that the next request would be rejected 407 * ({@code tokens < 1.0}). Convenience flag for at-a-glance operator dashboards. 408 * @param lastRequest The wall-clock {@link Instant} at which the bucket was last touched. 409 * 410 * @since 10.0.0 411 */ 412 public record BucketState(String key, double tokens, int remaining, boolean throttled, Instant lastRequest) {} 413 414 /** 415 * Returns the bucket capacity (the maximum number of tokens a bucket can hold). 416 * 417 * @return The bucket capacity. 418 * @since 10.0.0 419 */ 420 public int getCapacity() { 421 return capacity; 422 } 423 424 /** 425 * Returns the steady-state refill rate in permits per second. 426 * 427 * @return The refill rate. 428 * @since 10.0.0 429 */ 430 public double getPermitsPerSecond() { 431 return permitsPerSecond; 432 } 433 434 /** 435 * Returns whether {@code X-Forwarded-For}-aware key resolution is enabled. 436 * 437 * @return <jk>true</jk> if {@code X-Forwarded-For} resolution is enabled. 438 * @since 10.0.0 439 */ 440 public boolean isXForwardedForAware() { 441 return xForwardedForAware; 442 } 443 444 /** 445 * Returns the set of request paths that bypass throttling. 446 * 447 * @return The exempt paths. Never <jk>null</jk>. 448 * @since 10.0.0 449 */ 450 public Set<String> getExemptPaths() { 451 return exemptPaths; 452 } 453 454 /** 455 * Returns the bucket-state storage backend in use by this guard. 456 * 457 * @return The storage backend. Never <jk>null</jk>. 458 * @since 10.0.0 459 */ 460 public Storage getStorage() { 461 return storage; 462 } 463 464 /** 465 * Convenience accessor for {@link Storage#snapshot()} on the underlying storage backend. 466 * 467 * <p> 468 * Returns an empty map when the configured {@link Storage} does not override 469 * {@link Storage#snapshot()} (the default behavior for storages — e.g. Redis-backed — that can't cheaply 470 * enumerate every bucket). 471 * 472 * @return A point-in-time map of per-key bucket state. Never <jk>null</jk>. 473 * @since 10.0.0 474 */ 475 public Map<String,BucketState> snapshot() { 476 return storage.snapshot(); 477 } 478 479 /** 480 * SPI for storing per-key token-bucket state. 481 * 482 * <p> 483 * Implementations must be thread-safe. The default {@link #inMemory()} implementation is suitable for 484 * single-pod deployments. Multi-pod deployments that share a single rate envelope should substitute a 485 * distributed backend. 486 * 487 * @since 10.0.0 488 */ 489 public interface Storage { 490 491 /** 492 * Attempts to acquire one token from the bucket associated with the given key. 493 * 494 * @param key The bucket key. Never <jk>null</jk>. 495 * @param capacity The maximum tokens the bucket can hold. 496 * @param permitsPerSecond The steady-state refill rate. 497 * @return The outcome of the attempt. Never <jk>null</jk>. 498 */ 499 AcquireResult tryAcquire(String key, int capacity, double permitsPerSecond); 500 501 /** 502 * Evicts buckets idle for longer than the given TTL. 503 * 504 * <p> 505 * The default {@link #inMemory()} implementation evicts opportunistically when its size cap is exceeded; 506 * call this method to force an eager sweep (typically from a scheduled task). 507 * 508 * @param ttl The idle threshold. Buckets last touched longer than {@code ttl} ago are removed. 509 */ 510 void evict(Duration ttl); 511 512 /** 513 * Returns a point-in-time view of every per-key bucket currently held by this storage backend. 514 * 515 * <p> 516 * Optional operation for operational visibility. The default implementation returns an empty map so 517 * that external storage backends (Redis, DynamoDB, etc.) that can't cheaply enumerate every bucket 518 * stay backwards-compatible without code changes. In-memory implementations <b>SHOULD</b> override 519 * this to expose live bucket state — see the bundled {@link #inMemory()} implementation, which walks 520 * its internal map and returns one {@link BucketState} per entry. 521 * 522 * <p> 523 * The returned map is a snapshot, not a live view — concurrent modifications to the underlying 524 * storage after this method returns are not reflected. Per-bucket reads are individually consistent 525 * (the in-memory implementation reads each bucket under its own monitor), but the snapshot as a whole 526 * is not a global point-in-time consistent view. 527 * 528 * <p> 529 * <b>Cardinality warning:</b> on storages that hold many buckets (the bundled in-memory storage caps 530 * at 100 000 keys by default) the returned map can be large. Operator-facing callers should 531 * either filter client-side or use a paginated alternative if one is offered by the backend. 532 * 533 * @return A point-in-time map of per-key bucket state, keyed by per-request key. Never <jk>null</jk>. 534 * @since 10.0.0 535 */ 536 default Map<String,BucketState> snapshot() { 537 return Map.of(); 538 } 539 540 /** 541 * Creates a new in-memory storage backend with the default size cap (100 000 keys). 542 * 543 * @return A new in-memory storage backend. 544 */ 545 static Storage inMemory() { 546 return new InMemoryStorage(100_000); 547 } 548 549 /** 550 * Creates a new in-memory storage backend with a custom size cap. 551 * 552 * @param maxKeys The maximum number of keys held before LRU-style eviction kicks in. Must be {@code > 0}. 553 * @return A new in-memory storage backend. 554 */ 555 static Storage inMemory(int maxKeys) { 556 return new InMemoryStorage(maxKeys); 557 } 558 559 /** 560 * Outcome of a {@link #tryAcquire(String, int, double) tryAcquire} attempt. 561 * 562 * @param allowed <jk>true</jk> if the request was admitted. 563 * @param remaining Tokens left in the bucket after the attempt. 564 * @param secondsUntilReset Seconds until the bucket is full again. 565 */ 566 record AcquireResult(boolean allowed, int remaining, long secondsUntilReset) {} 567 } 568 569 /** 570 * Default in-memory {@link Storage} implementation. 571 * 572 * <p> 573 * Backed by a {@link ConcurrentHashMap} keyed by the per-request key. Each bucket tracks token count and last 574 * touch time. When the map exceeds {@code maxKeys} entries the least-recently-touched bucket is removed. 575 */ 576 static final class InMemoryStorage implements Storage { 577 578 private final ConcurrentHashMap<String,Bucket> buckets = new ConcurrentHashMap<>(); 579 private final int maxKeys; 580 private final LongSupplier nanoClock; 581 private final Clock clock; 582 583 InMemoryStorage(int maxKeys) { 584 this(maxKeys, System::nanoTime); 585 } 586 587 /** 588 * Constructor allowing the nanosecond time source to be overridden. 589 * 590 * <p> 591 * Test-only seam. Production code should always use {@link #InMemoryStorage(int)}, which defaults to 592 * real {@link System#nanoTime()}. Tests that need to pin the token-bucket refill window regardless of 593 * machine speed can supply a deterministic {@link LongSupplier} here instead of racing {@link Thread#sleep} 594 * against real time. 595 * 596 * @param maxKeys The maximum number of keys held before LRU-style eviction kicks in. Must be {@code > 0}. 597 * @param nanoClock Supplies the current time in nanoseconds for refill math and idle-eviction bookkeeping. Must not be <jk>null</jk>. 598 */ 599 InMemoryStorage(int maxKeys, LongSupplier nanoClock) { 600 this(maxKeys, nanoClock, Clock.systemUTC()); 601 } 602 603 /** 604 * Constructor allowing both the nanosecond and the wall-clock time sources to be overridden. 605 * 606 * <p> 607 * Test-only seam, like {@link #InMemoryStorage(int, LongSupplier)}. The {@link Clock} only stamps the 608 * {@link BucketState#lastRequest()} instant reported by {@link #snapshot()}; refill and eviction math stay on 609 * {@code nanoClock}. 610 * 611 * @param maxKeys The maximum number of keys held before LRU-style eviction kicks in. Must be {@code > 0}. 612 * @param nanoClock Supplies the current time in nanoseconds for refill math and idle-eviction bookkeeping. Must not be <jk>null</jk>. 613 * @param clock Supplies the wall-clock time recorded as each bucket's last-request instant. Must not be <jk>null</jk>. 614 */ 615 InMemoryStorage(int maxKeys, LongSupplier nanoClock, Clock clock) { 616 reqnn("nanoClock", nanoClock, "clock", clock); 617 if (maxKeys <= 0) 618 throw new IllegalArgumentException("Argument 'maxKeys' must be > 0."); 619 this.maxKeys = maxKeys; 620 this.nanoClock = nanoClock; 621 this.clock = clock; 622 } 623 624 @Override 625 public AcquireResult tryAcquire(String key, int capacity, double permitsPerSecond) { 626 var bucket = buckets.computeIfAbsent(key, k -> new Bucket(capacity, nanoClock, clock)); 627 var result = bucket.tryAcquire(capacity, permitsPerSecond); 628 if (buckets.size() > maxKeys) 629 evictOldest(); 630 return result; 631 } 632 633 @Override 634 public void evict(Duration ttl) { 635 var threshold = nanoClock.getAsLong() - ttl.toNanos(); 636 buckets.entrySet().removeIf(e -> e.getValue().lastTouchedNanos() < threshold); 637 } 638 639 @Override 640 public Map<String,BucketState> snapshot() { 641 var out = new LinkedHashMap<String,BucketState>(); 642 for (var e : buckets.entrySet()) { 643 var b = e.getValue(); 644 var tokens = b.tokens(); 645 out.put(e.getKey(), new BucketState( 646 e.getKey(), 647 tokens, 648 (int) Math.floor(tokens), 649 tokens < 1.0, 650 Instant.ofEpochMilli(b.lastWallMillis()) 651 )); 652 } 653 return Map.copyOf(out); 654 } 655 656 int size() { 657 return buckets.size(); 658 } 659 660 private void evictOldest() { 661 buckets.entrySet().stream() 662 .min(Comparator.comparingLong(e -> e.getValue().lastTouchedNanos())) 663 .map(Map.Entry::getKey) 664 .ifPresent(buckets::remove); 665 } 666 } 667 668 /** 669 * Per-key token-bucket state. 670 * 671 * <p> 672 * Holds a fractional token count and the {@link System#nanoTime()} value of the last touch. The 673 * {@code synchronized} block keeps the read-refill-write sequence atomic without the overhead of a lock-free 674 * CAS loop on this hot path. 675 */ 676 static final class Bucket { 677 678 private double tokens; 679 private long lastNanos; 680 private long lastWallMillis; 681 private final LongSupplier nanoClock; 682 private final Clock clock; 683 684 Bucket(int capacity) { 685 this(capacity, System::nanoTime, Clock.systemUTC()); 686 } 687 688 /** 689 * Constructor allowing the time sources to be overridden. 690 * 691 * <p> 692 * Test-only seam — see {@link InMemoryStorage#InMemoryStorage(int, LongSupplier, Clock)}. 693 * 694 * @param capacity The initial (full) token count. 695 * @param nanoClock Supplies the current time in nanoseconds for refill math. Must not be <jk>null</jk>. 696 * @param clock Supplies the wall-clock time recorded as the last-request instant. Must not be <jk>null</jk>. 697 */ 698 Bucket(int capacity, LongSupplier nanoClock, Clock clock) { 699 this.nanoClock = nanoClock; 700 this.clock = clock; 701 this.tokens = capacity; 702 this.lastNanos = nanoClock.getAsLong(); 703 this.lastWallMillis = clock.millis(); 704 } 705 706 synchronized Storage.AcquireResult tryAcquire(int capacity, double permitsPerSecond) { 707 var now = nanoClock.getAsLong(); 708 var elapsedSeconds = (now - lastNanos) / 1_000_000_000.0; 709 tokens = Math.min(capacity, tokens + elapsedSeconds * permitsPerSecond); 710 lastNanos = now; 711 lastWallMillis = clock.millis(); 712 if (tokens >= 1.0) { 713 tokens -= 1.0; 714 return new Storage.AcquireResult(true, (int) Math.floor(tokens), secondsUntilFull(capacity, permitsPerSecond)); 715 } 716 return new Storage.AcquireResult(false, 0, secondsUntilFull(capacity, permitsPerSecond)); 717 } 718 719 synchronized long lastTouchedNanos() { 720 return lastNanos; 721 } 722 723 synchronized double tokens() { 724 return tokens; 725 } 726 727 synchronized long lastWallMillis() { 728 return lastWallMillis; 729 } 730 731 private long secondsUntilFull(int capacity, double permitsPerSecond) { 732 var needed = Math.max(0.0, capacity - tokens); 733 return (long) Math.ceil(needed / permitsPerSecond); 734 } 735 } 736}