Documentation
¶
Overview ¶
Package storage provides durable per-request storage using an embedded SQLite database. Records flow in asynchronously from the hot path via a channel, so DB writes never block the proxy.
Index ¶
- Variables
- func DecodeAttemptsColumn(raw []byte) ([]metrics.RetryAttempt, error)
- func DecodeToolNamesColumn(raw []byte) ([]string, error)
- func PendingSnapshotPath(path string) string
- func RequestParamColumns() []string
- func StageSnapshot(path string, data []byte) error
- type LogCursor
- type Options
- type ProjectionCursor
- type PurgeFilter
- type PurgeResult
- type Store
- func (s *Store) Clear(ctx context.Context, filter *PurgeFilter, buffer *metrics.Buffer) (PurgeResult, error)
- func (s *Store) Close() error
- func (s *Store) CountDebugSession(ctx context.Context, sessionID string) (int64, error)
- func (s *Store) CountWhere(ctx context.Context, f PurgeFilter) (int64, error)
- func (s *Store) DBPath() string
- func (s *Store) DataVersion(ctx context.Context) (int64, error)
- func (s *Store) Dropped() uint64
- func (s *Store) ExpireDebugCaptures(ctx context.Context, beforeMs int64) (int64, error)
- func (s *Store) ExportWhere(ctx context.Context, f PurgeFilter, w io.Writer) (int64, error)
- func (s *Store) FilterUnwritten(ids []string) []string
- func (s *Store) Flush() error
- func (s *Store) HandleQuery(w http.ResponseWriter, r *http.Request)
- func (s *Store) ListClients(ctx context.Context) ([]string, error)
- func (s *Store) ListModels(ctx context.Context) ([]string, error)
- func (s *Store) ListProviders(ctx context.Context) ([]string, error)
- func (s *Store) LoadDebugCapture(ctx context.Context, id string) ([]byte, error)
- func (s *Store) LoadDebugSessions(ctx context.Context) ([]byte, error)
- func (s *Store) LoadMeta(ctx context.Context, key string) (string, error)
- func (s *Store) LoadPause(ctx context.Context) ([]byte, error)
- func (s *Store) LoadRecent(ctx context.Context, n int) ([]*metrics.Record, error)
- func (s *Store) LoadThrottle(ctx context.Context) ([]byte, error)
- func (s *Store) MarkWritten(ids []string)
- func (s *Store) MergeSnapshot(ctx context.Context, data []byte) (inserted, skipped int64, err error)
- func (s *Store) Query(ctx context.Context, query string, args ...any) (out []map[string]any, err error)
- func (s *Store) QueryBefore(ctx context.Context, before *LogCursor, limit int) ([]*metrics.Record, error)
- func (s *Store) Record(r *metrics.Record)
- func (s *Store) Recorder(buffer *metrics.Buffer) metrics.Recorder
- func (s *Store) RenameProviders(ctx context.Context, aliases map[string]string) (int64, error)
- func (s *Store) SaveDebugCapture(ctx context.Context, id, sessionID string, capturedAt, expiresAt int64, ...) error
- func (s *Store) SaveDebugSessions(ctx context.Context, raw []byte) error
- func (s *Store) SaveMeta(ctx context.Context, key, value string) error
- func (s *Store) SavePause(ctx context.Context, raw []byte) error
- func (s *Store) SaveThrottle(ctx context.Context, raw []byte) error
- func (s *Store) Snapshot(ctx context.Context) ([]byte, error)
- func (s *Store) SnapshotOverlap(ctx context.Context, data []byte) (int64, error)
- func (s *Store) StreamProjection(ctx context.Context, columns string, cursor ProjectionCursor, ...) (ProjectionCursor, bool, error)
- func (s *Store) Totals() Totals
- func (s *Store) TotalsAndUnwritten(ring []*metrics.Record) (Totals, []*metrics.Record)
- func (s *Store) TotalsDegraded() bool
- type Totals
Constants ¶
This section is empty.
Variables ¶
var ErrQueryLimit = errors.New("query result exceeds its byte, row, or column limit")
ErrQueryLimit rejects the entire result; callers must never serve a prefix.
var ErrStorageDisabled = errors.New("durable storage is disabled")
ErrStorageDisabled is the one sentinel answering "no durable store is available" (a nil *Store, or no db_path): every surface that refuses an operation for that reason returns or wraps it, so callers classify with errors.Is instead of matching drifted wordings. Each surface keeps its own status code; only the identity and the canonical message are shared.
Functions ¶
func DecodeAttemptsColumn ¶ added in v0.3.10
func DecodeAttemptsColumn(raw []byte) ([]metrics.RetryAttempt, error)
DecodeAttemptsColumn decodes the stored attempts JSON column through the shared decodeJSONColumn contract (external consumers live in internal/web).
func DecodeToolNamesColumn ¶ added in v0.3.10
DecodeToolNamesColumn decodes the stored tool_names JSON column with the same empty/"null" contract as DecodeAttemptsColumn.
func PendingSnapshotPath ¶ added in v0.3.6
PendingSnapshotPath is the staged SQLite file Open admits for path.
func RequestParamColumns ¶ added in v0.3.17
func RequestParamColumns() []string
RequestParamColumns lists, in durable bit order, the optional request parameter COLUMNS whose unset/zero distinction requestParamPresence preserves. It is derived from requestParamFields - the same owner as the bit mapping and the write/read presence - so a column added there cannot drift from what the MCP reference documents, and an external mirror (describe) can be pinned against it instead of a second hand-written list.
func StageSnapshot ¶ added in v0.3.6
StageSnapshot writes an integrity-checked SQLite snapshot that Open will admit in place of path. The live database is not touched.
Types ¶
type LogCursor ¶
LogCursor is the complete durable ordering key. A timestamp alone cannot distinguish requests started in the same millisecond.
type Options ¶
type Options struct {
// WriteChanCap bounds records buffered between the hot path and the
// background writer; overflow drops records to protect the proxy.
WriteChanCap int
// BatchCap is the max number of records flushed per transaction.
BatchCap int
// FlushInterval is how often the writer flushes a partial batch.
FlushInterval time.Duration
// QueryTimeout bounds ad-hoc dashboard SELECT queries so a slow query
// cannot hold a read connection indefinitely. Validate requires > 0;
// withQueryTimeout is the single choke point (a 0 fail-closes).
QueryTimeout time.Duration
// QueryMaxBytes/Rows bound ad-hoc SELECT results, not analytical scans.
// Both are required config values; zero fails closed, never defaults here.
QueryMaxBytes int
QueryMaxRows int
// WriteTrackCap bounds the in-memory set of recently written record ids.
// Dashboard aggregates merge un-flushed ring records against stored rows;
// the written set is the exact dedupe boundary. Sized 2x the ring
// (history_size) so it always covers every record the ring can hold.
WriteTrackCap int
// BusyTimeout shortens the sqliteBusyTimeout guardrail for tests that
// assert the contention window quickly (a 5s window costs 5s of wall
// time per phase). Zero keeps the production constant; config has no
// knob because the guardrail is deliberately not user-tunable.
BusyTimeout time.Duration
}
Options holds the storage pipeline tunables. All values come from config (single source of truth); nothing is defaulted here.
type ProjectionCursor ¶
ProjectionCursor pins one decoded analytical history to a consistent durable snapshot. RowID is a persistent high-water mark, not a request start time: late completions can carry arbitrarily old timestamps and still append.
type PurgeFilter ¶
type PurgeFilter struct {
Provider string `json:"provider"` // exact match
Model string `json:"model"` // exact match
Client string `json:"client"` // exact match
ConversationID string `json:"conversation_id"` // exact match
ErrorType string `json:"error_type"` // exact match (final error type)
StatusCode int `json:"status_code"` // exact match (0 = ignore)
HasError bool `json:"has_error"` // when true, only rows with a final error
HasDebug bool `json:"debug"` // when true, only operator-debug captures
BeforeMs int64 `json:"before_ms"` // started_at < BeforeMs (0 = ignore)
AfterMs int64 `json:"after_ms"` // started_at >= AfterMs (0 = ignore)
}
PurgeFilter describes a filtered purge: rows matching all non-empty fields are deleted. Since every request is one self-contained row (retries live in the row's attempts JSON, conversation/turn are just fields), deleting rows can never leave orphans.
func (PurgeFilter) IsEmpty ¶
func (f PurgeFilter) IsEmpty() bool
IsEmpty reports whether the filter matches everything (no constraint set).
func (PurgeFilter) MatchRecord ¶
func (f PurgeFilter) MatchRecord(r *metrics.Record) bool
MatchRecord is the in-memory predicate equivalent of the SQL filter: it reports whether a buffered record matches every set constraint. Used to drop the same records from the live ring buffer that a filtered Clear deletes from the database, so the two never disagree.
func (*PurgeFilter) UnmarshalJSON ¶
func (f *PurgeFilter) UnmarshalJSON(data []byte) error
Null is not an omitted constraint: accepting it beside another valid field would silently broaden a destructive selection. Keep this rule at the filter type so count and deletion decode exactly the same way.
func (PurgeFilter) Validate ¶
func (f PurgeFilter) Validate() error
Validate is shared by count, export, and deletion, including non-HTTP callers.
type PurgeResult ¶
PurgeResult carries the store-level deletion counts Clear returns: durable rows deleted and live-ring records removed. Purely internal - the purge response dropped its wire keys ({"ok": true} only, pinned by the strict response decoder), and the fields are the store-level contract the storage tests assert against.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a durable SQLite-backed store. It is safe for concurrent use.
func Open ¶
Open opens (or creates) the SQLite database at path and starts the background writer. Call Close to shut down cleanly. opts carries the pipeline tunables (channel/batch sizes, flush cadence, query timeout).
func (*Store) Clear ¶
func (s *Store) Clear(ctx context.Context, filter *PurgeFilter, buffer *metrics.Buffer) (PurgeResult, error)
Clear deletes finalized history through a single ring/enqueue fence. A nil filter explicitly selects all history; a supplied empty filter is rejected. Admin callers serialize outside sendMu. The only work under the producer lock is capturing the in-memory boundary and publishing one command; SQL and waits are exclusively on the writer/admin goroutines.
func (*Store) CountDebugSession ¶
CountDebugSession returns how many sidecar rows belong to sessionID.
func (*Store) CountWhere ¶
CountWhere returns how many stored rows match the filter (for the delete preview). It runs on the read pool like the other filter read paths (ExportWhere/QueryBefore): the HasError predicate's full-table json_each scan must never pin the sole writer connection up to QueryTimeout and stall insertBatch. With WAL the preview reads the committed snapshot, so it may miss the last un-flushed batch - exactly like the ExportWhere it previews, keeping the count==download contract.
func (*Store) DBPath ¶ added in v0.3.18
DBPath is the running database's own file: the boot-time path the store opened, not the reloadable db_path config value (db_path is startup-bound, so a reload that changes it leaves the live database at its boot path until the process restarts). Its directory is the volume that owns database-sized transient staging; see stageDir.
func (*Store) DataVersion ¶
DataVersion identifies committed database content for aggregate memoization. SQLite versions are comparable only on the same connection, so never query this through the pool or replace a failed connection and reuse its version. Errors disable cache reuse; ordinary history queries can still proceed.
func (*Store) ExpireDebugCaptures ¶
ExpireDebugCaptures deletes sidecar rows whose expires_at is before beforeMs.
func (*Store) ExportWhere ¶
ExportWhere streams every stored row matching the filter (an empty filter = all rows - export is read-only, so "everything" is safe) as a compact JSON array, one record per line, oldest-first, directly to the writer - memory stays bounded no matter how much history exists. Returns the number of records written.
func (*Store) FilterUnwritten ¶
FilterUnwritten takes a set of record ids and returns only those not in the written set - the exact dedupe boundary for ring/stored merges. Takes one aggMu hold instead of copying the full written map, so the caller pays O(ring) probes instead of O(written) allocation.
func (*Store) Flush ¶
Flush drains every record buffered in the write channel and commits it, then returns - without closing the pipeline (records sent afterwards are written by the writer as usual). This is the restart-handoff primitive: the draining process makes all finalized records durable BEFORE the fresh process opens the database, so the child's totals scan and ring backfill see the complete history while the old process keeps a working store for a failed-handoff resume. Concurrent Close waits for the actual final drain, including its error; a sealed input channel alone does not prove durability.
func (*Store) HandleQuery ¶
func (s *Store) HandleQuery(w http.ResponseWriter, r *http.Request)
HandleQuery serves a JSON array of rows for a SELECT query passed as ?q=... (URL-decoded). Enforces SELECT-only, single-statement, and a bounded execution time. A nil store returns 503, never inference fallback. Same-origin dashboard endpoint; no CORS header.
func (*Store) ListClients ¶
ListClients returns distinct non-empty Record.Client values from history.
func (*Store) ListModels ¶
ListModels returns distinct non-empty Record.Model values from history.
func (*Store) ListProviders ¶
ListProviders returns distinct non-empty Record.Provider values from history.
func (*Store) LoadDebugCapture ¶
LoadDebugCapture returns the payload for id, or nil if absent or expired.
func (*Store) LoadDebugSessions ¶
LoadDebugSessions returns persisted operator-debug session JSON, or nil if none.
func (*Store) LoadRecent ¶
LoadRecent returns up to n of the newest stored records, oldest-first. It powers the startup backfill (bounded by the configured history_size) so the dashboard survives restarts.
func (*Store) LoadThrottle ¶
LoadThrottle returns persisted provider-throttle JSON, or nil if none.
func (*Store) MarkWritten ¶
MarkWritten primes the written-id set with ids that were persisted BEFORE this process started (the startup ring backfill). Without it a fresh process would treat every backfilled record as "not yet written" and the dashboard aggregates would double-count the whole ring against the store.
func (*Store) MergeSnapshot ¶ added in v0.3.6
func (s *Store) MergeSnapshot(ctx context.Context, data []byte) (inserted, skipped int64, err error)
MergeSnapshot inserts snapshot rows whose ids are not already in the live store (INSERT OR IGNORE). Existing live rows win. The live file is not replaced; no restart is required.
func (*Store) Query ¶
func (s *Store) Query(ctx context.Context, query string, args ...any) (out []map[string]any, err error)
Query owns ad-hoc SELECT execution time and result resource bounds. The SELECT-only HTTP restriction stays in HandleQuery. SQLite working memory (sorting/aggregate intermediates) is not a strict per-query heap budget.
func (*Store) QueryBefore ¶
func (s *Store) QueryBefore(ctx context.Context, before *LogCursor, limit int) ([]*metrics.Record, error)
QueryBefore returns a newest-first page below the exclusive cursor. A nil cursor starts at durable newest, independently of the arrival-ordered ring.
func (*Store) Record ¶
Record implements metrics.Recorder. It never blocks: if the channel is full, the record is dropped (metrics are best-effort, not lossless). Records arriving after Close are dropped too: a send on the closed channel panics, which we recover and count as a drop rather than crash the proxy.
func (*Store) Recorder ¶
Recorder couples the live ring and durable enqueue at their shared, short publication fence. It does not wait for storage: channel overflow still drops only the durable copy. Lifecycle events remain exclusively in the live buffer.
func (*Store) RenameProviders ¶
RenameProviders merges provider labels in stored history: every row whose provider matches an alias key is rewritten to its canonical label. Driven by config (provider_aliases) - a label split by a naming-scheme change is repaired generically, never with provider-specific code. Serialization with the writer goroutine comes from the single-connection write pool (the same shape a filtered Clear relies on); totals are label-independent, so no rebuild is needed. Malformed pairs are skipped (config Validate already rejects them - this is the last-chance deny), and each UPDATE is parameterized.
func (*Store) SaveDebugCapture ¶
func (s *Store) SaveDebugCapture(ctx context.Context, id, sessionID string, capturedAt, expiresAt int64, payload []byte) error
SaveDebugCapture writes one request's debug document (sidecar). Synchronous on the writer connection so a drawer fetch after the request finalizes cannot miss the row.
func (*Store) SaveDebugSessions ¶
SaveDebugSessions persists operator-debug session JSON. Implements proxy.pausePersist.
func (*Store) SaveMeta ¶
SaveMeta writes a durable key/value (operator state, not request rows). Synchronous on the writer connection so a reboot sees the latest value.
func (*Store) SaveThrottle ¶
SaveThrottle persists provider-throttle JSON. Implements proxy.pausePersist.
func (*Store) Snapshot ¶ added in v0.3.6
Snapshot returns a packed, integrity-checked copy of the live database, including committed WAL pages. VACUUM INTO is the compact consistent snapshot (no free pages); it is not a file copy of an open DB.
func (*Store) SnapshotOverlap ¶ added in v0.3.6
SnapshotOverlap is how many request ids in a packed snapshot already exist in the live store. Used by restore inspect.
func (*Store) StreamProjection ¶
func (s *Store) StreamProjection(ctx context.Context, columns string, cursor ProjectionCursor, fn func(*sql.Rows) error) (ProjectionCursor, bool, error)
StreamProjection materializes either all analytical rows or only append deltas. The epoch, high-water mark, and rows share one read transaction, so another process committing during a scan cannot make its cursor outrun its rows. Rewrites/removals/schema changes require a replacement snapshot. columns is code-owned (the same list consumed by scanContrib), never input from an HTTP request. No prompts, responses, or full records are retained.
func (*Store) Totals ¶
Totals returns the since-inception aggregate (durably committed rows). For ring-merge consumers prefer TotalsAndUnwritten (atomic pair).
func (*Store) TotalsAndUnwritten ¶
TotalsAndUnwritten captures totals and filters the caller-owned ring slice under one accounting lock. A racing commit cannot remove a row from both sides of the merge. Only the bounded ring is probed; the written set is not copied, and record folding happens after the lock is released.
func (*Store) TotalsDegraded ¶ added in v0.3.10
TotalsDegraded reports whether the boot-time totals scan failed, leaving the aggregate a truthful subset (rows committed since the failed scan) instead of since-inception totals.
type Totals ¶
type Totals struct {
Requests int64 `json:"requests"`
Errors int64 `json:"errors"`
Cost float64 `json:"cost"`
InputTok int64 `json:"input_tokens"`
OutputTok int64 `json:"output_tokens"`
CacheRead int64 `json:"cache_read_tokens"`
Reasoning int64 `json:"reasoning_tokens"`
TTFTSum int64 `json:"-"`
TTFTN int64 `json:"-"`
TPSSum float64 `json:"-"`
TPSN int64 `json:"-"`
// Cost-bearing denominators for the KPI's blended unit economics: the
// count of requests that reported cost > 0 and their in+out tokens.
// Blended $/req and $/Mtok divide by these, so an unpriced provider
// (no cost in its response → 0) can never dilute the rate.
CostReqs int64 `json:"-"`
CostInOut float64 `json:"-"` // ratios are floating point; in+out need not fit int64
// contains filtered or unexported fields
}
Totals is the since-inception cross-request aggregate the dashboard's KPI surface reads. The single writer goroutine maintains it exactly: only rows that have been durably committed are counted, so a dashboard aggregate that merges totals with the un-flushed ring records (deduped via the written-id set) matches the durable history byte-for-byte with no flush-lag window. IsError semantics come from the canonical metrics.Record.IsError.
func (*Totals) Add ¶
Add is the shared durable/ring accounting owner. Arithmetic failure stays attached to the aggregate until a rebuild, never masquerading as zero.