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 ¶
- func Flush(ctx context.Context, s Store) error
- type Flushable
- type HybridStore
- func (s *HybridStore) Close() error
- func (s *HybridStore) DB() *sql.DB
- func (s *HybridStore) Delete(ctx context.Context, namespace, key string) error
- func (s *HybridStore) DeletePrefix(ctx context.Context, namespace, prefix string) error
- func (s *HybridStore) Flush(ctx context.Context) error
- func (s *HybridStore) Get(ctx context.Context, namespace, key string) (string, bool, error)
- func (s *HybridStore) List(ctx context.Context, namespace, prefix string) ([]string, error)
- func (s *HybridStore) ListNamespaces(ctx context.Context) ([]string, error)
- func (s *HybridStore) PersistentHealth() PersistentHealth
- func (s *HybridStore) Scan(ctx context.Context, namespace, prefix string) ([]KV, error)
- func (s *HybridStore) Set(ctx context.Context, namespace, key, value string) error
- func (s *HybridStore) WaitReady(ctx context.Context) error
- type KV
- type MemoryStore
- func (s *MemoryStore) Close() error
- func (s *MemoryStore) Delete(_ context.Context, namespace, key string) error
- func (s *MemoryStore) DeletePrefix(_ context.Context, namespace, prefix string) error
- func (s *MemoryStore) Get(_ context.Context, namespace, key string) (string, bool, error)
- func (s *MemoryStore) Len() int
- func (s *MemoryStore) List(_ context.Context, namespace, prefix string) ([]string, error)
- func (s *MemoryStore) ListNamespaces(_ context.Context) ([]string, error)
- func (s *MemoryStore) Reset()
- func (s *MemoryStore) Scan(_ context.Context, namespace, prefix string) ([]KV, error)
- func (s *MemoryStore) Set(_ context.Context, namespace, key, value string) error
- type NamespacedStore
- func (s *NamespacedStore) Close() error
- func (s *NamespacedStore) Delete(ctx context.Context, namespace, key string) error
- func (s *NamespacedStore) DeletePrefix(ctx context.Context, namespace, prefix string) error
- func (s *NamespacedStore) Get(ctx context.Context, namespace, key string) (string, bool, error)
- func (s *NamespacedStore) List(ctx context.Context, namespace, prefix string) ([]string, error)
- func (s *NamespacedStore) ListNamespaces(ctx context.Context) ([]string, error)
- func (s *NamespacedStore) Scan(ctx context.Context, namespace, prefix string) ([]KV, error)
- func (s *NamespacedStore) Set(ctx context.Context, namespace, key, value string) error
- type PersistentHealth
- type PersistentHealthReporter
- type PrefixDeleter
- type ReadyAwaiter
- type SQLiteDBProvider
- type SQLiteStore
- func (s *SQLiteStore) Close() error
- func (s *SQLiteStore) DB() *sql.DB
- func (s *SQLiteStore) Delete(ctx context.Context, namespace, key string) error
- func (s *SQLiteStore) DeletePrefix(ctx context.Context, namespace, prefix string) error
- func (s *SQLiteStore) Get(ctx context.Context, namespace, key string) (string, bool, error)
- func (s *SQLiteStore) List(ctx context.Context, namespace, prefix string) ([]string, error)
- func (s *SQLiteStore) ListNamespaces(ctx context.Context) ([]string, error)
- func (s *SQLiteStore) Scan(ctx context.Context, namespace, prefix string) ([]KV, error)
- func (s *SQLiteStore) Set(ctx context.Context, namespace, key, value string) error
- type Store
- type Tier
- type WALOptions
- type WALStore
- func (s *WALStore) Close() error
- func (s *WALStore) Delete(ctx context.Context, namespace, key string) error
- func (s *WALStore) DeletePrefix(ctx context.Context, namespace, prefix string) error
- func (s *WALStore) Get(ctx context.Context, namespace, key string) (string, bool, error)
- func (s *WALStore) List(ctx context.Context, namespace, prefix string) ([]string, error)
- func (s *WALStore) ListNamespaces(ctx context.Context) ([]string, error)
- func (s *WALStore) Scan(ctx context.Context, namespace, prefix string) ([]KV, error)
- func (s *WALStore) Set(ctx context.Context, namespace, key, value string) error
- type WALSyncMode
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Flushable ¶
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 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 small 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.
func NewHybridStore ¶
func NewHybridStore(dataDir string, flushInterval time.Duration) (*HybridStore, error)
NewHybridStore creates a HybridStore backed by a SQLite file in dataDir. 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.
func (*HybridStore) Close ¶
func (s *HybridStore) Close() error
Close stops the background flush goroutine, performs a final synchronous flush of all pending dirty entries to SQLite, then closes the database.
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.
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 tombstones before removing matching memory keys so lazy SQLite-backed reads cannot resurrect deleted persisted rows.
func (*HybridStore) Flush ¶
func (s *HybridStore) Flush(ctx context.Context) error
Flush synchronously persists all dirty writes accepted before this call.
func (*HybridStore) Get ¶
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.
func (*HybridStore) List ¶
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) PersistentHealth ¶
func (s *HybridStore) PersistentHealth() PersistentHealth
func (*HybridStore) Scan ¶
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) 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.
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) 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) Len ¶
func (s *MemoryStore) Len() int
Len returns the number of entries. Used by /_debug/state and tests.
func (*MemoryStore) ListNamespaces ¶
func (s *MemoryStore) ListNamespaces(_ context.Context) ([]string, error)
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 (the segment before the first ":").
namespace "sqs:queues" → service prefix "sqs" namespace "s3:objects" → service prefix "s3"
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.
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) ListNamespaces ¶
func (s *NamespacedStore) ListNamespaces(ctx context.Context) ([]string, error)
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.
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 ¶
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, SQLiteStore) do not need to implement this interface; callers must treat its absence as "already ready".
type SQLiteDBProvider ¶
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.
Schema is a single key-value table — deliberately simple. We don't need relational features; we need durable K/V storage with prefix scanning.
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 (*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) 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) ListNamespaces ¶
func (s *SQLiteStore) ListNamespaces(ctx context.Context) ([]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)
// 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.
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 served from an LRU-bounded memory cache with // SQLite as the overflow tier. When the memory budget is exceeded, least // recently accessed entries are evicted from memory but remain on disk. TierCached )
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) DeletePrefix ¶
func (*WALStore) ListNamespaces ¶
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" )