Skip to content

engine: RFC 0067 table pins, pinned resolution and promotion reconciler - #727

Closed
ragnorc wants to merge 2 commits into
claude/rfc-0066-0067-detached-commitsfrom
claude/rfc-0067-pins-and-promotion
Closed

ragnorc wants to merge 2 commits into
claude/rfc-0066-0067-detached-commitsfrom
claude/rfc-0067-pins-and-promotion

Conversation

@ragnorc

@ragnorc ragnorc commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Step 1 of 2 for RFC 0067 (detached table commits). Stacked on #725, which carries the RFCs and the Lance fences; this PR's base is that branch and retargets to main when it merges.

What lands here

Everything a reader, a promoter and cleanup need to handle a table pin that names a detached Lance version, without any writer producing one yet. On every existing graph this change is inert: no pin carries the new fields, so every path below short-circuits to today's behaviour.

  • Pin metadata. TableVersionMetadata gains staged_version and transaction_uuid, carried in the manifest row JSON and as omnigraph.staged_version / omnigraph.transaction_uuid in the namespace request metadata map. Absent on every pin published by the sidecar path.
  • Pinned resolution. One opener, open_pinned_dataset, resolves a pin: the linear target when it carries the pin's transaction; the staged version while the target is absent and the linear head is still behind it, or while a foreign commit occupies it; HistoricalVersionReclaimed once the head has passed an absent target. Both dataset-open paths (the manifest entry opener and the read-handle cache miss) route through it.
  • Identity witness. The Blob facade and the change feed's named-branch witness accept the transaction uuid as well as the manifest e-tag, since the promoted twin's manifest differs from the staged one. The Blob version check accepts the staged version of a pending pin.
  • Transaction identity without a request. Lance records the transaction file name {read_version}-{uuid}.txn in every manifest, so the resolver, both witnesses and the chain walk read a version's identity from the opened manifest. Only promotion reads the transaction it replays. The existing twin fence in lance_surface_guards now pins that name for linear, detached and chained commits.
  • Storage primitives. Two durable primitives on the sealed boundary, commit_staged_detached and promote_detached, plus the pure transaction_identity. Promotion replays the recorded transaction at target - 1 with zero retries, refuses a bare Append, and reports a landed twin, a refused replay or an unsafe state. Registered in forbidden_apis with exact call-site counts.
  • Promotion reconciler. db/omnigraph/promotion.rs: promote one pin, check the target first (any process may promote; the same pin is registered on every branch that inherits it), follow read-version links back to the linear base for a chain of detached commits, tolerate reaped predecessors when the base already exists, and report a foreign occupant as blocked while reads stay correct through the staged version.
  • Cleanup ordering. Cleanup promotes every pending pin inside its existing per-branch classification loop before any version is reclaimed and deletes the promoted pin's detached manifest, so stock Lance cleanup only sees linear history.
  • Seam. promotion.pre_replay between a promotion's existence check and its replay, for the race and crash matrix that step 2 ports.

Tests

  • db/omnigraph/promotion.rs: a detached commit is private until promoted, resolves through its pin, promotes exactly once and refuses a replay over its twin; a chain promotes oldest first and survives a reaped predecessor; a foreign commit at the target blocks promotion, the pin keeps reading its staged effect, and a target pruned behind the head is reclaimed.
  • db/manifest/metadata.rs: the namespace round trip carries both pin fields.
  • lance_surface_guards.rs: the transaction file name assertions on the existing twin fence.
  • forbidden_apis.rs: the new primitives and their single call sites.

Run locally: forbidden_apis, lance_surface_guards, failpoint_names_guard, maintenance, recovery, failpoints, lifecycle, end_to_end, changes, write_cost, warm_read_cost, branching, writes, the engine lib tests, both Clippy graphs, cargo fmt, check-docs.py, typos, check-agents-md.sh, check-workflow-action-pins.py.

Step 2

Switches every writer (mutation and load, merge, ensure-indices, Optimize as one detached Rewrite with an exact-base publish) to detached staging, stamps schema v10 with the upgrade route, promotes from held handles, ports the failure-window matrix and the race harness onto seams, and deletes the sidecar machinery, classifier, Restore compensation, recovery modes and drift repair together with their docs.

Review fixes (17 September 2026)

The stack review reproduced four defects in this PR's code once the later writers exercised it; a fifth surfaced as the server's concurrent-insert test returning 409 in about half of the runs. All five are fixed in the commit engine: close the pin, promotion, diff and cleanup boundaries the RFC 0067 review found, and the regression tests live on #734 where the writers that reach them exist:

  • Cleanup on a blocked pin (P1). Cleanup now records a table whose pin's promotion is blocked, refuses version GC for it (the table's stats row carries the reason) and protects every link of the blocked chain from the reaper, instead of trusting a numerically equal HEAD and handing the detached version's files to stock GC.
  • Detached-manifest reaper (P2). With --older-than, cleanup lists each table location's _versions/ and reaps every detached manifest older than the threshold that no pending chain protects: superseded pins, promoted chain links and attempts that never published. Without an age policy it reaps none, since a recent manifest may belong to a write in flight.
  • Snapshot diff over a pending chain (P1). The same-lineage diff hands a to endpoint that resolves to a detached version to the exact ID/content comparison, because rows staged on a detached predecessor carry stamps outside the linear interval. A replayed promotion re-stamps, so promoted history keeps the fast path.
  • Resolver race (P2). The pinned resolver re-reads the exact target once HEAD has reached it and reports reclaimed history only when the target is still absent.
  • Stale read set after a promotion. The baseline check now reads the current manifest before refusing; when it explains a HEAD one past the writer's published version, the writer gets ReadSetChanged and reprepares. Genuinely foreign drift is still refused with the repair conflict.

Docs: docs/dev/writes.md and docs/user/operations/maintenance.md describe the blocked-pin GC refusal and the age-gated reaper.

Readers, the promoter and cleanup learn to handle a table pin that names a
detached Lance version, without any writer producing one yet. On existing
graphs the change is inert: no pin carries the new fields.

- TableVersionMetadata carries staged_version and transaction_uuid in the
  manifest row and the namespace request metadata map.
- open_pinned_dataset resolves a pin to its linear target, its staged
  version while the target is pending or foreign, or reclaimed once the
  linear head has passed an absent target; both dataset-open paths use it.
- The Blob facade and the change-feed witness accept the transaction uuid
  as identity; the Blob version check accepts a pending pin's staged version.
- A version's transaction identity is read from the transaction file name
  Lance records in the manifest, so no request beyond the manifest is
  needed; the existing twin fence pins the name.
- TableStorage gains commit_staged_detached, promote_detached and
  transaction_identity; the registry pins their call sites.
- db/omnigraph/promotion.rs promotes one pin: target first, chains through
  read-version links, tolerates reaped predecessors, reports foreign
  occupants as blocked.
- Cleanup promotes pending pins inside its per-branch loop before any
  reclamation and deletes promoted pins' detached manifests.
- Seam promotion.pre_replay for the race and crash matrix of step 2.
… 0067 review found

Four defects reproduced against the stack, plus one the server's concurrent
insert test exposed once the writers landed:

- Cleanup skipped nothing when a pin's promotion was blocked: HEAD equalled
  the pin's target numerically, so stock version GC ran and could delete
  the detached version's data files once older than Lance's unverified
  threshold. Cleanup now records a blocked pin's table, refuses version GC
  for it in that table's stats row, and protects every link of the blocked
  chain from the reaper.
- Cleanup deleted only the current tip pin's detached manifest, so
  superseded pins, promoted chain links and attempts that never published
  accumulated forever. With `--older-than`, cleanup now lists each table
  location's `_versions/` and reaps every detached manifest older than the
  threshold that no pending chain protects; without an age policy it reaps
  none, since a recent manifest may belong to a write in flight.
- The snapshot diff filtered `_row_last_updated_at_version` against the
  linear interval even when the `to` endpoint resolved to a pending pin's
  detached version, whose rows staged on a detached predecessor carry
  stamps outside that interval. The same-lineage diff now hands a detached
  endpoint to the exact ID/content comparison.
- The pinned resolver reported reclaimed history when a promotion landed
  between its target probe and its HEAD read. It now re-reads the exact
  target once HEAD has reached it, and reports reclaimed history only when
  the target is still absent.
- A writer whose captured snapshot predated another writer's promotion saw
  the linear HEAD one past its published version and was refused with the
  non-retryable "run `omnigraph repair`" conflict; concurrent `/change`
  inserts returned 409 in about half of the runs. The baseline check now
  reads the current manifest first and returns `ReadSetChanged` when it
  explains the HEAD, so the writer reprepares; genuinely foreign drift is
  still refused.
@ragnorc

ragnorc commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Consolidated into #744, which carries this stack's complete history merged with current main and revalidated as one tree.

@ragnorc ragnorc closed this Sep 17, 2026
@azimafroozeh
azimafroozeh deleted the claude/rfc-0067-pins-and-promotion branch September 29, 2026 15:15
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.

1 participant