Documentation
¶
Overview ¶
Package ratelimit implements the global upstream token bucket gating outbound calls to Rivian's GraphQL gateway.
Why a token bucket and not a leaky bucket / fixed window:
- Bursty work is fine: a UI page that fires 4 parallel queries should sail through the bucket if there's budget, not get slow-walked by a leaky bucket.
- 429s come in waves; we want to absorb the wave with our budget then refill, not run a tight rolling window that punishes the recovery as much as the spike.
Why Redis and not in-process: at N>1 pods, every pod independently classifying its own outbound rate produces N× the real rate against Rivian. A shared bucket forces the whole fleet to honour a single budget.
Storage shape: one Redis hash per class (main + priority) with fields {tokens, ts}. The check-and-decrement runs as a Lua script so the whole "read → refill based on elapsed → maybe decrement → write" sequence is atomic with no client-side round trips.
Two classes:
- "priority" — user-initiated calls that block the UI (Login, RefreshToken, getUserInformation on first paint). Smaller bucket, refilled separately so background work can never starve a logged-in human.
- "main" — everything else (state polls, vehicle metadata, subscriptions' REST seeds). Bigger bucket; absorbs reconnect storms.
Allow returns (allowed, retryAfter). When allowed is false, retryAfter is the time until at least one token will be available, suitable for an HTTP Retry-After header. Callers should respect this — we'll plumb the breaker / api 503 path to use it in the next change.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Class ¶
type Class string
Class is the token-bucket selector. Stable on the wire — the strings appear in Redis keys and Prometheus labels.
const ( // ClassMain is the default for unattended, retry-friendly // upstream calls (state polls, REST seeds, metadata). ClassMain Class = "main" // ClassPriority is for user-blocking calls (Login, // RefreshToken). Reserved budget so a reconnect storm in // ClassMain can't lock new users out. ClassPriority Class = "priority" )
type Config ¶
type Config struct {
MainCapacity int
MainRefillPerSec float64
PriorityCapacity int
PriorityRefillPerSec float64
// KeyPrefix lets multiple rivolt deployments share one Redis.
// Defaults to "rivolt:rl".
KeyPrefix string
// Now is injectable for tests; defaults to time.Now.
Now func() time.Time
}
Config holds bucket parameters per class. Capacity is the burst allowance; RefillPerSec tunes steady-state throughput.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns sane defaults sized for a single-region deployment against Rivian's gateway. Tuned conservatively — the breaker still trips on bursts that exceed these.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter is the public type. Build with New, call Allow on every outbound Rivian call. Safe for concurrent use; all state lives in Redis.
func New ¶
New constructs a Limiter wrapping rdb. Pings Redis up to ctx's deadline and returns an error if the cluster is unreachable — callers can decide whether that's fatal at boot or whether to fall back to a no-op limiter.
func (*Limiter) Allow ¶
Allow attempts to take one token from the named class's bucket. Returns (true, 0) on success and (false, retryAfter) when out of budget. retryAfter is the time until at least one token would be available; suitable for HTTP Retry-After.
On Redis errors Allow fail-opens (returns true) and logs at warn — a Redis blip should never black-hole the entire upstream path. The breaker still gates if Rivian responds with 429s.
type Retrying ¶ added in v0.37.6
type Retrying struct {
// contains filtered or unexported fields
}
Retrying is a Limiter that may not exist yet. Redis can be unreachable when the process boots (a rollout that starts Redis and the app together); the limiter used to be disabled for the life of the process in that case, so a later rollout fired Rivian calls with no global budget at all. Retrying keeps trying in the background and starts limiting as soon as Redis answers. Until then, like a Redis error in Allow, it fails open.
func NewRetrying ¶ added in v0.37.6
func NewRetrying(ctx context.Context, rdb *redis.Client, cfg Config, logger *slog.Logger, pingTimeout, retryEvery time.Duration, onReady func()) *Retrying
NewRetrying tries New once (bounded by pingTimeout) and, if that fails, keeps trying every retryEvery in the background until it works or ctx is done. onReady, when set, is called once when the connection is made later.