Retry & CircuitBreaker
Resilience helpers for flaky outbound calls: exponential-backoff retries in Soli, and a per-name circuit breaker with a process-global state machine.
Retry.with_backoff(block, opts?)
Runs the block up to attempts times, sleeping between attempts with exponential backoff. Re-raises the last error when every attempt fails.
| Method | Description |
|---|---|
attempts | Total tries including the first (default 3). |
base_delay | Seconds before the first retry (default 0.5). |
max_delay | Backoff cap in seconds (default 8). |
factor | Multiplier per attempt (default 2). |
let body = Retry.with_backoff(fn() {
HTTP.post_json(webhook_url, payload)
}, { "attempts": 3, "base_delay": 0.5 });
Retry.within(block, opts?)
Keep retrying until a deadline passes rather than a fixed count — useful for boot work that must succeed within N seconds.
| Method | Description |
|---|---|
deadline | Give up after this many seconds (default 10). |
Retry.within(fn() { Solidb.ping() }, { "deadline": 10 });
CircuitBreaker state machine
After threshold consecutive failures the named circuit trips open and refuses calls for reset_after seconds — an Int, or a Float for sub-second cool-downs — then allows one probe (half-open). Concurrent callers are refused until that probe reports back, and a probe that never reports is retried after another reset_after. A failed probe re-opens it immediately; a success closes it and resets the count.
| Method | Description |
|---|---|
allow(name) | True when a call may proceed. |
record_success(name) | Reset failures and close. |
record_failure(name) | Count a failure; trips at the threshold. |
state(name) | "closed", "open", or "half_open". |
configure(name, opts) | {"threshold": 5, "reset_after": 30} defaults. |
reset(name) | Forget all state (ops/testing). |
if CircuitBreaker.allow("stripe") {
match HTTP.post_json(url, body) rescue null {
null => CircuitBreaker.record_failure("stripe"),
r => { CircuitBreaker.record_success("stripe"); return r; }
}
} else {
return fallback_response();
}
Scope
State is process-global: every worker thread and both engines see the same circuit. Single-process by design — cross-process coordination belongs to the job system’s atomic claiming.
CircuitBreaker.configure("stripe", { "threshold": 3, "reset_after": 60 });
CircuitBreaker.state("stripe"); # "closed"