Skip to content
quarry Artifact [6a29b54fb3]
Login

Artifact [6a29b54fb3]

Artifact 6a29b54fb396d2b2b7c3c957c7dc8f93bae13fade184111c5d73c0953f2d18f5:


package zitcli

import (
	"container/list"
	"sync"
)

// blobCache is a byte-bounded LRU over zit reads.
//
// Bounded by BYTES, not entries, because the values are file contents:
// the listing memo's 64-entry cap is fine for listings but meaningless
// for Cat, where one entry can be a 30MB blob and sixty-four of them is
// two gigabytes of resident memory.
//
// Per repository rather than shared across the fleet. A shared budget
// uses memory better, but it lets one busy store evict another's
// entries, so a reader on a quiet repository pays for traffic they have
// nothing to do with. Predictable beats optimal here.
//
// EVERY key must be derived from an immutable version. That is not a
// property this type can check, so it is the caller's obligation and the
// reason ops resolve names to hashes before reading โ€” see
// Deps.resolveVersion. A moving name cached here would serve stale
// content forever.
type blobCache struct {
	mu    sync.Mutex
	max   int64
	used  int64
	ll    *list.List               // front = most recently used
	items map[string]*list.Element // key -> element holding *cacheEntry
}

type cacheEntry struct {
	key  string
	val  []byte
	size int64
}

func newBlobCache(max int64) *blobCache {
	return &blobCache{max: max, ll: list.New(), items: map[string]*list.Element{}}
}

// get returns a cached value and marks it recently used. The returned
// slice is the cached one, so callers must not mutate it โ€” every caller
// hands it straight to a reader or a parser.
func (c *blobCache) get(key string) ([]byte, bool) {
	if c == nil {
		return nil, false
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	el, ok := c.items[key]
	if !ok {
		return nil, false
	}
	c.ll.MoveToFront(el)
	return el.Value.(*cacheEntry).val, true
}

// put stores a value, evicting least-recently-used entries until it
// fits. A single value larger than the whole budget is NOT cached and
// not an error: admitting it would evict everything else to hold one
// item, which is worse than missing.
func (c *blobCache) put(key string, val []byte) {
	if c == nil || int64(len(val)) > c.max {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if el, ok := c.items[key]; ok {
		e := el.Value.(*cacheEntry)
		c.used -= e.size
		e.val, e.size = val, int64(len(val))
		c.used += e.size
		c.ll.MoveToFront(el)
		return
	}
	e := &cacheEntry{key: key, val: val, size: int64(len(val))}
	c.items[key] = c.ll.PushFront(e)
	c.used += e.size
	for c.used > c.max {
		back := c.ll.Back()
		if back == nil {
			return
		}
		old := c.ll.Remove(back).(*cacheEntry)
		delete(c.items, old.key)
		c.used -= old.size
	}
}

// stats reports entries and bytes held, for tests and diagnostics.
func (c *blobCache) stats() (entries int, bytes int64) {
	if c == nil {
		return 0, 0
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	return len(c.items), c.used
}