Skip to main content

DurableContext

Struct DurableContext 

Source
pub struct DurableContext { /* private fields */ }
Expand description

The &self durable execution context handed to a consumer’s program.

Construct it with DurableContext::new from a shared backend (the read path) and a JournalWriterHandle (the write path) — both bound to the same durable.db. A fresh execution opens with is_resume = false; a resumed one with is_resume = true, which activates the ReplayCursor.

Implementations§

Source§

impl DurableContext

Source

pub fn new( execution_id: ExecutionId, kind: ExecutionKind, is_resume: bool, backend: Arc<DurableBackendEnum>, writer: JournalWriterHandle, config: &DurableConfig, ) -> Self

Build a context for an execution.

backend is the shared read path (the ReplayCursor reads segments through it) and must be the same durable.db instance the writer commits to. is_resume activates replay: pass true when reopening an execution that already has journal entries (as LocalBackend::open_execution reports), false for a brand-new execution.

Source

pub fn execution_id(&self) -> ExecutionId

The execution this context drives.

Source

pub fn kind(&self) -> ExecutionKind

The execution’s category.

Source

pub async fn step<T, F, Fut>( &self, desc: StepDescriptor, op: F, ) -> Result<T, DurableError>
where T: Serialize + DeserializeOwned + Send, F: FnOnce(StepHandle) -> Fut + Send, Fut: Future<Output = Result<T, StepError>> + Send,

Run a durable step, returning just its value.

On a fresh execution the operation op runs and its result is journaled; on replay the journaled result is returned and op is never invoked (INV-10). The closure receives a StepHandle carrying the step’s IdempotencyKey for boundary deduplication.

Use step_recorded when the live/replayed distinction or the step id matters.

§Errors
§Examples
use zeph_durable::StepDescriptor;

let lines: usize = ctx
    .step(StepDescriptor::idempotent("count", b"tool:count".to_vec()), |_handle| async {
        Ok(42)
    })
    .await?;
assert_eq!(lines, 42);
Source

pub async fn step_recorded<T, F, Fut>( &self, desc: StepDescriptor, op: F, ) -> Result<DurableStep<T>, DurableError>
where T: Serialize + DeserializeOwned + Send, F: FnOnce(StepHandle) -> Fut + Send, Fut: Future<Output = Result<T, StepError>> + Send,

Run a durable step, returning the full DurableStep record (id, key, and outcome).

§Errors

Identical to step.

Source

pub fn parallel(&self) -> ParallelScope<'_>

Open a ParallelScope whose children receive contiguous, eagerly-assigned step ids.

Construct the children synchronously (e.g. with a Vec or .map(...).collect()), then drive them with join_all/try_join_all; their ids are fixed by construction order and are stable across replay regardless of completion order (INV-2).

Source

pub async fn promise<T>(&self) -> Result<DurablePromise<T>, DurableError>

Create a durable promise resolved out of band by an operator or A2A reply (FR-DE-05).

The promise occupies a deterministic program position, so a resumed execution re-derives the same PromiseId and re-attaches to the pending row rather than minting an orphan. A fresh promise carries its 32-byte resolver token — hand it to the resolving channel via DurablePromise::resolver_token; a resumed promise carries none (the original token was delivered before the crash). Await the result with await_promise.

§Errors
  • DurableError::StepCapExceeded if the promise would exceed the per-execution step cap.
  • A storage error if the promise row cannot be read or inserted.
Source

pub async fn claim_promise_notification( &self, id: PromiseId, ) -> Result<bool, DurableError>

Claim the one-time out-of-band notification for a replayed promise result.

