Skip to content

Lifecycle — Service component (non-serving, resident)

A long-lived, stateful process — a #[rusm_rs::service] holding state (a counter, a cache, a pub/sub hub), reached via whereis + call/send. This is where "resident vs per-call" lives — never in the serving tier. See the overview for the shared two-domain model and failure vocabulary.

Shape (what you write)

rust
#[rusm_rs::service]
pub mod counter {
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNT: AtomicU64 = AtomicU64::new(0); // state the loop owns
    // the macro writes the receive → dispatch → reply loop
    pub fn bump(by: u64) -> u64 { COUNT.fetch_add(by, Ordering::Relaxed) + by }
    pub fn total() -> u64 { COUNT.load(Ordering::Relaxed) }
}
ts
// counter.ts — each exported function is a call; module scope holds the state.
let count = 0;
export function bump(by: number): number { count += by; return count; }
export function total(): number { return count; }

Declared as a [components.<name>] entry (with resident = true to be boot-spawned and supervised), spawned when the node starts, and addressed by name. A sibling calls it through the generated typed client — spawn<Counter>("counter") then await counter.bump(1) — and the cross-process round-trip reads like a local call (Rust and TypeScript interoperate over one wire).

Platform owns / you write

  • Platform owns: spawning it at node start, the mailbox, reply routing for each call (matching a reply to its request), the supervisor + restart policy, and parking the fiber on receive between requests.
  • You write: the handler functions and the state they own (the macro writes the serve() dispatch loop).

Lifecycle events

EventPlatform domainApplication domainResult
Normal runblocks the fiber on receive between requests; routes each call's reply back to its callerhandles calls/casts, mutates its own stateserves until shutdown
Graceful shutdownsignals shutdown; lets the loop drainfinishes in-flight work, returnsclean stop
Crash (trap)Crashed → the supervisor restarts it with fresh state (per intensity); monitors get Down, links get an exit signalthe panic! / .unwrap()restarted (state reset); a sibling's in-flight call fails — its reply ref is never answered, so the caller's receive-with-timeout returns an error rather than hanging
Memory crash (OOM)the StoreLimiter cap trips a trap → Crashed → restartexceeded max-memory-mbrestarted; state reset
Restart stormrestart-intensity window exceeded → the supervisor escalates (gives up) rather than crash-loop(a persistent bug)the failure surfaces upward
Kill (explicit or via a link)abort handle fires; the signal rides the mailboxreclaimed (restarted if supervised)

Notes

  • State resets on restart — by design. "Let it crash": a supervised service comes back clean rather than limping on corrupt state. If state must survive a restart, put it in durable kv, not the process heap.
  • A call to a dead service fails, it doesn't hang. The reply ref is never answered, so a caller using call-with-timeout gets a clear error and can react — back-pressure for failure, not a silent stall.
  • The home for shared state. HTTP, SSE, and WebSocket components are stateless and disposable; anything they need to share (a cache, a broker, a session map) lives in a service like this and is reached by message.

Prev: Worker component · Overview: Component lifecycles

MIT licensed