Rooms: typing reaches collaborators before the disk #83

Closed
kayg wants to merge 0 commits from issue/32-fanout-2 into dev
Owner

Closes #32.

Typing now reaches collaborators before the history fsync. A typed change is applied, sent to every other client at once, and its history write is queued. Queued writes waiting together share one transaction (one fsync per burst).

Promises kept (ADR 0020, updated):

  • "Saved" (101) waits until every change before it is durable. Save checks count changes applied vs durable instead of history positions.
  • Get, API edits, outside edits, renames and saves first settle the changes in flight, so a version always matches its text and the file is written only after its history is safe.
  • A failed queued write is retried in place, so order is kept, and "Saved" stays unanswered until it lands.
  • A crash between fan-out and fsync loses nothing a client holds: clients keep a change until Saved and resend it on reconnect.

New tests (crates/notes/src/tests/fanout.rs):

  • collaborators see typing while the disk is stalled, and Saved waits for the disk
  • a read during typing in flight returns a version that matches its text
  • 4 clients × 50 rounds converge, each token exactly once, history equals the text
  • kill -9 in the window (new crash point AfterFanOut): after restart the typist and the collaborator reconnect and the text is back exactly once, in history too

All 99 notes tests and the full workspace pass; fmt and clippy -D warnings are clean.

