The branch was 17 ahead / 25 behind with 11 conflicting files, and drifting further: db.c had been rewritten twice on master since (group commit, then compaction). Resolved rather than rebased so both histories stay legible. Conflicts, and how each was settled: - db.c: BOTH semantics kept. Master's fatal path and compaction check now sit behind the branch's `table_is_durable` predicate, in all three inline arms — a volatile table reaches neither the barrier nor the compaction check - db-bench sample: every mode from both sides (growth, growth-verify, randread, replayseed, wmix) and ONE `boot` mode, which both sides had added independently - db-bench.py: all six legs kept. Both sides had also grown the same WAL-size helper under different names; collapsed into one - perf-targets: the branch's §5 (RAM ceiling) then master's §6/§7 — master's numbering had already assumed a §5 it did not have - story frontmatter: master's `status` (the landing truth) plus the branch's `readiness` axis. 03 would have read `done` + `refine`, which is a contradiction — it was brainstormed and landed on master, so `ready` - board: both standup blocks newest-first; master's chain rows (a superset); the branch's databasev2 1-2 rows with master's 3-4. Fixed a stray `|` in master's row 3 - baseline: master's, then REGENERATED from a full campaign — 143 metrics, 132 checks, 0 failures with both sides' legs present TWO HALF-EXPOSED FEATURES FIXED, because the merge rule is that master gets no feature that is honoured in name only: - `resident: keys` PARSED, set a .wob flag, and did nothing: rows stayed fully resident. A developer could declare a 120 GB table keys-resident, watch it compile, and be OOM-killed. The loader now REFUSES it with a message naming what to write instead, until tasks 5c/5d land. The compiler still parses it and its AST golden still passes, so the grammar work stays tested - `durable: false` was honoured ONLY on the inline path. wo_db_exec_req had no guard at all, so a volatile table written from an actor on a worker shard would still be logged — precisely porch's session-table case, and precisely what iteration 2 exists to provide. All three request-path arms now carry the same predicate. Found by reading the merged code, not by a test: the obvious probe runs main() on the primary and therefore only exercises the inline path Verified on the merged tree: wovm-test 0, woc-test 0, oop-e2e 122/0, residency-accept 8/0, db-bench 132/0, linkcheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
129 lines
5.8 KiB
Markdown
129 lines
5.8 KiB
Markdown
---
|
|
iteration: "31"
|
|
status: done
|
|
readiness: ready
|
|
chain: 3
|
|
---
|
|
|
|
# Iteration 31 — actor lifecycle: request/response, backpressure, death, timers
|
|
|
|
> Format: `product/story-iteration-template`. Part of
|
|
> [Story — one language, one runtime, one database, one binary](00-story.md).
|
|
>
|
|
> **Inserted 2026-08-21** (concurrency-chain re-sequence; the iteration
|
|
> was named as "new 31" in the 2026-08-20 code-review re-sequence — this
|
|
> is its story file). Third in the chain,
|
|
> **stage 3 → 22 → 31 → 24 → 23 → 32**: chat
|
|
> ([iteration 24](24-chat-websocket-workload.md)) cannot be written
|
|
> honestly without these four mechanisms.
|
|
|
|
> **✅ LANDED 2026-08-27 — INSIDE [24](24-chat-websocket-workload.md)**, per
|
|
> the 2026-08-23 directive that absorbed it. All four mechanisms shipped:
|
|
> `call`/reply with a typed scalar reply (id 88, WO-E226), **bounded mailboxes**
|
|
> (`WO_MAILBOX`, default 1024, fail-fast with a catchable `WO_T_ACTOR`),
|
|
> **actor death** that traps callers instead of hanging them, `monitor`
|
|
> (id 89) and `time.after` (id 90). Ids 89 and 90 were reserved holes in
|
|
> `wob.h`; they are filled.
|
|
>
|
|
> **A fifth mechanism was added that this story did not anticipate**: the
|
|
> shutdown drain guarantee, [40](40-shutdown-drain-guarantee.md). It is
|
|
> lifecycle semantics — this story gave actors a death notice, 40 gives the
|
|
> program a shutdown that does not lose mail — and it was found by measurement
|
|
> while proving 24's gate, not by review.
|
|
>
|
|
> How each piece works: `runtime/src/CODE-LOGIC.md`, "Actor lifecycle".
|
|
|
|
## Why this iteration exists
|
|
|
|
The arc's stages 1+2 shipped `spawn`/`send` mechanism without lifecycle:
|
|
`send` is one-way and callers `sleep`-poll to await an answer; the
|
|
mailbox FIFO grows without bound (a hot sender can exhaust a shard's
|
|
memory); an actor that traps dies silently (nobody learns, nothing
|
|
restarts, its mailbox rots); and there is no timer surface beyond a
|
|
fiber blocking in `time.sleep`. Every real serving program — chat first —
|
|
is made of request/response turns, bounded queues, death notices, and
|
|
deadlines. Without this iteration the arc is a demo, not a runtime.
|
|
|
|
## Goals
|
|
|
|
- **Request/response over one-way sends.** A caller can send and park
|
|
until the reply arrives — one surface, no `sleep`-polling, no second
|
|
concurrency vocabulary. Ownership rules unchanged: the request moves,
|
|
the reply moves back.
|
|
- **Bounded mailboxes with a stated backpressure policy.** A mailbox has
|
|
a cap; what happens at the cap (park the sender vs error) is decided by
|
|
the spec, one policy, doctrine-pure — no silent unbounded growth
|
|
anywhere in the runtime.
|
|
- **Actor death is observable.** A trap or normal exit produces a signal
|
|
another actor can receive; a fiber-trap already isolates (stage 1) —
|
|
this makes the fact of death deliverable, so a supervisor CAN be
|
|
written in `.wo`.
|
|
- **Timers as messages.** A deadline or interval delivers to a mailbox
|
|
like any other send, riding the shard's existing io_uring/epoll
|
|
timeout plumbing (arc T4) — no new event loop.
|
|
|
|
## Acceptance Criteria (draft — the spec refines)
|
|
|
|
- What to achieve?
|
|
- **Given** an actor that answers requests,
|
|
- **when** a caller awaits the reply,
|
|
- **then** the caller's fiber parks (the shard serves other fibers,
|
|
TID-verified), resumes with the moved reply, and never busy-waits.
|
|
- What to achieve?
|
|
- **Given** a mailbox at its cap,
|
|
- **when** another send arrives,
|
|
- **then** the stated backpressure policy fires deterministically,
|
|
memory stays bounded (RSS flat under a hot-sender soak), and no
|
|
message is silently dropped.
|
|
- What to achieve?
|
|
- **Given** an actor that traps mid-message,
|
|
- **when** it dies,
|
|
- **then** its drop maps run (ASan zero leaks), a death signal
|
|
reaches the observer that asked for one, and a supervisor written
|
|
in `.wo` can respawn it.
|
|
- What to achieve?
|
|
- **Given** a timer armed by an actor,
|
|
- **when** it fires,
|
|
- **then** the actor receives it as an ordinary message on its own
|
|
shard, and cancelling before expiry means it never delivers.
|
|
|
|
## Out Of Scope
|
|
|
|
- Supervision TREES / OTP-scale restart policy — a `.wo` library once
|
|
death signals exist, not runtime policy.
|
|
- Priorities and custom scheduling — the reduction budget stays the only
|
|
fairness mechanism.
|
|
- Distributed (cross-process) supervision — no network layer exists.
|
|
- Changing the ownership-move rule or the unified address surface —
|
|
iteration 8's decisions stand.
|
|
|
|
## Info
|
|
|
|
Forks the spec must settle:
|
|
|
|
1. **The request/response surface.** A reply-address baked into the
|
|
message shape vs a runtime-level call that parks — and what the
|
|
compiler checks (does a request type name its reply type?).
|
|
2. **The backpressure policy at the cap.** Park the sender (natural with
|
|
fibers, risks deadlock cycles) vs fail the send (explicit, pushes
|
|
handling to the program). One policy, stated; not configurable per
|
|
mailbox in v1.
|
|
3. **The death-signal shape.** Erlang's link (bidirectional, dies
|
|
together) vs monitor (one-way notice) — likely monitor-only v1.
|
|
4. **The timer surface.** Builtin (`time.after` delivering a message) vs
|
|
actor-spawned sleeper fiber — and cancellation semantics.
|
|
|
|
Sources: the 2026-08-20 code-review findings (the gaps this iteration
|
|
answers), the arc plan's stage-2 deviations
|
|
([`2026-08-20-shard-fiber-arc.md`](../../superpowers/plans/2026-08-20-shard-fiber-arc.md)
|
|
— the unbounded FIFO is deviation 4's mutex inbox), and BEAM precedent
|
|
already surveyed in
|
|
[`docs/plan/exploration/fibers/00-fibers.md`](../../plan/exploration/fibers/00-fibers.md).
|
|
|
|
## Proposed Solution
|
|
|
|
Brainstorm → spec → plan after the arc's stage 3 lands and iteration 22
|
|
has its baseline (the mailbox-cap and inbox-ring decisions want 22's
|
|
mutex number). The spec is written against iteration 24's needs — chat
|
|
names the lifecycle mechanisms it consumes, this iteration names chat as
|
|
its first honest consumer.
|