Skip to main content

ReadableEventStore

Trait ReadableEventStore 

Source
pub trait ReadableEventStore:
    Send
    + Sync
    + 'static {
Show 15 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 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>, ) -> 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; // 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 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.

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>, ) -> 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.

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.

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 — single-shard backends (in-memory, libSQL) own everything unconditionally, so a None or any shard set leaves their behaviour byte-identical. Only a sharded backend (haematite) 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(()) — single-shard / non-distributed backends (in-memory, libSQL, and the single-node haematite mode) 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§