moonzero · control plane v0.15.0 · 49 packages · 4 targets · Apache-2.0 docs / resilience / ratelimit GitHub

03 / RESILIENCE

ratelimit

A token-bucket limiter over moonward's bucket, on the millisecond clock, and the rate_limit middleware, which answers 429 when it is empty.

struct TokenBucket

structA token-bucket rate limiter kept in this process: the bucket holds up to burst tokens and refills continuously at rate a second, and each admitted request spends one. Every decision is a function of the state and now, so the limiter is testable without a real clock. The bucket is local, so N replicas admit N times the rate. RedisTokenLimit is the same limiter with its bucket in redis and is what a fleet should run; this one serves a single process, and is what RedisTokenLimit itself falls back to when redis is unreachable.

fn TokenBucket::new( rate : Double, burst~ : Double = -1.0, now~ : Int64 = 0) -> TokenBucket

fnBuild a bucket admitting rate requests per second on average with room for a burst of that many back-to-back (default burst = rate). It starts full at time now, in milliseconds. The bucket is moonward's; this keeps shape and its millisecond clock. A capacity below one token is raised to one, because a bucket that can never hold a whole token admits nothing at all — which is what a fractional rate with no burst used to give.

fn TokenBucket::allow(self : TokenBucket, now : Int64) -> Bool

fnTry to admit one request at time now. Returns true when admitted, false when the bucket is empty.

fn TokenBucket::allow_n( self : TokenBucket, n : Double, now : Int64) -> Bool

fnTry to admit a request costing n tokens at time now. Returns false (spending nothing) when fewer than n tokens are available.

fn TokenBucket::available(self : TokenBucket, now : Int64) -> Double

fnThe (fractional) number of tokens currently available at now. Useful for metrics and tests.

let token_script : String =

letcore/limit/tokenscript.lua, verbatim: read the bucket and the second it was last touched, refill by the elapsed seconds capped at capacity, spend requested if that many are there, and write both back under a TTL of two fill times. Public so a caller can pre-load it, and so a redis double can evaluate the very text the limiter ships.

struct RedisTokenLimit

structA token-bucket rate limiter whose bucket lives in redis. The token count and the second it was last refilled sit under {key}.tokens and {key}.ts, so every replica pointed at the same redis and key spends from one bucket. When redis cannot be reached the limiter keeps limiting from rescue, its process-local TokenBucket, the way drops to its in-process rescueLimiter rather than letting an outage open the gate.

fn RedisTokenLimit::new( client : @moonredis.Client, rate~ : Int32, burst~ : Int32, key~ : String) -> RedisTokenLimit

fnA limiter admitting rate requests per second with room for a burst of that many back-to-back, over the bucket key names in client's redis.

fn RedisTokenLimit::rescue(self : RedisTokenLimit) -> TokenBucket

fnThe process-local bucket this limiter falls back to, so a caller can inspect it.

async fn RedisTokenLimit::allow( self : RedisTokenLimit, now_ms : Int64) -> Bool

itemTry to admit one request at now_ms.

async fn RedisTokenLimit::allow_n( self : RedisTokenLimit, n : Int32, now_ms : Int64) -> Bool

itemTry to admit a request costing n tokens at now_ms. The script is handed whole seconds because hands it now.Unix(): the shared bucket refills at one-second granularity however finely the clock is read. Lua's false arrives as a null reply and its true as 1; anything else, and any redis failure, is served by the local bucket instead.

fn RedisTokenLimit::is_alive(self : RedisTokenLimit) -> Bool

fnWhether this limiter is drawing on the shared bucket. false means it is on its local one, waiting for the probe to find redis again.

fn RedisTokenLimit::mark_down(self : RedisTokenLimit) -> Unit

fnMark redis as unreachable without waiting for a call to fail — what a liveness probe does when it sees the connection drop.

fn RedisTokenLimit::mark_alive(self : RedisTokenLimit) -> Unit

fnMark redis as answering again. The shared bucket still holds whatever the last replica left in it, so the limiter picks up the fleet's quota rather than a fresh one.

fn RedisTokenLimit::probing(self : RedisTokenLimit) -> Bool

fnWhether a background probe is running.

async fn RedisTokenLimit::probe(self : RedisTokenLimit) -> Bool

itemPing redis once and switch the limiter back to the shared bucket when it answers. The reading is the caller's: an unreachable redis is not an error here, it is the reason the local bucket exists.

async fn RedisTokenLimit::probe_forever( self : RedisTokenLimit, interval_ms~ : Int32 = 100, report~ : (Bool) -> Unit = _alive => ()) -> Unit

itemProbe every interval_ms until cancelled, and hand each reading to report. This is the background half of the liveness model: the limiter marks itself down the moment a call fails, and this loop is what brings it back. It is a function rather than a task the limiter starts for itself, so where the task belongs — which group, which lifetime — stays the caller's decision.