ratelimit

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 5, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package ratelimit gates Parsec's publish/subscribe/refresh-token paths behind configurable per-key budgets.

Two backends are shipped:

  • MemoryLimiter — process-local sliding window. Used when Parsec runs single-node (no Redis). Burst capacity is honored.
  • RedisLimiter — cross-node sliding window backed by Redis sorted sets and a single-round-trip Lua script. Used when Options.RedisClient is set so two parsec processes share the same budget.

Both implement the Limiter interface. Decisions surface as (allowed, remaining, reset) so callers can stamp a Retry-After header.

Index

Constants

View Source
const (
	BucketPublish    = "publish"
	BucketSubscribe  = "subscribe"
	BucketTokenIssue = "token-issue"
)

Bucket names used in keys and metrics. Exported so call sites stay consistent.

Variables

View Source
var AllowDecisionUnlimited = Decision{Allowed: true, Remaining: -1, Reset: 0}

AllowDecisionUnlimited is the canonical "unlimited" decision returned by both limiters when the configured Limit has Rate == 0.

Functions

This section is empty.

Types

type Decision

type Decision struct {
	// Allowed reports whether the budget had room for n events.
	Allowed bool
	// Remaining is the budget left in the current window AFTER this call.
	// It is approximate for sliding-window limiters.
	Remaining int
	// Reset is the time until the oldest event in the window expires —
	// callers may surface this as the HTTP Retry-After header on 429.
	Reset time.Duration
}

Decision describes the outcome of a single Allow call.

type Limit

type Limit struct {
	// Rate is the steady-state event budget over Per. Zero means unlimited.
	Rate int `json:"rate,omitempty"`
	// Per is the rolling window. Defaults to one second when Rate > 0.
	Per time.Duration `json:"per,omitempty"`
	// Burst is the instantaneous peak budget. Zero means Burst=Rate (no
	// peak above the steady-state). The effective ceiling at any instant
	// is max(Rate, Burst).
	Burst int `json:"burst,omitempty"`
}

Limit describes a single budget: at most Rate events over the rolling window Per, with an instantaneous Burst peak. The zero Limit (Rate=0) means "unlimited" — the limiter returns Allowed=true without touching state.

func (Limit) Normalize

func (l Limit) Normalize() Limit

Normalize fills derived defaults so callers can rely on coherent values. Per defaults to one second when Rate > 0; Burst defaults to Rate. Negative values are clamped to zero.

func (Limit) Unlimited

func (l Limit) Unlimited() bool

Unlimited reports whether l disables rate limiting (Rate == 0).

type Limiter

type Limiter interface {
	Allow(ctx context.Context, key string, n int) (Decision, error)
}

Limiter is the per-key budget gate every gated surface calls.

Allow returns Decision.Allowed=true when the caller may proceed and false when the key has exhausted its budget. n is the number of events being charged on this call (1 for every current callsite; reserved for future batch APIs). The Decision.Reset advises the caller how long to wait before the next attempt.

type MemoryLimiter

type MemoryLimiter struct {
	// contains filtered or unexported fields
}

MemoryLimiter is a per-process sliding-window limiter. Each key gets a ring of event timestamps; entries older than the window are dropped on every check. It is the default when Parsec runs without Redis.

MemoryLimiter is safe for concurrent use. The store grows with the active-key cardinality — call Sweep periodically (or rely on the implicit drop-on-touch) to keep the map bounded. For deployments expecting many distinct keys, prefer RedisLimiter.

func NewMemoryLimiter

func NewMemoryLimiter(limit Limit) *MemoryLimiter

NewMemoryLimiter constructs a MemoryLimiter for the given Limit. A zero-Rate limit yields a limiter that returns Allowed=true without recording state.

func (*MemoryLimiter) Allow

func (m *MemoryLimiter) Allow(ctx context.Context, key string, n int) (Decision, error)

Allow charges n events against key's budget using the limiter's default Limit. See AllowWithLimit for the override-capable variant.

