2026-03-11 15:12:02 +10:00
|
|
|
package cache
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"sync"
|
|
|
|
|
|
2026-03-11 15:30:49 +10:00
|
|
|
"github.com/syntrex/gomcp/internal/domain/memory"
|
2026-03-11 15:12:02 +10:00
|
|
|
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)
|