Artifact 4f1b256d0e7d931538aaffc2854d68a2b1ddf9c50674141b2f407455617cf828:
- File
internal/content/memo.go
— part of check-in
[e5bdac3d33]
at
2026-10-22 22:18:31
on branch feat/horizon-cones
— feat(content): an origin classification memo extended by appends
An origin re-reads its store for every horizon request: the reference facts behind the path classification and the trees of the whole held set. draft-fossil-horizon-00 §6.2 asks it to keep the classification keyed by cone digest and store prefix, extend it by the artifacts appended since, and recompute in full only when an arrival carries a name a held P, B or S card cited while it was absent.
Memo keeps the store's cone-independent references (the reference facts, the check-in graph and the dangling P/B/S names) at a prefix, and per cone digest and per horizon the classification over them. A view extends each by the appended and filled rids: the loader's decisions for those rids alone, classification of the facts' new tails, and the tree walk of the check-ins K gained. Anything other than an append or an uncited fill (a cited arrival, a deleted or phantomized row, a first tree) reads the store in full. Views are copy-on-write and reach the memo only when published, so a rolled-back request leaves nothing behind. (user: [email protected], size: 16214) [annotate] [blame] [check-ins using]
package content import ( "context" "database/sql" "encoding/json" "errors" "fmt" "maps" "slices" "sync" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/cone" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/horizon" ) // memoCones and memoCuts bound what a Memo keeps beside its store graph: // the classifications of the cones and the cuts of the horizons most // recently published. Each costs about one classification's sets. const ( memoCones = 8 memoCuts = 8 ) // Memo is an origin's classification memo for one open store // (draft-fossil-horizon-00 §6.2). It keeps the store's cone-independent // references, read once and extended by the artifacts appended since; and, // keyed by cone digest and by horizon, the classifications computed over // them, each keyed by the store prefix it covers and extended the same way. // It recomputes in full only when an appended artifact carries a name a held // P, B or S card cited while it was absent, or the store changed other than // by appending and filling phantoms. // // A classification is read from a View, which brings the memo up to the // store as one querier sees it, and the View is published back once what it // read is known to be durable. A View that is never published changes // nothing, so an origin classifying inside a request that rolls back leaves // no trace. Every value a Memo hands out is immutable; extending copies. type Memo struct { mu sync.Mutex store *memoStore cones []*memoCone // least recently published first cuts []*memoCut lineage uint64 // the last store read in full } // NewMemo returns an empty memo. func NewMemo() *Memo { return &Memo{} } // memoStore is the store's cone-independent references at one prefix: the // facts a path classification reads (kinds, F, P, B, S and A references), // the check-in graph a held set is computed over, and the P, B and S names // held artifacts cite that the store does not hold. Stores of one lineage // extend one another; a full read starts a lineage. type memoStore struct { lineage uint64 prefix storePrefix refs *referenceState graph *horizon.Graph dangling map[string]bool } // storePrefix is what a store held when a memoStore read it: its highest // rid, that rid's name, its row count, and its phantoms. The store goes on // from it by appending rows and filling phantoms; anything else (a row // deleted or renamed, an artifact made a phantom) is another store. type storePrefix struct { top libfossil.FslID topUUID string count int64 phantoms map[libfossil.FslID]bool } func readPrefix(q db.Querier) (storePrefix, error) { p := storePrefix{phantoms: map[libfossil.FslID]bool{}} err := q.QueryRow(`SELECT coalesce(max(rid),0), count(*), coalesce((SELECT uuid FROM blob WHERE rid=(SELECT max(rid) FROM blob)),'') FROM blob`).Scan(&p.top, &p.count, &p.topUUID) if err != nil { return storePrefix{}, fmt.Errorf("classification memo: store prefix: %w", err) } err = referenceRows(context.Background(), q, "SELECT rid FROM phantom", func(scan func(...any) error) error { var rid libfossil.FslID err := scan(&rid) p.phantoms[rid] = true return err }) if err != nil { return storePrefix{}, fmt.Errorf("classification memo: phantoms: %w", err) } return p, nil } func (p storePrefix) equal(o storePrefix) bool { return p.top == o.top && p.topUUID == o.topUUID && p.count == o.count && maps.Equal(p.phantoms, o.phantoms) } // MemoView is a Memo brought up to one store as one querier sees it. It is // used by one goroutine; Publish hands what it computed back to the Memo. type MemoView struct { memo *Memo store *memoStore read int cones map[string]*memoCone cuts map[cutKey]*memoCut } // View reads q's store prefix and returns the memo extended to it: by the // artifacts appended since the memo's prefix and the phantoms filled since, // or read in full when that is the only exact answer. func (m *Memo) View(ctx context.Context, q db.Querier) (*MemoView, error) { if m == nil || ctx == nil || q == nil { panic("content.Memo.View: nil memo, context or querier") } m.mu.Lock() base := m.store m.mu.Unlock() now, err := readPrefix(q) if err != nil { return nil, err } v := &MemoView{memo: m, cones: map[string]*memoCone{}, cuts: map[cutKey]*memoCut{}} if base != nil && base.prefix.equal(now) { v.store = base return v, nil } if base != nil { s, read, err := base.extend(ctx, q, now) if err != nil { return nil, err } if s != nil { v.store, v.read = s, read return v, nil } } m.mu.Lock() m.lineage++ lineage := m.lineage m.mu.Unlock() if v.store, err = loadMemoStore(ctx, q, now, lineage); err != nil { return nil, err } v.read = len(v.store.refs.facts.names) return v, nil } // Read is how many artifacts the view classified to bring the memo up to its // store: every one on a full read, those appended or filled on an // extension, none when the memo already covered the store. func (v *MemoView) Read() int { return v.read } // Publish makes what v computed the memo's, unless the memo has since been // brought further. Call it only once the store v read is durable. func (v *MemoView) Publish() { m := v.memo m.mu.Lock() defer m.mu.Unlock() if m.store != nil && m.store.prefix.top > v.store.prefix.top { return } m.store = v.store for key, c := range v.cones { m.cones = slices.DeleteFunc(m.cones, func(o *memoCone) bool { return o.key == key }) m.cones = append(m.cones, c) } if over := len(m.cones) - memoCones; over > 0 { m.cones = slices.Delete(m.cones, 0, over) } for key, c := range v.cuts { m.cuts = slices.DeleteFunc(m.cuts, func(o *memoCut) bool { return o.key == key }) m.cuts = append(m.cuts, c) } if over := len(m.cuts) - memoCuts; over > 0 { m.cuts = slices.Delete(m.cuts, 0, over) } } func loadMemoStore(ctx context.Context, q db.Querier, prefix storePrefix, lineage uint64) (*memoStore, error) { refs, _, err := loadReferenceState(ctx, q, defaultReferenceLimits.refs, true) if err != nil { return nil, err } graph, err := horizon.LoadGraph(ctx, q) if err != nil { return nil, err } dangling, err := danglingNames(ctx, q, refs.facts.records, "") if err != nil { return nil, err } return &memoStore{lineage: lineage, prefix: prefix, refs: refs, graph: graph, dangling: dangling}, nil } // extend returns s brought up to the store now describes, and how many // artifacts it classified, or nil when only a full read is exact. func (s *memoStore) extend(ctx context.Context, q db.Querier, now storePrefix) (*memoStore, int, error) { if now.top < s.prefix.top || now.count < s.prefix.count { return nil, 0, nil } var uuid string var count int64 err := q.QueryRow("SELECT coalesce((SELECT uuid FROM blob WHERE rid=?1),''), (SELECT count(*) FROM blob WHERE rid<=?1)", s.prefix.top).Scan(&uuid, &count) if err != nil { return nil, 0, fmt.Errorf("classification memo: store prefix: %w", err) } if uuid != s.prefix.topUUID || count != s.prefix.count { return nil, 0, nil } var filled []libfossil.FslID for rid := range s.prefix.phantoms { if !now.phantoms[rid] { filled = append(filled, rid) } } for rid := range now.phantoms { if rid <= s.prefix.top && !s.prefix.phantoms[rid] { return nil, 0, nil // made a phantom: not an append } } fresh, cited, err := s.arrivals(q, filled) if err != nil || cited { return nil, 0, err } refs, exact, err := extendReferenceState(ctx, q, s.refs, fresh) if err != nil || !exact { return nil, 0, err } graph, err := s.graph.Extend(ctx, q, fresh) if err != nil { return nil, 0, err } ids, err := ridsJSON(fresh) if err != nil { return nil, 0, err } added, err := danglingNames(ctx, q, refs.facts.records[len(s.refs.facts.records):], ids) if err != nil { return nil, 0, err } dangling := maps.Clone(s.dangling) maps.Copy(dangling, added) return &memoStore{lineage: s.lineage, prefix: now, refs: refs, graph: graph, dangling: dangling}, len(fresh), nil } // arrivals lists the rids appended past s's prefix and the phantoms filled // since, ascending, and reports whether one now held carries a name a held // P, B or S card cited while it was absent (§6.2): such an arrival can bring // old check-ins into a held set and old trees into a reach. func (s *memoStore) arrivals(q db.Querier, filled []libfossil.FslID) ([]libfossil.FslID, bool, error) { ids, err := ridsJSON(filled) if err != nil { return nil, false, err } if ids == "" { ids = "[]" } var fresh []libfossil.FslID cited := false err = referenceRows(context.Background(), q, `SELECT rid, uuid, size>=0 FROM blob WHERE rid>?1 OR rid IN (SELECT value FROM json_each(?2)) ORDER BY rid`, func(scan func(...any) error) error { var rid libfossil.FslID var uuid string var held bool err := scan(&rid, &uuid, &held) fresh = append(fresh, rid) cited = cited || held && s.dangling[uuid] return err }, s.prefix.top, ids) if err != nil { return nil, false, fmt.Errorf("classification memo: arrivals: %w", err) } return fresh, cited, nil } // danglingNames returns the names P, B and S cards of records cite, and the // parents and baselines plink names for the check-ins ids lists (every one // when ids is empty), that the store does not hold. func danglingNames(ctx context.Context, q db.Querier, records []coneReferenceRecord, ids string) (map[string]bool, error) { var cited []string for _, record := range records { d := record.deck cited = append(cited, d.P...) if d.B != "" { cited = append(cited, d.B) } for _, sc := range d.S { cited = append(cited, sc.UUID) } } out := map[string]bool{} add := func(scan func(...any) error) error { var uuid string err := scan(&uuid) out[uuid] = true return err } if len(cited) > 0 { names, err := jsonStrings(cited) if err != nil { return nil, err } err = referenceRows(ctx, q, `SELECT DISTINCT value FROM json_each(?1) WHERE NOT EXISTS(SELECT 1 FROM blob WHERE uuid=value AND size>=0)`, add, names) if err != nil { return nil, err } } for _, column := range []string{"pid", "baseid"} { query, args := restrictTo("SELECT b.uuid FROM plink JOIN blob b ON b.rid=plink."+column+ " WHERE b.size<0%s", "plink.cid", ids) if err := referenceRows(ctx, q, query, add, args...); err != nil { return nil, err } } return out, nil } // jsonStrings spells names as a JSON array for json_each. func jsonStrings(names []string) (string, error) { data, err := json.Marshal(names) if err != nil { return "", fmt.Errorf("classification memo: %w", err) } return string(data), nil } // References returns the path classification of c (see LoadConeReferences) // over the view's store: the memo's classification of c extended by what // the store gained since, or one computed over the store graph. func (v *MemoView) References(ctx context.Context, c *cone.Cone) (*ConeReferences, error) { if v == nil || ctx == nil { panic("content.MemoView.References: nil view or context") } facts := v.store.refs.facts if c == nil { return facts.Classify(ctx, nil) } key := c.Digest() prev := v.cones[key] if prev == nil { prev = v.memo.lookupCone(key) } if prev != nil && prev.lineage == v.store.lineage && prev.mark == markAt(facts, prev.mark.visited) { return prev.refs, nil } var p *ConeReferences from := factsMark{} if prev != nil && prev.lineage == v.store.lineage { p, from = facts.references(prev.refs), prev.mark } else { p = facts.references(nil) } through, err := facts.classifyFrom(ctx, c, p, from) if err != nil { return nil, err } v.cones[key] = &memoCone{key: key, lineage: v.store.lineage, mark: through, refs: p} return p, nil } // markAt is facts' mark with visited, which a mark compares on too. func markAt(facts *ConeReferenceFacts, visited int) factsMark { m := facts.mark() m.visited = visited return m } // memoCone is a cone's classification over a store lineage, through mark. type memoCone struct { key string lineage uint64 mark factsMark refs *ConeReferences } func (m *Memo) lookupCone(key string) *memoCone { m.mu.Lock() defer m.mu.Unlock() for _, c := range m.cones { if c.key == key { return c } } return nil } // cutKey names a horizon's cut, which neither its depth nor the request's // path members change. type cutKey struct { hash string rule cone.HorizonRule } // memoCut is a horizon's cut over a store lineage: the held set it walked // and what the walk produced, with the tree bytes it was charged. type memoCut struct { key cutKey lineage uint64 placed bool cut horizonCut inK map[libfossil.FslID]bool treeBytes int64 } func (m *Memo) lookupCut(key cutKey) *memoCut { m.mu.Lock() defer m.mu.Unlock() for _, c := range m.cuts { if c.key == key { return c } } return nil } // cut returns h's cut over the view's store (see horizonTrees): K over the // store graph, and the walk of K's trees extended from the memo's walk by // the check-ins K gained, which reads only their trees. A walk is a // reachability walk with one visited set, so walking the new roots into // the old sets is walking every root. func (v *MemoView) cut(ctx context.Context, q db.Querier, h cone.Horizon) (*memoCut, error) { key := cutKey{hash: h.Hash(), rule: h.Rule()} prev := v.cuts[key] if prev == nil { prev = v.memo.lookupCut(key) } if prev != nil && prev.lineage != v.store.lineage { prev = nil } var c libfossil.FslID err := q.QueryRow("SELECT rid FROM blob WHERE uuid=?", h.Hash()).Scan(&c) if errors.Is(err, sql.ErrNoRows) { return v.keepCut(&memoCut{key: key, lineage: v.store.lineage}), nil } if err != nil { return nil, fmt.Errorf("horizon: C: %w", err) } k, err := v.store.graph.HeldSet(ctx, c, h.Rule()) if errors.Is(err, horizon.ErrNotHeld) { return v.keepCut(&memoCut{key: key, lineage: v.store.lineage}), nil } if err != nil { return nil, err } out := &memoCut{key: key, lineage: v.store.lineage, placed: true, inK: make(map[libfossil.FslID]bool, len(k))} for _, rid := range k { out.inK[rid] = true } if prev != nil && prev.placed { for rid := range prev.inK { if !out.inK[rid] { prev = nil // K only grows within a lineage; walk it afresh if not break } } } var roots []libfossil.FslID for _, rid := range k { if prev == nil || !prev.inK[rid] { roots = append(roots, rid) } } if prev != nil && prev.placed && len(roots) == 0 && len(k) == len(prev.inK) { return prev, nil } walk := newTreeWalk(ctx, q, k, defaultReferenceLimits, &ReferenceWork{}) if prev != nil && prev.placed { walk.files, walk.visited = maps.Clone(prev.cut.above), maps.Clone(prev.cut.reach) walk.work.TreeBytes = prev.treeBytes } above, err := walk.named(roots) if err != nil { return nil, err } out.cut = horizonCut{k: k, above: above, reach: walk.visited} out.treeBytes = walk.work.TreeBytes return v.keepCut(out), nil } func (v *MemoView) keepCut(c *memoCut) *memoCut { v.cuts[c.key] = c return c } // HorizonReach is HorizonReach over the view's store. func (v *MemoView) HorizonReach( ctx context.Context, q db.Querier, h cone.Horizon, ) (k []libfossil.FslID, reach map[string]bool, placed bool, err error) { if v == nil || ctx == nil || q == nil { panic("content.MemoView.HorizonReach: nil view, context or querier") } c, err := v.cut(ctx, q, h) if err != nil || !c.placed { return nil, nil, false, err } return c.cut.k, c.cut.reach, true, nil } // HorizonBelow is p.HorizonBelow over the view's store, p a classification // the view returned. func (v *MemoView) HorizonBelow( ctx context.Context, q db.Querier, p *ConeReferences, c *cone.Cone, ) (func(libfossil.FslID) bool, error) { if v == nil || ctx == nil || q == nil || p == nil || c == nil { panic("content.MemoView.HorizonBelow: nil view, context, querier, references or cone") } h, ok := c.Horizon() if !ok { return nil, nil } cut, err := v.cut(ctx, q, h) if err != nil || !cut.placed { return nil, err } return func(rid libfossil.FslID) bool { return p.below(cut.cut.above, rid) }, nil }