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§
Sourcefn 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<'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.
Sourcefn 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_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 <= 1is equivalent toSelf::read_history: sequence numbers start at 1, so every recorded event satisfies the bound.from_seqbeyond 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 rejectsresume_from_seq > head + 1as 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_historyfor unknown workflows: empty history, neverStoreError::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).
Sourcefn 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 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.
Sourcefn 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_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.
Sourcefn 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 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.
Sourcefn 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_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.
Sourcefn 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 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.
Sourcefn 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 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.
Sourcefn 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 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.
Sourcefn 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 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.
Sourcefn 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,
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§
Sourcefn set_owned_shards(&self, shards: Option<&[usize]>)
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.
Sourcefn acquire_owned_shards(&self, shards: &[usize]) -> Result<(), StoreError>
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).
Sourcefn acquire_owned_shard(&self, shard: usize) -> Result<(), StoreError>
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).
Sourcefn is_current_owner(&self, shard: usize) -> bool
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.
Sourcefn extend_owned_shards(&self, shards: &[usize])
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.
Sourcefn publish_shard_owner(&self, shard: usize) -> Result<(), StoreError>
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".