perf(mutate): stage independent tables at the loader's write concurrency - #531
perf(mutate): stage independent tables at the loader's write concurrency#531memmmmike wants to merge 2 commits into
Conversation
stage_all pinned concurrency to 1 while the loader path already ran stage_all_with_concurrency under OMNIGRAPH_LOAD_CONCURRENCY (default 8). The resolver moves to exec::staging as stage_write_concurrency() with a pure, unit-tested parse half; both call sites share it and the loader's private copy is deleted. Staging is pre-publish; the single manifest-CAS publish is untouched, and failure semantics are unchanged (the staging stream drained before surfacing the first error at width 1 too). Ad-hoc local measurement (16 tables per mutation, MinIO behind a 25ms latency proxy, 5 runs each, warm-up excluded): end-to-end mutate 5.80s serial vs 4.00s concurrent. Closes ModernRelay#504 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4S9Xpe9G7pLEv9JaoXUAb
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e7e14a184
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| self.stage_all_with_concurrency(db, branch, stage_write_concurrency()) | ||
| .await |
There was a problem hiding this comment.
Add a history-depth cost gate for concurrent staging
This changes the mutation hot path for performance, but the added test only compares results on two fresh, shallow stores; it neither verifies that table stages actually overlap nor guards the write cost at realistic commit-history depth. A future change that restores width 1 or adds a history-dependent scan would therefore pass. Add or extend a helpers::cost test over a deep-history fixture that asserts the intended bounded staging behavior, as required by the repository's before-every-task testing checklist.
AGENTS.md reference: AGENTS.md:L7-L9
Useful? React with 👍 / 👎.
| pub(crate) fn stage_write_concurrency() -> usize { | ||
| parse_stage_write_concurrency(std::env::var("OMNIGRAPH_LOAD_CONCURRENCY").ok().as_deref()) |
There was a problem hiding this comment.
Document that the load knob now controls mutations
When an operator sets OMNIGRAPH_LOAD_CONCURRENCY to tune bulk loads, this change also applies that value to every constructive mutation, but the only repository documentation for the variable still describes it under “Load modes” and says it applies to the loader's fragment-writing stage. This newly broadened configuration contract can unexpectedly change mutation resource usage and latency; update the operator/developer documentation to state the shared scope, or use a mutation-specific/shared setting with an explicit compatibility story.
AGENTS.md reference: AGENTS.md:L194-L194
Useful? React with 👍 / 👎.
| unsafe { | ||
| match value { | ||
| Some(value) => std::env::set_var(name, value), | ||
| None => std::env::remove_var(name), | ||
| } |
There was a problem hiding this comment.
Avoid mutating the environment in the parallel test binary
Under the default parallel libtest harness, #[serial] only excludes other tests carrying the same serial lock; the other unannotated tests in writes.rs can still run while this code calls the Rust 2024 unsafe environment mutation APIs. That violates the APIs' requirement that no other thread concurrently reads or writes the process environment and can also make unrelated mutations observe concurrency 1 or 4. Exercise a test-only concurrency seam directly or isolate this check in a subprocess instead of changing process-global environment state.
Useful? React with 👍 / 👎.
…st at depth Addresses the review feedback on this PR. - The width override moves off the process-global env var onto a scoped task-local (instrumentation::with_stage_write_concurrency), mirroring the existing with_traversal_mode seam. #[serial] only excludes other #[serial] tests, so the EnvGuard raced every unannotated test in the binary, and set_var under a live multi-thread runtime violates setenv's thread-safety precondition. The seam is scope-bound and process-safe, so #[serial] and the EnvGuard are both gone. A unit test pins that the override reaches the resolver and does not leak past its future -- without it the equivalence test could silently run at the default width on both sides and prove nothing. - OMNIGRAPH_LOAD_CONCURRENCY is no longer load-only, so docs/dev/writes.md now says so in both places it is described: the stage_all bullet and the LoadMode::Overwrite paragraph that previously scoped it to loads. - New write_cost.rs gate: a two-table mutation's cost must be flat across commit-history depth. Concurrency is a latency change, not a cost change, and the equivalence test would keep passing if a future change made each stage re-resolve per-table state at depth. Measured local FS: depth~10 __manifest=11/data=21, depth~100 __manifest=10/data=19. Also runs cargo fmt: the original commit did not, and CI on this branch has never executed (action_required), so the fmt job would have failed on first approval. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4S9Xpe9G7pLEv9JaoXUAb
|
Thanks both bots — all three points addressed in 16a1e87. 1. Right, and the reasoning goes further than flakiness: Rather than annotate the rest of the file, the width override now rides the seam this repo already established for exactly this problem — This works because staging is explicitly off-queue (it runs on the caller's task; only the commit is queued), so the task-local reaches One thing I'd have missed without writing it down: a scoped override that silently fails to apply would make the equivalence test run at the default width on both sides and assert nothing. There's now a unit test pinning that the override reaches the resolver, that 2. Document that the load knob now controls mutations (greptile + codex, agreed)
3. History-depth cost gate (codex P1, agreed) Fair — the equivalence test proves sameness on two shallow stores and nothing about cost. Added On the other half of that P1 — asserting the stages overlap — I did not add a timing or peak-inflight assertion, deliberately. Every version I could write was either timing-based (flaky in CI) or required instrumenting the production staging loop to prove a scheduling property that Also: the original commit was never |
Closes #504.
Per the acceptance comment: the fix stays inside the existing mechanism.
stage_allnow delegates atstage_write_concurrency()(the loader'sOMNIGRAPH_LOAD_CONCURRENCYknob, default 8) instead of a pinned 1. The resolver moves toexec::stagingwith a pure parse half; the loader's private copy is deleted and both call sites share it.Scope kept, verified against source:
commit_allstill acquires sorted table gates.buffered(order-preserving), notbuffer_unordered.buffered(1).collect::<Vec<Result<_>>>), so wider staging changes only how much of that pre-existing drain overlaps; residue remains reclaimable and never graph-visible.Tests: a pure unit test on the parse rules, and a serial-vs-concurrent equivalence test on a multi-table mutation (Person + two Knows edges in one query) asserting affected counts, per-table row counts, the inserted row's values, and exact edge endpoint pairs agree between
OMNIGRAPH_LOAD_CONCURRENCY=1and=4(#[serial]-guarded per the search.rs precedent). Fullwritessuite: 43/43.Ad-hoc local measurement (16 node tables touched per mutation, MinIO behind a 25ms toxiproxy latency, 5 timed runs each after a warm-up): end-to-end
mutatemean 5.80s at concurrency 1 vs 4.00s at 8. The remaining gap to the theoretical staging ratio is the untouched validate/publish path, which stays serial by design. Happy to re-run under any harness you prefer, per docs/dev/testing.md's benchmark guidance.🤖 Generated with Claude Code
https://claude.ai/code/session_01E4S9Xpe9G7pLEv9JaoXUAb
Greptile Summary
The PR makes mutation staging use the loader’s configurable cross-table write concurrency and centralizes the resolver behind a scoped test override.
OMNIGRAPH_LOAD_CONCURRENCYbetween loader and mutation staging.Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
Reviews (2): Last reviewed commit: "review: scope the staging-width test sea..." | Re-trigger Greptile
Context used: