pub struct LocalBackend { /* private fields */ }Expand description
The always-compiled durable backend that journals to a dedicated durable.db.
Construct it from a zeph_db::DbPool (or open one with LocalBackend::open), then attach an
optional PayloadCipher and HMAC key with the builder methods. Call LocalBackend::init
once before use to apply the schema migrations.
§Examples
use zeph_durable::LocalBackend;
// 1 MiB payload ceiling, matching the spec default.
let backend = LocalBackend::open("durable.db", 1_048_576).await?;
backend.init().await?;Implementations§
Source§impl LocalBackend
impl LocalBackend
Sourcepub fn new(pool: DbPool, max_payload_bytes: u64) -> Self
pub fn new(pool: DbPool, max_payload_bytes: u64) -> Self
Wrap an existing zeph_db::DbPool as a local backend with the given payload ceiling.
Call LocalBackend::init before any journal operation to apply the schema. Attach a
cipher and HMAC key with with_cipher and
with_hmac_key.
Sourcepub async fn open(
path: &str,
max_payload_bytes: u64,
) -> Result<Self, DurableError>
pub async fn open( path: &str, max_payload_bytes: u64, ) -> Result<Self, DurableError>
Open (or create) a backend on a dedicated durable.db file (or :memory:).
Connecting also applies the schema migrations, so a freshly opened backend is ready to use;
init may still be called and is idempotent.
On the SQLite backend, also derives the lock directory used by
open_execution_exclusive from path (a sibling
<path>.locks/ directory), unless path is :memory:. The Postgres backend never derives
one — path there is a connection URL (which may embed credentials), not a filesystem path.
§Errors
Returns DurableError::Storage if the pool cannot be opened or migrations fail.
Sourcepub fn with_cipher(self, cipher: Arc<dyn PayloadCipher>) -> Self
pub fn with_cipher(self, cipher: Arc<dyn PayloadCipher>) -> Self
Inject the AEAD payload cipher used to seal and open payload-bearing entries.
Sourcepub fn with_hmac_key(self, key: [u8; 32]) -> Self
pub fn with_hmac_key(self, key: [u8; 32]) -> Self
Configure the keyed-BLAKE3 HMAC key stamped over control entries on shared-database deployments, and used to verify them again on every read (INV-8).
Sourcepub fn pool(&self) -> &DbPool
pub fn pool(&self) -> &DbPool
Borrow the underlying pool (for tests and adapters that need direct access).
Sourcepub async fn init(&self) -> Result<(), DurableError>
pub async fn init(&self) -> Result<(), DurableError>
Apply the durable schema migrations to the backing pool.
Idempotent: safe to call repeatedly. The schema is owned by zeph-db, not this crate.
§Errors
Returns DurableError::Storage if a migration fails.
Sourcepub async fn list_executions(
&self,
status: Option<&str>,
kind: Option<&str>,
limit: i64,
) -> Result<Vec<ExecutionSummary>, DurableError>
pub async fn list_executions( &self, status: Option<&str>, kind: Option<&str>, limit: i64, ) -> Result<Vec<ExecutionSummary>, DurableError>
List execution summaries for operability surfaces (the zeph durable CLI and TUI).
Returns at most limit executions, newest first, optionally filtered by status and kind
(each is matched against the raw column tag; None disables that filter). Only execution-level
metadata is read — never payload bytes or resolver tokens (INV-5). The per-execution step
count is the number of journal entries recorded for it.
Span: durable.backend.list.
§Errors
Returns DurableError::Storage if the query fails, or DurableError::Decode if a stored
id or status cannot be reconstructed (schema corruption — the status column is
CHECK-constrained, so this is a fail-closed guard rather than a routine path).
Sourcepub async fn read_execution_redacted(
&self,
id: ExecutionId,
) -> Result<Vec<RedactedEntry>, DurableError>
pub async fn read_execution_redacted( &self, id: ExecutionId, ) -> Result<Vec<RedactedEntry>, DurableError>
Read one execution’s journal entries as redaction-safe metadata, without decrypting payloads.
Unlike read_execution, this never touches the cipher, so it works
against a journal whose AEAD key is unavailable and never exposes plaintext (INV-5). It backs
the default (redacted) zeph durable show/inspect output. Entries are returned in append
order.
Span: durable.backend.read_redacted.
§Errors
Returns DurableError::Storage if the query fails.
Sourcepub async fn count_prunable(
&self,
policy: &RetentionPolicy,
) -> Result<u64, DurableError>
pub async fn count_prunable( &self, policy: &RetentionPolicy, ) -> Result<u64, DurableError>
Count terminal executions a prune sweep would delete under policy.
Read-only: backs zeph durable prune --dry-run. It applies the same TTL cutoffs as the
delete path, so the count is exactly what a real sweep would remove now.
§Errors
Returns DurableError::Storage if the query fails.
Sourcepub async fn count_orphans(
&self,
policy: &RetentionPolicy,
) -> Result<u64, DurableError>
pub async fn count_orphans( &self, policy: &RetentionPolicy, ) -> Result<u64, DurableError>
Count crash-orphaned executions a sweep_orphans sweep would
abort under policy (#6254).
Read-only: backs zeph durable prune --dry-run. Mirrors the real sweep’s staleness scan
and INV-15 flock liveness check (acquiring and immediately releasing each candidate’s
ExecutionLock, exactly as the real sweep does, so the count reflects genuinely
unowned rows rather than staleness alone) — but never mutates status. Returns 0 when
the sweep is disabled (stale_running_after_secs == 0) or this backend has no lock_dir.
§Errors
Returns DurableError::Storage if the query fails.
Sourcepub async fn open_execution(
&self,
id: ExecutionId,
kind: ExecutionKind,
) -> Result<bool, DurableError>
pub async fn open_execution( &self, id: ExecutionId, kind: ExecutionKind, ) -> Result<bool, DurableError>
Ensure a durable_executions row exists for id, returning whether this is a resume.
Inserts a fresh running row for a new execution (returning false) or detects an existing
row for a resumed one (returning true). The journal’s foreign key requires this row before
any entry is appended, so callers open the execution first.
Reopening a row previously finalized as completed, failed, or
aborted un-finalizes it: status resets to running and finalized_at clears (INV-16,
#6254). A caller reopening an execution is, by definition, still using it, so the retention
sweep (gated on finalized_at) must not consider it prunable while it does — without this,
a long-lived execution finalized at one process’s graceful shutdown and legitimately resumed
by a later process (e.g. a per-conversation AgentTurn execution) would keep a stale
finalized_at and could be pruned out from under its still-active journal. aborted rows
are included because the crash-orphan sweep (INV-17) makes aborted the common outcome of a
resumable crash: a resumed execution whose row keeps finalized_at set is prunable out from
under the active resume — the exact hazard this un-finalize prevents for completed/failed.
This is also strictly safer for the pre-existing divergence-recovery case, which reopens an
aborted row on purpose: it now also protects that fresh re-drive from prune.
The un-finalize is attempted as a single guarded UPDATE (no preceding SELECT) so there
is no read-then-write window against a concurrent prune sweep (#6251 critic S1): if the row
was deleted by prune between an earlier observation and this call, the UPDATE simply
matches zero rows rather than silently resurrecting a half-deleted row. A zero-row UPDATE
falls back to checking whether the row exists at all (already running/aborted, or
genuinely gone) before deciding between reporting a resume or inserting a fresh execution —
so this never reports is_resume = true for a row that turned out not to exist.
Span: durable.backend.open.
§Errors
Returns DurableError::Storage if the lookup, reset, or insert fails.
Sourcepub async fn open_execution_exclusive(
&self,
id: ExecutionId,
kind: ExecutionKind,
) -> Result<(bool, Option<ExecutionLock>), DurableError>
pub async fn open_execution_exclusive( &self, id: ExecutionId, kind: ExecutionKind, ) -> Result<(bool, Option<ExecutionLock>), DurableError>
Like open_execution, but additionally takes a non-blocking,
exclusive, process-scoped advisory lock on id before touching the row (INV-15, #6122).
Closes the race two processes deriving the same ExecutionId (e.g. two CLI instances
pointed at the same memory.sqlite_path and the same ConversationId) would otherwise hit
in open_execution’s unsynchronized SELECT-then-INSERT: both could
observe “no existing row”, both insert, and both then drive next_step from 0 against the
same journal, corrupting it. The lock is acquired first, so the loser never reaches the
row check at all.
Returns (is_resume, lock). The caller MUST hold lock for as long as it drives the
execution — dropping it releases the lock and allows another process to open the same
id. lock is None when this backend has no on-disk lock directory (a :memory:
database, a backend built via LocalBackend::new, or a Postgres deployment), in which
case process exclusivity is not enforced — the caller degrades the same way it already does
for open_execution’s other failure modes.
§Errors
Returns DurableError::ExecutionLocked if another process already holds id’s lock, or
any error open_execution can return.
Trait Implementations§
Source§impl Debug for LocalBackend
impl Debug for LocalBackend
Source§impl ExecutionBackend for LocalBackend
impl ExecutionBackend for LocalBackend
Source§fn capabilities(&self) -> BackendCapabilities
fn capabilities(&self) -> BackendCapabilities
Source§async fn lookup_committed_result(
&self,
id: ExecutionId,
idem_key: IdempotencyKey,
) -> Result<Option<JournalEntry>, DurableError>
async fn lookup_committed_result( &self, id: ExecutionId, idem_key: IdempotencyKey, ) -> Result<Option<JournalEntry>, DurableError>
Source§impl Journal for LocalBackend
impl Journal for LocalBackend
Source§async fn sweep_orphans(
&self,
policy: &RetentionPolicy,
) -> Result<u64, DurableError>
async fn sweep_orphans( &self, policy: &RetentionPolicy, ) -> Result<u64, DurableError>
Crash-orphan reclamation (INV-17, #6254). See Journal::sweep_orphans for the contract.
Source§async fn append(&self, entry: JournalEntry) -> Result<JournalSeq, DurableError>
async fn append(&self, entry: JournalEntry) -> Result<JournalSeq, DurableError>
Source§async fn read_execution(
&self,
id: ExecutionId,
) -> Result<Vec<JournalEntry>, DurableError>
async fn read_execution( &self, id: ExecutionId, ) -> Result<Vec<JournalEntry>, DurableError>
Source§async fn read_execution_range(
&self,
id: ExecutionId,
from_step_id: u32,
limit: usize,
) -> Result<Vec<JournalEntry>, DurableError>
async fn read_execution_range( &self, id: ExecutionId, from_step_id: u32, limit: usize, ) -> Result<Vec<JournalEntry>, DurableError>
Source§async fn finalize(
&self,
id: ExecutionId,
status: ExecutionStatus,
) -> Result<(), DurableError>
async fn finalize( &self, id: ExecutionId, status: ExecutionStatus, ) -> Result<(), DurableError>
Source§async fn prune(&self, policy: &RetentionPolicy) -> Result<u64, DurableError>
async fn prune(&self, policy: &RetentionPolicy) -> Result<u64, DurableError>
policy and return the number of rows deleted. Read moreAuto Trait Implementations§
impl !Freeze for LocalBackend
impl !RefUnwindSafe for LocalBackend
impl !UnwindSafe for LocalBackend
impl Send for LocalBackend
impl Sync for LocalBackend
impl Unpin for LocalBackend
impl UnsafeUnpin for LocalBackend
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
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