Artifact 074894ffdb5f6524392bcbe639071d25d6dca5f04c77884d4c42bc0a9e73cb18:
- File
internal/sync/horizon_memo.go
— part of check-in
[83f8e43ebd]
at
2026-10-22 22:26:27
on branch feat/horizon-cones
— feat(sync): an origin classifies horizon requests through its memo
Each request re-read the store to classify its cone, so a pull after a one-artifact push to a large origin cost a full read. The handler now takes each store's classification memo (one per pool DB, dropped with it) and classifies its inventory and its structural reach through a view of it. The view is published only once the request has committed and stored no artifact: the inventory classifies before received files are crosslinked, so such a view describes a store that never committed. A caller holding its own transaction (a bound DB) classifies afresh.
SparseClassified now reports what the classification read: the whole store on a full read, the arrivals on an extension, none when the memo covers the store.
A property test drives random commits in every manifest form, pushes in any order and announcements against an origin and checks, after each step, that the memo keeps exactly what a fresh classification keeps for path cones, content and structural horizons under both rules. (user: [email protected], size: 4025) [annotate] [blame] [check-ins using]
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) // 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) 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) 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 view is the handler's to // publish when classify is set. func (h *handler) classifier(ctx context.Context, q db.Querier, publish bool) (classifier, error) { if h.memo == nil { return freshClassifier{}, nil } v, err := h.memo.View(ctx, q) if err != nil { return nil, err } if publish { 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() }