ratelimit

package
v0.9.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

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

View Source
var ErrExceeded = errors.New("limit: budget exceeded")

ErrExceeded is the sentinel wrapped by *KeyQuotaExhausted.

Functions

func RegisterScripts

func RegisterScripts(m *kv.Mem)

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

type KeyQuotaExhausted struct {
	Rule       Rule
	RetryAfter time.Duration
}

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 New

func New(s kv.Store, log *slog.Logger, clock func() time.Time) *Limiter

New creates a Limiter. If clock is nil, time.Now is used.

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.

func (*Limiter) Reserve

func (l *Limiter) Reserve(ctx context.Context, scope string, rules []Rule) (*Reservation, error)

Reserve checks all rules and increments counters atomically via one RunScript call. On violation, all increments from this call are rolled back and *ExceededError is returned. scope is the Redis Cluster hash-tag anchor (e.g. "policy:prod-policy").

type Observations

type Observations struct {
	Tokens    map[string]int64
	Cancelled bool
}

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"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL