p2: digest with in-sift FIFO, SQLite caches, Honcho context, recover
Dixie is capped (max_inflight 2, queue_depth 8, queue_wait 20s) and never queued on llama-server. Streaming chat, thinking off, 45s fallback to clean. Digest and chunk-embedding caches are content-addressed SQLite (Honcho context is not cached). Recover uses the guarded Fetcher. guardian-star is refused in code.
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
// Package dcache is the content-addressed digest + chunk-embedding SQLite cache.
|
||||
// Honcho context is never stored here.
|
||||
package dcache
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const PromptVersion = "1"
|
||||
|
||||
type DB struct {
|
||||
sql *sql.DB
|
||||
maxBytes int64
|
||||
log *slog.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
DigestH uint64
|
||||
DigestM uint64
|
||||
EmbedH uint64
|
||||
EmbedM uint64
|
||||
Evictions uint64
|
||||
}
|
||||
|
||||
func Open(path string, maxBytes int64, log *slog.Logger) (*DB, error) {
|
||||
if maxBytes <= 0 {
|
||||
maxBytes = 500 << 20
|
||||
}
|
||||
if err := os.MkdirAll(dirOf(path), 0o750); err != nil && path != ":memory:" && path != "" {
|
||||
// ignore: sqlite will fail if it cannot create
|
||||
}
|
||||
dsn := path
|
||||
if path == "" {
|
||||
dsn = ":memory:"
|
||||
}
|
||||
sq, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sq.SetMaxOpenConns(1)
|
||||
if _, err := sq.Exec(`
|
||||
PRAGMA journal_mode=WAL;
|
||||
CREATE TABLE IF NOT EXISTS digest (
|
||||
k TEXT PRIMARY KEY,
|
||||
text TEXT NOT NULL,
|
||||
chunks_used INTEGER,
|
||||
last_hit INTEGER NOT NULL,
|
||||
nbytes INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS embed (
|
||||
k TEXT PRIMARY KEY,
|
||||
vec BLOB NOT NULL,
|
||||
last_hit INTEGER NOT NULL,
|
||||
nbytes INTEGER NOT NULL
|
||||
);
|
||||
`); err != nil {
|
||||
_ = sq.Close()
|
||||
return nil, err
|
||||
}
|
||||
d := &DB{sql: sq, maxBytes: maxBytes, log: log}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() error {
|
||||
if d == nil || d.sql == nil {
|
||||
return nil
|
||||
}
|
||||
return d.sql.Close()
|
||||
}
|
||||
|
||||
func NormalizeQuestion(q string) string {
|
||||
return strings.ToLower(strings.Join(strings.Fields(q), " "))
|
||||
}
|
||||
|
||||
func DigestKey(contentHash, question, model, promptVer string) string {
|
||||
sum := sha256.Sum256([]byte(contentHash + "\x00" + NormalizeQuestion(question) + "\x00" + model + "\x00" + promptVer))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func EmbedKey(contentHash string, chunkIndex int, embedModel string) string {
|
||||
sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%d\x00%s", contentHash, chunkIndex, embedModel)))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func QuestionEmbedKey(question, embedModel string) string {
|
||||
sum := sha256.Sum256([]byte("q\x00" + NormalizeQuestion(question) + "\x00" + embedModel))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
type DigestHit struct {
|
||||
Text string
|
||||
ChunksUsed int
|
||||
}
|
||||
|
||||
func (d *DB) GetDigest(key string) (DigestHit, bool) {
|
||||
if d == nil {
|
||||
return DigestHit{}, false
|
||||
}
|
||||
var h DigestHit
|
||||
var n int
|
||||
err := d.sql.QueryRow(`SELECT text, chunks_used FROM digest WHERE k = ?`, key).Scan(&h.Text, &n)
|
||||
if err != nil {
|
||||
d.mu.Lock()
|
||||
d.DigestM++
|
||||
d.mu.Unlock()
|
||||
return DigestHit{}, false
|
||||
}
|
||||
h.ChunksUsed = n
|
||||
_, _ = d.sql.Exec(`UPDATE digest SET last_hit = ? WHERE k = ?`, time.Now().Unix(), key)
|
||||
d.mu.Lock()
|
||||
d.DigestH++
|
||||
d.mu.Unlock()
|
||||
return h, true
|
||||
}
|
||||
|
||||
func (d *DB) PutDigest(key, text string, chunksUsed int) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
n := len(key) + len(text)
|
||||
_, err := d.sql.Exec(`INSERT OR REPLACE INTO digest(k, text, chunks_used, last_hit, nbytes) VALUES(?,?,?,?,?)`,
|
||||
key, text, chunksUsed, time.Now().Unix(), n)
|
||||
if err != nil && d.log != nil {
|
||||
d.log.Warn("digest cache write", "err", err)
|
||||
}
|
||||
d.evict()
|
||||
}
|
||||
|
||||
func (d *DB) GetEmbed(key string) ([]float64, bool) {
|
||||
if d == nil {
|
||||
return nil, false
|
||||
}
|
||||
var b []byte
|
||||
err := d.sql.QueryRow(`SELECT vec FROM embed WHERE k = ?`, key).Scan(&b)
|
||||
if err != nil {
|
||||
d.mu.Lock()
|
||||
d.EmbedM++
|
||||
d.mu.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
_, _ = d.sql.Exec(`UPDATE embed SET last_hit = ? WHERE k = ?`, time.Now().Unix(), key)
|
||||
d.mu.Lock()
|
||||
d.EmbedH++
|
||||
d.mu.Unlock()
|
||||
return blobToVec(b), true
|
||||
}
|
||||
|
||||
func (d *DB) PutEmbed(key string, vec []float64) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
b := vecToBlob(vec)
|
||||
_, err := d.sql.Exec(`INSERT OR REPLACE INTO embed(k, vec, last_hit, nbytes) VALUES(?,?,?,?)`,
|
||||
key, b, time.Now().Unix(), len(key)+len(b))
|
||||
if err != nil && d.log != nil {
|
||||
d.log.Warn("embed cache write", "err", err)
|
||||
}
|
||||
d.evict()
|
||||
}
|
||||
|
||||
func (d *DB) Size() int64 {
|
||||
if d == nil {
|
||||
return 0
|
||||
}
|
||||
var n sql.NullInt64
|
||||
_ = d.sql.QueryRow(`SELECT IFNULL(SUM(nbytes),0) FROM digest`).Scan(&n)
|
||||
var m sql.NullInt64
|
||||
_ = d.sql.QueryRow(`SELECT IFNULL(SUM(nbytes),0) FROM embed`).Scan(&m)
|
||||
return n.Int64 + m.Int64
|
||||
}
|
||||
|
||||
func (d *DB) evict() {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
for d.Size() > d.maxBytes {
|
||||
var k string
|
||||
err := d.sql.QueryRow(`
|
||||
SELECT k FROM (
|
||||
SELECT k, last_hit, 0 AS kind FROM digest
|
||||
UNION ALL
|
||||
SELECT k, last_hit, 1 FROM embed
|
||||
) ORDER BY last_hit ASC LIMIT 1`).Scan(&k)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = d.sql.Exec(`DELETE FROM digest WHERE k = ?`, k)
|
||||
_, _ = d.sql.Exec(`DELETE FROM embed WHERE k = ?`, k)
|
||||
d.mu.Lock()
|
||||
d.Evictions++
|
||||
d.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func vecToBlob(v []float64) []byte {
|
||||
b := make([]byte, 4*len(v))
|
||||
for i, x := range v {
|
||||
binary.LittleEndian.PutUint32(b[i*4:], math.Float32bits(float32(x)))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func blobToVec(b []byte) []float64 {
|
||||
n := len(b) / 4
|
||||
out := make([]float64, n)
|
||||
for i := 0; i < n; i++ {
|
||||
out[i] = float64(math.Float32frombits(binary.LittleEndian.Uint32(b[i*4:])))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func dirOf(path string) string {
|
||||
i := strings.LastIndex(path, "/")
|
||||
if i <= 0 {
|
||||
return "."
|
||||
}
|
||||
return path[:i]
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package dcache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHitMissAndNormalise(t *testing.T) {
|
||||
d, err := Open(filepath.Join(t.TempDir(), "c.db"), 1<<20, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Close()
|
||||
k1 := DigestKey("abc", "What Share?", "m", PromptVersion)
|
||||
k2 := DigestKey("abc", "what share?", "m", PromptVersion)
|
||||
if k1 != k2 {
|
||||
t.Fatal("normalise")
|
||||
}
|
||||
if _, ok := d.GetDigest(k1); ok {
|
||||
t.Fatal("miss")
|
||||
}
|
||||
d.PutDigest(k1, "forty two", 1)
|
||||
h, ok := d.GetDigest(k2)
|
||||
if !ok || h.Text != "forty two" {
|
||||
t.Fatalf("%v %v", ok, h)
|
||||
}
|
||||
k3 := DigestKey("abd", "what share?", "m", PromptVersion)
|
||||
if _, ok := d.GetDigest(k3); ok {
|
||||
t.Fatal("changed content must miss")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptVersionInvalidates(t *testing.T) {
|
||||
k1 := DigestKey("h", "q", "m", "1")
|
||||
k2 := DigestKey("h", "q", "m", "2")
|
||||
if k1 == k2 {
|
||||
t.Fatal("version")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoContextTable(t *testing.T) {
|
||||
d, err := Open(filepath.Join(t.TempDir(), "c.db"), 1<<20, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Close()
|
||||
var n int
|
||||
_ = d.sql.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='context'`).Scan(&n)
|
||||
if n != 0 {
|
||||
t.Fatal("must not store context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvictUnderCap(t *testing.T) {
|
||||
d, err := Open(filepath.Join(t.TempDir(), "c.db"), 200, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Close()
|
||||
for i := 0; i < 20; i++ {
|
||||
d.PutDigest(DigestKey("h", string(rune('a'+i)), "m", "1"), "xxxxxxxxxxxxxxxxxxxx", 1)
|
||||
}
|
||||
if d.Size() > 400 {
|
||||
t.Fatalf("size %d", d.Size())
|
||||
}
|
||||
if d.Evictions == 0 {
|
||||
t.Fatal("want evictions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorruptDBBypass(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "c.db")
|
||||
if err := os.WriteFile(p, []byte("not a database"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := Open(p, 1<<20, nil)
|
||||
if err == nil {
|
||||
_ = d.Close()
|
||||
// Open may succeed then query fails — GetDigest must miss not panic
|
||||
if _, ok := d.GetDigest("x"); ok {
|
||||
t.Fatal("corrupt hit")
|
||||
}
|
||||
return
|
||||
}
|
||||
// open failed: caller warns and bypasses
|
||||
}
|
||||
Reference in New Issue
Block a user