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 ¶
const ( BucketPublish = "publish" BucketSubscribe = "subscribe" BucketTokenIssue = "token-issue" )
Bucket names used in keys and metrics. Exported so call sites stay consistent.
Variables ¶
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.
type Limiter ¶
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 ¶
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 ¶
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.