func (*MemoryLimiter) AllowWithLimit

func (m *MemoryLimiter) AllowWithLimit(ctx context.Context, key string, n int, lim Limit) (Decision, error)

AllowWithLimit charges n events against key's budget using lim. When lim is unlimited (Rate == 0) the call returns Allowed=true without touching state. Callers that want to apply a per-token override should call this directly with the override's Limit.

The same bucket store is shared regardless of lim; the effective ceiling is whichever Limit the caller passes. This matches the redis limiter's behaviour, where the Lua script accepts max_count from ARGV on every call.

func (*MemoryLimiter) SetClock

func (m *MemoryLimiter) SetClock(c func() time.Time)

SetClock overrides the time source. Used in tests for deterministic window math.

func (*MemoryLimiter) Sweep

func (m *MemoryLimiter) Sweep() int

Sweep removes empty bucket entries. Safe to call from a background goroutine. Returns the number of entries removed.

type RateLimits

type RateLimits struct {
	// Publish caps the number of publish RPCs per token subject.
	Publish Limit `json:"publish,omitempty"`
	// Subscribe caps the number of subscribe attempts per client identity
	// (user id or IP for anonymous traffic).
	Subscribe Limit `json:"subscribe,omitempty"`
	// TokenIssue caps RefreshToken RPC attempts per remote IP. Protects
	// the token endpoint from credential-stuffing.
	TokenIssue Limit `json:"token_issue,omitempty"`
}

RateLimits is the per-bucket policy bundle. Each Limit applies to a distinct "bucket" so a publish-heavy token does not eat into the subscribe budget.

func (RateLimits) Empty

func (rl RateLimits) Empty() bool

Empty reports whether every limit is unlimited.

func (RateLimits) MarshalJSON

func (rl RateLimits) MarshalJSON() ([]byte, error)

MarshalJSON serializes RateLimits in an operator-friendly format — durations are surfaced as strings (e.g. "1s") so manifest output is readable.

func (RateLimits) Normalize

func (rl RateLimits) Normalize() RateLimits

Normalize returns rl with every Limit normalized.

type RedisLimiter

type RedisLimiter struct {
	// contains filtered or unexported fields
}

RedisLimiter is the cross-node sliding-window backend. Multiple Parsec nodes that share a Redis instance share the same budget when they use the same KeyPrefix.

The Lua script is loaded once (EVALSHA) and re-loaded on NOSCRIPT.

func NewRedisLimiter

func NewRedisLimiter(client redis.UniversalClient, limit Limit) *RedisLimiter

NewRedisLimiter constructs a RedisLimiter backed by client. keyPrefix defaults to "parsec" when empty so it matches the rest of the Parsec Redis namespace.

func (*RedisLimiter) Allow

func (r *RedisLimiter) Allow(ctx context.Context, key string, n int) (Decision, error)

Allow runs the sliding-window Lua script atomically on the Redis node holding KEYS[1]. The bucket label is encoded into the key so distinct surfaces (publish/subscribe/token-issue) cannot crowd each other.

The key format is `<prefix>:rl:<bucket>:<subject>`. The bucket label is extracted from the leading prefix of key; if key contains a ":" the portion before the first ":" is the bucket. Callers that do not encode a bucket get "default".

func (*RedisLimiter) AllowWithLimit

func (r *RedisLimiter) AllowWithLimit(ctx context.Context, key string, n int, lim Limit) (Decision, error)

AllowWithLimit is Allow with a caller-supplied Limit. The same Redis keyspace is used regardless of lim; the Lua script reads max_count from ARGV on every call so per-token overrides work without reconstructing the limiter.

func (*RedisLimiter) Limit

func (r *RedisLimiter) Limit() Limit

Limit returns the configured Limit.

func (*RedisLimiter) WithKeyPrefix

func (r *RedisLimiter) WithKeyPrefix(p string) *RedisLimiter

WithKeyPrefix overrides the Redis key prefix.

Jump to

Keyboard shortcuts

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