Resilience for MoonBit without I/O: circuit breaking, load shedding, rate limits and deadlines.
| Answers | Refuses when | |
|---|---|---|
| Breaker | Is this backend likely to work? | a draw falls under a refusal fraction built from the last ten seconds of outcomes |
| Shedder | Should this server take on another call? | the CPU is over its threshold and more calls are in flight than the server has shown it can finish |
| Limiter | How many calls may be in flight, judging by how fast they come back? | in flight has reached a limit that AIMD, Vegas or Gradient moves with every call that finishes |
| Bucket | Is this call within a rate? | the token is spent; next says when the next one lands |
| Sliding | Is it within a quota over any window-long stretch? | the count reaches the quota |
| Period | Is it within a quota this period? | the count for the open period is spent |
| Gate | Is there room right now? | held has reached limit — it never asks what time it is |
| Chained | Does the call get past every limiter in a row? | any of them refuses; what the others gave is given back |
| Partitioned | Is this caller within its own allowance? | the limiter kept for its key refuses |
| Backoff | When to try again, and how long first? | more(attempts) is false |
| Deadline | How much of the budget is left? | expired is true |
| Window | What does the rolling window hold? | never; it is the array of buckets the others read |
let breaker = @moonward.Breaker::new()
let now = 0L
match breaker.allow(now, 0.42) {
Some(call) => call.pass(now) // or call.fail(now)
None => println("refused")
}w = max( k − (k − floor) · failing / size , floor )
drop = ( total − protection − w · accepts ) / ( total + 1 )
drop ×= ( size − working ) / sizelet breaker = @moonward.Breaker::new()
let clock = () => 0L
let answer = breaker.call(clock, 0.42, () => 7, fallback=_ => 0) catch { _ => -1 }
assert_eq(answer, 7)let limiter = @moonward.Limiter::new(rule=Gradient(@moonward.gradient))
match limiter.acquire(0L) {
Some(lease) => lease.pass(12_000_000L) // or lease.fail(now) on a timeout, lease.ignore() on a bad request
None => println("over the limit")
}
assert_eq(limiter.limit(), 20)| Rule | Moves the limit by |
|---|---|
| Aimd | one up while at least half of it is used, a tenth down on a drop or a call over the timeout |
| Vegas | how many calls are queued: the response time over the shortest one seen, which it measures again now and then |
| Gradient | how much slower the latest call is than the long-run average, plus a few slots to grow into |
| MinRtt | how much slower the latest call is than the fastest since the last probe; every probe calls or so it drops to the queue size and measures the fastest again |
| Fixed | never |
let bucket = @moonward.Bucket::new(10.0, burst=20.0)
let stored = bucket.opening(0L) // what a key that is not there starts as
let grant = bucket.step(stored, 0L)
if grant.granted {
// write grant.state back, if the row still holds `stored`
}
assert_eq(grant.state.tokens, 19.0)
assert_eq(grant.wait.nanos, 0L)let daily = @moonward.Period::new(1000, @moondate.Span::new(days=1L), align=true)
let counted = daily.step(daily.opening(), 0L)
assert_eq(counted.quota, Under)
assert_eq(counted.state.used, 1)let per_client = @moonward.Partitioned::new(_ => @moonward.Bucket::new(5.0, burst=10.0))
let both = @moonward.Chained::new([
@moonward.Bucket::new(100.0, burst=100.0),
@moonward.Gate::new(64),
])
assert_true(per_client.acquire("203.0.113.7", 0L))
assert_true(both.acquire(0L))burst bucket sliding period
0 21 20 40
1 21 20 40
2 21 20 40
3 21 20 40
five tokens in 200 ms| Preset | Values |
|---|---|
| sre | k 1.5 decaying to 1.1, protection 5, a probe after 1 s, 40 buckets over 10 s |
| shed | 50 buckets over 5 s, CPU threshold 900‰, 1 s assumed response time, average weighted 0.9, 1 s to cool, never under 0.1 of capacity |
| backoff | 3 attempts, 100 ms doubling to a 1 s cap, each wait spread by a fifth either way |
| aimd | start at 20 within 20 and 200, keep 0.9 on a drop, a call over 5 s is a drop |
| vegas | start at 20, at most 1000, no smoothing, alpha 3, beta 6, threshold and step 1, probe 30 |
| gradient | start at 20 within 20 and 200, smoothing 0.2, 4 slots on top, a 600-call average, tolerance 1.5 |
| Program | What it shows |
|---|---|
| src/examples/minimal | one breaker, one failing backend, four lines of output — the smallest thing that works |
| src/examples/limiters | the same bursty traffic through all three quotas |
| src/examples/tour | a breaker under an outage, a shedder under rising load, one step through each limiter |
| src/examples/guide | the guide as one program, in the order you read it |
pub(open) trait Permit {
fn acquire(Self, now : Int64) -> Bool
fn release(Self, now : Int64) -> Unit
}pub(all) suberror Unavailablew = max(k - (k - floor) × failing / size, floor)
drop = (total - protection - w × accepts) / (total + 1)
drop ×= (size - working) / sizepub struct Deadline {
start : Int64
budget : Int64
}pub struct Gate {
limit : Int
held : Int
}| Rigid | the computed delay, unchanged. Reproducible, and it synchronises clients |
| Full | uniform over [0, cap]. The one that spreads clients furthest |
| Equal | half the cap plus uniform over [0, cap/2]. Keeps a floor under the wait |
| Decorrelated | uniform over [base, previous × 3]. Grows from where it last landed rather than from the attempt number |
| Spread(f) | uniform over [cap × (1 − f), cap × (1 + f)], f held to [0, 1]. So the wait can pass the cap by f |
let partitions : IntInstall
Download zipResilience for MoonBit without I/O: circuit breaking, load shedding, rate limits and deadlines.