Artifact 2e444de3becd53262d084cdfec467f4ac2b9342d04a0f1c5026258c057ba88fd:
- File
internal/manifest/schedule.go
— part of check-in
[e9eeab7e17]
at
2026-10-19 22:07:55
on branch perf/tag-repair-batch
— fix(manifest): propagate tags in the transaction that links the batch
Each link transaction (a sweep batch, a receive transaction, a retried waiter) now reparents and propagates from the parents it wrote plink rows for before it commits, as fossil's crosslink propagates from a check-in's parent in the transaction that links it. A link and the propagation it owes commit or roll back together, so a failed gate or an interrupted process can no longer leave a committed plink edge whose parent's tags never reached the child. The closing gate still applies the batch's leaf rechecks once, runs the reparent pass for parent tags no link transaction reached, and propagates from any parent left by a link outside a link transaction.
Tracked in #432. (user: [email protected], size: 3982) [annotate] [blame] [check-ins using]
package manifest import ( "slices" "github.com/danmestas/go-libfossil/db" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/leaf" "github.com/danmestas/go-libfossil/internal/repo" ) // batchSchedule is the work a crosslink batch defers to its repair gate: the // leaf rechecks its branch writes schedule (leaf.Pending, // draft-fossil-repo-state-01 Section 7.3) and the primary parents whose tags // must be propagated again because the batch gave each of them a child. Both // describe writes in the batch's transactions, so they are marked, rolled back // and reset together. // // It is not safe for concurrent use; one batch owns one. type batchSchedule struct { leaves *leaf.Pending parents parentSet } // linkTx runs link in one transaction on r and, before that transaction // commits, reparents and propagates tags from the parents sched holds, as // fossil's manifest_crosslink propagates from a check-in's parent in the // transaction that links it. A link and the propagation it owes therefore // commit or roll back together: no committed plink row waits on a later gate // for its parent's tags (issue #432). On commit the parents this transaction // added are dropped, propagated; on rollback everything it scheduled is. The // leaf rechecks stay on sched for the closing gate, which applies the whole // batch's at once (draft-fossil-repo-state-01 Section 7.3). func linkTx(r *repo.Repo, sched *batchSchedule, link func(tx *db.Tx) error) error { if r == nil { panic("manifest.linkTx: r must not be nil") } if sched == nil { panic("manifest.linkTx: sched must not be nil") } mark := sched.mark() err := r.WithTx(func(tx *db.Tx) error { if err := link(tx); err != nil { return err } if len(sched.parents.order) == mark.parents { return nil // linked no parent: nothing owes propagation } return repairPlinkAndTags(tx, sched) }) if err != nil { sched.rollback(mark) return err } sched.parents.truncate(mark.parents) return nil } // scheduleMark is a point a batchSchedule can be rolled back to. type scheduleMark struct { leaves leaf.Mark parents int } func newBatchSchedule() *batchSchedule { return &batchSchedule{ leaves: leaf.NewPending(), parents: parentSet{seen: make(map[libfossil.FslID]struct{})}, } } // mark records the schedule's current extent, for a caller whose transaction // or savepoint may roll back. func (s *batchSchedule) mark() scheduleMark { return scheduleMark{leaves: s.leaves.Mark(), parents: len(s.parents.order)} } // rollback forgets everything scheduled since m. func (s *batchSchedule) rollback(m scheduleMark) { s.leaves.Rollback(m.leaves) s.parents.truncate(m.parents) } // reset empties the schedule once the gate that ran it has committed. func (s *batchSchedule) reset() { s.leaves.Reset() s.parents.truncate(0) } // parentSet holds the primary parents a batch wrote plink rows for, in the // order it first recorded them, so a rollback drops exactly what it added. // They are the parents repairTagPropagation starts from (issue #432). type parentSet struct { seen map[libfossil.FslID]struct{} order []libfossil.FslID } // add records pid, a primary parent whose plink row the batch just wrote. func (p *parentSet) add(pid libfossil.FslID) { if pid <= 0 { panic("manifest.parentSet.add: pid must be positive") } if _, ok := p.seen[pid]; ok { return } p.seen[pid] = struct{}{} p.order = append(p.order, pid) } func (p *parentSet) truncate(n int) { if n < 0 || n > len(p.order) { panic("manifest.parentSet.truncate: mark is past the set's end") } for _, pid := range p.order[n:] { delete(p.seen, pid) } p.order = p.order[:n] } // sorted returns the parents in ascending rid order, the order the // whole-repository pass visited them in, so a gate's writes are reproducible. func (p *parentSet) sorted() []libfossil.FslID { out := slices.Clone(p.order) slices.Sort(out) return out }