Runtime Resilience
Production services need more than happy-path effects. Timeouts, overload, and flaky dependencies are normal. This chapter covers coordination primitives and the id_effect_resilience crate for keeping programs responsive under stress.
What This Chapter Covers
RequestResolver— batch parallel lookups through a single fetchSubscriptionRef— shared state plus change notificationsRedacted— schema-layer secrets with maskedDebugmatch_effect!— enum match helper with path-prefixed armsid_effect_resilience— circuit breaker, rate limiter, bulkhead, hedged requests
RequestResolver
Effect.ts batches data-source lookups so N parallel getUser(id) calls become one SQL WHERE id IN (…). In id_effect, each pending lookup is a [RequestEntry] with a [Deferred] result slot. A [RequestResolver::run_all] receives sequential batches; entries inside a batch may run in parallel.
batching deduplicates keys per batch and calls your fetch function once:
#![allow(unused)] fn main() { use id_effect::{Deferred, RequestEntry, batching, run_async}; let resolver = batching(|keys| Effect::new(move |_r| { // build HashMap from keys … Ok(map) })); }
SubscriptionRef
[SubscriptionRef] combines [Ref] with [PubSub]. Every set / update publishes the new value; subscribe returns a [Queue] that receives the current value first, then every subsequent change.
#![allow(unused)] fn main() { use id_effect::{Scope, SubscriptionRef, run_async}; let cell = run_async(SubscriptionRef::make(0u32), ()).await?; let scope = Scope::make(); let changes = run_async(cell.subscribe(), scope.clone()).await?; }
Redacted values
Use [Redacted<T>] anywhere a schema or domain type might reach logs. Debug and Display print <redacted>; call expose only at trust boundaries.
match_effect!
The match_effect! proc macro prefixes variant names so the compiler checks exhaustiveness without repeating the enum path:
#![allow(unused)] fn main() { use id_effect::match_effect; match_effect!(Color, paint, { Red(n) => n, Green => 0, Blue => 1, }) }
id_effect_resilience
Add id_effect_resilience to your workspace dependency when you need operational guardrails:
| Type | Role |
|---|---|
[CircuitBreaker] | Fail fast after repeated errors; half-open probe after cooldown |
[RateLimiter] | Token-bucket admission control |
[Bulkhead] | Cap concurrent in-flight effects via semaphore |
[hedged] | Race a delayed backup effect against a primary |
#![allow(unused)] fn main() { use id_effect_resilience::{CircuitBreaker, hedged}; use id_effect::run_async; let breaker = run_async(CircuitBreaker::make(5, Duration::from_secs(30)), ()).await?; let out = run_async(breaker.call(fetch_user(id)), ()).await?; }
Pair resilience primitives with Schedule retry policies from Part III — breakers shed load, schedules handle transient faults.