package identitycache
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/containerd/log"
bolt "go.etcd.io/bbolt"
)
var bboltCacheBucket = []byte("image-identity-cache-v1")
type boltBackend struct {
db *bolt.DB
closeOnce sync.Once
closeErr error
}
// NewBoltDBBackend creates a bbolt-backed persistent cache backend.
func NewBoltDBBackend(root string) (Backend, error) {
if root == "" {
return NewNopBackend(), nil
}
cacheDir := filepath.Join(root, "image")
if err := os.MkdirAll(cacheDir, 0o700); err != nil {
return nil, err
}
db, err := safeOpen(filepath.Join(cacheDir, "identity-cache.db"), 0o600, nil)
if err != nil {
return nil, err
}
b := &boltBackend{db: db}
if err := b.db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(bboltCacheBucket)
return err
}); err != nil {
_ = db.Close()
return nil, err
}
return b, nil
}
func (b *boltBackend) Load(_ context.Context, cacheKey string, now time.Time) (Entry, bool, error) {
var (
entry Entry
payload []byte
)
err := b.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bboltCacheBucket)
if bucket == nil {
return nil
}
value := bucket.Get([]byte(cacheKey))
if value == nil {
return nil
}
payload = append([]byte(nil), value...)
return nil
})
if err != nil {
return Entry{}, false, err
}
if len(payload) == 0 {
return Entry{}, false, nil
}
if err := json.Unmarshal(payload, &entry); err != nil {
_ = b.delete(cacheKey)
return Entry{}, false, nil
}
if now.After(entry.ExpiresAt) {
return Entry{}, false, nil
}
return entry, true, nil
}
func (b *boltBackend) Store(_ context.Context, cacheKey string, entry Entry, _ time.Time) error {
payload, err := json.Marshal(entry)
if err != nil {
return err
}
return b.db.Update(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bboltCacheBucket)
if err != nil {
return err
}
return bucket.Put([]byte(cacheKey), payload)
})
}
func (b *boltBackend) Walk(ctx context.Context, _ time.Time, fn func(cacheKey string, entry Entry) error) error {
return b.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bboltCacheBucket)
if bucket == nil {
return nil
}
cursor := bucket.Cursor()
for key, value := cursor.First(); key != nil; key, value = cursor.Next() {
if err := ctx.Err(); err != nil {
return err
}
var entry Entry
if err := json.Unmarshal(value, &entry); err != nil {
continue
}
if err := fn(string(key), entry); err != nil {
return err
}
}
return nil
})
}
func (b *boltBackend) PruneExpired(_ context.Context, now time.Time) error {
return b.pruneExpiredEntries(now)
}
func (b *boltBackend) Close() error {
if b == nil || b.db == nil {
return nil
}
b.closeOnce.Do(func() {
b.closeErr = b.db.Close()
})
return b.closeErr
}
func (b *boltBackend) delete(cacheKey string) error {
return b.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bboltCacheBucket)
if bucket == nil {
return nil
}
return bucket.Delete([]byte(cacheKey))
})
}
func (b *boltBackend) pruneExpiredEntries(now time.Time) error {
return b.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bboltCacheBucket)
if bucket == nil {
return nil
}
cursor := bucket.Cursor()
for key, value := cursor.First(); key != nil; key, value = cursor.Next() {
var entry Entry
if err := json.Unmarshal(value, &entry); err != nil || now.After(entry.ExpiresAt) {
if err := cursor.Delete(); err != nil {
return err
}
}
}
return nil
})
}
// safeOpen opens a bolt database with automatic recovery from corruption.
// If the database file is corrupted, it backs up the corrupted file and creates
// a new empty database.
func safeOpen(dbPath string, mode os.FileMode, opts *bolt.Options) (db *bolt.DB, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
// If we fail opening the DB, but can read a non-empty file, try resetting it.
if err != nil && fileHasContent(dbPath) {
db, err = fallbackOpen(dbPath, mode, opts, err)
}
}()
return openDB(dbPath, mode, opts)
}
func openDB(dbPath string, mode os.FileMode, opts *bolt.Options) (*bolt.DB, error) {
bdb, err := bolt.Open(dbPath, mode, opts)
if err != nil {
return nil, err
}
return bdb, nil
}
func fallbackOpen(dbPath string, mode os.FileMode, opts *bolt.Options, openErr error) (*bolt.DB, error) {
backupPath := dbPath + "." + fmt.Sprintf("%d", time.Now().UnixNano()) + ".bak"
log.L.Errorf("failed to open moby image identity cache database %s, resetting to empty. "+
"Old database is backed up to %s. This usually means dockerd crashed or was terminated abruptly, leaving the cache DB corrupted. "+
"If this keeps happening, please report at https://github.com/moby/moby/issues. original error: %v",
dbPath, backupPath, openErr)
if err := os.Rename(dbPath, backupPath); err != nil {
return nil, fmt.Errorf("failed to rename database file %s to %s: %w", dbPath, backupPath, err)
}
// Second open should create a new database; failure here is permanent.
return openDB(dbPath, mode, opts)
}
func fileHasContent(dbPath string) bool {
st, err := os.Stat(dbPath)
return err == nil && st.Size() > 0
}