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&nbsp;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}