v2.0.0: claims model - signed MMUR2 votes in a truth DB, merged view in a rebuildable cache DB; POST /api/claims, claims and merge routes, unsigned writes answer 410
Build and Push Docker Image / build (push) Successful in 5m1s

This commit is contained in:
Worph
2026-10-25 17:36:00 +00:00
parent b0adefb97b
commit 84af298792
78 changed files with 7203 additions and 5617 deletions
+52 -10
View File
@@ -84,6 +84,14 @@ type Options struct {
// values — the hook derived counters hang off (Tx.AddCounter). It must not
// write kv rows itself.
OnChange func(tx *Tx, key, old string, hadOld bool, new string, hasNew bool)
// NoKVTable opens the file without the kv table and its prepared
// statements: the caller owns every table through Schema and writes them
// with Tx.Exec / Tx.Query (meta-core's truth database). The kv methods
// (Get, Put, streams, counters) must not be used on such a store.
NoKVTable bool
// Synchronous is SQLite's synchronous pragma; default NORMAL. A store that
// is rebuildable from another (meta-core's cache) runs OFF.
Synchronous string
}
// Store is an open database.
@@ -134,7 +142,10 @@ func Open(path string, opts Options) (*Store, error) {
opts.EventMaxLen = 100_000
}
w, err := sql.Open("sqlite", dsn(path, opts.WriterCacheMB, false))
if opts.Synchronous == "" {
opts.Synchronous = "NORMAL"
}
w, err := sql.Open("sqlite", dsn(path, opts.WriterCacheMB, false, opts.Synchronous))
if err != nil {
return nil, fmt.Errorf("open writer: %w", err)
}
@@ -144,9 +155,11 @@ func Open(path string, opts Options) (*Store, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if _, err := w.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS kv(k TEXT PRIMARY KEY, v TEXT NOT NULL) WITHOUT ROWID`); err != nil {
w.Close()
return nil, fmt.Errorf("create kv: %w", err)
if !opts.NoKVTable {
if _, err := w.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS kv(k TEXT PRIMARY KEY, v TEXT NOT NULL) WITHOUT ROWID`); err != nil {
w.Close()
return nil, fmt.Errorf("create kv: %w", err)
}
}
if opts.Schema != "" {
// No deadline: on an existing store, the first open after a new index
@@ -162,7 +175,7 @@ func Open(path string, opts Options) (*Store, error) {
}
}
r, err := sql.Open("sqlite", dsn(path, opts.ReaderCacheMB, true))
r, err := sql.Open("sqlite", dsn(path, opts.ReaderCacheMB, true, opts.Synchronous))
if err != nil {
w.Close()
return nil, fmt.Errorf("open readers: %w", err)
@@ -174,7 +187,7 @@ func Open(path string, opts Options) (*Store, error) {
// Fresh deadline: the schema step above may legitimately have taken minutes.
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
for _, p := range []struct {
stmts := []struct {
dst **sql.Stmt
query string
}{
@@ -183,7 +196,11 @@ func Open(path string, opts Options) (*Store, error) {
{&st.stDelete, `DELETE FROM kv WHERE k = ? RETURNING v`},
{&st.stGet, `SELECT v FROM kv WHERE k = ?`},
{&st.stInsert, `INSERT INTO kv(k, v) VALUES(?, ?)`},
} {
}
if opts.NoKVTable {
stmts = nil
}
for _, p := range stmts {
if *p.dst, err = w.PrepareContext(ctx, p.query); err != nil {
r.Close()
w.Close()
@@ -199,11 +216,11 @@ func Open(path string, opts Options) (*Store, error) {
// dsn builds a modernc DSN. WAL + synchronous=NORMAL is the durability class
// Redis' AOF-everysec gave us: a power loss may drop the last transactions,
// never corrupt the file.
func dsn(path string, cacheMB int, readOnly bool) string {
func dsn(path string, cacheMB int, readOnly bool, synchronous string) string {
q := url.Values{}
q.Add("_pragma", "busy_timeout(30000)")
q.Add("_pragma", "journal_mode(WAL)")
q.Add("_pragma", "synchronous(NORMAL)")
q.Add("_pragma", "synchronous("+synchronous+")")
q.Add("_pragma", fmt.Sprintf("cache_size(-%d)", cacheMB*1024))
q.Add("_pragma", "temp_store(MEMORY)")
q.Add("_pragma", "journal_size_limit(268435456)")
@@ -520,6 +537,7 @@ func (s *Store) runGroup(reqs []*writeReq) {
return
}
g := &group{tx: sqlTx}
var commitHooks []func()
for i, r := range reqs {
if _, err := sqlTx.ExecContext(ctx, `SAVEPOINT req`); err != nil {
_ = sqlTx.Rollback()
@@ -534,6 +552,7 @@ func (s *Store) runGroup(reqs []*writeReq) {
}
if err == nil {
g.mergeCounters(tx.counters)
commitHooks = append(commitHooks, tx.onCommit...)
}
if err != nil {
results[i] = err
@@ -565,6 +584,12 @@ func (s *Store) runGroup(reqs []*writeReq) {
return
}
s.afterCommit(g.pending)
// Hooks run in the writer goroutine, in commit order, before any request
// returns: a caller that orders work by commit (meta-core's apply queue)
// sees it in exactly that order.
for _, h := range commitHooks {
h()
}
for i, r := range reqs {
r.done <- results[i]
}
@@ -578,6 +603,23 @@ type Tx struct {
g *group
events []map[string]string
counters map[string]int64
onCommit []func()
quiet bool
}
// OnCommit registers fn to run after the transaction commits, only if this
// request succeeded. Hooks run in the writer goroutine in commit order and
// must not block or call Update.
func (t *Tx) OnCommit(fn func()) { t.onCommit = append(t.onCommit, fn) }
// Quiet stops this request's changes from publishing events (a bulk rebuild
// whose consumers are told to take a snapshot instead). Counters and the
// OnChange hook still run.
func (t *Tx) Quiet() { t.quiet = true }
// Query runs a raw multi-row query in the transaction.
func (t *Tx) Query(query string, args ...any) (*sql.Rows, error) {
return t.sqlTx().QueryContext(t.ctx, query, args...)
}
func (t *Tx) sqlTx() *sql.Tx { return t.g.tx }
@@ -800,7 +842,7 @@ func (s *Store) WriteStats() (changed, unchanged int64) {
}
func (t *Tx) event(op, k string) {
if t.s.opts.EventStream == "" || t.s.opts.EventKey == nil || !t.s.opts.EventKey(k) {
if t.quiet || t.s.opts.EventStream == "" || t.s.opts.EventKey == nil || !t.s.opts.EventKey(k) {
return
}
t.events = append(t.events, map[string]string{