Skip to main content

ReadableEventStore

Trait ReadableEventStore 

Source
pub trait ReadableEventStore:
    Send
    + Sync
    + 'static {
Show 17 methods // Required methods fn read_history<'life0, 'life1, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, ) -> Pin<Box<dyn Future<Output = Result<Vec<Event>, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait; fn read_history_from<'life0, 'life1, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, from_seq: u64, ) -> Pin<Box<dyn Future<Output = Result<Vec<Event>, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait; fn read_run_chain<'life0, 'life1, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, ) -> Pin<Box<dyn Future<Output = Result<Vec<RunSummary>, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait; fn list_workflow_ids<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<WorkflowId>, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait; fn stream_heads<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<StreamHead>, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait; fn list_active<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<WorkflowId>, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait; fn list_paused<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<WorkflowId>, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait; fn query<'life0, 'life1, 'async_trait>( &'life0 self, filter: &'life1 WorkflowFilter, ) -> Pin<Box<dyn Future<Output = Result<Vec<WorkflowSummary>, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait; fn schedule_timer<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, timer_id: &'life2 TimerId, fire_at: DateTime<Utc>, armed_seq: u64, ) -> Pin<Box<dyn Future<Output = Result<(), StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait; fn expired_timers<'life0, 'async_trait>( &'life0 self, as_of: DateTime<Utc>, ) -> Pin<Box<dyn Future<Output = Result<Vec<TimerEntry>, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait; fn retire_timer<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, timer_id: &'life2 TimerId, fire_at: DateTime<Utc>, armed_seq: u64, ) -> Pin<Box<dyn Future<Output = Result<TimerRetirement, StoreError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait; // Provided methods fn set_owned_shards(&self, shards: Option<&[usize]>) { ... } fn acquire_owned_shards(&self, shards: &[usize]) -> Result<(), StoreError> { ... } fn acquire_owned_shard(&self, shard: usize) -> Result<(), StoreError> { ... } fn is_current_owner(&self, shard: usize) -> bool { ... } fn extend_owned_shards(&self, shards: &[usize]) { ... } fn publish_shard_owner(&self, shard: usize) -> Result<(), StoreError> { ... }
}
Expand description

Read and durable-timer contract for Aion event stores.

Required Methods§

Source

fn read_history<'life0, 'life1, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, ) -> Pin<Box<dyn Future<Output = Result<Vec<Event>, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Reads the complete event history for workflow_id in ascending sequence order.

A workflow with no recorded events is observed as an empty history. This includes unknown workflow identifiers: because the first append with expected_seq == 0 creates a workflow implicitly, “unknown workflow” and “empty history” are the same observable state for reads. This method must not return StoreError::NotFound for absent workflows.

Source

fn read_history_from<'life0, 'life1, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, from_seq: u64, ) -> Pin<Box<dyn Future<Output = Result<Vec<Event>, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Reads the event history for workflow_id restricted to events with sequence number greater than or equal to from_seq, in ascending sequence order.

This is the range-read primitive behind O(delta) WS resume: callers replaying from a cursor must not pay for the full history. Semantics:

  • from_seq <= 1 is equivalent to Self::read_history: sequence numbers start at 1, so every recorded event satisfies the bound.
  • from_seq beyond the current head returns an empty vector, never an error. Whether a beyond-head cursor is valid is protocol judgment, not store judgment: the WS resume protocol rejects resume_from_seq > head + 1 as an invalid cursor (ResumeCursorAheadOfHistory), but it makes that call by comparing the cursor against the head it observes — the store only answers which events exist at or after the requested sequence.
  • Unknown workflows behave exactly like Self::read_history for unknown workflows: empty history, never StoreError::NotFound, because “unknown workflow” and “empty history” are the same observable state for reads.

There is deliberately no default implementation: a read-all-then-filter fallback would silently reintroduce O(history) behavior. Every backend must implement this as a real range read (for SQL backends, an indexed seq >= ? range scan).

Source

fn read_run_chain<'life0, 'life1, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, ) -> Pin<Box<dyn Future<Output = Result<Vec<RunSummary>, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Reads the concrete run chain for workflow_id in continuation order.

Source

fn list_workflow_ids<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<WorkflowId>, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Lists every workflow identifier that has at least one event in history.

Unlike Self::list_active, this includes terminal workflows and exists to let projection repair jobs reconcile derived indexes against the authoritative event history.

Source

fn stream_heads<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<StreamHead>, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Lists every workflow’s event stream with the seq of its last event — the stream-side half of the handshake that lets a boot trust a visibility row instead of opening the history behind it (crate::visibility::head).

Covers the same streams as Self::list_workflow_ids (terminal ones included) in no particular order; a stream with no events is not listed. This is an index read — it never decodes an event.

