84 lines
3.6 KiB
Java
84 lines
3.6 KiB
Java
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.
|
|
*
|
|
* <p>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.</p>
|
|
*
|
|
* <p>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)}.</p>
|
|
*/
|
|
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.
|
|
*
|
|
* <p>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.</p>
|
|
*
|
|
* <p>The default implementation does nothing, which is correct for stateless limiters; any
|
|
* limiter that retains per-key state <strong>must</strong> override it to evict stale
|
|
* entries.</p>
|
|
*
|
|
* @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);
|
|
}
|
|
}
|
|
}
|