pub struct StateStore { /* private fields */ }Expand description
Thread-safe state store with transition logging.
All reads and writes go through this store. Every write produces a
StateTransition record for audit and replay. Optionally backed by
a JSONL journal file for durability across process restarts (see
StateStore::durable).
Implementations§
Source§impl StateStore
impl StateStore
pub fn new() -> StateStore
Sourcepub async fn lock_proposal_execution(&self) -> MutexGuard<'_, ()>
pub async fn lock_proposal_execution(&self) -> MutexGuard<'_, ()>
Shared proposal-transaction guard. Hold this across proposal execution, including rollback and replanning; actions inside the guarded proposal may still execute concurrently according to their DAG.
Sourcepub fn durable(path: impl Into<PathBuf>) -> Result<StateStore, Error>
pub fn durable(path: impl Into<PathBuf>) -> Result<StateStore, Error>
Open a durable, JSONL-backed StateStore. If the file exists, its transitions are replayed (last-write-wins per key, with TTLs honored) to rebuild current state. Subsequent writes append to the same file.
Returns an error only on filesystem-level failures (parent directory missing, permission denied, etc.). Malformed lines inside the journal are skipped with a warning rather than failing the open — agent persistence shouldn’t refuse to start over a single bad line.
Sourcepub fn sync(&self) -> Result<(), Error>
pub fn sync(&self) -> Result<(), Error>
Fsync the journal writer. Call after a batch of writes when you need durability guarantees beyond best-effort flush.
Sourcepub fn reap_expired(&self, now: DateTime<Utc>) -> Result<Vec<String>, Error>
pub fn reap_expired(&self, now: DateTime<Utc>) -> Result<Vec<String>, Error>
Drop expired keys (per ttl_secs on their last write) and
rewrite the journal as a compacted snapshot of the surviving
state. Returns the keys that were reaped.
TTL semantics: a ttl_secs of 0 means “expired
immediately” — the key is reapable on the next call. There
is no “0 = forever” sentinel; use set (no TTL) for keys
that should never auto-expire.
Latest-write-wins: a key rewritten WITHOUT a TTL after a TTL’d write is NOT reaped — the more recent write effectively cancels the TTL.
Single-pass over the transitions log via a key→latest index, so cost is O(n) in journal length (not O(n²)).
Sourcepub fn reap_expired_scoped(
&self,
now: DateTime<Utc>,
tenant: Option<&str>,
) -> Result<Vec<String>, Error>
pub fn reap_expired_scoped( &self, now: DateTime<Utc>, tenant: Option<&str>, ) -> Result<Vec<String>, Error>
Reap only the expired keys in one tenant’s namespace (EPIC E / E3).
tenant = Some(id) reaps tenant:<id>:*; tenant = None reaps only
the unscoped namespace. This is the per-tenant counterpart to
Self::reap_expired (which reaps across all tenants): it lets a
per-tenant reaping budget expire one tenant’s TTL’d keys without
touching another tenant’s — so one tenant’s memory pressure can’t
evict another’s state.
pub fn get(&self, key: &str) -> Option<Value>
pub fn get_or(&self, key: &str, default: Value) -> Value
pub fn exists(&self, key: &str) -> bool
pub fn set(&self, key: &str, value: Value, action_id: &str) -> StateTransition
Sourcepub fn set_with_ttl(
&self,
key: &str,
value: Value,
action_id: &str,
ttl_secs: u64,
) -> StateTransition
pub fn set_with_ttl( &self, key: &str, value: Value, action_id: &str, ttl_secs: u64, ) -> StateTransition
Set a key with a TTL (seconds from now). reap_expired
drops the key once the deadline passes; re-setting the key
without a TTL (set) cancels the TTL.
ttl_secs == 0 means “expire immediately” (reapable on the
next reap_expired call). It is NOT a “no expiry” sentinel
— use the plain set(...) method for keys that should
never auto-expire. This differs from the Unix/Redis
convention; the distinction matters because a TTL passed
from untrusted input could otherwise silently mean
“forever” when the caller intended “never store.”
Sourcepub fn set_batch(
&self,
entries: Vec<(String, Value)>,
action_id: &str,
) -> Vec<StateTransition>
pub fn set_batch( &self, entries: Vec<(String, Value)>, action_id: &str, ) -> Vec<StateTransition>
Apply several writes as ONE atomic mutation boundary
(Parslee-ai/car#1140). The state lock is held across every entry, so
a concurrent reader observes the complete old state or the complete
new state — never a prefix of the batch — and the journal receives
the whole batch as a single line ([BatchTransitionRecord]), giving
replay after an interrupted append the same old-or-complete
guarantee.
Entries apply in the order given; a duplicated key’s later entry
wins, each bumping the key’s version. An empty batch is a no-op. A
single-entry batch behaves exactly like Self::set, journal line
shape included.
Sourcepub fn version(&self, key: &str) -> Option<u64>
pub fn version(&self, key: &str) -> Option<u64>
Current version of key (number of writes/deletes applied to it),
or None if it was never written. Used by the transactional
conflict checker to detect stale reads (survey §5.2.4).
Sourcepub fn versions(&self) -> HashMap<String, u64>
pub fn versions(&self) -> HashMap<String, u64>
Snapshot of all key versions — the version map an action’s assumptions are checked against.
Sourcepub fn versioned_snapshot(
&self,
) -> (HashMap<String, Value>, HashMap<String, u64>)
pub fn versioned_snapshot( &self, ) -> (HashMap<String, Value>, HashMap<String, u64>)
Atomic snapshot of both the current values and the current versions,
taken under a single consistent lock acquisition (state then
versions, matching the write path) so the two maps can’t tear — a
caller never sees a value from version N+1 paired with version N
(neo review N2). This is the pair the transactional conflict checker
(car_verify::check_transaction) should consume.
pub fn delete(&self, key: &str, action_id: &str) -> Option<StateTransition>
Sourcepub fn restore(
&self,
snapshot: HashMap<String, Value>,
transition_count: usize,
) -> Result<RestoreDurability, Error>
pub fn restore( &self, snapshot: HashMap<String, Value>, transition_count: usize, ) -> Result<RestoreDurability, Error>
Restore state from a snapshot, truncating transitions. For durable stores the restored snapshot replaces the JSONL journal before the in-memory state is published; failures leave the current state intact.
Sourcepub fn snapshot_scoped(&self, tenant: Option<&str>) -> HashMap<String, Value>
pub fn snapshot_scoped(&self, tenant: Option<&str>) -> HashMap<String, Value>
Snapshot only the keys belonging to one tenant’s namespace
(Parslee-ai/car#187 / EPIC E task E2). tenant = Some(id) captures
tenant:<id>:*; tenant = None captures the unscoped (non-tenant:)
namespace. Keys are returned in their full (prefixed) form so the
result round-trips through Self::restore_scoped. This is the
per-tenant counterpart to Self::snapshot, which captures all
tenants and so can’t be used for a tenant-isolated rollback.
Sourcepub fn restore_scoped(
&self,
tenant: Option<&str>,
snapshot: HashMap<String, Value>,
transition_count: usize,
) -> Result<RestoreDurability, Error>
pub fn restore_scoped( &self, tenant: Option<&str>, snapshot: HashMap<String, Value>, transition_count: usize, ) -> Result<RestoreDurability, Error>
Restore a single tenant’s namespace from a scoped snapshot, leaving
every other tenant’s keys untouched (EPIC E / E2). Existing keys in
the target namespace are dropped and replaced by snapshot; keys
outside it are preserved. Fixes the cross-tenant clobber where a
rollback via the unscoped Self::restore wiped concurrent
tenants’ state.
The transition log is FILTERED, not truncated (linus review C-5):
only this tenant’s post-snapshot transitions are discarded.
Truncating shared history dropped transitions concurrent tenants
committed after transition_count, which both falsified the audit
trail and let reap_expired* treat another tenant’s stale TTL’d
transition as latest — deleting a live key. transition_count is
the log length captured when this tenant’s snapshot was taken.
pub fn transition_count(&self) -> usize
pub fn transitions(&self) -> Vec<StateTransition>
pub fn transitions_since(&self, index: usize) -> Vec<StateTransition>
pub fn keys(&self) -> Vec<String>
Sourcepub fn replace_all(&self, snapshot: HashMap<String, Value>)
pub fn replace_all(&self, snapshot: HashMap<String, Value>)
Replace the entire state map without recording transitions.
Used by checkpoint restore to avoid synthetic transition history.
Also clears the transitions log so callers of transitions_since()
don’t see stale history from the discarded state.
Sourcepub fn scoped<'a>(&'a self, tenant: Option<&'a str>) -> ScopedStateView<'a>
pub fn scoped<'a>(&'a self, tenant: Option<&'a str>) -> ScopedStateView<'a>
Build a tenant-scoped view over this store (Parslee-ai/car#187 phase 3 enforcement).
All reads / writes go through tenant:<tenant_id>:<key> so
distinct tenants can’t see each other’s keys. tenant = None
returns a view that hits the unscoped (legacy) namespace —
callers that don’t yet have a RuntimeScope get pre-#187
behaviour automatically.
Cheap to construct; holds a &self borrow plus the tenant
string. The view’s methods take the parking-lot lock the same
way the unscoped methods do.
Trait Implementations§
Source§impl Default for StateStore
impl Default for StateStore
Source§fn default() -> StateStore
fn default() -> StateStore
Source§impl StateView for StateStore
impl StateView for StateStore
Source§fn get_value(&self, key: &str) -> Option<Value>
fn get_value(&self, key: &str) -> Option<Value>
None if the key doesn’t exist.Source§fn key_exists(&self, key: &str) -> bool
fn key_exists(&self, key: &str) -> bool
Source§fn is_unknown(&self, _key: &str) -> bool
fn is_unknown(&self, _key: &str) -> bool
false by default — runtime state is always known.Auto Trait Implementations§
impl !Freeze for StateStore
impl !RefUnwindSafe for StateStore
impl !UnwindSafe for StateStore
impl Send for StateStore
impl Sync for StateStore
impl Unpin for StateStore
impl UnsafeUnpin for StateStore
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
impl<T> ErasedDestructor for Twhere
T: 'static,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more