gomcp/internal/infrastructure/cache/bolt_cache.go

126 lines
2.9 KiB
Go
Raw Normal View History

package cache
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/syntrex/gomcp/internal/domain/memory"
bolt "go.etcd.io/bbolt"
)
var l0Bucket = []byte("l0_facts")
// BoltCache implements memory.HotCache using bbolt for L0 fact caching.
type BoltCache struct {
db *bolt.DB
mu sync.RWMutex
}
// NewBoltCache opens or creates a bbolt database for caching.
func NewBoltCache(path string) (*BoltCache, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, fmt.Errorf("create cache directory: %w", err)
}
db, err := bolt.Open(path, 0o600, &bolt.Options{NoSync: false})
if err != nil {
return nil, fmt.Errorf("open bolt cache: %w", err)
}
// Ensure bucket exists.
err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(l0Bucket)
return err
})
if err != nil {
db.Close()
return nil, fmt.Errorf("create bucket: %w", err)
}
return &BoltCache{db: db}, nil
}
// NewBoltCacheInMemory creates an in-memory bolt cache (for testing).
// bbolt doesn't support true in-memory, so we use a temp file.
func NewBoltCacheFromDB(db *bolt.DB) (*BoltCache, error) {
err := db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(l0Bucket)
return err
})
if err != nil {
return nil, fmt.Errorf("create bucket: %w", err)
}
return &BoltCache{db: db}, nil
}
// GetL0Facts returns all cached L0 (project-level) facts.
func (c *BoltCache) GetL0Facts(_ context.Context) ([]*memory.Fact, error) {
c.mu.RLock()
defer c.mu.RUnlock()
var facts []*memory.Fact
err := c.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(l0Bucket)
if b == nil {
return nil
}
return b.ForEach(func(k, v []byte) error {
var f memory.Fact
if err := json.Unmarshal(v, &f); err != nil {
return fmt.Errorf("unmarshal fact %s: %w", string(k), err)
}
facts = append(facts, &f)
return nil
})
})
return facts, err
}
// InvalidateFact removes a single fact from the cache.
func (c *BoltCache) InvalidateFact(_ context.Context, id string) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(l0Bucket)
if b == nil {
return nil
}
return b.Delete([]byte(id))
})
}
// WarmUp populates the cache with a batch of L0 facts.
func (c *BoltCache) WarmUp(_ context.Context, facts []*memory.Fact) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(l0Bucket)
if b == nil {
return fmt.Errorf("l0_facts bucket not found")
}
for _, f := range facts {
data, err := json.Marshal(f)
if err != nil {
return fmt.Errorf("marshal fact %s: %w", f.ID, err)
}
if err := b.Put([]byte(f.ID), data); err != nil {
return fmt.Errorf("put fact %s: %w", f.ID, err)
}
}
return nil
})
}
// Close closes the bbolt database.
func (c *BoltCache) Close() error {
return c.db.Close()
}
// Ensure BoltCache implements memory.HotCache.
var _ memory.HotCache = (*BoltCache)(nil)