Artifact fcf8d08e4d6697ff5c7c40d4fd871734691f52f499b699e61f3dcebfea0be7f7:
- File
internal/sync/sparse_clone_walk.go
— part of check-in
[352a0d9e0b]
at
2026-10-22 12:02:50
on branch feat/horizon-cones
— feat(sync): serve the content horizon and send its pragma
A horizon store asked every peer for the whole history and threw away what fell below C on arrival, so a horizon clone cost a dense clone's bandwidth. draft-fossil-horizon-00 §6 lets the request carry the horizon and the origin restrict what it sends.
The client sends `pragma horizon HASH RULE` after its path-cone pragmas on every clone and pull, never horizon-structural, and counts the replies that say `pragma horizon-served content` (HorizonServed on the clone and sync results, internal and public). It still classifies every reply locally, so a horizon-ignorant peer stays correct (§6.3).
The handler reads the pragma into the request's cone and serves it through the existing keep machinery rather than a parallel path: the pull inventory's keep set drops the blobs below the horizon (via ConeReferences.HorizonBelow) and every cluster; the clone walk walks the tree of a held-set check-in only, counts a parent as sent only inside the held set, and sends every sub-manifest node at its row while sending no cluster. A crossing delta is inflated through the same sourceKept test the path cone uses, charged to the round's batch. req-clusters under a horizon answers nothing, and every reply says horizon-served content.
A horizon store refuses, through refuseClone, a dense clone and a horizon whose held set is not inside its own, both computed over the store with horizon.Place. That is the spec's subset test; zit refuses any other C or rule. A path-only store now also refuses a horizon-only request, which keeps every path.
Interop with zit at horizon-rfc bbd1435674 holds both ways: zit clones and pulls at a horizon from the handler, the client from `zit serve`, and each side's horizon store refuses the other's dense clone. (user: [email protected], size: 4038) [annotate] [blame] [check-ins using] [more...]
package sync import ( "fmt" "github.com/danmestas/go-libfossil/internal/xfer" ) // cloneResume is the walk state a cookie echoes: the row the round starts at // (aware), and for a check-in whose walk outgrew a round, the last rid of that // walk already sent (set). type cloneResume struct { row, last int aware, set bool } func cloneResumeFromCards(cards []xfer.Card) cloneResume { var resume cloneResume for _, card := range cards { c, ok := card.(*xfer.CookieCard) if !ok { continue } if fields, ok := cloneCapFields(c.Value); ok { resume = cloneResume{} if len(fields) >= 2 { resume = cloneResume{row: fields[1], aware: true} } if len(fields) == 3 { resume.last, resume.set = fields[2], true } } } return resume } func hasPullCard(cards []xfer.Card) bool { for _, card := range cards { if _, ok := card.(*xfer.PullCard); ok { return true } } return false } // emitCloneResume sends a sparse walk's single cookie: the snapshot bound // with the row the next round starts at, and a part-walked check-in's // progress. A client echoing the cookie verbatim carries both back. func (h *handler) emitCloneResume(round *cloneBatch) { if h.keep == nil || h.keep.walk == nil { return } value := fmt.Sprintf("%s%d", cloneCapPrefix, h.cloneSnapMax) switch { case round.pending.set: value = fmt.Sprintf("%s:%d:%d", value, round.pending.row, round.pending.last) case round.nextRID > 0: value = fmt.Sprintf("%s:%d", value, round.nextRID) } h.resp = append(h.resp, &xfer.CookieCard{Value: value}) } // cloneSparseRow is §6.2 for one row: content and sub-manifest nodes wait // for the check-in walk that reaches them in the cone, and are charged here // as an excluded row is, as is a cluster under a horizon; every other // artifact goes out at its row, followed by what its walk reaches, each // checked against the round's budget. func (h *handler) cloneSparseRow(round *cloneBatch, rid int, uuid string, fullSize int) (bool, error) { w := h.keep.walk kind, d := w.classify(rid) already := round.sent[rid] if !w.atRow(kind) { if !already { round.bytesSent += cloneCardOverheadBytes + max(fullSize, 0) } return true, nil } resuming := round.resume.set && round.resume.row == rid if !resuming && !already { sent, err := h.cloneSend(round, rid, uuid, fullSize) if err != nil || !sent { round.nextRID = rid return false, err } } if d == nil { return true, nil } items, err := w.items(rid, d) if err != nil { return false, err } last := 0 if resuming { last = round.resume.last } return h.cloneWalkItems(round, rid, items, last) } // cloneWalkItems sends a walk's rows above last, ascending. A full round // leaves the check-in as the cursor and its progress pending. func (h *handler) cloneWalkItems(round *cloneBatch, row int, items []int, last int) (bool, error) { for _, item := range items { if item <= last || round.sent[item] { continue } info, ok, err := h.loadCloneBlob(item) if err != nil { return false, err } if !ok { continue } sent := false if round.count == 0 || round.bytesSent < round.budget { if sent, err = h.cloneSend(round, item, info.uuid, info.size); err != nil { return false, err } } if !sent { return false, h.cloneWalkFull(round, row, last) } last = item } return true, nil } // cloneWalkFull ends a round inside row's walk. A request that started at row // without echoing the walk's state comes from a client that drops it: it // would return here forever, so the cursor steps past row and what the walk // still owed reaches it as transit phantoms its next pull names (§6.4). func (h *handler) cloneWalkFull(round *cloneBatch, row, last int) error { if row < round.entry { panic("sync.cloneWalkFull: walk row below the round's cursor") } if row == round.entry && round.resume.row != row { round.nextRID = row + 1 return nil } round.nextRID = row round.pending = cloneResume{row: row, last: last, aware: true, set: true} return nil }