Source

fn list_active<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<WorkflowId>, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Lists workflow identifiers whose projected status is exactly WorkflowStatus::Running.

Answered row-first: for every stream head, a visibility row at that head settles a finished or paused workflow without a history read (crate::visibility::head::verdict); only in-flight, unstamped or stale rows fold their history. Finished workflows are never opened.

Source

fn list_paused<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<WorkflowId>, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Lists workflow identifiers whose projected status is exactly WorkflowStatus::Paused.

Mirrors Self::list_active with a == Paused exact-equality filter: it is the durable source the dispatch-hold set is rebuilt from at startup and at shard adoption, so a run paused before a kill -9 keeps its outbox rows held after restart. A paused run is excluded from Self::list_active (which filters == Running), so nothing else would repopulate the hold.

Source

fn query<'life0, 'life1, 'async_trait>( &'life0 self, filter: &'life1 WorkflowFilter, ) -> Pin<Box<dyn Future<Output = Result<Vec<WorkflowSummary>, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Returns workflow summaries matching filter.

Source

fn schedule_timer<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, timer_id: &'life2 TimerId, fire_at: DateTime<Utc>, armed_seq: u64, ) -> Pin<Box<dyn Future<Output = Result<(), StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Persists a durable timer for workflow_id that is due at fire_at.

Timer scheduling remains on the public store surface because timers are not workflow-history appends and are used by the timer subsystem after the recorder has written TimerStarted.

armed_seq is the workflow-history sequence of the arming’s TimerStarted event and becomes part of the row’s identity (TimerEntry::armed_seq): together with fire_at it is what a Self::retire_timer compare matches, so a re-arm to the IDENTICAL instant still writes a distinguishable row. An engine-internal arming that records no TimerStarted (the schedule coordinator) passes 0 — history sequences start at 1, so 0 is unambiguous, and such rows are keyed per trigger and never alias a workflow arming.

Source

fn expired_timers<'life0, 'async_trait>( &'life0 self, as_of: DateTime<Utc>, ) -> Pin<Box<dyn Future<Output = Result<Vec<TimerEntry>, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Returns durable timers whose fire_at is less than or equal to as_of.

Source

fn retire_timer<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, workflow_id: &'life1 WorkflowId, timer_id: &'life2 TimerId, fire_at: DateTime<Utc>, armed_seq: u64, ) -> Pin<Box<dyn Future<Output = Result<TimerRetirement, StoreError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Removes the durable timer row for (workflow_id, timer_id) — but only while the row still carries exactly the (fire_at, armed_seq) identity of the arming being retired.

Called by the timer subsystem once the row’s consuming fact is durably recorded — the fire path after TimerFired lands (or is refused by a terminal run), the cancel path after TimerCancelled lands, and the boot sweep’s reconciliation of rows whose consumption predates this method. Without retirement every timer a workflow ever armed leaves a permanent row, Self::expired_timers returns the workflow’s whole consumed past on every call, and the startup sweep walks it — the 2026-08-24 estate outage’s 90-minute boot.

The (fire_at, armed_seq) condition is what makes retirement safe against re-arms: a named timer re-armed after its fire OVERWRITES the same row with the replacement arming’s values (Self::schedule_timer), and a retirement decided against the consumed arming must never delete the replacement’s row — that row is the re-armed timer’s only durable claim to a recovery fire. The armed_seq half is load-bearing for a re-arm to the IDENTICAL instant: fire_at alone cannot tell those two armings apart, and the replacement’s TimerStarted always carries a strictly higher sequence. A caller always knows the arming it is retiring (the sweep row it walked, or the TimerStarted it read), so the condition costs nothing; a mismatch means “already re-armed, nothing left to retire” and is the TimerRetirement::Superseded SUCCESS, not an error.

The condition must hold under CONCURRENT re-arming, not merely against a stale caller decision: a schedule_timer racing this call must either land before the compare (mismatch, Superseded) or after the delete (its row survives) — never inside it. A backend without an atomic conditional delete must serialize this method against Self::schedule_timer itself.

Idempotent: retiring an absent (never scheduled, or already retired) row succeeds as TimerRetirement::Retired — the boot sweep and a racing live fire may both retire the same row, and the second act must be a no-op, not an error. Replay never calls this: replay is read-only on the timer keyspace.

A distributed backend must ride retirement on the same stamped, replicated write path as Self::schedule_timer, routed onto the workflow’s shard — an adopted shard must not resurrect retired rows.

Provided Methods§

Source

fn set_owned_shards(&self, shards: Option<&[usize]>)

Restrict every per-workflow enumeration (active workflows, timers, outbox rows) to the named set of distribution shards this node owns, or restore the own-all-shards default when shards is None.

