Artifact a77dd918f12bf58f2455f6755dcefaa471cfd61d9043546b1da412e1f774b525:
- File
internal/manifest/dephantomize.go
— part of check-in
[94919ca357]
at
2026-07-23 14:09:06
on branch trunk
— manifest: wire AfterDephantomize into the phantom fill path (#130)
* Wire manifest.AfterDephantomize into the phantom fill path
AfterDephantomize was a faithful, tested port of Fossil's after_dephantomize (content.c:389-456) that nothing in production ever called — it crosslinked correctly in isolation but sat disconnected from the real receive path.
storeResolvedContent fills a phantom row in place when the full content for a previously-unknown UUID arrives (matching canonical's content_put_ex filling a phantom at content.c:626), but stopped short of crosslinking it or any delta children waiting on it. Return the filled rid from storeResolvedContent and call AfterDephantomize on it from storeReceivedFile once the fill transaction has committed — outside the transaction, since AfterDephantomize opens its own connection via repo.Repo.DB() and calling it from inside the tx's closure would contend with the still-open write lock.
Added an end-to-end regression test that delivers a checkin manifest as a delta against a base that hasn't arrived yet, then delivers the base, and asserts the manifest is crosslinked (its event row exists) immediately — with no explicit call to manifest.Crosslink anywhere in the test. This proves AfterDephantomize fires on the real fill path, not only under its own unit tests.
* Log crosslink failures during AfterDephantomize instead of swallowing them
A crosslink error while filling a phantom blob (or one of its orphan/delta dependents) previously vanished silently, leaving storeReceivedFile report success with the artifact filled but un-crosslinked. Surface it via slog.Warn so real corruption on the fill path is at least visible, matching the non-blocking nit from review. (user: [email protected], size: 3849) [annotate] [blame] [check-ins using]
package manifest import ( "fmt" "log/slog" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/deck" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/repo" ) // AfterDephantomize crosslinks a formerly-phantom blob and any dependents. // Matches Fossil's after_dephantomize (content.c:389-456). func AfterDephantomize(r *repo.Repo, rid libfossil.FslID) { if r == nil { panic("manifest.AfterDephantomize: r must not be nil") } if rid <= 0 { return } afterDephantomize(r, rid, true) } func afterDephantomize(r *repo.Repo, rid libfossil.FslID, linkFlag bool) { // Work stack replaces recursion. Bounded by total blob count in repo. type workItem struct { rid libfossil.FslID linkFlag bool } stack := []workItem{{rid: rid, linkFlag: linkFlag}} const maxIterations = 1_000_000 // Guard against pathological delta chains. iterations := 0 for len(stack) > 0 { iterations++ if iterations > maxIterations { return // Safety bound exceeded. } // Pop from stack. item := stack[len(stack)-1] stack = stack[:len(stack)-1] current := item.rid if current <= 0 { continue } if item.linkFlag { if err := crosslinkSingle(r, current); err != nil { slog.Warn("manifest.AfterDephantomize: crosslink failed, blob left un-crosslinked", "rid", current, "error", err) } } // Process orphaned delta manifests whose baseline is this rid. orphanRows, err := r.DB().Query("SELECT rid FROM orphan WHERE baseline=?", current) if err == nil { var orphans []libfossil.FslID for orphanRows.Next() { var orid int64 if orphanRows.Scan(&orid) == nil { orphans = append(orphans, libfossil.FslID(orid)) } } orphanRows.Close() for _, orid := range orphans { if err := crosslinkSingle(r, orid); err != nil { slog.Warn("manifest.AfterDephantomize: crosslink of orphan failed, blob left un-crosslinked", "rid", orid, "baseline", current, "error", err) } } if len(orphans) > 0 { if _, err := r.DB().Exec("DELETE FROM orphan WHERE baseline=?", current); err != nil { continue } } } // Find delta children not yet crosslinked. childRows, err := r.DB().Query( `SELECT rid FROM delta WHERE srcid=? AND NOT EXISTS (SELECT 1 FROM mlink WHERE mid=delta.rid)`, current) if err != nil { continue } var children []libfossil.FslID for childRows.Next() { var crid int64 if childRows.Scan(&crid) == nil { children = append(children, libfossil.FslID(crid)) } } childRows.Close() // Push all children onto work stack (reverse order for LIFO processing). for i := len(children) - 1; i >= 0; i-- { stack = append(stack, workItem{rid: children[i], linkFlag: true}) } } } // crosslinkSingle crosslinks a single blob by rid. func crosslinkSingle(r *repo.Repo, rid libfossil.FslID) error { if r == nil { panic("crosslinkSingle: r must not be nil") } if rid <= 0 { panic("crosslinkSingle: rid must be positive") } data, err := content.Expand(r.DB(), rid) if err != nil { return fmt.Errorf("crosslinkSingle expand rid=%d: %w", rid, err) } d, err := deck.Parse(data) if err != nil { return fmt.Errorf("crosslinkSingle parse rid=%d: %w", rid, err) } switch d.Type { case deck.Checkin: return crosslinkCheckin(r, rid, d) case deck.Wiki: _, err = crosslinkWiki(r, rid, d) return err case deck.Ticket: _, err = crosslinkTicket(r, rid, d) return err case deck.Event: _, err = crosslinkEvent(r, rid, d) return err case deck.Attachment: return crosslinkAttachment(r, rid, d) case deck.Cluster: return CrosslinkCluster(r.DB(), rid, d) case deck.ForumPost: return crosslinkForum(r, rid, d) case deck.Control: return crosslinkControl(r, rid, d) } return nil }