Skip to content

Commit 268003a

Browse files
authored
Merge pull request #481 from ModernRelay/cursor/merge-complexity-investigation-a080
2 parents 861200b + f76c7ec commit 268003a

21 files changed

Lines changed: 2085 additions & 267 deletions

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ Full diagram and concurrency model: [docs/dev/architecture.md](docs/dev/architec
104104
| Transactions and atomicity (per-query atomic; branches as multi-query transactions) | [docs/user/branching/transactions.md](docs/user/branching/transactions.md) |
105105
| Direct-publish write path (staging, D2, recovery sidecars; the former Run state machine) | [docs/dev/writes.md](docs/dev/writes.md) |
106106
| Three-way merge and conflict kinds | [docs/dev/merge.md](docs/dev/merge.md) |
107+
| Branch-merge complexity / timeout diagnosis (OmniGraph + Lance) | [docs/dev/merge-complexity.md](docs/dev/merge-complexity.md) |
108+
| Merge latency L1–L3 implementation plan | [docs/dev/merge-l1-l3-plan.md](docs/dev/merge-l1-l3-plan.md) |
107109
| Diff / change feed (`diff_between`, `diff_commits`) | [docs/user/branching/changes.md](docs/user/branching/changes.md) |
108110
| Query execution, mutation execution, bulk loader, `load` vs `ingest` | [docs/dev/execution.md](docs/dev/execution.md) |
109111
| `optimize` (compaction) and `cleanup` (version GC) | [docs/user/operations/maintenance.md](docs/user/operations/maintenance.md) |
@@ -273,7 +275,7 @@ omnigraph policy explain --cluster ./company-brain --graph knowledge --actor act
273275
| Repair uncovered drift || `omnigraph repair` explicitly classifies uncovered table `HEAD > manifest` drift: verified maintenance drift (`ReserveFragments`/`Rewrite`) can be published with `--confirm`; suspicious or unverifiable drift requires `--force --confirm`. Sidecar-covered crash residuals still recover automatically on open. |
274276
| Cleanup (`cleanup_old_versions`) || `omnigraph cleanup` derives requested `--keep` / `--older-than` cutoffs from each table's available versions; Lance refs plus OmniGraph's live-lazy-branch and recovery floors may retain additional versions. It fails closed on unopenable pins, recovery intent, or uncovered main-table HEAD drift |
275277
| BTREE / inverted (FTS) / vector indexes || `@index`/`@key` declares intent; physical indexes are derived state and never fail a logical operation. One type-dispatched chokepoint builds BTREE, FTS, or vector indexes idempotently and lazily across branches. Schema apply and mutation/load publish only logical effects. `ensure_indices` first runs the roll-forward-only recovery barrier, then materializes declared-but-missing artifacts through one staged mixed CreateIndex transaction under ordinary recovery-v9 authority; untrainable vector columns remain pending. |
276-
| Strict insert / upsert ingestion | ✅ transaction conflict filters + uncommitted fragment staging | Internal schema v6 owns the explicit logical mode. Strict insert and upsert use the sealed exact-`id`, forced-v2 MergeInsert adapter; Mutation/Load remains one transaction per table, capped before arm at 8,192 rows / 32 MiB. BranchMerge's proven all-new route accepts only a complete certificate chain plus final source/target native-incarnation checks; missing or malformed proof falls back to the general ordered diff. Raw Lance writers are outside the supported graph-writer topology. |
278+
| Strict insert / upsert ingestion | ✅ transaction conflict filters + uncommitted fragment staging | Internal schema v6 owns the explicit logical mode. Strict insert exact-probes its pinned parent, then stages a join-free exact-`id` filtered insertion-only Update; upsert uses the sealed exact-`id`, forced-v2 MergeInsert adapter. Mutation/Load remains one transaction per table, capped before arm at 8,192 rows / 32 MiB. BranchMerge's proven all-new route accepts only a complete certificate chain plus final source/target native-incarnation checks; ordered adopt fallback routes new rows through join-free StrictInsert and changed rows through a sealed update-only (`UpdateAll` + `DoNothing`) arm that fails closed unless every classified id updates. Raw Lance writers are outside the supported graph-writer topology. |
277279
| Bounded graph-batch ingestion || Raw graph-level NDJSON is parsed at a strict logical envelope and committed through the shared ordinary Load transaction. One request produces one graph commit and is acknowledged only after manifest visibility. The public shape names logical node/edge declarations, never physical datasets. There is no MemWAL, token ledger, lifecycle, hidden stream metadata, or stream-specific recovery path. |
278280
| Vector search || `nearest()` query op; embedding pipeline (Gemini / OpenAI clients); `@embed` in schema |
279281
| Full-text search || `search/fuzzy/match_text/bm25` query ops |

crates/omnigraph/src/db/omnigraph.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2991,14 +2991,6 @@ impl Omnigraph {
29912991
.await
29922992
}
29932993

2994-
pub(crate) async fn build_indices_on_dataset(
2995-
&self,
2996-
table_key: &str,
2997-
ds: &mut SnapshotHandle,
2998-
) -> Result<Vec<PendingIndex>> {
2999-
table_ops::build_indices_on_dataset(self, table_key, ds).await
3000-
}
3001-
30022994
// Used only by in-tree tests (`#[cfg(test)]`); the runtime path now
30032995
// uses `commit_updates_on_branch_with_expected` exclusively.
30042996
#[cfg(test)]

crates/omnigraph/src/exec/merge.rs

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2464,33 +2464,19 @@ async fn publish_rewritten_merge_table(
24642464
)));
24652465
}
24662466

