Documentation
¶
Overview ¶
Package persist wires AgentGuard's in-memory fast-path state to a durable store.Store using the write-behind dual-tier model (docs/v0.6-ARCHITECTURE- PLAN.md §2.3–2.4).
The Syncer is the ONLY component that bridges memory and disk. It:
- Hydrate(): on boot, loads persisted state back into the in-memory maps (rate-limit buckets, session-cost accumulators, approval queue) so a restart does not lose them.
- flush loop: on a ticker with a hard 1s floor, snapshots each in-memory structure and upserts it to the store. This runs on its own goroutine and NEVER touches the /v1/check request path, so the <3ms latency budget is unaffected (CLAUDE.md rule #1).
- purge loop: periodically GCs stale rows (resolved approvals, idle costs, fully-refilled buckets), mirroring the in-memory eviction.
- Close(): stops the loops and performs one final flush so the last second of state is durable across a graceful shutdown.
Index ¶
Constants ¶
const DefaultPurgeInterval = 1 * time.Minute
DefaultPurgeInterval is how often stale rows are GC'd. Much coarser than the flush — GC is housekeeping, not durability.
const MinFlushInterval = 1 * time.Second
MinFlushInterval is the hard floor on the flush cadence (plan §2.4): the syncer never writes more often than once per second, bounding write amplification / WAL churn.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
Store store.Store
Limiter *ratelimit.Limiter
Engine *policy.Engine
Approvals *proxy.ApprovalQueue
// FlushInterval is clamped up to MinFlushInterval. Zero => MinFlushInterval.
FlushInterval time.Duration
// PurgeInterval is how often GC runs. Zero => DefaultPurgeInterval.
PurgeInterval time.Duration
// Retention cutoffs for GC. Zero disables that purge (rows kept forever).
CostTTL time.Duration // idle session-cost rows
ApprovalTTL time.Duration // resolved approvals
BucketTTL time.Duration // fully-refilled buckets
// NodeID identifies THIS node when writing multi-node reconciliation rows
// (v1.0). Each node owns its own `(…, node_id)` consumption rows, so writes
// are idempotent last-writer-wins with no cross-node read-modify-write race.
// Empty NodeID disables reconciliation (a row cannot be attributed).
NodeID string
// ReconcileInterval is the cadence of the background rate-limit / cost
// reconciliation loop. Zero disables it. Reconciliation also requires the
// Store to satisfy the reconcileStore capability interface; when it does not
// (or NodeID is empty, or this is 0) the reconcile loop never starts and the
// hot-path limiter/engine state behaves exactly as in single-node mode.
ReconcileInterval time.Duration
}
Config wires the syncer to the store and the in-memory sources. Any source may be nil (that slice is simply skipped), which keeps the syncer usable in focused tests.
type Syncer ¶
type Syncer struct {
// contains filtered or unexported fields
}
Syncer drives the write-behind loops.
func New ¶
New builds a Syncer, clamping the flush interval to the 1s floor. When ReconcileInterval>0, NodeID is set, and the store exposes the reconcile capability, the multi-node reconciler is armed; otherwise it stays disabled and the syncer behaves exactly as the v0.6 write-behind-only syncer.
func (*Syncer) Close ¶
func (s *Syncer) Close()
Close stops the loops, waits for the goroutine to exit, then performs one final flush so the last interval of state is durable. Idempotent.
func (*Syncer) Flush ¶
Flush snapshots every in-memory source and upserts it to the store in one pass. Exposed (rather than purely internal) so tests and the shutdown path can force a synchronous flush. Safe to call concurrently with the flush loop only via Close (which stops the loop first).
func (*Syncer) Hydrate ¶
Hydrate loads persisted state into the in-memory structures. Call once, on boot, BEFORE the server starts serving traffic. A store read error is returned so the operator can decide whether to proceed with empty state.