Skip to content

perf(storage): stop producing and reading the UUID membership index - #1925

Merged
DecisionNerd merged 9 commits into
mainfrom
perf/1902-drop-uuid-membership-index
Oct 9, 2026
Merged

DecisionNerd merged 9 commits into
mainfrom
perf/1902-drop-uuid-membership-index

Conversation

@DecisionNerd

@DecisionNerd DecisionNerd commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1902

Slice of #1881, decision 5. New generations no longer produce or read the UUID membership index (topology/uuid-membership/manifest.json, identities-v5-*.uuidx, node-surrogates-v5-*.uuidx, topology-receipt.json). It duplicated the UUID-ordered node_uuid/edge_uuid columns of the published Parquet at 17-25 B/edge. Under the pre-v1 policy the format changes in place; a project that still carries the files opens normally and the files are ignored. The ordinal node-identity facet that shares the directory is unchanged.

What changed

  • TopologyIdentityProbe (topology_identity.rs) answers every identity question from the published topology Parquet. A sorted, deduplicated candidate set prunes row groups by min/max, then pages by the Parquet column index, and decodes only the selected pages of node_uuid (+ node_id) or edge_uuid. UuidProbeMetrics gains pages_considered and pages_read; TopologyWriteWork carries both.
  • Node and edge UUIDs share one namespace. Validation and the writer commit refuse a UUID that is a live node, a live edge or a deleted entity, and refuse a missing edge endpoint.
  • Deleted UUIDs stay spent through topology/deleted_identities.parquet (sorted, unique, carried forward by each generation that deletes; a graph that never deletes never has one). It replaces the index's tombstones.
  • The bulk builder no longer spills sorted edge UUIDs to scratch for the index, and the staged encoder keeps no retained parent index. Both encode only the ordinal facet.
  • A footer-cache bug the move exposed: hydration hard-links one object under many workspace paths, and a cache hit returned a fragment pointing at the first workspace's (possibly deleted) path. A hit is now re-pointed at the path asked for (regression test a_cached_footer_never_sends_a_probe_to_the_path_it_was_first_read_from, red before the fix).
  • Probe hardening from review: a fragment replaced after its footer was read is refused when decoded (pruning metadata would no longer describe it), a UUID held by two rows is a typed error, and absent or truncated statistics mean "may hold" (all tested, each test killed by a mutation). The probe takes no lock: it is advisory for validation and authoritative at commit, where the rewrite lock refuses a commit whose probed generation moved.
  • G500 certification evidence drops the index's fsync/write counters (scripts/ci/schemas/g500-certification.schema.json, its validator test, scale_g500_ladder.rs), and the ladder treats uuid_and_surrogates as node-bearing.

Reader inventory (every consumer of the index on main, and where it reads now)

