Artifact 6148456f48c6a0774dc137fb37aaa2aefcafb00fd3c77243f92ea1a4280235af:
- File
repo_content_cache.go
— part of check-in
[d367012009]
at
2026-10-25 23:10:47
on branch feat/read-blob-content-cache
— feat: read an artifact by hash and cache content on Repo
Repo.ReadBlob expands one artifact by its full hash after the availability check ReadFile applies, and names an unknown hash with ErrArtifactNotFound.
Repo.SetContentCache attaches a byte-bounded content.Cache that ReadFile, ReadFileAt, ReadBlob, ListFiles, Diff and Annotate share; check-in parses are kept in its derived memo, so reads at one check-in parse the manifest once. Default off. A read that finds the DB's finished-write count moved swaps in an empty cache, so a purged, compacted or elided artifact, and a rid SQLite reuses after a purge, is never served from it. Writes by another process are not seen; the method's doc says so.
Tracked in #492. (user: [email protected], size: 3935) [annotate] [blame] [check-ins using] [more...]
package libfossil import ( "fmt" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/hash" ) // contentCache is the cache SetContentCache attached, valid for the store as // it stood after writes finished writes (db.DB.Writes). // // Content named by hash never changes, but a write can take it away: a purge // deletes the row, and SQLite gives a deleted newest rid to the next blob // stored; compaction and horizon or cone changes turn held blobs into // phantoms. What the cache keeps under a rid, and the manifests parsed from // it, are therefore good only until the next write. Rather than have every // such write know about the cache, a read that sees the write count moved // replaces the cache with an empty one. Reads in flight keep the cache they // started with, so nothing they store lands in its successor. type contentCache struct { cache *content.Cache bytes int64 writes int64 } // SetContentCache attaches a cache of expanded artifact content, bounded by // bytes, that ReadFile, ReadFileAt, ReadBlob, ListFiles, Diff and Annotate // share. It replaces any cache already attached; bytes <= 0 detaches it. The // default is no cache. It is safe to call while other goroutines read. // // The bound covers expanded content; the parses of the few check-in // manifests read most recently are kept beside it. Any write through this // Repo empties the cache. A write by another process, or through another // Repo opened on the same file, does not: attach a cache only where this // Repo is the store's one writer, or where no other writer purges, compacts // or narrows it. func (r *Repo) SetContentCache(bytes int64) { if bytes <= 0 { r.cache.Store(nil) return } r.cache.Store(newContentCache(bytes, r.DB().Writes())) } func newContentCache(bytes, writes int64) *contentCache { if bytes <= 0 { panic("libfossil.newContentCache: bytes must be > 0") } return &contentCache{cache: content.NewCache(bytes), bytes: bytes, writes: writes} } // liveCache returns the cache a read may use now, or nil to read uncached. // A cache that predates the latest finished write is replaced by an empty // one first. Losing that swap to another reader or to SetContentCache leaves // this read uncached, which is always correct. func (r *Repo) liveCache() *content.Cache { cc := r.cache.Load() if cc == nil { return nil } writes := r.DB().Writes() if cc.writes == writes { return cc.cache } fresh := newContentCache(cc.bytes, writes) if !r.cache.CompareAndSwap(cc, fresh) { return nil } return fresh.cache } // ReadBlob returns the content of the artifact named uuid, a full SHA1 or // SHA3-256 hash in lowercase hex, such as the UUID ListFiles reports for a // file. An unknown hash returns ErrArtifactNotFound (wrapped). An artifact // the store knows but does not hold, such as one elided below a horizon or // outside a sparse cone, fails as ReadFile of a path naming it does. func (r *Repo) ReadBlob(uuid string) ([]byte, error) { if uuid == "" { return nil, fmt.Errorf("libfossil: read blob: uuid is required") } if !hash.IsValidHash(uuid) { return nil, fmt.Errorf("libfossil: read blob: %q is not a full lowercase artifact hash", uuid) } if _, known := blob.Exists(r.DB(), uuid); !known { return nil, fmt.Errorf("libfossil: read blob %s: %w", uuid, ErrArtifactNotFound) } data, err := readHeld(r, r.liveCache(), uuid) if err != nil { return nil, fmt.Errorf("libfossil: read blob: %w", err) } return data, nil } // readHeld expands the artifact named uuid through cache (nil reads uncached) // once the store proves it holds it, as every read by hash here does. func readHeld(r *Repo, cache *content.Cache, uuid string) ([]byte, error) { rid, err := content.RequireAvailableByUUID(r.DB(), uuid) if err != nil { return nil, err } return cache.Expand(r.DB(), rid) }