ratelimit

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 12 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 ChannelRule added in v0.3.0

type ChannelRule struct {
	// Pattern is the compiled channel-name matcher. See
	// channels.ParsePattern for the grammar.
	Pattern channels.Pattern `json:"-"`
	// Raw is the source string of Pattern, preserved so the manifest
	// and access log can identify which rule matched.
	Raw string `json:"pattern"`
	// Limit is the budget that applies when Pattern matches.
	Limit Limit `json:"limit"`
}

ChannelRule binds a channel-name pattern to a Limit. Used by the publish gate to apply tighter (or looser) budgets on a per-channel basis: a hot channel like "public:metrics.heartbeat" can carry a higher ceiling than the default while a sensitive one like "private:admin.broadcast" can be locked down to a few events per minute.

func CompileChannelRules added in v0.3.0

func CompileChannelRules(raw map[string]Limit) ([]ChannelRule, error)

CompileChannelRules parses raw[pattern]=limit map entries into compiled rules sorted most-specific first (more literal segments win; ties broken by raw string ascending). Invalid patterns or negative-rate limits return an error.

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"`
	// PerChannelPublish is the operator-configured override of Publish
	// for channels matching a pattern. Compiled by parsec.New; the
	// dispatcher picks the most-specific matching rule per call. Empty
	// = no overrides, every channel falls through to Publish.
	PerChannelPublish []ChannelRule `json:"per_channel_publish,omitempty"`
	// PerChannelSubscribe mirrors PerChannelPublish for the subscribe
	// bucket: a channel-name pattern can carry a tighter (or looser)
	// per-subject subscribe budget than the global default. Empty = no
	// overrides, every subscribe attempt falls through to Subscribe.
	PerChannelSubscribe []ChannelRule `json:"per_channel_subscribe,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) MatchPublish added in v0.3.0

func (rl RateLimits) MatchPublish(channel channels.Name) (Limit, string)

MatchPublish returns the Limit + matched rule's raw string for channel, or (Publish, "") when no per-channel rule matches.

func (RateLimits) MatchSubscribe added in v0.3.0

func (rl RateLimits) MatchSubscribe(channel channels.Name) (Limit, string)

MatchSubscribe returns the Limit + matched rule's raw string for channel, or (Subscribe, "") when no per-channel rule matches. The rule list is pre-sorted most-specific first so the first match wins.

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