Artifact 367b43ab13cd5f27ff9ff9df359d515eb6ada63685fbadffe525b603826859b8:
- File
internal/sync/horizon_pushcheck.go
— part of check-in
[6083693442]
at
2026-10-24 04:58:32
on branch main
— fix(sync): keep a refused seed's bytes; record the refusal only (#471)
Final review of c077fa4. F.1 requires recording a refusal, not deleting the bytes, and every finding came from the deletion:
- A delta built on a swept seed lost its basis, was withheld forever, and could not be re-pushed, because it counted as held. - The end-of-sync sweep ran outside a transaction. - A refused hash pulled from an upstream was dropped, which is a receive-side behaviour F.1 does not have.
So a refused seed stays stored: - Its hash is recorded in seedrefused. - It leaves unclustered, so no inventory or cluster names it. - It is withheld from every reply, and the link step never links it. - Deltas on it stay readable and are judged like any other pending row. - Pulling the same hash stores nothing new: the row is held, and stays refused. - A re-push of it is refused on its bytes. Every form (full text, delta on a held basis, delta whose own row is held) expands to the seed.
Removed: - Sweep, sweepRow and its no-root marking. - Every Sweep call: handler request end, end of sync, check-in, tag and rebuild. - requestPhantoms' refused-hash filter (a refused seed is held, so never a phantom). - ownSeedReadable's refused-hash check and its Clear (the fill's JudgeFilled already clears a non-seed). - JudgeFilled's lazy re-root, which nothing reached: only full text fills a root JudgeFilled hears of.
Kept, each pinned by a test: - Pending rows keyed by chain root, with MarkPending moving the rows waiting on a rid when that rid is itself pushed as a dangling delta. Filling a phantom with a delta triggers no cascade, so this is the re-root that matters. - JudgeFilled examining only its subtree, and its hash-mismatch skip. - The link-step judge and the refused-hash check in it. - The push-time atomic refusal. - Withholding pending and refused rows, and Purge forgetting pending rows (rids are reused).
Only pushed dangling deltas are marked pending. A dangling delta stored by a pull onto a pending row's basis is not, so rows waiting on it stay withheld until the link step reaches them; a plain file among them stays withheld.
Tests: - The reviewer's probe, for a control artifact and a plain file built on a pending seed, pushed after it and before it: after the base arrives both are readable and not withheld, and the control artifact is linked and served. - A plain file after a forged delta is still cleared. - Every judge, filter and mark fails a test when disabled, except two performance mechanisms no behaviour shows: the per-transaction table cache and IsSeedBytes' byte prefilter. (user: [email protected], size: 5041) [annotate] [blame] [check-ins using] [more...]
package sync import ( "database/sql" "errors" "fmt" "strings" "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/deck" "github.com/danmestas/go-libfossil/internal/seedcheck" ) // staleCancel is the push-time check of draft-fossil-horizon-00 §5.7, run // over what the request stored once it is crosslinked. A branch-creating // check-in cancels its parent's branch by the name its clone derived, which // under a structural horizon came from the seed. When this origin derives // the parent on another branch, the seed was stale: staleCancel returns the // refusal, which rolls the whole push back. A check-in is admitted when this // push or the store holds a control artifact cancelling the parent's real // branch on it (the correction), and so is one whose parent this origin // cannot place on a branch: the check protects against a wrong seed, as // zit's pushSeedCancelFault does, not against every unusual push. func (h *handler) staleCancel() (string, error) { q := h.repo.DB() for _, uuid := range h.received { rid, ok := blob.Exists(q, uuid) if !ok { continue } var checkin bool if err := q.QueryRow("SELECT EXISTS(SELECT 1 FROM event WHERE objid=? AND type='ci')", rid).Scan(&checkin); err != nil { return "", fmt.Errorf("handler: push check: %w", err) } if !checkin { continue } data, err := content.Expand(q, rid) if err != nil { return "", fmt.Errorf("handler: push check: %w", err) } d, err := deck.Parse(data) if err != nil || len(d.P) == 0 { continue } for _, t := range d.T { cancelled, ok := strings.CutPrefix(t.Name, "sym-") if t.Type != deck.TagCancel || t.UUID != "*" || !ok { continue } on, placed, err := branchOfCheckin(q, d.P[0]) if err != nil { return "", err } if !placed || on == cancelled { continue } corrected, err := cancelledOn(q, int64(rid), "sym-"+on) if err != nil { return "", err } if corrected { continue } return fmt.Sprintf("stale seed: the parent of %s is on branch '%s', not '%s'; re-sync, then push "+ "a control artifact cancelling sym-%s on %s (a move to its branch) with it", abbrev(uuid), on, cancelled, on, uuid), nil } } return "", nil } // seedRefusedText is the error an origin refuses a pushed seed with (horizon // §5.2.3, as the zit 0.7.1 erratum words it). const seedRefusedText = "seed refused: a seed is private to the clone that holds it; it is never pushed" // ownSeedReadable reports whether a delta this request stored before its // basis was readable has become readable and is a seed (horizon §5.2.3): // the seed is judged within the request that carried it, which is then // refused whole. It reads the bytes, since the cascade that judged the // delta when its basis arrived refused the seed's hash but cannot refuse // the request; that cascade also cleared the pending row of a delta that is // not a seed. A delta still unreadable stays pending. func (h *handler) ownSeedReadable() (bool, error) { if len(h.danglingHere) == 0 { return false, nil } if h.pass == nil { panic("handler.ownSeedReadable: a stored delta implies a receive pass") } q := h.repo.DB() for _, d := range h.danglingHere { if !content.IsAvailable(q, d.rid) { continue } // The pass's basis cache keeps every node it rebuilds, so a chain of // deltas is expanded once along its length, not once per delta. data, err := h.pass.basis.ExpandUnchecked(q, d.rid) if err != nil { return false, fmt.Errorf("handler: seed check: %w", err) } if seedcheck.IsSeedBytes(data) { return true, nil } } return false, nil } // branchOfCheckin is the branch this repository derives for the check-in // named uuid; placed is false when it holds no branch row for it. func branchOfCheckin(q db.Querier, uuid string) (string, bool, error) { var on string err := q.QueryRow(`SELECT tagxref.value FROM tagxref JOIN tag USING(tagid) JOIN blob ON blob.rid=tagxref.rid WHERE blob.uuid=? AND tag.tagname='branch' AND tagxref.tagtype>0`, uuid).Scan(&on) if errors.Is(err, sql.ErrNoRows) { return "", false, nil } if err != nil { return "", false, fmt.Errorf("handler: push check: %w", err) } return on, true, nil } // cancelledOn reports a control artifact's cancellation of tag on rid: a // cancel row sourced from an artifact other than rid itself. func cancelledOn(q db.Querier, rid int64, tag string) (bool, error) { var found bool err := q.QueryRow(`SELECT EXISTS(SELECT 1 FROM tagxref JOIN tag USING(tagid) WHERE tagxref.rid=? AND tag.tagname=? AND tagxref.tagtype=0 AND tagxref.srcid>0 AND tagxref.srcid<>tagxref.rid)`, rid, tag).Scan(&found) if err != nil { return false, fmt.Errorf("handler: push check: %w", err) } return found, nil } // abbrev is a hash's first ten digits, as a diagnostic names a check-in. func abbrev(uuid string) string { return uuid[:min(len(uuid), 10)] }