Numbers (crates/notes/examples/fanout_bench.rs, calternal-dev SSD, a parallel fsync'd 8 MiB writer, before vs after interleaved; the box was at load 5-30 from other agents, and the perf VM was busy with #44):

before after
collaborator sees typing, disk loaded p50 13.4-14.6 ms, p95 28.1-28.2 ms p50 0.18-0.21 ms, p95 0.35-0.62 ms
collaborator sees typing, disk idle p50 8.5 ms, p95 18.1 ms p50 0.16 ms, p95 1.8 ms
4 typists × 400 changes, all durable (loaded) 35-36 s (44-46 changes/s) 0.16-0.24 s (6.7k-10k changes/s)
typed until Saved (loaded) p50 14.3 ms, p95 35-39 ms p50 15-25 ms, p95 39-65 ms
  • Saved latency is unchanged by design: "Saved" still means one fsync. The higher after-run p95 tracks box load (load average rose from 5 to 19 across those runs), not the code; the idle run shows 13.0 vs 17.0 ms p95.
  • #9 rename bench (API typing during a 1k-link rename): within noise before vs after. Order-reversed reruns gave rename 1.4-1.8 s in both and API typing p95 20-38 ms in both, because API edits still wait for durability exactly as before. The 16 ms budget for API typing during a rename is still missed; that cost is the fsync itself, not fan-out.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T

Closes #32. Typing now reaches collaborators before the history fsync. A typed change is applied, sent to every other client at once, and its history write is queued. Queued writes waiting together share one transaction (one fsync per burst). **Promises kept (ADR 0020, updated):** - "Saved" (101) waits until every change before it is durable. Save checks count changes applied vs durable instead of history positions. - Get, API edits, outside edits, renames and saves first settle the changes in flight, so a version always matches its text and the file is written only after its history is safe. - A failed queued write is retried in place, so order is kept, and "Saved" stays unanswered until it lands. - A crash between fan-out and fsync loses nothing a client holds: clients keep a change until Saved and resend it on reconnect. **New tests** (`crates/notes/src/tests/fanout.rs`): - collaborators see typing while the disk is stalled, and Saved waits for the disk - a read during typing in flight returns a version that matches its text - 4 clients × 50 rounds converge, each token exactly once, history equals the text - kill -9 in the window (new crash point `AfterFanOut`): after restart the typist and the collaborator reconnect and the text is back exactly once, in history too All 99 notes tests and the full workspace pass; fmt and clippy `-D warnings` are clean. **Numbers** (`crates/notes/examples/fanout_bench.rs`, calternal-dev SSD, a parallel fsync'd 8 MiB writer, before vs after interleaved; the box was at load 5-30 from other agents, and the perf VM was busy with #44): | | before | after | |---|---|---| | collaborator sees typing, disk loaded | p50 13.4-14.6 ms, p95 28.1-28.2 ms | p50 0.18-0.21 ms, p95 0.35-0.62 ms | | collaborator sees typing, disk idle | p50 8.5 ms, p95 18.1 ms | p50 0.16 ms, p95 1.8 ms | | 4 typists × 400 changes, all durable (loaded) | 35-36 s (44-46 changes/s) | 0.16-0.24 s (6.7k-10k changes/s) | | typed until Saved (loaded) | p50 14.3 ms, p95 35-39 ms | p50 15-25 ms, p95 39-65 ms | - **Saved latency is unchanged by design:** "Saved" still means one fsync. The higher after-run p95 tracks box load (load average rose from 5 to 19 across those runs), not the code; the idle run shows 13.0 vs 17.0 ms p95. - **#9 rename bench (API typing during a 1k-link rename):** within noise before vs after. Order-reversed reruns gave rename 1.4-1.8 s in both and API typing p95 20-38 ms in both, because API edits still wait for durability exactly as before. The 16 ms budget for API typing during a rename is still missed; that cost is the fsync itself, not fan-out. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Rooms: typing reaches collaborators before the disk (#32)
Some checks failed
CI / rust (pull_request) Failing after 12s
CI / web (pull_request) Successful in 2m21s
de7c65a632
A typed change is applied, sent to every other client at once, and its
history write is queued. Queued writes waiting together share one
transaction, so a burst of typing costs one fsync instead of one each.

Every durability promise holds:
- "Saved" (101) waits until every change before it is durable (save
  checks now count changes applied vs durable).
- Get, API edits, outside edits, renames and saves first settle the
  changes in flight, so a version always matches its text and the file is
  written only after its history is safe.
- A failed queued write is retried in place, keeping order.
- A crash between fan-out and fsync loses nothing a client holds: clients
  keep a change until Saved and resend it on reconnect. New test kills the
  server in exactly that window (crash point AfterFanOut).

Measured on calternal-dev's SSD with a parallel fsync'd 8 MiB writer
(examples/fanout_bench.rs, before vs after, interleaved):
- collaborator sees typing p50 13-15 ms -> 0.2 ms, p95 28 ms -> 0.4-0.6 ms
- 4 typists, all durable: 44-46 changes/s -> 6.7k-10k changes/s
- typed until Saved: unchanged (one fsync, by design)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
fanout bench: rustfmt
All checks were successful
CI / rust (pull_request) Successful in 1m28s
CI / web (pull_request) Successful in 2m15s
d8844dbadd
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Author
Owner

Reviewed by Claude: apply → fan out → queued group-commit history; Saved acks by applied-vs-durable count; every reader/flush settles in-flight first so versions match text and files follow history; retries keep order; AfterFanOut kill -9 test. Collaborator latency p50 ~14 ms → 0.2 ms, 4-typist throughput 45/s → 6.7k+/s. CI green. Merging.

Reviewed by Claude: apply → fan out → queued group-commit history; Saved acks by applied-vs-durable count; every reader/flush settles in-flight first so versions match text and files follow history; retries keep order; AfterFanOut kill -9 test. Collaborator latency p50 ~14 ms → 0.2 ms, 4-typist throughput 45/s → 6.7k+/s. CI green. Merging.
Author
Owner

Closing: this branch is already fully contained in dev (it landed through a merge train). Branch cleanup, 2026-10-15.

Closing: this branch is already fully contained in `dev` (it landed through a merge train). Branch cleanup, 2026-10-15.
kayg closed this pull request 2026-10-15 08:35:17 +00:00
All checks were successful
CI / rust (pull_request) Successful in 1m28s
CI / web (pull_request) Successful in 2m15s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!83
No description provided.