Skip to content

core(actor): rebuild Actor trait, lifecycle hooks & run-loop (#116)#142

Merged
joeldsouzax merged 21 commits into
mainfrom
core/116-actor-trait-loop
Jul 6, 2026
Merged

core(actor): rebuild Actor trait, lifecycle hooks & run-loop (#116)#142
joeldsouzax merged 21 commits into
mainfrom
core/116-actor-trait-loop

Conversation

@joeldsouzax

@joeldsouzax joeldsouzax commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Closes #116.

Implements #116 — the local actor spine in bombay-core: the Actor trait, its lifecycle hooks, the run-loop that drives it, a minimal ActorRef scaffold, and the spawn entry points. Send-saturated (MaybeSend #9 deferred); rebuilt from the kameo reference, stripped of the supervision/links/console entanglement that belongs to later cards.

Spec: docs/superpowers/specs/2026-07-05-116-actor-trait-loop-design.md · Plan: docs/superpowers/plans/2026-07-05-116-actor-trait-loop.md

Public surface (bombay-core)

  • Actor: Mailboxed<Msg: Msg>on_start / handle / on_panic (infallible, stop-only) / on_stop (Err logged, never unwrapped).
  • Run-loop: on_startloop { CancellationToken::run_until_cancelled(recv) }on_stop, four catch_unwind sites.
  • ActorRef / WeakActorRef (minimal scaffold: id + mailbox + cancel + abort), PreparedActor, RunResult (Stopped / StartupFailed / Killed), Spawn ext-trait.
  • PanicError::from_panic_any added to error.rs.

Design decisions (some diverge from the card's original wording — all recorded on the issue)

  • No startup buffer / no VecDequeawait on_start fully; the bounded flume mailbox is the FIFO buffer (a side queue would be unbounded, contradicting the mailbox card).
  • run_until_cancelled instead of select! — a plain match that wraps only the receive, so an in-flight handler always finishes before a stop is observed (finish-current-then-stop, no drain).
  • &mut self kept, poisoned-on-panic — handler panic caught → on_panicon_stop still runs with Panicked on the torn state (resource-release only) → discard. No resume. A returned Err routes to Panicked(HandlerPanic) too.
  • Hard kill() = futures::Abortable around the whole lifecycle (uniform for run + spawn) → RunResult::Killed, skips on_stop.

Testing — verified to the actor-system bar, not just coverage

  • Behavioral (TDD): trait, loop, hooks, spawn — red→green per commit.
  • Mutation (cargo-mutants): zero survivors on all core(actor): rebuild from kameo reference — Actor trait, lifecycle hooks, run-loop #116 code. 10 real survivors found and killed — including stop/kill mutants that hung rather than failed (now fail-fast via timeouts). One equivalent mutant (stderr-only diagnostic) excluded with justification in .cargo/mutants.toml.
  • DST (tests/dst_races.rs): 8 deterministic stop/cancel/kill/startup race tests, deterministic across 10 runs.
  • Direct invariant suite (tests/invariants.rs), I1–I23 — every invariant measured, none violated: single-writer mutual exclusion (max_concurrent == 1 under 8 concurrent senders), macro-step atomicity, no-reentrancy/self-send-queued, FIFO + exactly-once, lifecycle order + exactly-once hooks, panic containment + stop-reason fidelity, fault isolation (one crash doesn't touch another actor or the runtime), alive-window, send-to-dead-fails, backpressure + capacity-freed-by-draining, termination/no-hang.
  • 90 tests total. Full nix flake check green (clippy at the god-level all+pedantic+nursery deny bar, fmt, nextest, doc, doctest, deny, typos).

Deferred invariants — filed, not lost

The full actor-system invariant catalog is posted on #116; the invariants whose machinery doesn't exist yet are cross-linked to their cards, each marked "must land as a direct test when built": supervision/restart/death-watch → #120/#8, ref-count-stop/Recipient#117, AID uniqueness/addressability → #121, passivation → #20, durability/rehydrate → #12, remote/location-transparency → #67.

Carry-forwards (non-blocking, filed on cards)

Test plan

…ine (#116)

Scoped cargo-mutants over the #116 spine (kind.rs, spawn.rs, actor_ref.rs,
mod.rs) surfaced 7 missed + 3 timeout mutants. Add/strengthen tests to kill
each; no production behavior changed.

Missed (now caught):
- Actor::name default stubbed to ""/"xyzzy" -> assert name() contains the
  concrete type name.
- Debug impls (ActorRef, WeakActorRef, RunResult, PreparedActor) stubbed to an
  empty formatter -> assert the rendered debug names the struct/variant.

Timeouts (now caught, converted to fail-fast): stop/kill/stop-flag mutants
turned hangs into 20s harness timeouts because tests that rely on
stop()/kill()/`*stop` to terminate would park forever. Bound every such await
(cancel_finishes, messages_during_on_start, stop_flag, kill) in a 5s
tokio::time::timeout, and add stop_terminates_idle_actor, so the mutant fails
fast instead of hanging.

log_on_stop_outcome is diagnostic-only (eprintln, no state) and its
"replace with ()" mutant is unobservable without capturing stderr; skipped via
.cargo/mutants.toml exclude_re (config-file equivalent of #[mutants::skip],
avoiding a no-op production dependency on the `mutants` crate).

Scoped mutants: 0 missed / 0 timeout on the four actor files and on error.rs
(from_panic_any covered).
@joeldsouzax joeldsouzax enabled auto-merge July 6, 2026 12:26
@joeldsouzax joeldsouzax merged commit 50d3e44 into main Jul 6, 2026
6 checks passed
@joeldsouzax joeldsouzax deleted the core/116-actor-trait-loop branch July 6, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core(actor): rebuild from kameo reference — Actor trait, lifecycle hooks, run-loop

1 participant