2467-
// Failpoint: crash after the Phase 2 delete commit, before the index build.
2467+
// Failpoint: crash after the Phase 2 delete commit, before confirmation.
24682468
// Models a partial Phase B on the three-way path — constructive rows +
24692469
// deletes are on Lance HEAD but the achieved-version intent has not been
2470-
// recorded, so recovery must roll BACK (the index is reconciler-owned derived
2471-
// state, but the merge itself never reached its commit boundary). See
2470+
// recorded, so recovery must roll BACK. See
24722471
// tests/failpoints.rs::branch_merge_rewrite_partial_after_delete_rolls_back.
24732472
crate::failpoints::maybe_fail(
2474-
crate::failpoints::names::BRANCH_MERGE_REWRITE_AFTER_DELETE_PRE_INDEX,
2473+
crate::failpoints::names::BRANCH_MERGE_REWRITE_AFTER_DELETE_PRE_CONFIRM,
24752474
)?;
24762475

2477-
// Phase 3: rebuild indices.
2478-
//
2479-
// `build_indices_on_dataset` stages every missing BTREE/FTS/vector artifact
2480-
// into one table-level `CreateIndex` tail transaction. This rebuildable
2481-
// derived-state tail is not part of the merge's logical pre-minted data
2482-
// chain; Armed v9 recovery accepts it only after that complete exact chain
2483-
// and discards it with a rollback.
2484-
let row_count = target_db
2485-
.storage()
2486-
.table_state(&full_path, &current_ds)
2487-
.await?
2488-
.row_count;
2489-
if row_count > 0 {
2490-
target_db
2491-
.build_indices_on_dataset(table_key, &mut current_ds)
2492-
.await?;
2493-
}
2476+
// Index coverage is reconciler-owned derived state. As on the adopt path,
2477+
// publish the logical merge without waiting for BTREE / FTS / vector work;
2478+
// `ensure_indices` / `optimize` converges coverage later while reads remain
2479+
// correct over uncovered fragments.
24942480
let final_state = target_db
24952481
.storage()
24962482
.table_state(&full_path, &current_ds)
@@ -2825,19 +2811,19 @@ async fn publish_adopted_delta(
28252811
crate::failpoints::names::BRANCH_MERGE_ADOPT_AFTER_APPEND_PRE_UPSERT,
28262812
)?;
28272813

