Skip to content

docs: propose rebuilding the graph without blocking writes - #347

Open
Artexis10 wants to merge 1 commit into
mainfrom
spec/graph-rebuild-without-global-boundary
Open

docs: propose rebuilding the graph without blocking writes#347
Artexis10 wants to merge 1 commit into
mainfrom
spec/graph-rebuild-without-global-boundary

Conversation

@Artexis10

Copy link
Copy Markdown
Owner

Design-only OpenSpec change. No behaviour change in this PR.

The bug is confirmed and it affects us

Reproduced on a maintainer workstation against main, one upsert_after_write with no usable sidecar:

vault first write steady-state write ratio
500 pages 7,727.8 ms 314.5 ms 24.6x
2,000 pages 32,381.5 ms 1,186.2 ms 27.3x
vault mutation boundary held too long ... operation=epistemic_graph_refresh_paths holder_kind=graph hold_ms=32371.03

CI's write-latency benchmark shows the same shape larger: hold_ms=39092 at 2,000 pages, hold_ms=172205 at 8,000. Every other vault mutation is blocked for that whole window, and a client that gives up meanwhile sees a timeout on a write that then lands.

(The warning names the holder because of #344. Before that it read operation=unknown holder_kind=unknown, which is why this went unnoticed.)

The trigger is routine

_open_read_snapshot treats the sidecar as unusable when any of schema_version, core_registry_version, or extension_registry_hash disagrees with the running build. So a full rebuild is provoked by:

A maintainer-sized vault eats ~32 s on the next write after any of those.

Why the obvious fix is unavailable

#346 removed the escalation so writes deferred to reconcile. CI rejected it, correctly: test_refresh_missing_sidecar_routes_to_full_rebuild requires a write against a missing sidecar to index the whole vault and leave it available, and three further tests depend on the same escalation. The rebuild must happen. What must change is that it stops holding the boundary while it happens.

Shape of the fix

Build rebuild passes into a temporary database beside the sidecar with no boundary held, then take the boundary only to swap it in.

Two implementation facts drive this rather than something simpler:

  • _rebuild_all_pass wipes the live sidecar (DELETE FROM graph_edges/graph_nodes/graph_parent_refs) before refilling. That is only safe today because the boundary excludes everyone, so temp-build-and-swap is required, not merely tidier.
  • The stabilization loop currently guards against out-of-band edits, since the boundary already excludes Exomem writes. Outside the boundary it becomes the primary consistency mechanism, which is why the design re-verifies freshness under the boundary immediately before the swap.

Rebuilds also become single-flight: today the boundary serialises them implicitly, and without it N concurrent writers would each start a full rebuild.

Deliberately unresolved

Two open questions are recorded rather than guessed:

  1. Whether a writer arriving during an in-flight rebuild should block on its result or defer. Deferring keeps the write bounded but may exclude the writer's own edit from the published graph, which the current contract does not permit.
  2. Whether REBUILD_STABILIZATION_ATTEMPTS needs raising now that concurrent Exomem writes can perturb freshness mid-pass, where previously only out-of-band edits could.

Also flagged as a risk: replacing an open SQLite file on Windows does not follow POSIX rename semantics, and the service runs on Windows. Task 2.4 requires verifying that path rather than assuming.

What is in this PR

  • openspec/changes/rebuild-graph-without-blocking-writes/ — proposal, design, spec, tasks
  • scripts/repro_graph_rebuild_hold.py — the harness that produced the numbers above, so the fix can be measured against the same baseline

npm exec @fission-ai/openspec -- validate --specs --strict — 23 passed, 0 failed.

A full epistemic graph rebuild runs inside the vault mutation boundary, so it blocks every other vault mutation for its duration. Measured on a maintainer workstation: one write against an unusable sidecar costs 7.7s over 500 pages and 32.4s over 2,000, against 315ms and 1.19s steady state. CI records hold_ms=39092 at 2,000 pages and hold_ms=172205 at 8,000.

The trigger is routine, not exotic: the sidecar is unusable whenever schema_version, core_registry_version, or extension_registry_hash disagrees with the running build. SCHEMA_VERSION is at 7, and editing one extension relation invalidates it too.

Design-only. Includes the reproduction harness and the pre-change baseline to beat, plus why #346's approach (defer to reconcile) is not available: four tests require the write path to leave the graph built.
@Artexis10

Copy link
Copy Markdown
Owner Author

Independent merge-gate review confirms the availability problem is real, but the current design needs revision before merge: explicitly modify/supersede the active bounded-index-maintenance contract; choose and prove a Windows-safe publication protocol for open SQLite readers; make single-flight and temp ownership process-safe; prevent cleanup from deleting an active build; and resolve whether writers arriving during rebuild block or defer without losing their own edit. Please update onto current main and rerun review/CI after those choices are closed.

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