Reader Now
Writer endpoint lookup (register_existing_endpoints) and commit validation (uuid_membership/topology_delta.rs) TopologyIdentityProbe + deleted-identities record
Staged shaping/encoding parent snapshot and retained artifacts (graph_construction.rs, shape.rs, graph_construction_encoding.rs, inventory.rs) removed; nothing retained
Bulk builder identity stream and edge-UUID scratch (bulk.rs, bulk/identities.rs, bulk/scratch_edges.rs) removed
Index reader, rebuild, construction encoder, orphan GC (uuid_membership/{probing,rebuild,construction,maintenance}.rs) removed; ordinal-facet paths kept
label_membership_counts.rs (labels by UUID) probe over the pinned TopologyFiles
ordinal_identity_v4.rs discovery freshness check, project_generation.rs rebuild-if-missing, runtime_entity_labels.rs, mutator.rs removed / probe
API node selectors by UUID or handle (node_selector.rs, added by #1926) cached_identity_probe; the 1M-capped scan stays for label and property selectors only
API bulk validation (bulk_construction.rs), branches/import_graph.rs, graph_publication.rs, composite_publish.rs, maintenance.rs, resumable_construction.rs, lib.rs cache field probe, or rebuild-if-stale removed
Search node identity (graphforge-search/src/node_identity.rs) ordinal authority; without one, rows are checked for repeats only
Hydration/materialization, export, verify, import (graph_admission.rs, staging.rs, durable_rewrite.rs, graph files inventory) no reader; legacy files are ordinary inventory entries; new generations carry none

Remaining references to uuid-membership/ are the ordinal facet (ordinal-v4-*, forward-v4-*, tombstones-v4-*).

Acceptance evidence

  • No membership artifacts in new generations. neither_bulk_route_publishes_a_membership_index (memory and scratch routes), the staged-encoder inventory assertion in encoding_publication/tests.rs, and a_new_project_carries_no_membership_index_and_round_trips.
  • Refusals. commit_refuses_a_uuid_the_published_topology_already_holds (node/node, node/edge, edge/edge, edge reusing a node UUID, at the writer commit), the bulk_construction publication/normalization tests (duplicate node and edge, edge equal to node, missing endpoint, deleted UUID), and the legacy and new-project API tests.
  • Page pruning, shown with counters and with bytes read.
    • Fixture of 4 row groups x 10 pages written with the production Parquet properties: a candidate set inside one page reads 1 of 40 pages; two row groups, 2 pages; a value in the gap between pages reads 0; every page, 40; a file without a page index falls back to row-group pruning.
    • Through the production writer: 45,000 nodes, 3 pages; endpoints in one page pages_read=1/3 (identity_bytes_read=106,588), endpoints in the first and last page 2/3 (115,990; the dictionary page is read once per probed chunk).
    • Real reads: the probe is attributed to the caller's lifecycle capture (rayon workers now carry it). One page read 1,510 bytes, every page 21,862, of a 26,312-byte file (page_pruning_reduces_the_bytes_the_file_system_serves).
  • Reopen and recovery. legacy_membership_index (real pre-change project: opens, appends, refuses, deletes, exports, verifies, imports, deleted UUID still spent after the round trip), the construction crash tests (ordinal_encoding_crashes_recover_every_durable_boundary, bulk_builder killed-in-any-pass tests, lane crash tests), and identity_first_touch (a flipped node Parquet is refused by the commit that probes it).
  • Portable round trip. The new-project test exports a bundle, asserts the tar names no index file (the same check finds them in the legacy bundle), verifies, imports, and refuses a reused UUID on the import.

Mutation proofs (each applied to the committed tree, run, reverted)

Mutation Result
taken() ignores live nodes and edges 6 tests fail (writer refusal, 2 bulk publication, deleted-identity probe, legacy, new project)
API live_nodes reports every candidate live 5 fail (missing-endpoint normalization and publication, deleted-identity probe, legacy, new project)
writer defaults an absent surrogate to 0 1 fails (wave13_validation_display_and_disappeared_endpoint_are_structured)
deleted-identity record ignored 3 fail
edge fragments never probed 5 fail
page-index pruning disabled 6 of 10 probe tests fail
row selection dropped (counters unchanged) the bytes-served test fails
probe resolves nothing 5 probe tests fail
crashed-attempt ordinal residue not cleared ordinal_encoding_crashes_recover_every_durable_boundary fails

Verification

  • make check: exit 0.
  • graphforge-storage nextest: 1796 passed, 0 failed, 9 skipped. graphforge-api nextest: 1668 passed, 0 failed, 11 skipped (includes fix(api): resolve UUID selectors by identity and keep single-pass algorithms off the iteration budget #1926's analyst_verb_limits: BFS from a UUID on 1,000,100 nodes). API BDD: 118 scenarios passed.
  • Run with GF_TEST_TMPDIR=/tmp/...: the launcher's default root sits inside the enclosing checkout when the worktree lives under .claude/worktrees, which makes 17 repository::* tests see Git-tracked data and one socket test exceed SUN_LEN. Those pass with a short ext4 root; they are environmental.
  • Python/Node bindings were not rebuilt or run: no binding source changed. The public change is to Rust-internal graphforge-storage exports (UuidMembershipIndex and the index maintenance functions are gone; TopologyIdentityProbe is new), and the workspace compiles under clippy.

Append-validation time (acceptance 3), contended host

S20 graph (1,048,576 nodes, 16,777,216 edges) receiving an S18-sized append (262,144 nodes, 4,194,304 edges, existing endpoints), gf import-session validate and commit, base 171120cbd against this branch, release builds in separate target dirs (binary sha256 differ), 5 alternating pairs, host load average 14-27 on 16 cores. Full method, commands, digests and per-step load are on the issue: #1902 (comment)

pair validate+commit base -> candidate per edge (us)
1 564.5 s -> 66.7 s (-88%) 134.6 -> 15.9
2 156.6 s -> 64.8 s (-59%) 37.3 -> 15.5
3 158.0 s -> 74.7 s (-53%) 37.7 -> 17.8
4 172.0 s -> 76.8 s (-55%) 41.0 -> 18.3
5 169.7 s -> 58.4 s (-66%) 40.5 -> 13.9

The candidate was faster in all five pairs, in both orders, on validate, commit and the sum, and in four pairs its mean load was the higher one. Per-edge ingest cost did not regress. This is contended-host evidence for the direction, not a size: a quiet-host run is still needed for a number to quote. While measuring I found that main's bulk builder fails S20 builds on a nearly full disk (captured encoded source identity or length changed: the installer records an encoded shard's allocated bytes before writeback, which can add an extent block); it is not caused by this PR and is described on the issue.

Accepted behaviour changes for projects written before this change

Both are accepted under the pre-v1 in-place format policy and documented in docs/book/architecture/uuid-membership-index.md:

  • A UUID deleted before the upgrade becomes reusable: the deleted-identities record holds only deletions made since. Entities deleted afterwards stay spent.
  • Search verifies nodes by repeat-check when a project has no ordinal facet.

The stale index files in an upgraded project are inert: no code reads them, orphan collection considers only canonical ordinal artifact names, and they stay in the graph-files inventory (and travel through hydration, export, verify and import) until a future cleanup drops them.

Follow-ups

🤖 Generated with Claude Code

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: CurateLabs/graphforge/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2375bf24-660d-4f74-80aa-8fe1660cb6af

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DecisionNerd
DecisionNerd force-pushed the perf/1902-drop-uuid-membership-index branch from cc24993 to 8c44329 Compare October 8, 2026 20:12
@github-actions github-actions Bot added core Core source code changes testing Test coverage and testing infrastructure documentation Improvements or additions to documentation ci-cd CI/CD configuration changes tooling Developer tooling and automation labels Oct 8, 2026
@DecisionNerd
DecisionNerd force-pushed the perf/1902-drop-uuid-membership-index branch 2 times, most recently from 213a18b to 565d894 Compare October 9, 2026 00:23
@DecisionNerd
DecisionNerd added this pull request to the merge queue Oct 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 9, 2026
DecisionNerd and others added 9 commits October 9, 2026 00:53
…1902)