2828-
// Phase 1b: upsert the CHANGED rows. The fenced merge join is
2829-
// bounded to the genuinely-changed set, not the whole delta. It runs against
2830-
// the committed view that already includes the inserts; the changed ids are
2831-
// disjoint from the inserted ids (each id is classified into exactly one of
2832-
// new / changed / deleted / unchanged in the single ordered walk), so the
2833-
// join never collides with an appended row. Every logical data step uses
2834-
// the next identity in the exact transaction chain armed before Phase B.
2814+
// Phase 1b: update the CHANGED rows. Classification proved these ids present
2815+
// in the target-equals-base image; the sealed update-only adapter forbids
2816+
// insertion and fails closed if final staging cannot update every id. The
2817+
// changed ids are disjoint from the inserted ids (each id is classified into
2818+
// exactly one of new / changed / deleted / unchanged in the single ordered
2819+
// walk). Every logical data step uses the next identity in the exact
2820+
// transaction chain armed before Phase B.
28352821
if let Some(upsert_table) = &delta.upserts {
28362822
current_ds = commit_staged_keyed_chunks(
28372823
target_db,
28382824
table_key,
28392825
upsert_table,
2840-
KeyedWriteSemantics::Upsert,
2826+
KeyedWriteSemantics::KnownPresentUpdate,
28412827
current_ds,
28422828
planned_transactions,
28432829
&mut planned_index,

crates/omnigraph/src/failpoints.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ pub mod names {
8383
/// Every merge table effect is complete, but the sidecar is still in its
8484
/// pre-confirmation shape.
8585
pub const BRANCH_MERGE_POST_EFFECTS_PRE_CONFIRM: &str = "branch_merge.post_effects_pre_confirm";
86-
pub const BRANCH_MERGE_REWRITE_AFTER_DELETE_PRE_INDEX: &str =
87-
"branch_merge.rewrite_after_delete_pre_index";
86+
pub const BRANCH_MERGE_REWRITE_AFTER_DELETE_PRE_CONFIRM: &str =
87+
"branch_merge.rewrite_after_delete_pre_confirm";
8888
pub const BRANCH_MERGE_REWRITE_AFTER_MERGE_PRE_DELETE: &str =
8989
"branch_merge.rewrite_after_merge_pre_delete";
9090
pub const CLASSIFY_FRESH_READ: &str = "classify.fresh_read";

crates/omnigraph/src/instrumentation.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@ pub struct MergeWriteProbes {
239239
pub stage_append_rows: Arc<AtomicU64>,
240240
pub stage_merge_insert_calls: Arc<AtomicU64>,
241241
pub stage_merge_insert_rows: Arc<AtomicU64>,
242+
/// Update-only keyed stages whose ids were proven present by merge
243+
/// classification. Kept separate from insertion-capable Upsert.
244+
pub stage_known_present_update_calls: Arc<AtomicU64>,
245+
pub stage_known_present_update_rows: Arc<AtomicU64>,
242246
/// Strict-insert transactions that write new fragments directly and carry
243247
/// Lance's inserted-row key filter without running a target merge join.
244248
pub stage_fenced_insert_calls: Arc<AtomicU64>,
@@ -295,6 +299,13 @@ impl MergeWriteProbes {
295299
pub fn stage_merge_insert_rows(&self) -> u64 {
296300
self.stage_merge_insert_rows.load(Ordering::Relaxed)
297301
}
302+
pub fn stage_known_present_update_calls(&self) -> u64 {
303+
self.stage_known_present_update_calls
304+
.load(Ordering::Relaxed)
305+
}
306+
pub fn stage_known_present_update_rows(&self) -> u64 {
307+
self.stage_known_present_update_rows.load(Ordering::Relaxed)
308+
}
298309
pub fn stage_fenced_insert_calls(&self) -> u64 {
299310
self.stage_fenced_insert_calls.load(Ordering::Relaxed)
300311
}
@@ -420,6 +431,17 @@ pub(crate) fn record_stage_merge_insert(rows: u64) {
420431
});
421432
}
422433

434+
/// Record one update-only keyed stage whose ids were proven present by merge
435+
/// classification. No-op when no test or benchmark probe is installed.
436+
pub(crate) fn record_stage_known_present_update(rows: u64) {
437+
let _ = MERGE_WRITE_PROBES.try_with(|p| {
438+
p.stage_known_present_update_calls
439+
.fetch_add(1, Ordering::Relaxed);
440+
p.stage_known_present_update_rows
441+
.fetch_add(rows, Ordering::Relaxed);
442+
});
443+
}
444+
423445
/// Record one join-free, filter-bearing strict insert of `rows` rows against
424446
/// the active probes. This is distinct from `stage_merge_insert`: both commit
425447
/// a fenced Lance `Operation::Update`, but only the latter runs a target join.

crates/omnigraph/src/storage_layer.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ pub enum KeyedWriteSemantics {
143143
StrictInsert,
144144
/// Insert new ids and replace the full row for ids already present.
145145
Upsert,
146+
/// Replace full rows only for ids already proven present by a coherent
147+
/// merge classification. Missing ids are an internal read-set change,
148+
/// never inserts.
149+
KnownPresentUpdate,
146150
}
147151

148152
/// One exact chunk admitted to the join-free strict-insert adapter by the
@@ -607,9 +611,11 @@ pub trait TableStorage: sealed::Sealed + Send + Sync + Debug {
607611

608612
/// Stage one RFC-023 fenced keyed write from an in-memory batch.
609613
///
610-
/// This production adapter accepts only the graph `id` key, checks
611-
/// that the target dataset declares exactly that unenforced primary key,
612-
/// and forces Lance's filter-bearing non-index merge route.
614+
/// This production adapter accepts only the graph `id` key and checks that
615+
/// the target dataset declares exactly that unenforced primary key.
616+
/// Insertion-capable Upsert forces Lance's filter-bearing non-index route;
617+
/// known-present update-only staging may use the indexed route because
618+
/// `DoNothing` makes insertion structurally unreachable.
613619
async fn stage_keyed_write(
614620
&self,
615621
snapshot: SnapshotHandle,

0 commit comments

Comments
 (0)