mirror of
https://github.com/block/buzz.git
synced 2026-11-06 01:11:33 +00:00
🤖 Adds append-only channel canvas history, restore, and relay-authoritative optimistic concurrency so stale edits cannot silently replace a newer canvas. ## What this adds - **Desktop history and restore** — browse revisions with author, timestamp, preview, and diff; restore an older revision as a new signed head after explicit confirmation. Persisted empty canvases remain reachable, mutation outcomes are announced accessibly, and focus moves to a stable destination after save or restore. - **CLI history and restore** — `buzz canvas history --channel …`, `buzz canvas get --channel … --revision <id>`, and `buzz canvas restore --channel … --revision <id>` use keyset pagination over the retained revision stream. - **SDK canvas builders** — optional `expected-revision` tags, bounded ancestry classification, and shared timestamp discipline keep first-party writes ahead of the head they read without propagating poisoned future timestamps. ## Authoritative concurrency contract Tagged canvas writes use compare-and-swap semantics in the relay and database: - `expected-revision=none` succeeds only when no live canvas exists. - An event ID succeeds only when it names the canonical live head. - A byte-identical replay succeeds only when that event is already the canonical live head. - A successful candidate must sort strictly ahead of the current head under `created_at DESC, id ASC`. - Malformed preconditions and stale or non-advancing writes are rejected before event rows, mentions, or fan-out are committed. The database serializes tagged writes, unconditional kind `40100` writes, and canvas soft deletion on the same `(community, kind, channel)` advisory key. Untagged `buzz canvas set` remains an unconditional write, but participates in that serialization boundary so it cannot race a tagged save or deletion. Deletion classification is derived inside the datastore transaction; unrelated event kinds do not take the canvas lock. ## Read and timestamp consistency Precondition and post-write ancestry reads request strong consistency from `/query`, pinning them to the writer when replica routing is enabled. Display-only history remains replica-eligible. First-party canvas writers stamp `created_at = max(now, head + 1)` with a conservative client skew limit. The relay applies a canvas-specific future-timestamp ceiling before persistence, preventing an accepted boundary head from making subsequent first-party writes invalid while preserving the broader ingest limit for other event kinds. ## Durable outcomes and recovery After an accepted publish, a failed verification read is reported as durable success with `verified: false` rather than a failed save. Desktop and CLI preserve the new event ID and direct users to History when verification is unavailable. A successful read walks the bounded, cycle-guarded `expected-revision` ancestry chain so transitive descendants are not misclassified as supersession. Canvas caches invalidate for every settled mutation outcome, including a retained but superseded revision. The 409 reconciliation path in `cmd_restore_canvas` now separately establishes persistence evidence from ancestry classification. `canvas_write_survived` checks reachability only; a secondary writer-pinned IDs lookup distinguishes "A absent" from "A present but superseded by a legacy write." The four-way outcome (survived / persisted-but-superseded / genuinely-absent / read-fail) is now correct for all lost-response shapes including unconditional legacy writes. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
75 lines
4.1 KiB
Markdown
75 lines
4.1 KiB
Markdown
---
|
|
title: "Replica Full-Read Routing — Caller Classification"
|
|
tags: [relay, replica-routing, consistency]
|
|
status: active
|
|
created: 2026-08-28
|
|
---
|
|
|
|
# Replica Full-Read Routing — Caller Classification
|
|
|
|
`Db::query_events` always reads the **writer** pool. `Db::query_events_routed`
|
|
(and its `_bounded` / `count` / feed siblings) opt a read into **replica
|
|
routing**: the read may be served from a read replica when
|
|
`BUZZ_REPLICA_READ_MAX_AGE_MS` is set, subject to the soundness predicate
|
|
`RoutePredicate::for_query` derives from the query shape. The seam fails closed
|
|
to the writer at every error and is a genuine no-op until the budget is
|
|
configured (`crates/buzz-db/src/lib.rs`).
|
|
|
|
## Routing rule
|
|
|
|
**If a read's result influences a write or a permission decision, it reads from
|
|
the writer.** A replica can lag behind the caller's own just-committed write; a
|
|
read that gates the next write against that lag would make a wrong decision
|
|
(e.g. validate a save against a stale head, or miss the caller's own accepted
|
|
event during post-write verification and report a false conflict). Every such
|
|
read stays on `query_events` / writer.
|
|
|
|
Display reads — a page the user scrolls, a count on a badge, a history list —
|
|
tolerate bounded staleness and take the routed path.
|
|
|
|
Adding, removing, or reclassifying a caller **requires updating the table
|
|
below**; the `query_events_routed` doc-comment points here.
|
|
|
|
### Client-carried intent: the `consistency` extension field
|
|
|
|
Some write-influencing reads are issued by clients (Desktop, CLI) through the
|
|
HTTP `/query` bridge, and are **indistinguishable by query shape** from display
|
|
reads — a kind-40100 `limit:1` read serves both `get_canvas` (display) and a
|
|
canvas save's head precondition. The client therefore signals intent with the
|
|
`consistency` extension field on the raw filter:
|
|
|
|
- `"consistency": "strong"` → the bridge serves that filter from the writer
|
|
(`query_events`), never a replica.
|
|
- absent → the default routed path.
|
|
- any other value → `400 Bad Request` (fails loud, never silently degrades).
|
|
|
|
The field only ever forces the **writer**, which is always the sound direction;
|
|
there is deliberately **no** inverse "force replica" value, so it cannot be
|
|
sprayed to bypass the replica-staleness guard on reads that should not. Parsed
|
|
in `crates/buzz-relay/src/api/bridge.rs` (`extract_consistency`).
|
|
|
|
## Caller classification table
|
|
|
|
Rows below are every `query_events_routed` / `query_events_routed_bounded`
|
|
call site at head, plus the writer-pinned canvas row this change adds. (The
|
|
`count` and feed routed families — `count_events_routed`,
|
|
`get_events_by_ids_routed`, `query_feed_*_routed`, and the `get_channel_window`
|
|
cursor/head reads — are all display or bounded-count surfaces on the routed
|
|
path; they carry their own soundness notes at their definitions in
|
|
`crates/buzz-db/src/lib.rs` and are out of scope for this table.)
|
|
|
|
| Caller / path label | Pool | Justification |
|
|
|---|---|---|
|
|
| `bridge_query` (default `/query` filter) | routed | Display reads over the HTTP bridge; bounded staleness acceptable. |
|
|
| `bridge_query` + `consistency: strong` | **writer** | Client-declared write-influencing read (canvas save precondition, restore precondition, post-write ancestry verification). |
|
|
| `req_historical` (WS REQ historical page) | routed | Display backfill of a subscription; per-row re-filter absorbs a briefly-stale row. |
|
|
| `bridge_thread_aux` (`AuxReader::Routed`, thread aux page) | routed | Thread reply hydration; display, post-verified against the fence wall. |
|
|
| `bridge_count_fallback` (`query_events_routed_bounded`) | routed (bounded arm) | COUNT fallback that materializes rows; bounded arm only, never covered. |
|
|
| `count_req_fallback` (`query_events_routed_bounded`) | routed (bounded arm) | WS COUNT fallback that materializes rows; bounded arm only. |
|
|
|
|
The **writer** row is the only write-influencing entry; every other caller is a
|
|
display or count surface that tolerates bounded staleness. Client canvas reads
|
|
that gate a write set `consistency: strong` (Desktop
|
|
`desktop/src-tauri/src/commands/canvas.rs`, CLI
|
|
`crates/buzz-cli/src/commands/channels.rs`) so they land on the writer row.
|