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 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.
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"`
// 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 ¶
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.