state

package
v0.0.1-alpha.26 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package state defines the Store interface and provides four implementations:

  • MemoryStore — in-process maps; lost on restart; fastest; best for tests & CI.
  • SQLiteStore — synchronous SQLite writes (persistent mode).
  • WALStore — memory reads + append-log durability with replay on startup.
  • HybridStore — memory reads + async SQLite flush; default for local development.
  • NamespacedStore — dispatcher that routes operations to per-service stores.

All service handlers receive a Store — they never know or care which implementation is backing them. This is Go's equivalent of programming to an interface rather than a concrete type.

TypeScript analogy:

interface Store {
  get(namespace: string, key: string): Promise<string | null>
  set(namespace: string, key: string, value: string): Promise<void>
  delete(namespace: string, key: string): Promise<void>
  list(namespace: string, prefix: string): Promise<string[]>
}

Index

Constants

This section is empty.

Variables

View Source
var ErrStorePressure = errors.New("state: storage under write pressure; retry")

ErrStorePressure is a sentinel marking a storage read/write error that resulted from transient write pressure — SQLite busy/locked contention that outlasted every retry the store attempted internally (see hybrid.go's shouldRetryHybridSQLiteRead, hybridBusyTimeoutMillis, and hybridSQLiteReadRetryTimeout) — rather than data corruption or an unrecoverable infrastructure failure.

Real AWS signals this class of condition with a throttling error that SDK retry policies handle automatically; a plain "context deadline exceeded" wrapped as InternalError (the pre-existing behavior) instead causes most SDKs to give up immediately. Wrap this sentinel into an error with fmt.Errorf("...: %w", ErrStorePressure) so errors.Is can find it anywhere in the chain — including through a *protocol.AWSError built via protocol.Wrap, since AWSError.Unwrap exposes its cause. See protocol/errors.go's Write*Error functions for where this gets remapped into a wire-appropriate throttling response — that is the only place any code needs to check for this sentinel; no service needs to change.

Only the hybrid SQLite read-retry path (hybrid.go) currently attaches this — MemoryStore has no notion of storage pressure, and genuine non-retryable SQLite errors (corruption, disk failure, schema problems) are deliberately left unwrapped so they keep surfacing as InternalError.

Functions

func Flush

func Flush(ctx context.Context, s Store) error

Flush persists buffered writes when the store supports it. Stores without a buffered durability layer are already current, so this is a no-op.

func RegisterMigration

func RegisterMigration(m Migration)

RegisterMigration adds m to the set of migrations applied by RunMigrations. Call this from an init() function in any package that owns a piece of the shared schema (e.g. a future internal/services/cloudwatch/logs package registering its events-table migration) — internal/state never imports those packages, so this is the standard Go registry pattern (database/sql drivers use the same trick).

Panics on a nil Up func or a duplicate Version — both are programmer errors caught at binary startup (via the registering package's own init()), not runtime conditions calling code should handle.

func RunMigrations

func RunMigrations(ctx context.Context, db *sql.DB, dbPath string, log *zap.Logger) error

RunMigrations applies every migration registered via RegisterMigration that is newer than db's current PRAGMA user_version, in ascending Version order, and takes a pre-migration backup when at least one migration is pending (see backupBeforeMigration). Call this from the same background goroutine that already performs schema setup off the request path (see SQLiteStore.runMigrate) — never from a request-handling goroutine.

dbPath is the on-disk path to the SQLite file backing db; it is used only to name and write the pre-migration backup file. Pass "" to skip backups (e.g. an in-memory database in a test that doesn't care about the backup path).

log is optional — pass nil to disable logging. When nothing is pending (the common case on every startup after the first), RunMigrations does not log at Info level, to avoid startup noise.

Types

type DataDirProbeResult

type DataDirProbeResult struct {
	// FsyncMillis is how long the probe's write+fsync took, in milliseconds.
	FsyncMillis int64 `json:"fsyncMillis"`

	// Slow is true when FsyncMillis exceeded the configured threshold
	// (dataDirSlowFsyncThreshold by default) — the same condition that
	// triggers the one-time startup warning log line.
	Slow bool `json:"slow"`

	// ProbedAt is when the probe ran, UTC.
	ProbedAt time.Time `json:"probedAt"`

	// FsType is the filesystem type hosting the data dir per /proc/mounts
	// (e.g. "ext4", "9p", "fuse.grpcfuse"); empty when it couldn't be read
	// (non-Linux, or /proc/mounts unavailable).
	FsType string `json:"fsType,omitempty"`

	// MountClass is a coarse classification of FsType used to tailor the
	// slow-filesystem advisory: "native" (real in-VM/host filesystem —
	// slowness means I/O pressure, not a bind mount), "shared" (Docker
	// Desktop file-sharing protocol — the bind-mount tax; switching to a
	// named volume applies), or "unknown".
	MountClass string `json:"mountClass,omitempty"`
}

DataDirProbeResult is the outcome of the startup fsync micro-probe (see HybridStore's runDataDirProbe / probeDataDirFsync): a small write+fsync timed against dataDirSlowFsyncThreshold, run once per process so a slow data directory is visible before it degrades flush/read latency (docs/ performance.md "Data dir placement").

type DebugFlushRecord

type DebugFlushRecord struct {
	Timestamp      time.Time `json:"timestamp"`
	DurationMillis int64     `json:"durationMillis"`
	Entries        int       `json:"entries"`
	Committed      bool      `json:"committed"`

	// Chunks is how many separate SQLite transactions this flush attempt
	// split Entries across (storage-pressure-handling item 2 — see
	// HybridStore's chunkHybridFlushOps). 1 for a batch small enough to fit
	// in a single transaction; 0 for backends that don't chunk flushes.
	Chunks int `json:"chunks,omitempty"`
}

DebugFlushRecord is one entry in DebugMetrics.FlushHistory: a single attempt to persist buffered writes, whether or not it succeeded.

type DebugMetrics

type DebugMetrics struct {
	// Mode identifies which backend produced this snapshot (e.g. "hybrid",
	// "persistent"), matching config.Config.State's values.
	Mode string `json:"mode"`

	// FlushHistory holds the most recent flush attempts, oldest first,
	// bounded to a small ring buffer. Empty for backends that write
	// synchronously and never batch/flush (e.g. SQLiteStore).
	FlushHistory []DebugFlushRecord `json:"flushHistory,omitempty"`

	// SeedDurationMillis is how long the background TierHot seed took to
	// complete, in milliseconds, or nil if seeding hasn't finished yet, is
	// still in flight, degraded to memory-only before finishing (see
	// HybridStore.degradeToMemoryOnly — there is no coherent "seed
	// duration" for a seed that didn't complete), or never happens for this
	// backend at all.
	SeedDurationMillis *int64 `json:"seedDurationMillis,omitempty"`

	// PendingLogBytes is the current on-disk size of the not-yet-flushed
	// write-ahead log, in bytes. 0 for backends without one.
	PendingLogBytes int64 `json:"pendingLogBytes,omitempty"`

	// NamespaceRowCounts maps namespace -> row count, populated only when
	// DebugMetricsOptions.IncludeNamespaceRowCounts was set.
	NamespaceRowCounts map[string]int `json:"namespaceRowCounts,omitempty"`

	// ReadRetryCount is the total number of individual retry attempts made
	// by the hybrid SQLite read-retry path (retrySQLiteGet/List/
	// ListNamespaces/Scan/ScanPage in hybrid.go) since process start, after
	// an initial read hit busy/locked contention or a canceled context. One
	// Get/List/Scan call under contention can contribute several retries.
	// 0 for backends without a read-retry path (MemoryStore, WALStore).
	ReadRetryCount int64 `json:"readRetryCount,omitempty"`

	// ReadTimeoutCount is the number of hybrid SQLite reads whose retry
	// window (hybridSQLiteReadRetryTimeout) was fully exhausted without
	// success since process start. Each of these is tagged with
	// ErrStorePressure (see HybridStore.wrapStorePressure) so callers up
	// the stack receive an AWS-shaped throttling error instead of a generic
	// InternalError — see protocol/errors.go. 0 for backends without a
	// read-retry path.
	ReadTimeoutCount int64 `json:"readTimeoutCount,omitempty"`

	// DataDirProbe reports the outcome of the one-time startup fsync
	// micro-probe (see HybridStore's runDataDirProbe) that flags a slow
	// data directory (e.g. a Docker Desktop bind mount) before it manifests
	// as flush/read latency. nil for backends that don't run the probe.
	DataDirProbe *DataDirProbeResult `json:"dataDirProbe,omitempty"`

	// JournalMode is the LIVE `PRAGMA journal_mode` readback from this
	// backend's SQLite connection, queried once during store initialisation
	// (after the connection is open and migrated, never per-request) — see
	// hybrid.go's seedFromSQLite and sqlite.go's runMigrate. Empty for
	// backends with no real SQLite connection to ask (MemoryStore, WALStore)
	// or when the backend degraded before the readback could run.
	//
	// This field exists because of a real historical failure, not a
	// hypothetical one: every persistent connection's DSN silently disabled
	// WAL mode for this project's entire history due to a driver-spelling
	// bug (see hybrid_journalmode_internal_test.go), while every doc comment
	// in this package confidently claimed WAL was active the whole time.
	// Surfacing the ACTUAL, LIVE pragma value here — rather than trusting the
	// DSN parameter we asked for — means that class of silent misconfiguration
	// can never hide again; see internal/router/advisories.go's
	// journal-mode-not-wal rule, which alerts the moment this diverges from
	// "wal" on a persistent/hybrid backend.
	JournalMode string `json:"journalMode,omitempty"`

	// Degraded is true when the persistent backend permanently fell back to
	// memory-only for this process's lifetime (see
	// HybridStore.degradeToMemoryOnly) — every write since is memory-only and
	// will not survive a restart. Always false for backends that cannot
	// degrade this way: SQLiteStore fails outright rather than falling back,
	// and MemoryStore/WALStore never persist at all.
	Degraded bool `json:"degraded,omitempty"`

	// Counters holds cumulative storage-layer read/write activity since
	// process start (health/metrics "storage activity" — the answer to "how
	// much is this backend actually doing"). Every implementation
	// (MemoryStore, SQLiteStore, WALStore, HybridStore) populates at least
	// Reads/Writes; see StoreCounters's doc comment for the hybrid-specific
	// tier split.
	Counters StoreCounters `json:"counters"`
}

DebugMetrics is a snapshot of storage-layer diagnostics for GET /_debug/metrics (storage-plan.md item 3.6): recent flush history, the one-time TierHot seed duration, the pending write-ahead log's on-disk size, and — only when DebugMetricsOptions.IncludeNamespaceRowCounts is set — per-namespace row counts.

func DebugMetricsSnapshot

func DebugMetricsSnapshot(ctx context.Context, store Store, opts DebugMetricsOptions) ([]DebugMetrics, bool)

DebugMetricsSnapshot returns one DebugMetrics entry per distinct underlying store that implements DebugMetricsReporter. Since every store implementation in this package now implements it (see DebugMetricsReporter's doc comment), the returned bool is false only for a Store from outside this package that chooses not to implement the interface.

Deliberately does NOT follow PersistentHealthSnapshot's merge-into-one approach for a *NamespacedStore: PersistentHealth's fields combine sensibly across backends (Healthy is a meaningful AND, PendingWrites is a meaningful sum), but DebugMetrics's fields do not — merging two distinct stores' flush-history ring buffers into one timeline, or averaging two unrelated seed durations, would produce a number that doesn't correspond to anything real. So instead of one merged snapshot, this returns one snapshot per distinct underlying store and lets the caller (the /_debug/metrics handler) render them as a list. It also doesn't follow NotReadyReporter's direct-implementation-on-NamespacedStore approach, since NotReady's single boolean OR *is* meaningful to compute directly on the wrapper, whereas "the" DebugMetrics of a NamespacedStore isn't a single value at all once more than one distinct backend is in play.

type DebugMetricsOptions

type DebugMetricsOptions struct {
	// IncludeNamespaceRowCounts additionally populates
	// DebugMetrics.NamespaceRowCounts. For TierCached namespaces this issues
	// one SQL COUNT(*) per namespace currently known to the store — cheap
	// enough for an on-demand debug call, but not something to compute
	// unconditionally on every /_debug/metrics hit, so callers opt in.
	IncludeNamespaceRowCounts bool
}

DebugMetricsOptions controls which (potentially expensive) fields DebugMetricsReporter.DebugMetrics computes.

type DebugMetricsReporter

type DebugMetricsReporter interface {
	DebugMetrics(ctx context.Context, opts DebugMetricsOptions) DebugMetrics
}

DebugMetricsReporter is an optional Store extension exposing the diagnostics in DebugMetrics. Every implementation in this package (MemoryStore, SQLiteStore, WALStore, HybridStore) implements it, if only to report StoreCounters — MemoryStore and WALStore have no async write path or one-time startup seed, so every other DebugMetrics field stays at its zero value for them. Callers must still treat a failed type assertion as "nothing to report" for any future Store implementation that chooses not to implement this interface, the same convention PersistentHealthReporter and NotReadyReporter use.

type Flushable

type Flushable interface {
	Flush(ctx context.Context) error
}

Flushable is an optional Store extension for backends that buffer writes. Flush blocks until all writes accepted before the call are persisted or an error proves the persistent backend is currently unavailable.

type HybridOptions

type HybridOptions struct {
	// FlushInterval is how often the background loop flushes dirty writes to
	// SQLite on a timer, independent of the size-triggered early flush.
	FlushInterval time.Duration

	// SyncMode controls how the pending log file is fsync'd: WALSyncAlways
	// syncs inline on every append, WALSyncInterval (default) syncs on a
	// timer, WALSyncNever relies on the OS page cache plus the sync always
	// performed on Close.
	SyncMode WALSyncMode

	// SyncInterval is used only when SyncMode is WALSyncInterval. Defaults to
	// 100ms.
	SyncInterval time.Duration

	// DirtyEntryThreshold triggers an out-of-band flush signal once this many
	// unflushed pending-log operations have accumulated, ahead of the next
	// FlushInterval tick. Defaults to 10,000. A value <= 0 disables this
	// trigger (byte threshold still applies unless it is also disabled).
	DirtyEntryThreshold int

	// DirtyByteThreshold triggers an out-of-band flush once the approximate
	// byte size of unflushed writes exceeds this many bytes. Defaults to
	// 8 MiB. A value <= 0 disables this trigger.
	DirtyByteThreshold int64

	// MaintenanceInterval controls how often the background loop runs
	// routine SQLite housekeeping (3.5 in docs/plans/storage-plan.md): a passive
	// WAL checkpoint plus a conditional incremental vacuum. Never runs on
	// the request path. Defaults to 5 minutes. A value <= 0 falls back to
	// the default rather than disabling the loop — unlike the dirty
	// thresholds above, there's no useful "off" state for routine
	// maintenance, so this intentionally doesn't support disabling it via a
	// non-positive value.
	MaintenanceInterval time.Duration

	// DataDirSlowFsyncThreshold overrides the duration above which the
	// one-time startup fsync micro-probe (runDataDirProbe) considers the
	// data directory "slow" and logs a warning (storage-pressure-handling
	// item 3). Defaults to dataDirSlowFsyncThreshold. A value <= 0 falls
	// back to the default.
	DataDirSlowFsyncThreshold time.Duration
	// contains filtered or unexported fields
}

HybridOptions configures HybridStore durability and burst-flush behavior. The zero value is not directly usable for SyncMode validation purposes — use NewHybridStore/NewHybridStoreWithLogger for the documented defaults, or leave fields unset when calling NewHybridStoreWithOptions to get the same defaults (interval sync at 100ms, 10,000-entry / 8 MiB dirty thresholds).

type HybridStore

type HybridStore struct {
	// contains filtered or unexported fields
}

HybridStore serves all reads from an in-memory map (memory speed) and asynchronously flushes writes to SQLite at a configurable interval.

On startup it opens the SQLite file AND seeds the in-memory layer from it in a background goroutine. The constructor returns immediately. Reads use indexed SQLite fallback until seeding completes, then switch to memory; writes are accepted immediately and take precedence over loaded state. DB() blocks only until the SQLite handle is ready. This keeps full-table restore and the modernc SQLite driver's cold-start cost off the startup path.

Accepted writes are appended to a pending log before returning, then batch-flushed to SQLite. This protects against process crashes before the next SQLite flush without forcing a full SQLite transaction on every service mutation. The pending log's fsync policy is configurable (see HybridOptions.SyncMode); a size/count-triggered early flush keeps the log and the in-memory overlay bounded during write bursts (see HybridOptions.DirtyEntryThreshold / DirtyByteThreshold).

If the persistent backend cannot be opened, migrated, or seeded, the store degrades to memory-only rather than failing every subsequent request: see degradeToMemoryOnly.

func NewHybridStore

func NewHybridStore(dataDir string, flushInterval time.Duration) (*HybridStore, error)

NewHybridStore creates a HybridStore backed by a SQLite file in dataDir, using default durability and burst-flush settings (interval sync at 100ms, 10,000-entry / 8 MiB dirty thresholds). SQLite is opened and existing data loaded in a background goroutine; the constructor returns immediately. Reads fall back to SQLite until the cache is loaded. Writes are accepted right away and take precedence over loaded state.

func NewHybridStoreWithLogger

func NewHybridStoreWithLogger(dataDir string, flushInterval time.Duration, logger *zap.Logger) (*HybridStore, error)

NewHybridStoreWithLogger creates a HybridStore and emits structured timing diagnostics for startup seeding and flushes when logger is non-nil. Uses default durability and burst-flush settings — see NewHybridStoreWithOptions to override them.

func NewHybridStoreWithOptions

func NewHybridStoreWithOptions(dataDir string, opts HybridOptions, logger *zap.Logger) (*HybridStore, error)

NewHybridStoreWithOptions creates a HybridStore with full control over durability and burst-flush behavior. Zero-valued fields in opts fall back to the documented defaults (see HybridOptions).

func (*HybridStore) Close

func (s *HybridStore) Close() error

Close stops the background goroutines, performs a final synchronous flush of all pending dirty entries to SQLite (skipped when the store is degraded to memory-only), then closes the database and read pool.

func (*HybridStore) DB

func (s *HybridStore) DB() *sql.DB

DB returns the underlying *sql.DB, satisfying SQLiteDBProvider. Blocks until the background SQLite open has completed. Returns nil if the open failed — callers that hit this path should have already seen the load error propagate via a Get/List/Scan call. Unlike KV routing, this is not gated on sqliteDegraded: a KV-seed failure doesn't necessarily mean the connection is unusable for service-owned dedicated tables (e.g. DynamoDB).

func (*HybridStore) DebugMetrics

func (s *HybridStore) DebugMetrics(ctx context.Context, opts DebugMetricsOptions) DebugMetrics

DebugMetrics implements state.DebugMetricsReporter (storage-plan.md item 3.6).

func (*HybridStore) Delete

func (s *HybridStore) Delete(ctx context.Context, namespace, key string) error

Delete publishes the tombstone before removing memory for the same reason as Set: the dirty overlay is what makes lazy reads linearizable with writes.

func (*HybridStore) DeletePrefix

func (s *HybridStore) DeletePrefix(ctx context.Context, namespace, prefix string) error

DeletePrefix publishes a single ranged tombstone instead of enumerating and tombstoning every matching key (the pre-1.8 behavior, O(n) log lines and O(n) flush statements for a namespace with many keys under prefix). Reads resolve the tombstone against per-key overlay entries by sequence number (see resolvePendingLocked) and flush executes it as one ranged DELETE (see hybridFlushDeletePrefix).

func (*HybridStore) Flush

func (s *HybridStore) Flush(ctx context.Context) error

Flush synchronously persists all dirty writes accepted before this call. A no-op when the persistent backend is degraded to memory-only.

func (*HybridStore) Get

func (s *HybridStore) Get(ctx context.Context, namespace, key string) (string, bool, error)

Get serves from memory after the initial seed completes. During seed it falls back to SQLite so persisted state stays visible without blocking on a full-table cache warmup. A degraded persistent backend (see degradeToMemoryOnly) never blocks a read — it simply serves from memory.

func (*HybridStore) List

func (s *HybridStore) List(ctx context.Context, namespace, prefix string) ([]string, error)

List serves from memory after the initial seed completes. During seed it uses SQLite plus the dirty overlay to avoid first-request stalls.

func (*HybridStore) ListNamespaces

func (s *HybridStore) ListNamespaces(ctx context.Context) ([]string, error)

func (*HybridStore) NotReady

func (s *HybridStore) NotReady() bool

NotReady implements state.NotReadyReporter: true only while the background schema migration is still in flight (sqliteReady not yet closed) — deliberately not gated on sqliteDegraded or isLoaded. A store that finished migrating and then degraded to memory-only (see degradeToMemoryOnly) is done with its startup phase and serves memory reads correctly forever after; that is an ongoing health condition (PersistentHealth), not a "still starting up" one, so NotReady must not keep reporting true for it. Likewise, the seed phase that follows a successful migration is not included here: once sqliteReady closes, Get/List/Scan already fall back to querying SQLite directly for anything not yet loaded into memory, so reads are accurate (just slower) — only the migration itself has a window where TierHot reads would otherwise silently return "not found" for data that exists once migration finishes.

func (*HybridStore) PersistentHealth

func (s *HybridStore) PersistentHealth() PersistentHealth

func (*HybridStore) Scan

func (s *HybridStore) Scan(ctx context.Context, namespace, prefix string) ([]KV, error)

Scan serves from memory after the initial seed completes. During seed it uses SQLite plus the dirty overlay to avoid first-request stalls.

func (*HybridStore) ScanPage

func (s *HybridStore) ScanPage(ctx context.Context, namespace, prefix, startAfter string, limit int) ([]KV, string, error)

ScanPage is the paginated counterpart to Scan (3.2 in docs/plans/storage-plan.md). Branching mirrors Scan/List exactly: once a TierHot namespace has finished seeding, memory alone is authoritative (every write updates mem synchronously inside Set/Delete/DeletePrefix, so there is nothing left to merge) and ScanPage delegates straight to MemoryStore.ScanPage's seek-based pagination. Every other case — a TierCached namespace, or a TierHot namespace still mid-seed — must merge a paginated base read (SQLite once ready, memory as a startup fallback) against the pending write overlay; see hybridScanPageMerged for how that merge preserves exact pagination correctness across the boundary between persisted/seeded state and not-yet-flushed writes.

func (*HybridStore) Set

func (s *HybridStore) Set(ctx context.Context, namespace, key, value string) error

Set publishes the dirty overlay before writing memory so lazy SQLite-backed reads cannot observe stale persisted state between the two updates.

func (*HybridStore) WaitReady

func (s *HybridStore) WaitReady(ctx context.Context) error

WaitReady blocks until the background SQLite open and migration has completed, satisfying state.ReadyAwaiter. Returns nil when the store is ready for reads, or ctx.Err() if the context is cancelled first. Callers that need to guarantee persisted data is visible (e.g. startup reload routines) should call this before performing a full Scan. Returns the seed error when the store degraded to memory-only (see degradeToMemoryOnly) — that is the correct signal for such callers, even though ordinary Get/List/Scan callers are unaffected.

type KV

type KV struct {
	Key   string
	Value string
}

KV is a key-value pair returned by Scan.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore is the default Store implementation. All data lives in-process and is lost when the process exits. This is the right default for local development: zero configuration, instant startup, deterministic test state.

Performance characteristics:

  • Get/Set/Delete: O(log n) per namespace, protected by RWMutex
  • List/Scan: O(log n + m) prefix scan via btree (m = matching keys)
  • RWMutex allows many concurrent readers OR one exclusive writer

Memory note: MemoryStore does not enforce size limits. For local dev and CI the footprint is naturally bounded by test duration. Overcast is not designed for production use where unbounded growth would be a concern.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an initialised MemoryStore.

func (*MemoryStore) Close

func (s *MemoryStore) Close() error

func (*MemoryStore) DebugMetrics

DebugMetrics implements state.DebugMetricsReporter. MemoryStore has no async write path, background seed, or persistent backend to report on, so every field besides Mode and Counters stays at its zero value.

func (*MemoryStore) Delete

func (s *MemoryStore) Delete(_ context.Context, namespace, key string) error

func (*MemoryStore) DeletePrefix

func (s *MemoryStore) DeletePrefix(_ context.Context, namespace, prefix string) error

DeletePrefix removes all keys with prefix under a single write lock.

func (*MemoryStore) Get

func (s *MemoryStore) Get(_ context.Context, namespace, key string) (string, bool, error)

func (*MemoryStore) Len

func (s *MemoryStore) Len() int

Len returns the number of entries. Used by /_debug/state and tests.

func (*MemoryStore) List

func (s *MemoryStore) List(_ context.Context, namespace, prefix string) ([]string, error)

func (*MemoryStore) ListNamespaces

func (s *MemoryStore) ListNamespaces(_ context.Context) ([]string, error)

func (*MemoryStore) Reset

func (s *MemoryStore) Reset()

Reset wipes all stored data atomically.

func (*MemoryStore) Scan

func (s *MemoryStore) Scan(_ context.Context, namespace, prefix string) ([]KV, error)

Scan returns all key-value pairs whose keys start with prefix, under a single RLock. This is the preferred way to read a batch of items — it avoids N individual Get calls each acquiring their own lock.

func (*MemoryStore) ScanPage

func (s *MemoryStore) ScanPage(_ context.Context, namespace, prefix, startAfter string, limit int) ([]KV, string, error)

ScanPage returns up to limit key-value pairs whose keys start with prefix, in key order, starting strictly after startAfter — see Store.ScanPage for the full contract. Uses the underlying btree's ordered Ascend to seek directly to the resume point rather than re-scanning from the start of the prefix range on every page, and stops as soon as limit is reached instead of fetching limit+1 rows the way the SQL-backed implementations do — an in-memory ordered seek makes that trick unnecessary here.

func (*MemoryStore) Set

func (s *MemoryStore) Set(_ context.Context, namespace, key, value string) error

type Migration

type Migration struct {
	// Version is this migration's position in PRAGMA user_version terms.
	// Must be unique across every call to RegisterMigration in the binary.
	Version int

	// Name is a short, human-readable label used in log lines and error
	// messages — it is not used to order or identify the migration, Version
	// is.
	Name string

	// Up applies the migration's schema/data change. It runs inside a
	// transaction the runner opens and commits together with the
	// PRAGMA user_version bump for this migration — if Up returns an error,
	// the whole transaction (including the version bump) rolls back, so a
	// failed migration never leaves user_version pointing past a change that
	// didn't actually apply.
	Up func(ctx context.Context, tx *sql.Tx) error

	// AfterCommit, if set, runs immediately after Up's transaction commits
	// (and user_version has advanced to Version) — for statements SQLite
	// refuses to run inside a transaction. VACUUM is the motivating case: it
	// opens its own internal transaction and errors with "cannot VACUUM from
	// within a transaction" if issued through tx.ExecContext. AfterCommit
	// runs against the raw *sql.DB instead, right after the wrapping
	// transaction commits, still gated by the same pending-migration check
	// as every other migration.
	//
	// An AfterCommit failure is still reported as a failed migration (the
	// runner returns an error naming this migration and stops before running
	// any later ones), but because Up's transaction already committed and
	// user_version already advanced, a restart will not retry Up or
	// AfterCommit for this version — only Up's effects are guaranteed
	// idempotent/transactional; AfterCommit is best-effort and its own
	// statements should be safe to have partially applied (VACUUM is: it
	// either finishes or SQLite leaves the database in its pre-VACUUM
	// state).
	AfterCommit func(ctx context.Context, db *sql.DB) error
}

Migration is one versioned, ordered schema/data change applied to the shared overcast.db SQLite file. Version must be unique across the whole binary and migrations run in ascending Version order regardless of registration order, so package init() order does not matter.

Version numbering is a reserved-range convention, not auto-assigned, so packages can pick a version without coordinating with every other package's source at once:

  • 1-9: internal/state core (kv table, auto_vacuum). Only 1 and 2 are used today — see migrationKVTableVersion and migrationAutoVacuumVersion below.
  • 10-19: reserved for the CloudWatch Logs events table (storage-plan.md Phase 2 item 2.3) — see internal/services/cloudwatch/logs/migrations.go.
  • 20-29: reserved for the DynamoDB items/stream-records tables (storage-plan.md Phase 3 item 3.9) — see internal/services/dynamodb/migrations.go.
  • 30-39: reserved for the SQS messages table (storage-plan.md item 3.10) — see internal/services/sqs/migrations.go.
  • 40+: free for future dedicated tables. Claim the next unused decade and document it here when a package registers into it.

There are no down-migrations. The runner takes a file-copy backup of overcast.db before applying the first pending migration (see backupBeforeMigration) — restoring that backup file is the documented rollback story; there is no automated restore tooling.

type NamespacedStore

type NamespacedStore struct {
	// contains filtered or unexported fields
}

NamespacedStore routes Store operations to different backends based on the service prefix of the namespace argument. Two rules apply, depending on whether the namespace contains a colon:

  • Namespaced keys use the segment before the first ":" as the service prefix:

    namespace "sqs:queues" → service prefix "sqs" namespace "s3:objects" → service prefix "s3"

  • Colonless namespaces are matched by their whole name, since some services pass their bare service name as the namespace with no colon at all. As of this fix, the known colonless-namespace services are: appsync, cloudfront, kms, ssm, and stepfunctions.

    namespace "ssm" → service prefix "ssm" namespace "kms" → service prefix "kms"

Operations whose namespace has no matching override are forwarded to the default store. This allows per-service storage modes with a single Store interface visible to the rest of the codebase.

This interplay matters because route keys come from config.ServiceNamespacePrefix(service) in cmd/overcast: for the colonless services, service name == namespace == route key, so whole-name matching is what makes their OVERCAST_STATE_<SVC> overrides take effect at all.

func NewNamespacedStore

func NewNamespacedStore(defaultStore Store, routes map[string]Store) *NamespacedStore

NewNamespacedStore creates a NamespacedStore. routes maps service prefixes (e.g. "s3", "sqs") to their dedicated Store. Services absent from routes use defaultStore.

func (*NamespacedStore) Close

func (s *NamespacedStore) Close() error

Close closes the default store and all per-service stores. Each underlying store is closed exactly once even if it serves multiple namespace prefixes.

func (*NamespacedStore) Delete

func (s *NamespacedStore) Delete(ctx context.Context, namespace, key string) error

func (*NamespacedStore) DeletePrefix

func (s *NamespacedStore) DeletePrefix(ctx context.Context, namespace, prefix string) error

func (*NamespacedStore) Get

func (s *NamespacedStore) Get(ctx context.Context, namespace, key string) (string, bool, error)

func (*NamespacedStore) List

func (s *NamespacedStore) List(ctx context.Context, namespace, prefix string) ([]string, error)

func (*NamespacedStore) ListNamespaces

func (s *NamespacedStore) ListNamespaces(ctx context.Context) ([]string, error)

func (*NamespacedStore) NotReady

func (s *NamespacedStore) NotReady() bool

NotReady implements state.NotReadyReporter directly on *NamespacedStore itself — rather than via a package-level aggregator like PersistentHealthSnapshot's — so a caller that type-asserts a possibly- wrapped Store to NotReadyReporter (as middleware.NotReady does) sees the same interface-erasure protection WaitReady already established in Phase 1: any one underlying store still completing startup work makes the whole NamespacedStore not ready yet.

func (*NamespacedStore) Scan

func (s *NamespacedStore) Scan(ctx context.Context, namespace, prefix string) ([]KV, error)

func (*NamespacedStore) ScanPage

func (s *NamespacedStore) ScanPage(ctx context.Context, namespace, prefix, startAfter string, limit int) ([]KV, string, error)

func (*NamespacedStore) Set

func (s *NamespacedStore) Set(ctx context.Context, namespace, key, value string) error

func (*NamespacedStore) StoreFor

func (s *NamespacedStore) StoreFor(servicePrefix string) Store

StoreFor returns the store responsible for a given service prefix — the namespace segment before the colon (e.g. "dynamodb", "s3", "sqs"). Returns the default store when no override is registered for servicePrefix.

Callers that need to type-assert a Store to an optional interface (state.SQLiteDBProvider, state.ReadyAwaiter, ...) should resolve through this method first — see Unwrap — rather than asserting directly against a possibly-wrapped Store, which silently erases the capability whenever any unrelated service has an OVERCAST_STATE_<SVC> override configured.

func (*NamespacedStore) UnderlyingStores

func (s *NamespacedStore) UnderlyingStores() []Store

UnderlyingStores returns the default store plus every distinct routed store, each exactly once even if the same Store instance is shared by multiple service prefixes (or is also the default store).

func (*NamespacedStore) WaitReady

func (s *NamespacedStore) WaitReady(ctx context.Context) error

WaitReady implements ReadyAwaiter by waiting on every distinct underlying store that itself implements ReadyAwaiter. Stores that don't implement it are treated as already ready, per the ReadyAwaiter contract. Returns the first error encountered (including ctx cancellation), or nil once every underlying store that needed it is ready.

type NotReadyReporter

type NotReadyReporter interface {
	// NotReady reports, without blocking, whether the store is still
	// completing one-time startup work.
	NotReady() bool
}

NotReadyReporter is implemented by stores with a distinguishable "still completing one-time startup work" state — currently, an in-progress schema migration (see internal/state/migrate.go). Unlike ReadyAwaiter, this is a non-blocking check: middleware.NotReady uses it once per request to return a proper "service unavailable, retry" response instead of letting the request observe whatever the store would otherwise do during this window — HybridStore's TierHot reads silently returning empty because the seed hasn't started yet, or SQLiteStore blocking the request indefinitely inside ensureReady.

Once a store's one-time startup work finishes (successfully, or by degrading — see HybridStore's degradeToMemoryOnly), NotReady must return false for the rest of the process's life; it reports a startup phase, not an ongoing health condition — PersistentHealthReporter is the interface for that.

Stores that never have this kind of startup phase (MemoryStore, WALStore) do not need to implement this interface; callers must treat its absence as "already ready", the same convention ReadyAwaiter uses.

type PersistentHealth

type PersistentHealth struct {
	Mode          string    `json:"mode"`
	Healthy       bool      `json:"healthy"`
	PendingWrites int       `json:"pendingWrites"`
	LastError     string    `json:"lastError,omitempty"`
	LastErrorAt   time.Time `json:"lastErrorAt,omitempty"`
	LastSuccessAt time.Time `json:"lastSuccessAt,omitempty"`
}

PersistentHealth is a small, JSON-friendly snapshot of persistent backend status. LastError is intentionally text: callers should not branch on it.

func PersistentHealthSnapshot

func PersistentHealthSnapshot(s Store) (PersistentHealth, bool)

PersistentHealthSnapshot returns persistent backend health when the store has one. The boolean is false for memory-only or otherwise non-reporting stores.

A *NamespacedStore does not itself implement PersistentHealthReporter — a direct type assertion against it would silently report "no persistent health" for every service whenever any unrelated OVERCAST_STATE_<SVC> override is configured, even though the underlying per-service stores do have real health to report (the same erasure class Unwrap exists to guard against). Since this function's callers (the health endpoint, shutdown logging) want one aggregate view rather than a specific service's, it unwraps a NamespacedStore into its distinct underlying stores and combines their reports instead of delegating to Unwrap.

type PersistentHealthReporter

type PersistentHealthReporter interface {
	PersistentHealth() PersistentHealth
}

PersistentHealthReporter is an optional Store extension for exposing live persistent-backend health without forcing all Store implementations to carry storage-specific fields.

type PrefixDeleter

type PrefixDeleter interface {
	DeletePrefix(ctx context.Context, namespace, prefix string) error
}

PrefixDeleter is an optional Store extension for deleting a key range without first reading values. Callers should type-assert this for large purges.

type ReadyAwaiter

type ReadyAwaiter interface {
	// WaitReady blocks until the store's background initialisation is
	// complete or ctx is cancelled. Returns nil when the store is ready.
	WaitReady(ctx context.Context) error
}

ReadyAwaiter is implemented by stores that have an asynchronous initialisation phase (e.g. HybridStore, which opens SQLite in the background). Callers that need to guarantee all persisted data is visible before reading — such as startup reload routines — should type-assert the Store to ReadyAwaiter and wait before scanning.

Stores that are always immediately ready (MemoryStore, WALStore) do not need to implement this interface; callers must treat its absence as "already ready".

type SQLiteDBProvider

type SQLiteDBProvider interface {
	DB() *sql.DB
}

SQLiteDBProvider is implemented by stores backed by SQLite (SQLiteStore, HybridStore). Services that need dedicated tables (e.g. DynamoDB items) can type-assert a Store to this interface to get direct DB access.

type SQLiteStore

type SQLiteStore struct {
	// contains filtered or unexported fields
}

SQLiteStore is the persistent Store implementation. State survives process restarts, stored in a single SQLite file under DataDir.

The core schema is a single key-value table — deliberately simple. We don't need relational features; we need durable K/V storage with prefix scanning. Other packages may add their own dedicated tables to the same file (see SQLiteDBProvider); every schema change, including the kv table itself, is applied by the PRAGMA user_version-based migration runner in migrate.go.

The file path is: <DataDir>/overcast.db.

Construction is non-blocking: `sql.Open` is cheap (it does not connect or migrate), and the schema migration runs in a background goroutine. The first DB-touching method (Get/Set/Delete/List/Scan/DB/loadAll) blocks on a ready channel until the migration finishes. This keeps the modernc/sqlite cold-start cost (~200–300ms for the first CREATE TABLE when the driver initialises its parser) off the critical startup path — the same approach used by HybridStore. Quick-start aware.

func NewSQLiteStore

func NewSQLiteStore(dataDir string) (*SQLiteStore, error)

NewSQLiteStore opens (or creates) the SQLite database at dataDir/overcast.db with PRAGMA synchronous=NORMAL — writes are durable across OS crashes. The data directory is created if it doesn't exist.

Returns immediately; the schema migration runs in a background goroutine. The first call into any DB-touching method blocks until the migration completes (or returns the migration error if it failed).

func NewSQLiteStoreWAL

func NewSQLiteStoreWAL(dataDir string) (*SQLiteStore, error)

NewSQLiteStoreWAL opens (or creates) the SQLite database at dataDir/overcast.db with PRAGMA synchronous=OFF for maximum write throughput. The OS may buffer writes and a power loss between writes can corrupt data — acceptable for local-only emulation but not for production use.

Returns immediately; the schema migration runs in a background goroutine (see NewSQLiteStore for details).

func NewSQLiteStoreWithLogger

func NewSQLiteStoreWithLogger(dataDir string, logger *zap.Logger) (*SQLiteStore, error)

NewSQLiteStoreWithLogger is NewSQLiteStore with structured migration-runner diagnostics (see RunMigrations) when logger is non-nil.

func (*SQLiteStore) Close

func (s *SQLiteStore) Close() error

func (*SQLiteStore) DB

func (s *SQLiteStore) DB() *sql.DB

DB returns the underlying *sql.DB so that service-specific stores can add their own tables to the same database file. Blocks until the background schema migration has finished.

If the migration failed, the returned *sql.DB is still non-nil (sql.Open itself succeeded) but service-specific schema setup against it will likely fail. The migration error is logged eagerly by runMigrate so it is visible in the daemon log even if no kv operation is ever issued. Callers must not close the returned DB — call Close() on the SQLiteStore.

func (*SQLiteStore) DebugMetrics

func (s *SQLiteStore) DebugMetrics(ctx context.Context, opts DebugMetricsOptions) DebugMetrics

DebugMetrics implements state.DebugMetricsReporter (storage-plan.md item 3.6). SQLiteStore writes synchronously and has no pending log or background seed, so FlushHistory/SeedDurationMillis/PendingLogBytes stay at their zero values — only NamespaceRowCounts (opt-in, see DebugMetricsOptions) applies to this backend.

func (*SQLiteStore) Delete

func (s *SQLiteStore) Delete(ctx context.Context, namespace, key string) error

func (*SQLiteStore) DeletePrefix

func (s *SQLiteStore) DeletePrefix(ctx context.Context, namespace, prefix string) error

func (*SQLiteStore) Get

func (s *SQLiteStore) Get(ctx context.Context, namespace, key string) (string, bool, error)

func (*SQLiteStore) List

func (s *SQLiteStore) List(ctx context.Context, namespace, prefix string) ([]string, error)

func (*SQLiteStore) ListNamespaces

func (s *SQLiteStore) ListNamespaces(ctx context.Context) ([]string, error)

func (*SQLiteStore) NotReady

func (s *SQLiteStore) NotReady() bool

NotReady implements state.NotReadyReporter: true only while the background schema migration is still in flight (s.ready not yet closed). Unlike HybridStore, SQLiteStore has no memory-backed fallback for an in-flight migration — every method blocks on ensureReady until it finishes — so this is what lets middleware.NotReady turn that indefinite per-request block into a single fast, explicit "not ready yet" response instead.

func (*SQLiteStore) Scan

func (s *SQLiteStore) Scan(ctx context.Context, namespace, prefix string) ([]KV, error)

Scan returns all key-value pairs whose keys start with prefix in a single query — prefer this over List+Get when you need both keys and values.

func (*SQLiteStore) ScanPage

func (s *SQLiteStore) ScanPage(ctx context.Context, namespace, prefix, startAfter string, limit int) ([]KV, string, error)

ScanPage returns up to limit key-value pairs whose keys start with prefix, in key order, starting strictly after startAfter — see Store.ScanPage for the full contract. Shares its query-building and page-trimming logic (scanPageQuery / finalizeScanPage) with HybridStore's raw SQLite helpers in hybrid.go, which run the identical query shape against a different *sql.DB (the dedicated read pool) — see hybridSQLiteRawScanPage.

func (*SQLiteStore) Set

func (s *SQLiteStore) Set(ctx context.Context, namespace, key, value string) error

type Store

type Store interface {
	// Get retrieves a value by namespace+key.
	// Returns ("", false, nil) if the key does not exist.
	Get(ctx context.Context, namespace, key string) (value string, found bool, err error)

	// Set stores a value. Overwrites any existing value for the same key.
	Set(ctx context.Context, namespace, key, value string) error

	// Delete removes a key. Returns nil (not an error) if the key does not exist.
	Delete(ctx context.Context, namespace, key string) error

	// List returns all keys in namespace whose names start with prefix.
	// Returns an empty slice (not nil) when no keys match.
	List(ctx context.Context, namespace, prefix string) (keys []string, err error)

	// ListNamespaces returns all namespaces that currently contain at least one
	// key. Returns an empty slice (not nil) when the store is empty.
	ListNamespaces(ctx context.Context) (namespaces []string, err error)

	// Scan returns all key-value pairs in namespace whose keys start with prefix,
	// in a single atomic read. Prefer Scan over List+Get when you need both keys
	// and values — it avoids N individual Get calls and holds the lock only once.
	// Returns an empty slice (not nil) when no keys match.
	Scan(ctx context.Context, namespace, prefix string) ([]KV, error)

	// ScanPage returns up to limit key-value pairs in namespace whose keys
	// start with prefix, in key order, starting strictly after startAfter —
	// a paginated variant of Scan for namespaces too large to return in one
	// response (e.g. sqs:messages, logs:events). Pass startAfter == "" for
	// the first page.
	//
	// nextKey is the startAfter value to pass for the next page, or "" when
	// this page reached the end of the prefix range (no more results).
	// limit <= 0 means "no limit" — behaves exactly like Scan(ctx, namespace,
	// prefix) with nextKey always "". This keeps ScanPage a strict superset
	// of Scan (a caller can always pass limit 0 and get Scan's behavior)
	// rather than making an unbounded request an error, matching this
	// package's general preference for permissive zero-value defaults (see
	// e.g. HybridOptions' zero-valued fields falling back to documented
	// defaults) over forcing every caller to think about a limit.
	//
	// Returns an empty slice (not nil) when no keys match.
	ScanPage(ctx context.Context, namespace, prefix, startAfter string, limit int) (page []KV, nextKey string, err error)

	// Close releases any resources held by the store (file handles, DB connections).
	// Called once on graceful shutdown.
	Close() error
}

Store is the single interface all service state flows through. Implementations must be safe for concurrent use from multiple goroutines.

The namespace parameter segments keys by service (e.g. "s3", "sqs") so that different services can use the same key names without collision.

func Unwrap

func Unwrap(store Store, servicePrefix string) Store

Unwrap returns the store that actually handles operations for the given service prefix. If store is a *NamespacedStore, it resolves to the routed store (or the default store when no override exists for servicePrefix). For any other Store, it returns store unchanged.

Any consumer that type-asserts a Store to an optional interface (SQLiteDBProvider, ReadyAwaiter, PersistentHealthReporter, ...) MUST call Unwrap first with its own service prefix. Without it, wrapping the store in NamespacedStore — triggered by an unrelated OVERCAST_STATE_<SVC> override — silently erases the capability for every other service, because *NamespacedStore itself does not implement most optional interfaces.

type StoreCounters

type StoreCounters struct {
	Reads  int64 `json:"reads"`
	Writes int64 `json:"writes"`

	ReadsMemory       int64 `json:"readsMemory,omitempty"`
	ReadsSQLite       int64 `json:"readsSQLite,omitempty"`
	WritesFlushedRows int64 `json:"writesFlushedRows,omitempty"`
}

StoreCounters is a cheap, atomically-maintained tally of storage-layer operations since process start — no locks and no per-op allocations on any store's hot Get/Set path (sync/atomic only).

Reads counts every Get/List/Scan/ScanPage/ListNamespaces call; Writes counts every Set/Delete/DeletePrefix call. These two fields are populated by all four store implementations.

ReadsMemory/ReadsSQLite/WritesFlushedRows are populated only by HybridStore, the only implementation that actually serves reads/writes from two distinct tiers:

  • ReadsMemory is a read served from the in-memory tier — either a pending-overlay hit (an accepted-but-not-yet-flushed write, which lives only in RAM) or a plain MemoryStore read. ReadsSQLite is a read that fell through to a live SQLite query. ReadsMemory + ReadsSQLite always equals Reads.
  • WritesFlushedRows is the count of pending-op rows the background flush loop has actually committed to SQLite so far — necessarily a subset of Writes, since not every accepted write has been flushed yet. This reuses flushOnce's existing per-attempt entry count (see HybridStore.recordFlushHistory) rather than adding a second, separately maintained tally that could drift from it.

Zero-valued (omitted from JSON) for every other backend, which has no second tier to split against.

type Tier

type Tier int

Tier classifies a namespace for the HybridStore's memory management strategy.

const (
	// TierHot namespaces are always held in memory and never evicted.
	// These contain resource definitions (queues, topics, tables, etc.) which
	// are small, finite, and needed for instant topology/dashboard renders.
	TierHot Tier = iota

	// TierCached namespaces are read straight from SQLite on every access
	// (HybridStore's lazy SQLite-backed path — see
	// shouldReadHybridNamespaceFromSQLite in hybrid.go), overlaid with a small
	// pending-write cache for changes not yet flushed. There is currently no
	// in-memory LRU cache in front of SQLite for these namespaces — every read
	// not covered by the pending overlay is a SQLite round trip. An
	// LRU-bounded cache tier is a possible future enhancement, not
	// implemented today.
	TierCached
)

func TierFor

func TierFor(namespace string) Tier

TierFor returns the tier for a namespace. Unknown namespaces default to TierHot.

type WALOptions

type WALOptions struct {
	SyncMode WALSyncMode

	// SyncInterval is used only when SyncMode is WALSyncInterval.
	SyncInterval time.Duration

	// MaxLogBytes triggers compaction when the append log reaches this size.
	MaxLogBytes int64
}

WALOptions configures WALStore durability and compaction behavior.

type WALStore

type WALStore struct {
	// contains filtered or unexported fields
}

WALStore is a memory-first store with an append-only write-ahead log. Reads are served from memory. Every mutation is appended to disk and can be replayed on restart.

func NewWALStore

func NewWALStore(dataDir string, opts WALOptions) (*WALStore, error)

NewWALStore creates or opens the WAL-backed store rooted at dataDir.

func (*WALStore) Close

func (s *WALStore) Close() error

func (*WALStore) DebugMetrics

DebugMetrics implements state.DebugMetricsReporter. WALStore always reads and (indirectly, through Set/Delete/DeletePrefix) writes via its embedded *MemoryStore, so Counters simply reads that store's existing atomic reads/writes tallies rather than maintaining a second, redundant pair that could drift from it. WALStore has no async flush loop or SQLite tier, so every other DebugMetrics field stays at its zero value.

func (*WALStore) Delete

func (s *WALStore) Delete(ctx context.Context, namespace, key string) error

func (*WALStore) DeletePrefix

func (s *WALStore) DeletePrefix(ctx context.Context, namespace, prefix string) error

func (*WALStore) Get

func (s *WALStore) Get(ctx context.Context, namespace, key string) (string, bool, error)

func (*WALStore) List

func (s *WALStore) List(ctx context.Context, namespace, prefix string) ([]string, error)

func (*WALStore) ListNamespaces

func (s *WALStore) ListNamespaces(ctx context.Context) ([]string, error)

func (*WALStore) Scan

func (s *WALStore) Scan(ctx context.Context, namespace, prefix string) ([]KV, error)

func (*WALStore) ScanPage

func (s *WALStore) ScanPage(ctx context.Context, namespace, prefix, startAfter string, limit int) ([]KV, string, error)

ScanPage delegates to the underlying MemoryStore exactly like Get/List/ Scan above — WALStore always reads from memory; only writes touch the append-only log.

func (*WALStore) Set

func (s *WALStore) Set(ctx context.Context, namespace, key, value string) error

type WALSyncMode

type WALSyncMode string

WALSyncMode controls how frequently WAL writes are fsync'd to disk.

const (
	WALSyncAlways   WALSyncMode = "always"
	WALSyncInterval WALSyncMode = "interval"
	WALSyncNever    WALSyncMode = "never"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL