pub struct ProjectionRouter { /* private fields */ }Expand description
Event-stream dispatcher. Matches incoming events to registered
projections by TypeCode, enforces stream-order dedup + gap detection
via a router-held (tick, sequence) cursor, and propagates observer
state transitions.
Implementations§
Source§impl ProjectionRouter
impl ProjectionRouter
Sourcepub fn new() -> Self
pub fn new() -> Self
Build a router in the Passive state — promote to Active before
committing writes.
Sourcepub fn register(&mut self, projection: Box<dyn Projection>)
pub fn register(&mut self, projection: Box<dyn Projection>)
Register a projection. Its observed TypeCodes are folded into the
dispatch index so dispatch only visits matching projections.
Sourcepub fn state(&self) -> ObserverState
pub fn state(&self) -> ObserverState
Current observer state.
Sourcepub fn promote_to_active(&mut self) -> Result<(), ProjectionError>
pub fn promote_to_active(&mut self) -> Result<(), ProjectionError>
Transition to Active — fails if currently Draining.
Sourcepub fn evaluate_auto_promote(
&self,
manifest: &ManifestSnapshot,
primary_down_duration: Duration,
health: &MultiChannelHealth,
threshold_ready: bool,
) -> Option<PromotionDecision>
pub fn evaluate_auto_promote( &self, manifest: &ManifestSnapshot, primary_down_duration: Duration, health: &MultiChannelHealth, threshold_ready: bool, ) -> Option<PromotionDecision>
Evaluate the shell’s kms_auto_promote policy against three inputs:
the elapsed outage, the multi-channel KMS health quorum, and the
threshold-HSM share readiness.
Policy values:
kms_auto_promote | Decision matrix |
|---|---|
"manual" | Always Some(Wait) — operator drives the promotion manually. |
"after_60min" | Some(Promote) iff primary_down_duration >= 1h and the KMS health quorum has at least HF2_HEALTH_QUORUM_MIN channels healthy; otherwise Some(Wait). |
"threshold_hsm" | Some(Promote) iff threshold_ready (t-of-n shares collected); otherwise Some(Wait). |
| other | None — unrecognised policy string, operator intervention required. |
Returning None is conservative by design: callers treat unknown
policies as “do not auto-promote” and fall back to manual operator
action.
Sourcepub fn demote_to_passive(&mut self) -> Result<(), ProjectionError>
pub fn demote_to_passive(&mut self) -> Result<(), ProjectionError>
Transition to Passive. Used when ceding primary to a peer.
Sourcepub fn begin_draining(&mut self) -> Result<(), ProjectionError>
pub fn begin_draining(&mut self) -> Result<(), ProjectionError>
Transition to Draining. Terminal — no further state changes.
Sourcepub fn dispatch(
&mut self,
event: &EventRecord,
ctx: &ProjectionContext<'_>,
) -> Result<usize, ProjectionError>
pub fn dispatch( &mut self, event: &EventRecord, ctx: &ProjectionContext<'_>, ) -> Result<usize, ProjectionError>
Dispatch an event to every matching projection. Returns Ok(n)
where n is the number of projections that applied the event.
Matching projections are visited in registration order (the dispatch
index stores their indices in that order), so a projection registered
earlier always sees an event before one registered later — callers may
rely on that ordering. A TypeCode with no registered observer is a
cheap miss (no index entry) returning Ok(0) — the event still
advances the stream cursor.
§Stream-order contract
EventRecord.sequence is per-compute: every compute’s batch starts
at 0 and is contiguous, so an event’s stream identity is the
composite (tick, sequence) pair, ordered tick-major. Callers must
feed every drained event of a compute in emission order and must
advance the tick between computes. The kernel scheduler permits
same-tick scheduled actions, but two same-tick computes emit
colliding positions — this router rejects the collision loudly
rather than conflating the streams: a colliding head behind the
cursor is ProjectionError::SequenceBackward, and a DIFFERENT
event at exactly the cursor position (the single-event-batch
shape) is ProjectionError::PositionConflict, distinguished
from a true redelivery by event identity (type code + payload).
Checks against the stream cursor (last accepted event):
- the same
(tick, sequence)redelivered with the same identity — duplicate,Ok(0); - a different event at the cursor position —
ProjectionError::PositionConflict; - a position behind the cursor —
ProjectionError::SequenceBackward; - a same-tick sequence jump, or a new tick whose batch does not
start at sequence 0 —
ProjectionError::SequenceGap; - a fresh router (no cursor yet) accepts any position as its resume anchor.
The cursor advances only after a fully successful fan-out. A
FAILED fan-out (first or mid-stream) pins the unapplied event’s
position AND identity: only a retry of that exact event may
proceed — a skip-ahead is a gap (it would silently advance past
the lost event), a different event at the pinned position is a
ProjectionError::PositionConflict — and the projections that
already applied it before the failure are skipped on the retry
via Projection::last_applied.
Only the Active state may dispatch — Passive and Draining
reject with ProjectionError::NotActive. The Passive rejection
is the production guardrail for active-passive HA: a secondary
observer that mistakenly accepts writes would create split-brain
rows in the PG-backed store. The dylint arkhe-trait-default-check
CI gate ensures the contract is honoured by every L2 deployment.