Repository navigation
fix(indexer): self-heal missing File->CONTAINS edges caused by empty-index race - #1722
Conversation
…index race On FalkorDB Lite an index created while parallel write workers are still running can come up operational but empty: it routes queries and matches zero rows while a label scan finds all values. Since CodeGraphContext#1393 the per-file linking MERGE keys its MATCH partly on occurrence_index, so under this race it silently writes zero File-[:CONTAINS] edges (nodes exist, no edges, no error) and scoped counters that traverse Repository-[:CONTAINS*]->Function report 0 functions/classes. Add GraphWriter.repair_missing_contains_links(repo_path): a post-index invariant check that, for each of Function/Class/Variable, warns and back-fills the edges from the stored path property when the label has nodes under the repo prefix but zero File-[:CONTAINS] links. Idempotent (MERGE), conservative (partially-linked labels are left alone), and backend-agnostic via execute_write_operation. Wire it into the pipeline after the C++ linking pass. Tests: tests/unit/tools/test_contains_invariant_repair.py (6 cases, RecordingSession pattern). Mutation-verified guard coverage.
|
@MorningStar0709 is attempting to deploy a commit to the shashankss1205's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Heads-up on the red check, so it does not read as a defect in this change:
Two observations:
(Separately: #1725 is unrelated to this race — it only touches |
Shashankss1205
left a comment
There was a problem hiding this comment.
Strong work — the investigation in #1721 is what makes this mergeable. Distinguishing "index routing failed" from "data missing" by forcing the predicate off-index with toInteger(n.occurrence_index) = 0 and getting 3,589 back is a clean, decisive experiment, and it's what turns this from a guess into a diagnosis.
The repair is scoped the way I'd want:
- Zero-vs-nonzero is the right trigger. Backfilling only when a label has symbol nodes and zero
CONTAINSedges separates this class of total failure from ordinary per-file defects, so a partially-linked repo is left alone rather than papered over. A repair routine that fires on partial data is how you turn a visible bug into an invisible one. - Backfilling from the stored
pathproperty rather than re-deriving it means the repair uses data the failed statement already wrote successfully — no re-parse, no new source of truth. MERGEkeeps it idempotent, so a re-run after a successful index is a no-op rather than a duplicate-edge generator.- Routing through
execute_write_operationkeeps it correct on Neo4j/Nornic managed transactions and passthrough elsewhere.
I also agree with the scope call: the empty-index race lives in FalkorDB Lite's index building, and patching third-party internals from here would be worse than detecting the consequence. Please do file that upstream report — link it back on #1721 so the trail survives.
Verified locally against the full unit + integration suite: 1596 passed, 8 skipped.
Summary
Closes #1721 — full root-cause analysis and before/after evidence in the issue.
On FalkorDB Lite, a property index created while the parallel write workers are
still running can come up operational but empty: it participates in query
routing but matches zero rows, while a label scan finds all values. Since
#1393, the per-file
MATCH (f:File) MATCH (n:<Label> {… occurrence_index …}) MERGE (f)-[:CONTAINS]->(n)linking statement keys part of its MATCH onoccurrence_index, so under this race the statement silently writes zeroFile-[:CONTAINS]edges — nodes exist, no edges, no error.Observed in production indexing of a 419-file Python repo (CGC 0.6.13):
cgc stats <repo>→Functions 0 / Classes 0(the scoped counters traverseRepository-[:CONTAINS*]->Function);MATCH (f:File)-[:CONTAINS]->(n:Function) RETURN count(n)→ 0, while aglobal
MATCH (n:Function)→ 3,589;toInteger(n.occurrence_index) = 0)→ 3,589. Index routing was the failure, not the data.
Changes
writer.py—GraphWriter.repair_missing_contains_links(repo_path_str)Post-index invariant check: for
Function/Class/Variable, if therepo prefix has symbol nodes but zero
File-[:CONTAINS]edges, log awarning and back-fill the edges from the already-stored
pathproperty(
MATCH (f:File) WHERE f.path STARTS WITH $repo_path MATCH (n:<Label>) WHERE n.path = f.path MERGE (f)-[:CONTAINS]->(n)).MERGEon an already-linked pair is a no-op.distinguishes this class of total failure from per-file defects).
execute_write_operationhelper, so it issafe on Neo4j / Nornic managed transactions and passthrough on
FalkorDB / KuzuDB.
pipeline.py— call the check after the C++ out-of-line linking passat the end of post-processing, with job status + warning surfaces.
Out of scope (intentionally): the underlying empty-index race lives in
FalkorDB Lite index building; this PR detects and repairs its only observable
consequence inside CGC rather than patching third-party internals. A separate
upstream report is recommended.
Test plan
New
tests/unit/tools/test_contains_invariant_repair.py(6 tests, followingthe
_RecordingSession/_FakeDBManagerpattern fromtest_graph_builder_perf_fixes.py):STARTS WITH $repo_path) and joinsn.path = f.pathpairwisepipeline.pywires the check inVerified locally:
AttributeErrorbefore the implementation.pytest tests/unit→ 1,484 passed, 8 skipped, 9 pre-existing failuresbyte-identical to baseline (verified via
git stash; scip/sse env issues,unrelated).
ruffon changed files: no new findings.Real-world evidence: the exact back-fill statement this PR adds recovered
3,589 Function / 250 Class / 15,529 Variable
File-[:CONTAINS]edges on alive corrupt index, after which
cgc stats <repo>reported correct counts.