Artifact cea8b64a2687bd2611415ca9d3bf3b49e9924b95a7a0ae20e5d8a2424641965a:
- File
internal/sync/horizon_pushcheck.go
— part of check-in
[776eae66e9]
at
2026-10-24 01:44:26
on branch main
— fix(sync): judge pending pushed seeds at the crosslink step (#471)
Re-review found two faults in the shun-based design:
- Only the push path judged a dangling seed. A base filled by a pull, a local check-in or a later sweep still crosslinked the seed. - Shunning the seed was wrong. The shun table syncs through the shun config group. A horizon clone that pushed its own current seed back as a dangling delta could get the origin to shun that hash, and every clone pulling the group would then drop its seed.
New design, in internal/seedcheck:
- A store-local seedcheck(rid, refused) table, created on first use and carried by no configuration group. - The push handler records a delta it cannot expand as pending. Only pushed artifacts are recorded: a seed-shaped artifact that arrives by any other route is ordinary (B.2). - One choke point: manifest.linkArtifact. AfterDephantomize, the sweep, rebuild, the receive linker and required-content linking all reach it. There a pending control artifact is judged: a seed becomes refused, is not linked, and leaves unclustered and unsent. Anything else loses its row and links as usual. A pending non-control row is left, inert. - A seed judged within the request that carried it still refuses that request atomically (ownSeedReadable). It expands through the pass's basis cache, so a dangling chain is expanded once along its length. - Serving honours refused: gimme/file, inventory and private igots, both clone paths, and the client's file cards when it pushes on.
judgeGrounded, the shun use and the shun filters added to the sweep, rebuild and cascade are removed.
Tests: - The pull-fills-base probe and a check-in-fills-base case. - A refused seed not cloned, and not pushed on even when a peer asks for it by name. - The seedcheck table is in no config group. - B.4.4 also asserts the real minted seed is refused while the shun table stays empty. - Disabling the choke point, or any of the serving filters, fails a test.
Clone receive is not covered: a fresh clone has no pending rows, so that path cannot meet one. (user: [email protected], size: 5035) [annotate] [blame] [check-ins using]
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. A delta still unreadable stays pending in seedcheck and is // judged at the crosslink step of whatever later makes it readable; one // found not to be a seed loses its pending row. 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 _, rid := range h.danglingHere { if !content.IsAvailable(q, 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, rid) if err != nil { return false, fmt.Errorf("handler: seed check: %w", err) } if seedcheck.IsSeedBytes(data) { return true, nil } if err := seedcheck.Clear(q, rid); err != nil { return false, err } } 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)] }