Records the outcomes of the existing-identity check on the membership-index
path before it is removed, so the removal can show which outcomes it kept and
which it changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… current path (#1902)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…1902)

The index (`topology/uuid-membership/manifest.json`, `identities-v5-*.uuidx`,
`node-surrogates-v5-*.uuidx`, `topology-receipt.json`) duplicated the
UUID-ordered `node_uuid`/`edge_uuid` columns of the published Parquet at 17-25
B per edge and was rewritten with every generation. Under the pre-v1 policy the
format changes in place.

- Neither the bulk nor the staged encoder writes it. The bulk builder no longer
  spills the sorted edge UUIDs to scratch for it, and the staged encoder keeps no
  retained parent index. Only the ordinal node-identity facet is encoded.
- `TopologyIdentityProbe` answers every identity question from the published
  Parquet: a sorted, deduplicated candidate set prunes row groups by `min/max`,
  then pages by the column index, and decodes only the selected pages of
  `node_uuid` (and `node_id`) or `edge_uuid`. `UuidProbeMetrics` reports
  `pages_considered` and `pages_read`. Footers are cached by file identity, and
  a hit is re-pointed at the path asked for, because hydration hard-links one
  object under many workspace paths.
- Append validation and the writer commit refuse a UUID that is a live node,
  a live edge or a deleted entity (one namespace), and a missing endpoint.
- Deleted UUIDs are never reusable. `topology/deleted_identities.parquet`, a
  sorted unique column carried forward by each generation that deletes, replaces
  the index's tombstones; a graph that never deletes never has one.
- Readers moved off the index: writer endpoint lookup and commit, the topology
  rewrite participant, label lookup by UUID, graph publication, branch import,
  composite publish, bulk validation, resumable construction, search node
  identity, and the ordinal discovery freshness check. Projects that still carry
  the files open; the files are ignored.
- The G500 certification evidence drops the index's fsync and write counters,
  and its storage category policy now treats the identity category as
  node-bearing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The identity authority page now describes the Parquet probe (row-group and
page-index pruning, the deleted-identities record, the legacy-project
behaviour). ADR 0049 and 0058, the storage and resumable-import pages, the
direct-fsync guard, the fsync site census, the digest census overrides and the
storage CI filter drop the membership index's functions and tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nds, refuses and round-trips (#1902)

The fixture is a real durable project written by the binary that produced the
UUID membership index: an initial bulk build and one staged append, so the
index carries a base run and a delta run. Once nothing reads the index the
files are ordinary graph entries; the test proves such a project still opens,
refuses duplicate and missing identities from the Parquet, accepts appends,
exports, verifies and imports, and that a deleted UUID stays spent.

A second test builds a new project and proves it never writes the index and
its portable bundle never names one. The bundle check is validated against the
legacy bundle, which must name the index files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#1902)

Pre-upgrade deleted edge UUIDs become reusable, and search verifies nodes by
repeat-check when a project has no ordinal facet. Both are accepted under the
pre-v1 in-place format policy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…y probe and document the legacy upgrade (#1902)

A fragment replaced after its footer was read is refused when decoded, because
the pruning metadata would no longer describe it. A UUID held by two rows is a
typed error. Absent and truncated statistics are tested to mean 'may hold'. The
probe's lock-free contract is stated: it is authoritative only at commit, where
the rewrite lock refuses a commit whose probed generation moved. The docs say
that every pre-upgrade deletion becomes reusable and that the stale index files
are inert and carried forward.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…obe (#1902)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…enamed ordinal pass and the retired index directory (#1902)

The identity probe keyed its footer cache on Unix-only metadata, so Windows
Storage did not compile; it now uses the portable file identity, length and
modification time of an open handle. The CLI bulk-build report names its pass
ordinal, and a search test no longer deletes an index directory a new project
never has.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@DecisionNerd
DecisionNerd force-pushed the perf/1902-drop-uuid-membership-index branch from 565d894 to 5727328 Compare October 9, 2026 02:15
@DecisionNerd
DecisionNerd added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 04b2ded Oct 9, 2026
17 checks passed
@DecisionNerd
DecisionNerd deleted the perf/1902-drop-uuid-membership-index branch October 9, 2026 02:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-cd CI/CD configuration changes core Core source code changes documentation Improvements or additions to documentation testing Test coverage and testing infrastructure tooling Developer tooling and automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(storage): stop producing and reading the UUID membership index

1 participant