This is the engine-lifecycle hook behind a multi-shard deployment: the boot path tells the store which shards this node serves so recovery and enumeration see only that node’s slice of the cluster’s state. The default implementation is a deliberate no-op — the single-shard in-memory backend owns everything unconditionally, so a None or any shard set leaves its behaviour byte-identical. The sharded haematite backend overrides this to scope its enumeration. Decorators that wrap another store must forward this call to their inner store.

Source

fn acquire_owned_shards(&self, shards: &[usize]) -> Result<(), StoreError>

Acquire-and-serve ownership of each named distribution shard BEFORE the boot path recovers or enumerates over them, so the node is the fenced owner and its replicated state is union-merged locally first.

This is the SS-2 election hook the engine boot path calls right after Self::set_owned_shards and BEFORE startup recovery: a distributed backend wins the per-shard election and becomes the live owner, so the subsequent recovery reads see the full committed history for its shards.

The default implementation is a deliberate no-op returning Ok(()) — non-distributed backends (the in-memory store and single-node haematite) own everything unconditionally and elect nothing, so boot stays byte-identical. Only a DISTRIBUTED sharded backend overrides this to run the election. Decorators that wrap another store must forward this call to their inner store.

§Errors

Returns StoreError::Backend when a distributed backend cannot win the election or become the live owner of one of shards; the node must not serve those shards in that case (fail-closed).

Source

fn acquire_owned_shard(&self, shard: usize) -> Result<(), StoreError>

Acquire-and-serve ownership of a SINGLE distribution shard — the per-shard primitive Self::acquire_owned_shards is a loop over, exposed so the failover path can drive a per-shard abort seam: a clean election loss on one shard (StoreError::NotOwner) drops only that shard rather than failing the whole adoption batch (ADR-021 clean-partial).

The default implementation is a deliberate no-op returning Ok(()) — single-shard / non-distributed backends own everything unconditionally and elect nothing. Only a DISTRIBUTED sharded backend (haematite) overrides it. Decorators that wrap another store must forward this call.

§Errors

Returns StoreError::NotOwner when a strictly higher ballot deposed this candidate (a clean, droppable election loss), and StoreError::Backend for a quorum-unavailable election or any transport fault (retryable).

Source

fn is_current_owner(&self, shard: usize) -> bool

Whether this node currently holds LIVE serve-authority for shard — it won the per-shard election THIS process lifetime and has not been deposed in-process.

This is the residual-window re-assertion the failover path uses to exclude a survivor that lost its epoch between winning acquire+publish and widening its enumeration scope (ADR-021 clean-partial). It is a POINT-IN-TIME ADVISORY, not a durable lock — the authoritative gate remains the per-write CAS fence.

The default implementation returns true — single-shard / non-distributed backends own everything unconditionally, so the failover path’s re-assertion is a no-op there and behaviour stays byte-identical. Only a DISTRIBUTED sharded backend (haematite) overrides it. Decorators that wrap another store must forward this call.

Source

fn extend_owned_shards(&self, shards: &[usize])

Add shards to this node’s owned-enumeration scope, UNIONING them with the shards it already owns rather than replacing the set.

This is the SS-5 failover hook: when a live node absorbs a dead peer’s shards it must KEEP serving its own shards while ALSO enumerating the adopted ones. Self::set_owned_shards replaces the scope (the boot path’s one-shot assignment); this widens it in place. The boot path uses set_owned_shards; the failover path uses this.

The default implementation is a deliberate no-op — single-shard backends own everything unconditionally, so widening their scope is meaningless and leaves their behaviour byte-identical. Only a sharded backend (haematite) overrides this. Decorators that wrap another store must forward this call.

When the store currently owns ALL shards (the None / single-node default), it already enumerates shards, so a sharded backend leaves the own-all scope untouched.

Source

fn publish_shard_owner(&self, shard: usize) -> Result<(), StoreError>

Publish THIS node as the current owner of shard in the cluster’s shard-owner directory, so other nodes’ request-routing edges resolve shard to this node (SS-3).

This is the failover-publish hook the engine calls from adopt_shards right after it has won shard’s election: it records, durably and cluster-visibly, that this node has adopted shard, so a request reaching a DIFFERENT survivor routes to this adopter rather than mis-resolving to the dead declared owner (gap #2).

The default implementation is a deliberate no-op returning Ok(()) — single-shard / non-distributed backends own everything unconditionally and have no peers to coordinate, so boot and adoption stay byte-identical. Only a DISTRIBUTED sharded backend overrides this. Decorators that wrap another store must forward this call.

§Errors

Returns StoreError::NotOwner when a distributed backend’s fenced directory write is out-voted (this node is not actually the owner), and StoreError::Backend for any other replication/transport failure.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§