Every given test compiled against a panic-only interface skeleton (go vet clean); nothing was implemented. modernc.org/sqlite v1.59.0 vetted in a scratch module (WAL works); go.sum given. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6.8 KiB
v1 task 01: the SQLite store
Branch: v1 (create it from master: git switch master && git switch -c v1; git status --short must be empty first, otherwise stop)
Commit subject: Add the SQLite store for leases and accounting
Goal
One SQLite file holds crossbar's durable state (the lease table) and its accounting log (one row
per proxied request, lease events, poller observations), with rollup queries that answer
per-route / per-model / per-host usage. This is PLAN.md §7a.
Context
The driver is modernc.org/sqlite (pure Go, no cgo — the arm64 static build stays a plain
go build), registered under the database/sql name "sqlite". Open with WAL and a busy
timeout: sql.Open("sqlite", "file:"+path+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)").
Volume is a few rows per request, so nothing here is performance-sensitive; correctness of the
sums is what matters. Times are stored as Unix milliseconds (INTEGER) and returned as
time.Time in UTC.
Files
- Copy (never edit afterwards):
go.sum(replaces; adds the driver's lines),internal/store/store_test.go - Create:
internal/store/store.go(andschema.goif you want the SQL separate; both under 400 lines) - Modify:
go.mod(addmodernc.org/sqlite v1.59.0torequire),docs/implementer-log.md
Interfaces
Produces, in internal/store, package store:
type State string
const ( Active State = "active"; Pinned State = "pinned" )
const (
ReasonNew = "new"; ReasonUnhealthy = "unhealthy"; ReasonIdle = "idle"
ReasonPin = "pin"; ReasonRelease = "release"; ReasonDrain = "drain"
)
type By string
const ( ByRoute By = "route"; ByModel By = "model"; ByHost By = "host" )
type Lease struct {
Route, FP, Model, Host string
State State
Created, LastUsed time.Time
}
type LeaseEvent struct {
TS time.Time
Route, Model, FromHost, ToHost string
Reason string
}
type Request struct {
Route, FP, Model, Host string
Started time.Time
QueuedMs, TTFBMs, TotalMs int64
Status int
Streamed bool
PromptTokens, CachedTokens, CompletionTokens int64
Err string
}
type HostHealth struct {
TS time.Time
Host string
Healthy bool
Loaded []string // stored as a JSON array in one TEXT column
}
type UsageRow struct {
Key string `json:"key"`
Requests int64 `json:"requests"`
Errors int64 `json:"errors"` // rows with Status >= 400
BusyMs int64 `json:"busy_ms"` // sum(TotalMs)
QueuedMs int64 `json:"queued_ms"`
PromptTokens int64 `json:"prompt_tokens"`
CachedTokens int64 `json:"cached_tokens"`
CompletionTokens int64 `json:"completion_tokens"`
}
func (u UsageRow) CacheHitRatio() float64 // CachedTokens / PromptTokens; 0 when PromptTokens == 0
type Store struct { /* private: *sql.DB */ }
func Open(path string) (*Store, error) // creates tables if missing; fails if the directory does not exist
func (s *Store) Close() error
func (s *Store) JournalMode() string // "wal"
func (s *Store) SaveLease(l Lease) error // INSERT OR REPLACE on (route, fp, model)
func (s *Store) DeleteLease(route, fp, model string) error
func (s *Store) ListLeases() ([]Lease, error)
func (s *Store) RecordEvent(e LeaseEvent) error
func (s *Store) RecordRequest(r Request) error
func (s *Store) RecordHostHealth(h HostHealth) error
func (s *Store) Usage(since time.Time, by By) ([]UsageRow, error) // rows with Started >= since, grouped by `by`; a zero `since` means all time
func (s *Store) Events(since time.Time, limit int) ([]LeaseEvent, error) // oldest first
func (s *Store) Prune(now time.Time, retention time.Duration) (int64, error)
Rules the tests check:
- Schema (create with
IF NOT EXISTS, soOpentwice on one file works):leases(route, fp, model, host, state, created, last_used, PRIMARY KEY(route, fp, model)),lease_events(ts, route, model, from_host, to_host, reason),requests(id INTEGER PRIMARY KEY, route, fp, model, host, started, queued_ms, ttfb_ms, total_ms, status, streamed, prompt_tokens, cached_tokens, completion_tokens, err),host_health(ts, host, healthy, loaded_models),requests_daily(day, route, model, host, requests, errors, busy_ms, queued_ms, prompt_tokens, cached_tokens, completion_tokens, PRIMARY KEY(day, route, model, host)). Usagesumsrequestsrows withstarted >= sinceplusrequests_dailyrows whoseday >= since(day = UTC midnight ofstarted), grouped by thebycolumn.Errorscountsstatus >= 400. A zerosince(time.Time{}) means everything. Result order: byKey.Prune(now, retention)moves everyrequestsrow withstarted < now - retentionintorequests_daily(adding into the existing day row if there is one), deletes them, and returns the number deleted. In one transaction.- Nothing here panics; every
sqlerror is returned wrapped (fmt.Errorf("store: …: %w", err)). LoadedinHostHealthis written as a JSON array;nilis written as[].
Steps
- 1. Branch and copy.
git switch master && git switch -c v1
cp docs/plans/v1/_files/go.sum go.sum
mkdir -p internal/store && cp docs/plans/v1/_files/internal/store/store_test.go internal/store/
- 2. Add the dependency. In
go.mod, therequirebecomes a block with both modules:
require (
github.com/BurntSushi/toml v1.6.0
modernc.org/sqlite v1.59.0
)
Then go mod download modernc.org/sqlite (network, once) and go mod verify. Expected:
all modules verified. If go mod tidy wants to change go.sum or add // indirect lines to
go.mod, let it, and stage the result; go.sum must end up a superset of the given file.
- 3. See the test fail.
go test ./internal/store/. Expected: it does not compile. - 4. Write
internal/store/store.go.gofmt -w internal/store/. - 5. See the test pass.
go test -race -count=1 ./internal/store/. Expected:ok. The first compile of the driver takes a minute or two. - 6. Run the gate.
make gate. Expected last line:gate: ok. - 7. Log and commit. Row
v1/01-store.
git add go.mod go.sum internal/store docs/implementer-log.md
git commit
Done when
go test -race -count=1 ./internal/store/isok;make gateprintsgate: ok.cmp internal/store/store_test.go docs/plans/v1/_files/internal/store/store_test.goprints nothing.
Stop and report if
- The module cannot be downloaded, or
go vetrejects the driver on this Go version.