Artifact f8a265c4b77d8db50406049a17c1e160cc6ff860506a1f288ff6b8f80a8a4c1f:
- File
internal/sync/horizon_memo.go
— part of check-in
[0de31d6f8e]
at
2026-10-22 23:43:27
on branch feat/horizon-cones
— feat(sync): withhold out-of-cone nodes of the trees a horizon keeps
Under either horizon with path members an origin now sends only the sub-manifest nodes on paths into the cone of the trees its horizon keeps (every check-in's at the content depth, K's at the structural one), announces none of the rest and answers no gimme for them; a request without path members still receives every node (draft-fossil-horizon-00 §6.2 at zit 3a2df62515).
The walk is zit's walkSlots (src/sparse.zig): only slots that may hold an in-cone path are descended, a last slot takes its parent's bound, and a node is walked again only under a wider bound. It runs over the classification's facts, never the store, and the origin's memo keeps it by cone so a later request walks only the roots appended since, falling back to a fresh walk when a root arrives below the ones walked. A clone round sends nodes by the per-check-in tree walk it already uses without a horizon, walking a check-in outside the held set for its nodes alone.
Each kept node records the lowest root that keeps it, so a cursor is void (§6.2.6) exactly when some node at or below it is kept only by a root above it: stateless, and the same for a fresh classification and the memo, where zit voids every cursor below the store count at the moment its sidecar sees the change.
The memo returned a cone's earlier classification whenever no fact list grew, keeping that store's names, so an artifact appended or filled without becoming a record read as unnamed; it now carries the current facts. Found by the memo equivalence test, which now covers path cones under both horizons, the slot walk, cursor voiding and a tree form that widens a node's bound. (user: [email protected], size: 4631) [annotate] [blame] [check-ins using] [more...]
package sync import ( "context" "runtime" gosync "sync" "weak" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/cone" "github.com/danmestas/go-libfossil/internal/content" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" ) // originMemos holds the classification memo of each store this process // serves (draft-fossil-horizon-00 §6.2), keyed by the store's pool DB and // dropped when that DB is collected. var originMemos = struct { gosync.Mutex m map[weak.Pointer[db.DB]]*content.Memo }{m: map[weak.Pointer[db.DB]]*content.Memo{}} // originMemo returns d's memo, nil when d is bound to a transaction it does // not own: a memo is published only after the request that read the store // commits, which such a caller decides. func originMemo(d *db.DB) *content.Memo { if d == nil { panic("sync.originMemo: nil DB") } if d.Bound() { return nil } key := weak.Make(d) originMemos.Lock() defer originMemos.Unlock() if m := originMemos.m[key]; m != nil { return m } m := content.NewMemo() originMemos.m[key] = m runtime.AddCleanup(d, func(key weak.Pointer[db.DB]) { originMemos.Lock() defer originMemos.Unlock() delete(originMemos.m, key) }, key) return m } // classifier is how an origin classifies a request's cone: over the store // read afresh, or over its memo. type classifier interface { references(ctx context.Context, q db.Querier, c *cone.Cone) (*content.ConeReferences, error) below(ctx context.Context, q db.Querier, refs *content.ConeReferences, c *cone.Cone) (func(libfossil.FslID) bool, error) reach(ctx context.Context, q db.Querier, h cone.Horizon) ([]libfossil.FslID, map[string]bool, bool, error) // slots is the walk of the trees a horizon keeps for c's path members: // every held tree, or with reach those a structural horizon reaches. slots(ctx context.Context, refs *content.ConeReferences, c *cone.Cone, reach map[string]bool) (*content.SlotKeep, error) // read is how many artifacts classifying read from the store. read(refs *content.ConeReferences) int } type freshClassifier struct{} func (freshClassifier) references(ctx context.Context, q db.Querier, c *cone.Cone) (*content.ConeReferences, error) { return content.LoadConeReferences(ctx, q, c) } func (freshClassifier) below( ctx context.Context, q db.Querier, refs *content.ConeReferences, c *cone.Cone, ) (func(libfossil.FslID) bool, error) { return refs.HorizonBelow(ctx, q, c) } func (freshClassifier) reach(ctx context.Context, q db.Querier, h cone.Horizon) ([]libfossil.FslID, map[string]bool, bool, error) { return content.HorizonReach(ctx, q, h) } func (freshClassifier) slots( ctx context.Context, refs *content.ConeReferences, c *cone.Cone, reach map[string]bool, ) (*content.SlotKeep, error) { return refs.SlotKeep(ctx, c, reach) } func (freshClassifier) read(refs *content.ConeReferences) int { return refs.Artifacts() } type memoClassifier struct{ v *content.MemoView } func (m memoClassifier) references(ctx context.Context, _ db.Querier, c *cone.Cone) (*content.ConeReferences, error) { return m.v.References(ctx, c) } func (m memoClassifier) below( ctx context.Context, q db.Querier, refs *content.ConeReferences, c *cone.Cone, ) (func(libfossil.FslID) bool, error) { return m.v.HorizonBelow(ctx, q, refs, c) } func (m memoClassifier) reach(ctx context.Context, q db.Querier, h cone.Horizon) ([]libfossil.FslID, map[string]bool, bool, error) { return m.v.HorizonReach(ctx, q, h) } func (m memoClassifier) slots( ctx context.Context, refs *content.ConeReferences, c *cone.Cone, reach map[string]bool, ) (*content.SlotKeep, error) { return m.v.SlotKeep(ctx, refs, c, reach) } func (m memoClassifier) read(*content.ConeReferences) int { return m.v.Read() } // classifier returns how this request classifies over q: through the // store's memo when it has one, else afresh. The memo's view is the // handler's to publish. func (h *handler) classifier(ctx context.Context, q db.Querier) (classifier, error) { if h.memo == nil { return freshClassifier{}, nil } v, err := h.memo.View(ctx, q) if err != nil { return nil, err } h.view = v return memoClassifier{v}, nil } // publishView hands the memo what this request's classification computed, // once the request is known to have committed and to have stored no // artifact: the inventory classifies before the request's received files // are crosslinked, so a view from such a request describes a store that // never committed. func (h *handler) publishView() { if h.view == nil || h.fatal || h.aborted || h.filesRecvd > 0 { return } h.view.Publish() }