Files
buildkit/solver/cachemanager.go
Tonis Tiigi 044e09c28c buildkitd: add cache store debugging endpoints
These endpoints show the contents of current boltdb
cache database together with debug plaintexts if
they have been saved.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2025-07-08 00:05:53 -07:00

458 lines
11 KiB
Go

package solver
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/cachedigest"
digest "github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
)
// NewInMemoryCacheManager creates a new in-memory cache manager
func NewInMemoryCacheManager() CacheManager {
return NewCacheManager(context.TODO(), identity.NewID(), NewInMemoryCacheStorage(), NewInMemoryResultStorage())
}
// NewCacheManager creates a new cache manager with specific storage backend
func NewCacheManager(ctx context.Context, id string, storage CacheKeyStorage, results CacheResultStorage) CacheManager {
cm := &cacheManager{
id: id,
backend: storage,
results: results,
}
if err := cm.ReleaseUnreferenced(ctx); err != nil {
bklog.G(ctx).Errorf("failed to release unreferenced cache metadata: %+v", err)
}
return cm
}
type cacheManager struct {
mu sync.RWMutex
id string
backend CacheKeyStorage
results CacheResultStorage
}
func (c *cacheManager) ReleaseUnreferenced(ctx context.Context) error {
visited := map[string]struct{}{}
return c.backend.Walk(func(id string) error {
return c.backend.WalkResults(id, func(cr CacheResult) error {
if _, ok := visited[cr.ID]; ok {
return nil
}
visited[cr.ID] = struct{}{}
if !c.results.Exists(ctx, cr.ID) {
c.backend.Release(cr.ID)
}
return nil
})
})
}
func (c *cacheManager) ID() string {
return c.id
}
func (c *cacheManager) Query(deps []CacheKeyWithSelector, input Index, dgst digest.Digest, output Index) (rcks []*CacheKey, rerr error) {
depsField := make([]map[string]any, len(deps))
for i, dep := range deps {
depsField[i] = dep.TraceFields()
}
lg := bklog.G(context.TODO()).WithFields(logrus.Fields{
"cache_manager": c.id,
"op": "query",
"deps": depsField,
"input": input,
"digest": dgst,
"output": output,
"stack": bklog.TraceLevelOnlyStack(),
})
defer func() {
rcksField := make([]map[string]any, len(rcks))
for i, rck := range rcks {
rcksField[i] = rck.TraceFields()
}
lg.WithError(rerr).WithField("return_cachekeys", rcksField).Trace("cache manager")
}()
c.mu.RLock()
defer c.mu.RUnlock()
type dep struct {
results map[string]struct{}
key CacheKeyWithSelector
}
allDeps := make([]dep, 0, len(deps))
for _, k := range deps {
allDeps = append(allDeps, dep{key: k, results: map[string]struct{}{}})
}
allRes := map[string]*CacheKey{}
for _, d := range allDeps {
if err := c.backend.WalkLinks(c.getID(d.key.CacheKey.CacheKey), CacheInfoLink{input, output, dgst, d.key.Selector}, func(id string) error {
d.results[id] = struct{}{}
if _, ok := allRes[id]; !ok {
allRes[id] = c.newKeyWithID(id, dgst, output)
}
return nil
}); err != nil {
return nil, err
}
}
// link the results against the keys that didn't exist
for id, key := range allRes {
for _, d := range allDeps {
if _, ok := d.results[id]; !ok {
if err := c.backend.AddLink(c.getID(d.key.CacheKey.CacheKey), CacheInfoLink{
Input: input,
Output: output,
Digest: dgst,
Selector: d.key.Selector,
}, c.getID(key)); err != nil {
return nil, err
}
}
}
}
if len(deps) == 0 {
if !c.backend.Exists(rootKey(dgst, output).String()) {
return nil, nil
}
return []*CacheKey{c.newRootKey(dgst, output)}, nil
}
keys := make([]*CacheKey, 0, len(deps))
for _, k := range allRes {
keys = append(keys, k)
}
return keys, nil
}
func (c *cacheManager) Records(ctx context.Context, ck *CacheKey) (rrecs []*CacheRecord, rerr error) {
lg := bklog.G(context.TODO()).WithFields(logrus.Fields{
"cache_manager": c.id,
"op": "records",
"cachekey": ck.TraceFields(),
"stack": bklog.TraceLevelOnlyStack(),
})
defer func() {
rrercsField := make([]map[string]any, len(rrecs))
for i, rrec := range rrecs {
rrercsField[i] = rrec.TraceFields()
}
lg.WithError(rerr).WithField("return_records", rrercsField).Trace("cache manager")
}()
outs := make([]*CacheRecord, 0)
if err := c.backend.WalkResults(c.getID(ck), func(r CacheResult) error {
if c.results.Exists(ctx, r.ID) {
outs = append(outs, &CacheRecord{
ID: r.ID,
cacheManager: c,
key: ck,
CreatedAt: r.CreatedAt,
})
} else {
c.backend.Release(r.ID)
}
return nil
}); err != nil {
return nil, err
}
return outs, nil
}
func (c *cacheManager) Load(ctx context.Context, rec *CacheRecord) (rres Result, rerr error) {
lg := bklog.G(context.TODO()).WithFields(logrus.Fields{
"cache_manager": c.id,
"op": "load",
"record": rec.TraceFields(),
"stack": bklog.TraceLevelOnlyStack(),
})
defer func() {
rresID := "<nil>"
if rres != nil {
rresID = rres.ID()
}
lg.WithError(rerr).WithField("return_result", rresID).Trace("cache manager")
}()
c.mu.RLock()
defer c.mu.RUnlock()
res, err := c.backend.Load(c.getID(rec.key), rec.ID)
if err != nil {
return nil, err
}
return c.results.Load(ctx, res)
}
type LoadedResult struct {
Result Result
CacheResult CacheResult
CacheKey *CacheKey
}
func (r *LoadedResult) TraceFields() map[string]any {
return map[string]any{
"result": r.Result.ID(),
"cache_result": r.CacheResult.ID,
"cache_key": r.CacheKey.TraceFields(),
}
}
func (c *cacheManager) filterResults(m map[string]Result, ck *CacheKey, visited map[string]struct{}) (results []LoadedResult, err error) {
id := c.getID(ck)
if _, ok := visited[id]; ok {
return nil, nil
}
visited[id] = struct{}{}
if err := c.backend.WalkResults(id, func(cr CacheResult) error {
res, ok := m[id]
if ok {
results = append(results, LoadedResult{
Result: res,
CacheKey: ck,
CacheResult: cr,
})
delete(m, id)
}
return nil
}); err != nil {
for _, r := range results {
r.Result.Release(context.TODO())
}
}
for _, keys := range ck.Deps() {
for _, key := range keys {
res, err := c.filterResults(m, key.CacheKey.CacheKey, visited)
if err != nil {
for _, r := range results {
r.Result.Release(context.TODO())
}
return nil, err
}
results = append(results, res...)
}
}
return
}
func (c *cacheManager) LoadWithParents(ctx context.Context, rec *CacheRecord) (rres []LoadedResult, rerr error) {
lg := bklog.G(context.TODO()).WithFields(logrus.Fields{
"cache_manager": c.id,
"op": "load_with_parents",
"record": rec.TraceFields(),
"stack": bklog.TraceLevelOnlyStack(),
})
defer func() {
rresField := make([]map[string]any, len(rres))
for i, rres := range rres {
rresField[i] = rres.TraceFields()
}
lg.WithError(rerr).WithField("return_results", rresField).Trace("cache manager")
}()
lwp, ok := c.results.(interface {
LoadWithParents(context.Context, CacheResult) (map[string]Result, error)
})
if !ok {
res, err := c.Load(ctx, rec)
if err != nil {
return nil, err
}
return []LoadedResult{{Result: res, CacheKey: rec.key, CacheResult: CacheResult{ID: c.getID(rec.key), CreatedAt: rec.CreatedAt}}}, nil
}
c.mu.RLock()
defer c.mu.RUnlock()
cr, err := c.backend.Load(c.getID(rec.key), rec.ID)
if err != nil {
return nil, err
}
m, err := lwp.LoadWithParents(ctx, cr)
if err != nil {
return nil, err
}
results, err := c.filterResults(m, rec.key, map[string]struct{}{})
if err != nil {
for _, r := range m {
r.Release(context.TODO())
}
}
for _, r := range m {
// refs added to results are deleted from m by filterResults
// so release any leftovers
r.Release(context.TODO())
}
return results, nil
}
func (c *cacheManager) Save(k *CacheKey, r Result, createdAt time.Time) (rck *ExportableCacheKey, err error) {
lg := bklog.G(context.TODO()).WithFields(logrus.Fields{
"cache_manager": c.id,
"op": "save",
"result": r.ID(),
"stack": bklog.TraceLevelOnlyStack(),
})
defer func() {
if err != nil {
lg = lg.WithError(err)
} else {
lg = lg.WithField("return_cachekey", rck.TraceFields())
}
lg.Trace("cache manager")
}()
c.mu.Lock()
defer c.mu.Unlock()
res, err := c.results.Save(r, createdAt)
if err != nil {
return nil, err
}
if err := c.backend.AddResult(c.getID(k), res); err != nil {
return nil, err
}
if err := c.ensurePersistentKey(k); err != nil {
return nil, err
}
rec := &CacheRecord{
ID: res.ID,
cacheManager: c,
key: k,
CreatedAt: res.CreatedAt,
}
return &ExportableCacheKey{
CacheKey: k,
Exporter: &exporter{k: k, record: rec},
}, nil
}
func newKey() *CacheKey {
return &CacheKey{ids: map[*cacheManager]string{}}
}
func (c *cacheManager) newKeyWithID(id string, dgst digest.Digest, output Index) *CacheKey {
k := newKey()
k.digest = dgst
k.output = output
k.ID = id
k.ids[c] = id
return k
}
func (c *cacheManager) newRootKey(dgst digest.Digest, output Index) *CacheKey {
return c.newKeyWithID(rootKey(dgst, output).String(), dgst, output)
}
func (c *cacheManager) getID(k *CacheKey) string {
k.mu.Lock()
id, ok := k.ids[c]
if ok {
k.mu.Unlock()
return id
}
if len(k.deps) == 0 {
k.ids[c] = k.ID
k.mu.Unlock()
return k.ID
}
id = c.getIDFromDeps(k)
k.ids[c] = id
k.mu.Unlock()
return id
}
func (c *cacheManager) ensurePersistentKey(k *CacheKey) error {
id := c.getID(k)
for i, deps := range k.Deps() {
for _, ck := range deps {
l := CacheInfoLink{
Input: Index(i),
Output: k.Output(),
Digest: k.Digest(),
Selector: ck.Selector,
}
ckID := c.getID(ck.CacheKey.CacheKey)
if !c.backend.HasLink(ckID, l, id) {
if err := c.ensurePersistentKey(ck.CacheKey.CacheKey); err != nil {
return err
}
if err := c.backend.AddLink(ckID, l, id); err != nil {
return err
}
}
}
}
return nil
}
func (c *cacheManager) getIDFromDeps(k *CacheKey) string {
matches := map[string]struct{}{}
for i, deps := range k.deps {
if i == 0 || len(matches) > 0 {
for _, ck := range deps {
m2 := make(map[string]struct{})
if err := c.backend.WalkLinks(c.getID(ck.CacheKey.CacheKey), CacheInfoLink{
Input: Index(i),
Output: k.Output(),
Digest: k.Digest(),
Selector: ck.Selector,
}, func(id string) error {
if i == 0 {
matches[id] = struct{}{}
} else {
m2[id] = struct{}{}
}
return nil
}); err != nil {
matches = map[string]struct{}{}
break
}
if i != 0 {
for id := range matches {
if _, ok := m2[id]; !ok {
delete(matches, id)
}
}
}
}
}
}
for k := range matches {
return k
}
return identity.NewID()
}
func rootKey(dgst digest.Digest, output Index) digest.Digest {
out, _ := cachedigest.FromBytes(fmt.Appendf(nil, "%s@%d", dgst, output), cachedigest.TypeString)
if strings.HasPrefix(dgst.String(), "random:") {
return digest.Digest("random:" + dgst.Encoded())
}
return out
}