Artifact 15b31ae5c0e77995e2b991df38a493e602c1e4995c50b0b9bf4da2ed456420ef:
- File
internal/sync/chain_bound_test.go
— part of check-in
[eac16cf7ed]
at
2026-10-24 13:08:15
on branch main
— fix(sync): bound basis-chain walks at the read depth
Any pusher could hold the write lock about 30s per push with a two-delta cycle. The receive pass's basis-chain walks, and the horizon seed check's pending-row walk, ran to 1<<24 steps before giving up.
All of them now stop at content.MaxDeltaChainDepth, the depth at which content.Expand stops, so no walk outlasts what a read could ever use. What crossing the bound means at each site:
- chainEnd (rootOf, for a dangling delta being stored) keeps a visited set. A chain that loops is refused, as it was, but at the revisit rather than after 1<<24 steps. A chain longer than the bound returns errChainTooDeep. storeDeltaContent then stores the delta untracked: the push succeeds, as a push onto a chain between 16k and 16M deep always did, and since no basis can make the delta readable there is nothing to check against its name later. Storing matches the current behaviour, and an honest transfer never fails for a deep chain it did not build. - verifyFrom's cascade carries each row's depth from the basis that arrived, its own chain included. It does not check, or descend past, a row deeper than the bound: no read can produce those bytes, so the pass owes them no check. A delta that does not hash to its name past the bound no longer aborts the pass. - pathRows loses its step counter. Its visited marks already stop it at a row on a path, so it visits each row once, and the cascade's depth check bounds what it verifies. - seedcheck's chainRoot gives a chain that loops or runs past the bound no root, consistent with ErrChainUnresolvable: the row stays pending, withheld, and is never judged.
Tests: - Closing a two-delta cycle is refused in under 5s (it took 27s). - A push onto a chain one past the bound is stored. - chainEnd stops exactly at its limit. - A dangling delta with a wrong name one node past the bound does not abort the push, whether the basis that arrives is full text or a delta deep on a held chain, while one within the bound is still checked and readable. - MarkPending gives an over-deep chain no root, so filling its far end examines nothing, and returns at once on a cycle. - TestHorizonOverDeepPendingChainDoesNotBlockFill still passes. - Removing each bound, the cycle check, the untracked store or the root depth fails a test. - chainDepth's own early stop is an equivalent mutant: the cascade's root has just been expanded, so its chain is within the bound unless the store is damaged. (user: [email protected], size: 6420) [annotate] [blame] [check-ins using] [more...]
package sync import ( "errors" "fmt" "testing" "time" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/delta" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/xfer" ) // chainLink is the content of link i of a planted chain. func chainLink(i int) []byte { return fmt.Appendf(nil, "chain link %08d\n", i) } // plantRealChain stores links 0..n-1 of a chain, link i a delta on link // i+1, link n-1 a delta on link n, held as full text or left a phantom, and // returns the top link's rid and the root's content. Every link expands // once the root is held, so a walk over the chain reads real bytes. func plantRealChain(t *testing.T, r *repo.Repo, n int, rootHeld bool) (libfossil.FslID, []byte) { t.Helper() root := chainLink(n) var top libfossil.FslID err := r.WithTx(func(tx *db.Tx) error { src, err := blob.StorePhantom(tx, hash.SHA1(root)) if rootHeld { src, _, err = blob.Store(tx, root) } if err != nil { return err } for i := n - 1; i >= 0; i-- { d, err := delta.Create(chainLink(i+1), chainLink(i)) if err != nil { return err } rid, err := blob.StoreDeltaRaw(tx, hash.SHA1(chainLink(i)), d, src, nil) if err != nil { return err } src, top = rid, rid } return nil }) if err != nil { t.Fatal(err) } return top, root } // A delta pushed to close a two-delta cycle is refused at once, rather than // after a walk of 1<<24 steps that held the write lock half a minute. func TestCyclicDeltaPushRefusedQuickly(t *testing.T) { o := newPCOrigin(t) _, a := o.labelled("u", "one side of a cycle") _, b := o.labelled("u", "the other side") x := deltaCard(t, b, hash.SHA1(b), a) y := deltaCard(t, a, hash.SHA1(a), b) if errs := errorsOf(o.push(x)); len(errs) != 0 { t.Fatalf("the first delta: errors %q", errs) } start := time.Now() errs := errorsOf(o.push(y)) if elapsed := time.Since(start); elapsed > 5*time.Second { t.Fatalf("closing the cycle took %v", elapsed) } if len(errs) == 0 { t.Fatal("the delta closing a cycle was stored") } } // A delta pushed onto a chain longer than content.MaxDeltaChainDepth can // never be expanded, but it is an honest artifact: the push stores it, as a // push onto any chain deeper than the bound always did. func TestOverDeepDeltaPushStored(t *testing.T) { o := newPCOrigin(t) held := []byte("unused\n") phantom, err := blob.StorePhantom(o.r.DB(), hash.SHA3(held)) if err != nil { t.Fatal(err) } top := plantDeepChain(t, o.r, phantom, content.MaxDeltaChainDepth+5) var topUUID string if err := o.r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", top).Scan(&topUUID); err != nil { t.Fatal(err) } _, onTop := o.labelled("u", "an artifact pushed onto the deep chain") d := deltaCard(t, []byte("whatever the top link holds\n"), topUUID, onTop) if errs := errorsOf(o.push(d)); len(errs) != 0 { t.Fatalf("the push onto an over-deep chain was refused: %q", errs) } if _, ok := blob.Exists(o.r.DB(), d.UUID); !ok { t.Fatal("the delta was not stored") } } // A dangling delta whose path to the basis that arrives runs past // content.MaxDeltaChainDepth is not checked against its name when the // basis arrives: no read can expand it, so the pass owes it nothing. One // within the bound on the same basis is checked as before. func TestOverDeepDanglingDeltaNotVerified(t *testing.T) { o := newPCOrigin(t) top, root := plantRealChain(t, o.r, content.MaxDeltaChainDepth-3, false) var topUUID string if err := o.r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", top).Scan(&topUUID); err != nil { t.Fatal(err) } r1Bytes := []byte("r1 pushed onto the planted chain\n") r1 := deltaCard(t, chainLink(0), topUUID, r1Bytes) d0Bytes := []byte("d0 pushed onto r1\n") d0 := deltaCard(t, r1Bytes, r1.UUID, d0Bytes) // d1 does not hash to its name; its path is one node past the bound. d1 := deltaCard(t, d0Bytes, d0.UUID, []byte("d1 pushed onto d0\n")) d1.UUID = hash.SHA1([]byte("a name d1's bytes do not have\n")) cards := []xfer.Card{d1, d0, r1, &xfer.FileCard{UUID: hash.SHA1(root), Content: root}} if errs := errorsOf(o.push(cards...)); len(errs) != 0 { t.Fatalf("the push was refused: %q", errs) } for _, u := range []string{d0.UUID, r1.UUID} { rid, _ := blob.Exists(o.r.DB(), u) if !content.IsAvailable(o.r.DB(), rid) { t.Fatalf("%.10s within the bound is not readable", u) } } } // chainEnd stops at its limit: a chain one node longer is errChainTooDeep, // one exactly as long has its root. func TestChainEndStopsAtLimit(t *testing.T) { o := newPCOrigin(t) phantom, err := blob.StorePhantom(o.r.DB(), hash.SHA3([]byte("the root\n"))) if err != nil { t.Fatal(err) } const limit = 64 top := plantDeepChain(t, o.r, phantom, limit) if _, err := chainEnd(o.r.DB(), top, nil, limit); !errors.Is(err, errChainTooDeep) { t.Fatalf("a chain of %d nodes under limit %d: %v, want errChainTooDeep", limit+1, limit, err) } if root, err := chainEnd(o.r.DB(), top, nil, limit+1); err != nil || root != phantom { t.Fatalf("a chain of %d nodes under limit %d: root %d (%v), want %d", limit+1, limit+1, root, err, phantom) } } // The same when the basis that makes the dangling deltas readable is itself // a delta deep on a held chain: their depth counts that chain too. func TestOverDeepDanglingDeltaOnDeepBasisNotVerified(t *testing.T) { o := newPCOrigin(t) top, _ := plantRealChain(t, o.r, content.MaxDeltaChainDepth-3, true) var topUUID string if err := o.r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", top).Scan(&topUUID); err != nil { t.Fatal(err) } r1Bytes := []byte("r1 pushed onto the held chain\n") r1 := deltaCard(t, chainLink(0), topUUID, r1Bytes) d0Bytes := []byte("d0 pushed onto r1\n") d0 := deltaCard(t, r1Bytes, r1.UUID, d0Bytes) d1 := deltaCard(t, d0Bytes, d0.UUID, []byte("d1 pushed onto d0\n")) d1.UUID = hash.SHA1([]byte("a name d1's bytes do not have\n")) if errs := errorsOf(o.push(d1, d0, r1)); len(errs) != 0 { t.Fatalf("the push was refused: %q", errs) } rid, _ := blob.Exists(o.r.DB(), d0.UUID) if !content.IsAvailable(o.r.DB(), rid) { t.Fatal("d0, within the bound, is not readable") } }