Repository navigation
Conversation
Format v8 records each entry's rollup layout and a bounded catalog of every split group's claim. Wants, holes and partsync resolve identities through both sides' lineage, closing the nested false hole, and the rollup guard checks an unreadable adopted part by its entry's layout. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcLq1G8KpWBvM53JaQb1tS
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Repair carries the engine's catalog through every index it builds, joint coverage and Missing follow every group the lineage records, the catalog never ages out a claim a want, hole or entry still reaches, and partsync installs the merged catalog whenever its contents differ. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcLq1G8KpWBvM53JaQb1tS
A Relater indexes the claims' block runs and follows each closure by a work queue, remembering it, so TrimWants, repair and partsync pay for each part's closure once instead of rescanning the catalog per candidate per want. TrimWants, 256 nested wants over a 4000-claim catalog and 100 live parts, vs 35ea0fb (n=6): TrimWantsNestedLineage/wants=256 6739.8ms → 8.6ms -99.87% TrimWantsNestedLineage/wants=1 26.7ms → 2.8ms -89.51% SatisfyingWithNestedLineage/16 1802.1ms → 3.7ms -99.79% TrimWants (no catalog) 143.2µs → 95.0µs -33.61% Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcLq1G8KpWBvM53JaQb1tS
Subsumed, joint answers and hole revocation now judge every group the lineage records, not only the groups some entry carries. Repair also completes an outer group a lost member descends through when a divergent copy of its ancestry is live, so the fragments no longer overlap it forever. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcLq1G8KpWBvM53JaQb1tS
Trimming kept only a live entry's own claim, so a saturated catalog could age out the outer claim a live nested member descends through, and losing that member later brought the false hole back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcLq1G8KpWBvM53JaQb1tS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #725 (the second of three PRs). Fixes #745.
Changes
Index.Catalogrecords every split group's claim (RecordLineage, called on every commit). The record outlives the entries that carry the claim. That matters because a merge that consumes a whole group folds its claim away, and a group split again leaves the outer claim on no entry. The catalog is canonical: sorted by group, one entry per group, validated on decode.MaxLineage(=MaxRemovals) is a target, not a hard cap.TrimCatalogages the oldest groups out first, but never a claim that an outstanding want's or hole's ancestry passes through, or that a live entry carries. A catalog whose claims are all reachable stays above the target, and the engine warns once.MergeLineageno longer trims; only a writer that has the whole index does.Index.Lineage()returns the catalog plus the claims still on entries and wants.Satisfying,Discharging,TrimWantsandTrimHoles(and through itRevokes) all relate identities by it, and so do joint coverage (Coveredrealizes any group the lineage records, transitively) andMissing.Missingnow asks for the members of every group derived from a want's rows that no later group consumed, so a member that was split again is fetched through its own split.internal/repair) carries the engine's catalog through every index it builds: planning, the member rounds,committable/groupsNeeded,Admitandprune. A successor that partsync found through a retired outer claim is admitted, not dropped. The catalog reaches the pass throughHost.ObligationsLocked.FetchWantsreads the local index and answers each want withSatisfyingWithover both sides' lineage. If the local index can't be read, every unanswered want in that pass is an error, not absence.accountsForuses both lineages. Installing a peer's index keeps the local catalog's groups, trims the merged catalog against the installed index, and installs it whenever its contents differ from the peer's (not its length).Entry.Rollup(nil means unknown) sits behind a new flag bit. Every commit writes its own parts' layouts from their manifests, and an adopted entry's layout is filled in once its part opens. The rollup guard reads an unopened adopted part's layout from its entry, and planning sees it through a stand-in part with no reader. An entry whose layout is still unknown and whose part can't be opened only exists in a pre-v8 index. It counts as raw and never aborts a commit; aborting would stall that merge for good.Subsumed(and through it repair'sprune) judges every group the parts complete, including an outer group whose members were all split again. It retires what the group claims below the lowest level among the parts derived from it. Before, a live divergent copy of the ancestry was never retired, the repaired unit was refused for overlapping it, and the want stayed stuck.Satisfying,Discharging) names any part that descends through a recorded split, not only one that carries a claim. A merge that consumed a whole inner group has folded its claim away.TrimHolesrevokes a hole that a complete split group covers jointly.Runalready counted such a hole as revoked on every pass, but it stayed in the index.groupsNeedednow considers every group that a unit's rows derive through, including catalog-only ones, when the group's ancestry overlaps what the node holds. That also takes in the group's members that were split again. A lost outer member owed beside a divergent copy of its ancestry now pulls in the rest of the outer group through the members' own splits, and the commit retires the copy. Before, the inner fragments were refused for overlapping it on every pass.TrimCatalogused to protect only a live entry's own claim. A saturated catalog could then age out the outer claim a live nested member descends through. If that member was lost later, its want carried only the inner claim and the false hole came back. Every live entry now reaches its whole ancestry, as wants and holes already did. The walk shares one visited set across the whole part set (Relater.reachAncestry), so each group is followed once.FetchesPerCycle: repair: split-group completion is not bounded by FetchesPerCycle #751. Split-group completion rounds have run outside the per-cycle cap since fix(repair): commit split groups whole and cap a cycle in units #720 (repair: a split group wider than repairFetchesPerCycle+1 is never repaired, and records duplicate its first fragment every cycle #717). This PR widens which groups they reach: catalog-derivedMissing, and the ancestry groups ingroupsNeeded. It does not change the loop, so the bound is tracked in repair: split-group completion is not bounded by FetchesPerCycle #751 (performance,area/cluster,area/engine,area/recordengine; no rows are dropped). There is a one-line note inengine/ARCH.md.backend/,engine/andcluster/ARCH.mdandARCHITECTURE.md: the catalog, the layout, the v8 format and the v7 migration.v1–v7 indexes still decode. A v7 index has no layouts and an empty catalog, and the first v8 commit fills in both.
Measurements
Lineage closures (Codex round 2). Every identity question over a lineage now goes through one
Relater, built per index or batch (Index.Relations). TheRelaterindexes the claims' block runs, follows each closure with a work queue that visits only the claims a newly gained run meets, and caches the result. So a commit'sTrimWants, a repair pass, or partsync's answers to one peer compute each part's closure once.TestRelaterMatchesTheFixedPointschecks it against the naive fixed points on 3000 random nested lineages.New scaling benchmarks (
bench_lineage_test.go): a nested catalog nearMaxLineage(500 chains, 8 levels deep, 4000 claims), 100 live parts, and a rising number of wants. Old is 35ea0fb, new is this head, n=6:(The 35ea0fb side of
SatisfyingWithNestedLineageis partsync's old pattern, oneSatisfyingWithper want. The head side builds relations once per peer, which is what partsync now does.)Codex round 3 fix. Old is b02ed6f, new is this head. n=6, alternating runs;
SubsumedNestedLineageis new and completes n nested chains over the 4000-claim catalog:TrimWants(100 entries, no claims, no catalog) runs the same code path on both sides: every want misses and returns at the coverage check before any changed code. The whole difference is flat time inRelations.satisfying, whose code is unchanged. Reverting this head'sjointlySatisfyingalone leaves the +6.3%, and so does a pointer-iteration variant (+10%). So this is code layout, not work.Codex round 4: catalog trimming over the whole live set.
RecordLineageSaturated(new) trims a catalog 200 claims over the target, with 4200 claims and 600 live parts: every chain's innermost member and the 100 successors. Old is e1da206, new is this head, n=8, alternating runs:The +1ms is the fuller reachability: walking 600 live parts' ancestry, where the old code walked only the wants and holes (none here). One closure per part cost 6.6ms (19k allocations), and one closure over the union of all the parts' blocks cost 390ms, because growing an interval of thousands of runs rebuilds it on every step. The shared visited set costs one visit per group.
e2e profile. The engine package ran locally under the exact e2e command (
go test -race -v -coverpkg=./... ./engine/):The CI e2e timeouts on this PR come from the runner: main's own e2e already took 874.9s of the 900s budget. They are unrelated to this change, and #750 stays open for them.
Old is main (v7), new is this PR, on the same 100-entry index with 512 tombstones and no layouts or catalog. n=8, alternating runs:
The same index with a two-tier layout on every entry and a 256-group catalog (new benchmarks, n=6):
Decoding the catalog checks its order in one pass. The first version re-sorted it to compare, and that decode took 65.8µs.
Tests
TestEngineSuite/NestedWantRepairedThroughTheCatalog(both engines, H1): the fetcher returns a successor that satisfies the want only through a retired outer claim, and repair admits it and discharges the want. The test fails with the catalog withheld from the pass.TestJointCoverageThroughRetiredOuterClaim(H2): every outer member was split again into complete inner groups. The want for the original ancestor is answered jointly, a peer missing one inner member is asked for exactly that one, and a node that knows only the outer split asks for its members.TestTrimCatalogKeepsReachedAncestryandTestTrimCatalogOutgrowsTheTargetOnlyByReachedClaims(M1): an old nested want plus more thanMaxLineagenewer groups keeps its ancestry, and the want is still satisfiable. When every claim is reachable, the catalog exceeds the target and reports it.TestSyncKeepsTheLocalLineageBesideAFullCatalog(M2): a full peer catalog plus a newer claim only this node recorded; the merged result is installed.TestAdmitRetiresAncestorOfCatalogOnlyGroup(repair): the reported case.Admitgets a catalog-only outer claim over {1,2}, complete inner groups, and a live divergent copy of block 1. The copy is retired, and the fragments commit.TestRunMemberBesideDivergentAncestor(repair):RunplusAdmitfor a lost outer member beside the same copy. The pass fetches the whole outer group through member 11's split, and the commit retires the copy. It fails with the oldgroupsNeeded.TestSubsumedByCatalogOnlyGroup,TestJointlySatisfyingByMergedFragmentsandTestTrimHolesRevokedByCompleteGroup(bucketindex).TestRelaterMatchesTheFixedPointsalso checks which groupsrealizecompletes against the naive fixed point.TestTrimCatalogKeepsLiveEntryAncestry(written first; it failed before the fix). A live nested member carries only the inner claim, the outer claim is only in the catalog, and there are more thanMaxLineagenewer groups. The outer claim is kept, and once the member is lost, a successor of the outer ancestry answers its want.TestRelaterMatchesTheFixedPointschecksreachAncestryagainst the naive ancestry, including accumulation across calls.TestFetchWantNestedGroupFromOuterSuccessor(partsync) is the reproducer the issue asks for. Blocks {1,2} were split into {10..12}, and 12 with 7 into {20..22}. Member 20 is lost, and the only surviving copy is the peer's higher-level successor of {1,2,7}. The want is now answered instead of reported absent. It failed before the partsync change.TestRebaseBlindToAdoptedPart(engine: rollup guard misses the layout of an unreadable adopted part #745).TestEntryLayoutFilledFromTheManifestcovers both fill paths, adopted on rebase and own at load.TestUnknownLayoutOfAnUnreadablePartIsRawcovers the unreadable legacy path: the commit is not aborted.TestEngineSuite/SplitLineageOutlivesItsMembers(both engines): the claim stays in the catalog after retention drops every member.MaxLineagebound,SatisfyingWith,TrimWants/TrimHolesthrough the catalog, andLineage.Withaliasing.accountsForthrough either side's lineage, and install keeping the local catalog.FuzzRoundTrip, and a v7 seed forFuzzDecode.go test -p 2 ./...go test -raceon the changed packages and./internal/...golangci-lint run --concurrency=2 ./...(exit 0)OTELDB_STORAGE_REPRODUCE=1 go test -p 2 ./...: every failure is gated on another issue. They areTestClusterHandoffUnsyncedFlushReadsOnce/logs(Deduplication: no signal dedups re-delivered records #572),TestRerollLateOverwrite(engine: a late overwrite of an already-rolled sample cannot replace it #732), andTestRerollDuplicateSplitByMaxParts,TestRerollOracleDuplicatesandFuzzRerollOracleDuplicates(engine: raw duplicates rolled in separate merges are double-counted by the rollup fold #739).FuzzRoundTrip,FuzzDecode,FuzzLivePart,FuzzPartOf,FuzzDecodeKeyList.PR3 (bounded handoff catch-up) follows.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KcLq1G8KpWBvM53JaQb1tS