package dev.coph.nextusweb.server.ratelimit; /** * Strategy interface for rate limiting. An implementation decides, per logical key, whether a * single request may proceed right now. * *
Concrete strategies in this package include {@link TokenBucketLimiter}, * {@link LeakyBucketLimiter}, {@link FixedWindowLimiter} and {@link SlidingWindowLimiter}. * Implementations are expected to be thread-safe, since the same limiter is shared across all * request-handling threads.
* *The interface remains effectively functional ({@link #tryAcquire} is its single abstract * method), so simple stateless limiters can still be written as a lambda; stateful limiters that * keep one entry per key should additionally override {@link #cleanup(long)}.
*/ public interface RateLimiter { /** * Attempts to consume one unit of quota for the given key at the given timestamp. * * @param key the logical bucket key (for example a client IP or user identifier) * @param nowNanos the current time in nanoseconds, typically {@link System#nanoTime()} * @return a {@link Result} describing whether the request was allowed and the remaining * quota */ Result tryAcquire(String key, long nowNanos); /** * Evicts per-key state that has not been accessed within the given age, bounding the memory * a limiter consumes when it has seen many distinct keys. * *Implementations keep one entry per key seen ({@code clientIp}, API key, ...). Without * periodic eviction those maps grow without bound, which is both a memory leak and a denial * of service vector (an attacker that varies the key on every request can exhaust the heap). * {@link RateLimitGate} calls this periodically for every configured limiter.
* *The default implementation does nothing, which is correct for stateless limiters; any * limiter that retains per-key state must override it to evict stale * entries.
* * @param olderThanNanos maximum idle age in nanoseconds before an entry is removed */ default void cleanup(long olderThanNanos) { } /** * Immutable outcome of a {@link #tryAcquire(String, long)} call. * * @param allowed whether the request may proceed * @param remaining the remaining quota in the current window/bucket * @param limit the configured limit, surfaced as {@code X-RateLimit-Limit} * @param retryAfterMillis when denied, how long the caller should wait before retrying, in * milliseconds (0 when allowed) */ record Result( boolean allowed, long remaining, long limit, long retryAfterMillis ) { /** * Creates a result representing an allowed request. * * @param remaining the remaining quota after this request * @param limit the configured limit * @return an "allowed" result with no retry delay */ public static Result allow(long remaining, long limit) { return new Result(true, remaining, limit, 0); } /** * Creates a result representing a denied (rate-limited) request. * * @param limit the configured limit * @param retryAfterMillis how long to wait before retrying, in milliseconds * @return a "denied" result with zero remaining quota */ public static Result deny(long limit, long retryAfterMillis) { return new Result(false, 0, limit, retryAfterMillis); } } }