Documentation
¶
Overview ¶
Package ratelimit implements a sliding-window-counter rate limiter with a two-phase Reserve/Commit API. Five strategies are supported: sliding-window, fixed-window, token-bucket, leaky-bucket, session-window. Concurrency is a special gauge meter that works with any strategy.
This package is pure — it has no imports from github.com/wyolet/relay/internal. Relay-specific translation (catalog.ResolvedRule → Rule) lives in internal/ratelimit.
Expected kv ops per request:
- Reserve: 1 RunScript call (all counter checks + increments atomic).
- Commit: 1 RunScript call (concurrency decrement + token post-increment).
- CommitBoth: 1 batched round trip committing two reservations that live under different hash tags (inbound policy scope + upstream hostkey scope), replacing two sequential Commit calls. Each reservation's script is individually atomic; the batch is not cross-atomic (a single CROSSSLOT script across the two tags is impossible on Redis Cluster).
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrExceeded = errors.New("limit: budget exceeded")
ErrExceeded is the sentinel wrapped by *KeyQuotaExhausted.
Functions ¶
func RegisterScripts ¶
RegisterScripts registers the Go emulators for limit.reserve and limit.commit on the given MemStore. Called once per Limiter constructed from a MemStore.
Types ¶
type ExceededError
deprecated
type ExceededError = KeyQuotaExhausted
ExceededError is an alias kept for backward compatibility with callers that use the old name. New code should use KeyQuotaExhausted directly.
Deprecated: Use KeyQuotaExhausted.
type KeyQuotaExhausted ¶
KeyQuotaExhausted is returned by Reserve when a budget is violated. The name reflects that exceeding a per-key rule is the primary signal this type carries in the post-Pick path (pipeline issue #89).
func (*KeyQuotaExhausted) Error ¶
func (e *KeyQuotaExhausted) Error() string
func (*KeyQuotaExhausted) Unwrap ¶
func (e *KeyQuotaExhausted) Unwrap() error
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter enforces rate-limit rules using pkg/kv for counters. One Limiter is shared across the process; concurrent calls are safe.
func (*Limiter) Commit ¶
func (l *Limiter) Commit(ctx context.Context, res *Reservation, obs Observations) error
Commit finalizes a Reservation via one RunScript call. Tokens are incremented post-hoc; concurrency is always decremented. Calling Commit twice is a no-op.
obs.Tokens is a map[string]int64. Per-meter increments are derived as:
- meter "tokens": sum of all values in obs.Tokens (backward compat)
- meter "tokens.<key>": obs.Tokens["<key>"]
- meter "requests": always 1 (counted at Reserve; not post-hoc)
- meter "concurrency": decremented (not incremented)
func (*Limiter) CommitBoth ¶ added in v0.9.8
func (l *Limiter) CommitBoth(ctx context.Context, a, b *Reservation, obs Observations) error
CommitBoth finalizes two reservations. When both are present and the runner supports batching, it commits them in a single round trip (one Redis pipeline) — the reservations live under different hash tags (inbound policy scope vs. upstream hostkey scope) so a single CROSSSLOT script is impossible; batching the two independent, individually-atomic commit scripts is the Cluster-safe way to collapse the two sequential round trips into one. Both reservations receive the same obs, matching the sequential Commit call pair.
Falls back to sequential Commit when only one reservation exists, when the runner is not batch-capable, or (returning joined errors) when a batched call fails. Semantics are byte-identical to two Commit calls because the per-reservation script and args are produced by the same builder.
type Observations ¶
Observations are passed to Commit to supply post-hoc measurements. Tokens is a map of token-type → count (e.g. {"input": 300, "output": 200}). Legacy callers that only have a total may pass {"tokens": total}. Nil Tokens is treated as all-zero.
type Reservation ¶
type Reservation struct {
ID string
// contains filtered or unexported fields
}
Reservation is returned by a successful Reserve call.
type Rule ¶
type Rule struct {
// Key is the caller-built scope identifier used as the last segment of
// Redis keys. Example: "Route:test-route:rl-basic".
// The pkg prepends "limit:{<scope>}:" and a strategy prefix.
Key string
// Name is for human-readable error messages only (e.g. "requests on rl-basic").
Name string
// Meter is "requests" | "tokens" | "tokens.<suffix>" | "concurrency".
Meter string
// Strategy controls the rate-limit algorithm.
Strategy Strategy
// Amount is the budget (requests, tokens, or concurrency slots).
Amount int64
// Window is the measurement period. For concurrency rules the window is
// used only to set key TTLs.
Window time.Duration
}
Rule is a single rate-limit rule passed to Reserve by the caller. The caller is responsible for building Rule.Key as a stable, unique string that identifies the (scope, rate-limit-name, meter) tuple.
type Strategy ¶
type Strategy string
Strategy names the rate-limiting algorithm for a Rule.
const ( // StrategyTokenBucket is the default. Tokens refill continuously at // amount/window rate; burst = amount. StrategyTokenBucket Strategy = "token-bucket" // StrategySlidingWindow uses a two-bucket weighted interpolation. StrategySlidingWindow Strategy = "sliding-window" // StrategyFixedWindow resets the counter at every floor(now/window) boundary. StrategyFixedWindow Strategy = "fixed-window" // StrategyLeakyBucket drains at a constant rate; excess is rejected. StrategyLeakyBucket Strategy = "leaky-bucket" // StrategySessionWindow anchors the window to the first request after a // reset; the window does not reset in the background. StrategySessionWindow Strategy = "session-window" )