Artifact c65dd2bb19e3bca7e988a23e359630a2860dcd37e9de7137ca282c765d34d5ec:
- File
internal/sync/sparse.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: 3643) [annotate] [blame] [check-ins using]
package sync import ( "fmt" "github.com/danmestas/go-libfossil/internal/cone" "github.com/danmestas/go-libfossil/internal/deck" "github.com/danmestas/go-libfossil/internal/xfer" ) // The draft defines request pragmas, but no capability advertisement spelling. // Unknown pragmas remain ignored by peers that predate sparse transfer. // coneServedPragma acknowledges a reply that applied the request's whole cone // (draft-fossil-sparse-cone-00 §6.3). It travels only from server to client. const coneServedPragma = "cone-served" // horizonPragma carries a request's horizon, `pragma horizon HASH RULE`, and // horizonServedPragma the level an origin serves it at, `pragma // horizon-served LEVEL` (draft-fossil-horizon-00 §6.1, §6.2.5). This // implementation serves and asks for the content level only. const ( horizonPragma = "horizon" horizonServedPragma = "horizon-served" ) // conePragmas spells c for a request: its path members, then its horizon. // The horizon goes without horizon-structural whatever depth c records. func conePragmas(c *cone.Cone) []xfer.Card { if c == nil { return nil } cards := make([]xfer.Card, 0, len(c.Include())+len(c.Exclude())+1) for _, m := range c.Include() { cards = append(cards, &xfer.PragmaCard{Name: "cone", Values: []string{deck.FossilEncode(m)}}) } for _, m := range c.Exclude() { cards = append(cards, &xfer.PragmaCard{Name: "cone-not", Values: []string{deck.FossilEncode(m)}}) } if h, ok := c.Horizon(); ok { cards = append(cards, &xfer.PragmaCard{Name: horizonPragma, Values: []string{h.Hash(), h.Rule().String()}}) } return cards } // requestCone reads a request's cone: its path members and its horizon, at // the content depth. A malformed horizon pragma is ignored, as zit ignores // one, and the last well-formed one stands. func requestCone(cards []xfer.Card) (*cone.Cone, error) { var include, exclude []string var horizon *cone.Horizon for _, card := range cards { p, ok := card.(*xfer.PragmaCard) if !ok { continue } if p.Name == horizonPragma { if h, ok := requestHorizon(p); ok { horizon = &h } continue } if p.Name != "cone" && p.Name != "cone-not" { continue } if len(p.Values) != 1 || len(p.Values[0]) > 2*cone.MaxMemberLen { return nil, fmt.Errorf("sparse cone: invalid member") } if len(include)+len(exclude) >= cone.MaxMembers { return nil, fmt.Errorf("sparse cone: too many members") } m := deck.FossilDecode(p.Values[0]) if p.Name == "cone" { include = append(include, m) } else { exclude = append(exclude, m) } } if len(include)+len(exclude) == 0 && horizon == nil { return nil, nil } var c *cone.Cone var err error if horizon != nil { c, err = cone.NewWithHorizon(include, exclude, *horizon) } else { c, err = cone.New(include, exclude) } if err != nil { return nil, fmt.Errorf("sparse cone: %w", err) } return c, nil } func requestHorizon(p *xfer.PragmaCard) (cone.Horizon, bool) { if len(p.Values) != 2 { return cone.Horizon{}, false } rule, err := cone.ParseHorizonRule(p.Values[1]) if err != nil { return cone.Horizon{}, false } h, err := cone.NewHorizon(p.Values[0], rule, cone.DepthContent) return h, err == nil } // servesContentHorizon reports a horizon-served pragma at the content level, // the only one this client asks for. Another level is not taken up: the // client never enters the structural horizon (§6.2.5), and every reply is // classified locally regardless, so this only reports the mode. func servesContentHorizon(p *xfer.PragmaCard) bool { return len(p.Values) >= 1 && p.Values[0] == cone.DepthContent.String() }