Returns true for the first caller (fire the side effects) and false on every later call (suppress — already claimed). The claim is a single conditional UPDATE on the promise’s notified_at column keyed by its PromiseId; it does not allocate a durable step id, so — unlike wrapping the notice in step — it has zero interaction with the INV-2 step-id counter and can never cause a DurableError::ReplayDivergence, regardless of how many times the parent restarts (#6027).

Because PromiseId::derive is deterministic across runs (a resumed execution re-derives the same id at the same program position), the claim converges on one row for the fresh run and every replay.

§Errors

Returns DurableError::Storage on a database error.

Source

pub async fn await_promise<T: DeserializeOwned>( &self, promise: DurablePromise<T>, ) -> Result<T, DurableError>

Await a durable promise’s resolved value, parking until it is resolved.

Returns immediately if the promise is already resolved (the common replay case). Otherwise it parks on an in-process notify keyed by the promise id and falls back to a database poll every promise_poll_interval_secs; above max_parked_promises concurrent waiters it polls without parking. A resolution committed by DurableHandle::resolve wakes the waiter at once.

§Errors
  • DurableError::UnknownPromise if the promise row is missing (e.g. pruned).
  • A decode/integrity error if the resolved payload cannot be opened into T.
Source

pub async fn take_resolved_promise<T: DeserializeOwned>( &self, id: PromiseId, ) -> Result<Option<T>, DurableError>

Read a promise’s state and, if resolved, open and decode its value — without parking.

Unlike await_promise, this performs a single backend read and returns immediately regardless of resolution state: Ok(None) means the promise row exists but has not been resolved yet. Callers on an interactive path (e.g. a resumed parent deciding whether to replay a journaled subagent result or spawn a fresh one) use this to avoid an unbounded park on a promise that may never resolve (INV-9: a resumed promise’s resolver token is unrecoverable, so nothing can ever resolve it if the original resolver is gone).

§Errors
  • DurableError::UnknownPromise if the promise row is missing (e.g. pruned).
  • A decode/integrity error if the resolved payload cannot be opened into T.
Source

pub async fn sleep_until(&self, due: SystemTime) -> Result<(), DurableError>

Durably sleep until due, surviving a process restart (FR-DE-06).

Arms a durable_timers row at a deterministic position (so a resume re-attaches to it), then parks until the instant arrives — firing the timer itself when due, or returning at once if a restart finds it already fired or past due. The DurableTimerService, when running, fires due timers and wakes the waiter; without it, this loop still makes progress on its own.

§Errors
  • DurableError::StepCapExceeded if the timer would exceed the per-execution step cap.
  • A storage error if the timer cannot be armed or its state read.
Source

pub fn resolver_handle(&self) -> DurableHandle

Build an out-of-band DurableHandle over this context’s backend.

The handle is the operator/A2A resolution surface; it MUST NOT be exposed to an LLM tool (INV-9). It shares the same backend, so a resolution it commits wakes an await_promise parked on the same process at once.

Source

pub async fn finalize( &self, status: ExecutionStatus, ) -> Result<(), DurableError>

Transition this execution to a terminal status (FR-DE / retention section).

Consumers call this on the two production terminal transitions the journal itself never observes: Completed when the unit of work this execution represents finishes successfully, and Failed on an unrecoverable (non-retryable) error. Aborted is reserved for the replay-divergence guard (DurableError::ReplayDivergence) and is set internally, not by consumers.

Idempotent: calling this more than once, or racing it against an internal Aborted transition, is safe — only the first call to observe the execution as running applies (see crate::journal::Journal::finalize). The retention sweep only reclaims a finalized execution after its configured TTL, so a finalized-then-reopened execution (e.g. a resumed conversation) automatically un-finalizes back to running on the next DurableContext::new with is_resume = true, protecting it from a stale finalized_at.

§Errors

Returns a storage error if the transition cannot be committed.

§Examples
use zeph_durable::ExecutionStatus;

ctx.finalize(ExecutionStatus::Completed).await?;
Source

pub async fn drain_background(&self)

Await any in-flight background checkpoint folds — a turn-boundary / test barrier.

The soft step-cap fold runs on a spawned task so it never blocks step dispatch; call this at a turn boundary to ensure the journal is compacted before the next phase observes it.

Trait Implementations§

Source§

impl Debug for DurableContext

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more