| Comment: | fix: defer cascade-linked checkins referencing unavailable blobs (#180) (#183)
The whole-repository crosslink sweep holds back a checkin whose referenced blobs (F-cards or B-card baseline) have not arrived locally, via deferCheckin, so a later sweep can rediscover and complete it once the blob shows up. The mid-round phantom-fill cascade (afterDephantomize) had no equivalent guard and linked such a checkin unconditionally, writing its event row despite insertCheckinMlinks silently skipping the missing-blob F-card. Because collectCrosslinkCandidates only reselects a rid with no event row, that half-linked checkin was permanently stranded: no future sweep, no matter how many sync rounds ran or when the missing blob eventually arrived, would ever revisit it. A clone that terminated before the blob arrived could leave the repository with a leaf checkin selected by CalcUpdateVersion whose Checkout.Update would fail with "blob not found" -- with no recovery path. Factor the sweep's deferral bookkeeping (availability cache, deferred rids, missing blobs) into checkinDeferralGuard, shared by both linkState (the sweep) and cascadeLinker (the cascade). The cascade now checks shouldDefer before opening a checkin's savepoint, so a deferred checkin writes nothing to the batch -- matching the sweep's defer semantics exactly, and preserving the invariant both paths depend on: a deferred rid must carry no event row, or the sweep's own candidate query can never find it again. deferCheckin's sweep-facing behavior, and linkBatch's call site, are unchanged -- it is now a thin wrapper over the shared guard. |
|---|---|
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
36d61bd58257a1239030da45e567d062 |
| User & Date: | [email protected] on 2026-07-27 17:41:00.000 |
| Other Links: | manifest | tags |
|
2026-07-27
| ||
| 19:19 |
test: add corpus parity gate comparing clone output against canonical fossil (#188) (#191)
The per-package parity tests compare our derivation against fossil's for fixtures built in-test, so they only cover the shapes someone thought to build. Issue #184 -- a silent 100% loss of the ticket-change artifact shape (D,J,K,U,Z) -- passed that suite throughout and was found by an external benchmark cloning the real Fossil repository. Add TestCloneCorpusParity, which builds a repository with the canonical binary, serves it over loopback, clones it with both fossil and libfossil.Clone, and compares what each derived. The comparison is per-event-type, not just totals. On the real Fossil repository the totals read 23,396 vs 27,099 -- a 14% shortfall that reads like rounding -- while the per-type breakdown put ticket events at 96 vs 3,808. Totals for blob, event, mlink, plink, tagxref and leaf are compared too, with deltas reported in both directions: our tagxref ran +17,119 over fossil's there, which is what writing a tag without finishing the work it marks looks like. The corpus is built by the canonical binary at test time rather than downloaded -- seconds and under a megabyte, against the ~10 minutes and ~1 GB the real repository costs (#187) -- and covers check-ins with a branch and a merge, ticket creations and updates, a wiki page, a technote, an attachment, and a tag control artifact. requireCorpusShapes classifies every blob by its card set and fails if a shape falls below its floor, so a corpus that quietly stopped producing tickets cannot let the gate pass while covering nothing. Gated behind LIBFOSSIL_CORPUS_PARITY=1 in addition to the existing fossil binary requirement, so the default suite is unchanged in runtime and outcome. The gate fails on this commit, which is correct: #184 is unfixed, and it also surfaces a second divergence where a merged branch tip is missing from leaf. Forum posts are not covered -- fossil exposes no CLI to create them. check-in: d01b94f4c1 user: [email protected] tags: trunk | |
| 17:41 |
fix: defer cascade-linked checkins referencing unavailable blobs (#180) (#183)
The whole-repository crosslink sweep holds back a checkin whose referenced blobs (F-cards or B-card baseline) have not arrived locally, via deferCheckin, so a later sweep can rediscover and complete it once the blob shows up. The mid-round phantom-fill cascade (afterDephantomize) had no equivalent guard and linked such a checkin unconditionally, writing its event row despite insertCheckinMlinks silently skipping the missing-blob F-card. Because collectCrosslinkCandidates only reselects a rid with no event row, that half-linked checkin was permanently stranded: no future sweep, no matter how many sync rounds ran or when the missing blob eventually arrived, would ever revisit it. A clone that terminated before the blob arrived could leave the repository with a leaf checkin selected by CalcUpdateVersion whose Checkout.Update would fail with "blob not found" -- with no recovery path. Factor the sweep's deferral bookkeeping (availability cache, deferred rids, missing blobs) into checkinDeferralGuard, shared by both linkState (the sweep) and cascadeLinker (the cascade). The cascade now checks shouldDefer before opening a checkin's savepoint, so a deferred checkin writes nothing to the batch -- matching the sweep's defer semantics exactly, and preserving the invariant both paths depend on: a deferred rid must carry no event row, or the sweep's own candidate query can never find it again. deferCheckin's sweep-facing behavior, and linkBatch's call site, are unchanged -- it is now a thin wrapper over the shared guard. check-in: 36d61bd582 user: [email protected] tags: trunk | |
| 17:24 |
test(content): build the chain-bound fixture in one transaction (#182)
TestChainBoundBitesAtSameLength inserted its fixture a statement at a time: one blob row per chain node and one delta row per edge, across two arms sized maxDeltaChainDepth and maxDeltaChainDepth+1. At the current constant that is ~65k statements, each in its own implicit transaction and each paying a commit. The chain is incidental setup; the subject of the test is the bound. But the setup cost dominated the runtime and was sensitive to how loaded the machine was, so on a contended runner it consumed the package's 120s budget and failed a PR that touched only CI YAML. Wrap both loops in a single db.WithTx so the chain commits once. This follows buildBackwardChain in chain_depth_bench_test.go, which already builds its chain this way in the same package. The assertions are untouched, byte for byte: the two walks must still agree at maxDeltaChainDepth and disagree one past it. Verified by mutation -- widening one walk's loop bound by a single node still fails the test with the same diagnostic it was written to produce. Measured on this machine, three runs each, whole-package wall clock: modernc before 19.7s / 25.0s / 7.1s after 1.19s / 1.36s / 0.80s ncruces before 3.5s / 10.0s / 7.5s after 1.27s / 1.59s / 1.13s The spread collapsing matters more than the median: the old fixture's cost tracked machine load, which is what made it a timeout risk rather than merely slow. Refs #179 check-in: 39f9ea4d8a user: [email protected] tags: trunk | |