Skip to main content

laminar_db/
recovery_manager.rs

1//! Checkpoint recovery: selects a committed manifest and resolves its operator-state sidecar.
2//! Runtime owners restore sources, sinks, tables, and operators from that single recovered cut.
3#![allow(clippy::disallowed_types)] // cold path
4
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use bytes::Bytes;
9use futures::{StreamExt, TryStreamExt};
10#[cfg(feature = "cluster")]
11use laminar_core::checkpoint::ClusterRecoveryCapsule;
12use laminar_core::checkpoint_decision::{CheckpointOutcome, CheckpointScope, CheckpointVerdict};
13use laminar_core::state::{CheckpointAttempt, CheckpointSealInventory, StateBackend};
14#[cfg(feature = "cluster")]
15use laminar_core::storage::checkpoint_manifest::ConnectorCheckpoint;
16use laminar_core::storage::checkpoint_manifest::{
17    CheckpointManifest, DurableCheckpointPhase, PipelineIdentity,
18};
19use laminar_core::storage::checkpoint_store::{
20    CheckpointArtifacts, CheckpointStore, CheckpointStoreError,
21};
22use sha2::{Digest, Sha256};
23use tracing::{debug, error, info, warn};
24
25use crate::error::DbError;
26
27const VNODE_REHYDRATION_CONCURRENCY: usize = 32;
28
29/// Result of a successful recovery from a checkpoint.
30#[derive(Debug)]
31pub struct RecoveredState {
32    /// Manifest that was restored from.
33    pub manifest: CheckpointManifest,
34    outcome: Option<CheckpointOutcome>,
35    #[cfg(feature = "cluster")]
36    cluster_capsule: Option<ClusterRecoveryCapsule>,
37}
38
39impl RecoveredState {
40    /// Returns the recovered epoch.
41    #[must_use]
42    pub fn epoch(&self) -> u64 {
43        self.manifest.epoch
44    }
45
46    #[cfg(feature = "cluster")]
47    pub(crate) fn outcome(&self) -> Option<&CheckpointOutcome> {
48        self.outcome.as_ref()
49    }
50
51    #[cfg(feature = "cluster")]
52    pub(crate) fn set_cluster_capsule(&mut self, capsule: ClusterRecoveryCapsule) {
53        self.cluster_capsule = Some(capsule);
54    }
55
56    #[cfg(feature = "cluster")]
57    pub(crate) fn cluster_capsule(&self) -> Option<&ClusterRecoveryCapsule> {
58        self.cluster_capsule.as_ref()
59    }
60}
61
62/// Outcome of rehydrating a set of vnodes from durable state.
63#[derive(Debug, Default)]
64pub struct VnodeRehydration {
65    /// Exact sealed attempt the partials were read from. `None` only for an empty request.
66    pub attempt: Option<CheckpointAttempt>,
67    /// vnode → recovery chain (oldest→newest): a FULL base followed by any delta partials.
68    pub restored: HashMap<u32, Vec<Bytes>>,
69}
70
71impl VnodeRehydration {
72    /// Number of vnodes successfully read back.
73    #[must_use]
74    pub fn restored_count(&self) -> usize {
75        self.restored.len()
76    }
77}
78
79/// Reads an exact Commit-outcome-bound `partial.bin` chain for requested vnodes.
80/// Applying the bytes is the caller's responsibility.
81pub struct VnodeRehydrator<'a> {
82    backend: &'a dyn StateBackend,
83    seal_cache: tokio::sync::Mutex<HashMap<CheckpointAttempt, Arc<CheckpointSealInventory>>>,
84}
85
86impl<'a> VnodeRehydrator<'a> {
87    /// Create a rehydrator over `backend`.
88    #[must_use]
89    pub fn new(backend: &'a dyn StateBackend) -> Self {
90        Self {
91            backend,
92            seal_cache: tokio::sync::Mutex::new(HashMap::new()),
93        }
94    }
95
96    fn require_canonical_attempt(attempt: CheckpointAttempt) -> Result<(), DbError> {
97        if attempt.is_canonical() {
98            return Ok(());
99        }
100        Err(DbError::Checkpoint(format!(
101            "[LDB-6050] vnode rehydration requires one nonzero canonical checkpoint ID; received epoch {} checkpoint {}",
102            attempt.epoch, attempt.checkpoint_id
103        )))
104    }
105
106    async fn sealed_inventory(
107        &self,
108        attempt: CheckpointAttempt,
109    ) -> Result<Arc<CheckpointSealInventory>, DbError> {
110        Self::require_canonical_attempt(attempt)?;
111        if let Some(inventory) = self.seal_cache.lock().await.get(&attempt).cloned() {
112            return Ok(inventory);
113        }
114        let inventory = self
115            .backend
116            .checkpoint_seal_inventory(attempt)
117            .await
118            .map_err(|error| {
119                DbError::Checkpoint(format!(
120                    "[LDB-6050] failed to read exact state seal for checkpoint {} epoch {}: {error}",
121                    attempt.checkpoint_id, attempt.epoch
122                ))
123            })?
124            .ok_or_else(|| {
125                DbError::Checkpoint(format!(
126                    "[LDB-6050] checkpoint {} epoch {} has no exact state seal",
127                    attempt.checkpoint_id, attempt.epoch
128                ))
129            })?;
130        if inventory.attempt != attempt {
131            return Err(DbError::Checkpoint(format!(
132                "[LDB-6050] requested state attempt {attempt:?} does not match seal inventory attempt {:?}",
133                inventory.attempt
134            )));
135        }
136        let partial_vnodes: Vec<u32> = inventory
137            .sealed_partials
138            .iter()
139            .map(|partial| partial.vnode)
140            .collect();
141        if inventory
142            .required_vnodes
143            .windows(2)
144            .any(|pair| pair[0] >= pair[1])
145            || partial_vnodes != inventory.required_vnodes
146            || inventory
147                .sealed_partials
148                .iter()
149                .any(|partial| partial.assignment_version != inventory.assignment_version)
150        {
151            return Err(DbError::Checkpoint(format!(
152                "[LDB-6050] checkpoint {} epoch {} has a non-canonical sealed vnode inventory",
153                attempt.checkpoint_id, attempt.epoch
154            )));
155        }
156        let inventory = Arc::new(inventory);
157        self.seal_cache
158            .lock()
159            .await
160            .insert(attempt, Arc::clone(&inventory));
161        Ok(inventory)
162    }
163
164    async fn read_verified_partial(
165        &self,
166        attempt: CheckpointAttempt,
167        vnode: u32,
168    ) -> Result<Bytes, DbError> {
169        let inventory = self.sealed_inventory(attempt).await?;
170        let index = inventory
171            .sealed_partials
172            .binary_search_by_key(&vnode, |partial| partial.vnode)
173            .map_err(|_| {
174                DbError::Checkpoint(format!(
175                    "[LDB-6050] vnode {vnode} is absent from the exact state seal for checkpoint {} epoch {}",
176                    attempt.checkpoint_id, attempt.epoch
177                ))
178            })?;
179        let attestation = &inventory.sealed_partials[index];
180        let bytes = self
181            .backend
182            .read_partial(attempt, vnode)
183            .await
184            .map_err(|error| {
185                DbError::Checkpoint(format!(
186                    "[LDB-6051] failed to read vnode {vnode} at sealed epoch {} checkpoint {}: {error}",
187                    attempt.epoch, attempt.checkpoint_id
188                ))
189            })?
190            .ok_or_else(|| {
191                DbError::Checkpoint(format!(
192                    "[LDB-6051] vnode {vnode} is missing at sealed epoch {} checkpoint {}",
193                    attempt.epoch, attempt.checkpoint_id
194                ))
195            })?;
196        let actual_digest = format!("{:x}", Sha256::digest(&bytes));
197        if u64::try_from(bytes.len()).ok() != Some(attestation.payload_len)
198            || actual_digest != attestation.payload_sha256
199        {
200            return Err(DbError::Checkpoint(format!(
201                "[LDB-6051] vnode {vnode} payload does not match the exact state seal for checkpoint {} epoch {}",
202                attempt.checkpoint_id, attempt.epoch
203            )));
204        }
205        Ok(bytes)
206    }
207
208    /// Read each vnode's partial chain pinned at `epoch` (a committed cut chosen by the caller),
209    /// so boot recovery restores state at the same epoch its source offsets resume from.
210    ///
211    /// # Errors
212    /// Returns a checkpoint error unless every requested vnode has a complete, decodable chain.
213    pub async fn rehydrate_at(
214        &self,
215        vnodes: &[u32],
216        attempt: CheckpointAttempt,
217    ) -> Result<VnodeRehydration, DbError> {
218        Self::require_canonical_attempt(attempt)?;
219        let mut report = VnodeRehydration::default();
220        if vnodes.is_empty() {
221            return Ok(report);
222        }
223        let inventory = self.sealed_inventory(attempt).await?;
224        if let Some(unsealed) = vnodes
225            .iter()
226            .find(|vnode| inventory.required_vnodes.binary_search(vnode).is_err())
227        {
228            return Err(DbError::Checkpoint(format!(
229                "[LDB-6050] vnode {unsealed} is absent from the exact state seal for checkpoint {} epoch {}",
230                attempt.checkpoint_id, attempt.epoch
231            )));
232        }
233        report.attempt = Some(attempt);
234
235        let chains = futures::stream::iter(vnodes.iter().copied().map(|vnode| async move {
236            let chain = self.collect_chain(vnode, attempt).await?;
237            Ok::<_, DbError>((vnode, chain))
238        }))
239        .buffer_unordered(VNODE_REHYDRATION_CONCURRENCY)
240        .try_collect::<Vec<_>>()
241        .await?;
242        for (vnode, chain) in chains {
243            debug!(
244                vnode,
245                epoch = attempt.epoch,
246                checkpoint_id = attempt.checkpoint_id,
247                links = chain.len(),
248                "rehydrated vnode chain"
249            );
250            report.restored.insert(vnode, chain);
251        }
252
253        info!(
254            epoch = attempt.epoch,
255            checkpoint_id = attempt.checkpoint_id,
256            restored = report.restored.len(),
257            "vnode rehydration complete"
258        );
259        Ok(report)
260    }
261
262    /// Resolve a vnode's recovery chain at an exact attempt: collapse leading reference hops,
263    /// then walk exact parent attempts until each head op has its FULL base (oldest→newest).
264    async fn collect_chain(
265        &self,
266        vnode: u32,
267        attempt: CheckpointAttempt,
268    ) -> Result<Vec<Bytes>, DbError> {
269        use crate::vnode_partial::VnodePartial;
270
271        let (bytes, mut current, head) = self.resolve_reference_head(vnode, attempt).await?;
272        // Walk back until each delta operator has its FULL base.
273        let mut need: std::collections::HashSet<String> = head
274            .deltas
275            .iter()
276            .map(|(n, _)| n.clone())
277            .filter(|n| !head.operators.iter().any(|(on, _)| on == n))
278            .collect();
279        let mut chain_rev: Vec<Bytes> = vec![bytes];
280        let mut cur = head;
281        while !need.is_empty() {
282            let Some(parent) = cur.base else {
283                return Err(DbError::Checkpoint(format!(
284                    "[LDB-6051] vnode {vnode} delta chain has no FULL base for operators {need:?}"
285                )));
286            };
287            if parent.epoch >= current.epoch || parent.checkpoint_id >= current.checkpoint_id {
288                return Err(DbError::Checkpoint(format!(
289                    "[LDB-6051] vnode {vnode} has non-decreasing delta link \
290                     {current:?}->{parent:?}"
291                )));
292            }
293            let pbytes = self.read_verified_partial(parent, vnode).await.map_err(|error| {
294                DbError::Checkpoint(format!(
295                    "[LDB-6051] vnode {vnode} delta parent {parent:?} failed seal verification: {error}"
296                ))
297            })?;
298            let pp = VnodePartial::decode(&pbytes).map_err(|e| {
299                DbError::Checkpoint(format!(
300                    "[LDB-6051] vnode {vnode} delta parent {parent:?} is invalid: {e}"
301                ))
302            })?;
303            for (n, _) in &pp.operators {
304                need.remove(n);
305            }
306            chain_rev.push(pbytes);
307            current = parent;
308            cur = pp;
309        }
310        chain_rev.reverse();
311        Ok(chain_rev)
312    }
313
314    /// Collapse reference-only partials and return the first FULL or DELTA head without decoding
315    /// that head twice on the recovery path.
316    async fn resolve_reference_head(
317        &self,
318        vnode: u32,
319        attempt: CheckpointAttempt,
320    ) -> Result<(Bytes, CheckpointAttempt, crate::vnode_partial::VnodePartial), DbError> {
321        use crate::vnode_partial::VnodePartial;
322
323        let mut bytes = self.read_verified_partial(attempt, vnode).await?;
324
325        let mut current = attempt;
326        loop {
327            let partial = VnodePartial::decode(&bytes).map_err(|error| {
328                DbError::Checkpoint(format!(
329                    "[LDB-6051] vnode {vnode} has an invalid partial at epoch {} checkpoint {}: \
330                     {error}",
331                    current.epoch, current.checkpoint_id
332                ))
333            })?;
334            if !partial.operators.is_empty() || !partial.deltas.is_empty() {
335                return Ok((bytes, current, partial));
336            }
337            let Some(base) = partial.base else {
338                return Ok((bytes, current, partial));
339            };
340            if base.epoch >= current.epoch || base.checkpoint_id >= current.checkpoint_id {
341                return Err(DbError::Checkpoint(format!(
342                    "[LDB-6051] vnode {vnode} has non-decreasing reference \
343                     {current:?}->{base:?}"
344                )));
345            }
346            bytes = self.read_verified_partial(base, vnode).await.map_err(|error| {
347                DbError::Checkpoint(format!(
348                    "[LDB-6051] vnode {vnode} reference base {base:?} failed seal verification: {error}"
349                ))
350            })?;
351            current = base;
352        }
353    }
354}
355
356/// One operator's resolved recovery chain: FULL base bytes + ordered changed-state deltas.
357#[cfg(feature = "cluster")]
358pub(crate) type ResolvedOpChain<'a> = (&'a [u8], Vec<&'a [u8]>);
359
360/// From a vnode's recovery chain (oldest→newest decoded partials), resolve one operator's FULL base
361/// bytes + ordered delta payloads. Returns `None` when no FULL for `op` is present (start fresh).
362#[cfg(feature = "cluster")]
363#[must_use]
364pub(crate) fn resolve_op_chain<'a>(
365    chain: &'a [crate::vnode_partial::VnodePartial],
366    op: &str,
367) -> Option<ResolvedOpChain<'a>> {
368    let base_idx = chain
369        .iter()
370        .rposition(|p| p.operators.iter().any(|(n, _)| n == op))?;
371    let base = chain[base_idx]
372        .operators
373        .iter()
374        .find(|(n, _)| n == op)
375        .map(|(_, b)| b.as_slice())?;
376    let mut deltas = Vec::new();
377    for p in &chain[base_idx + 1..] {
378        if let Some((_, d)) = p.deltas.iter().find(|(n, _)| n == op) {
379            deltas.push(d.as_slice());
380        }
381    }
382    Some((base, deltas))
383}
384
385/// Loads the latest viable [`CheckpointManifest`] and resolves its external operator state.
386pub struct RecoveryManager<'a> {
387    store: &'a dyn CheckpointStore,
388    expected_pipeline_identity: PipelineIdentity,
389    expected_deployment_id: String,
390    expected_outcome_scope: CheckpointScope,
391}
392
393#[derive(Clone, Copy)]
394enum RecoveryOutcomeAuthority<'a> {
395    Local(&'a laminar_core::checkpoint_decision::CheckpointDecisionStore),
396    #[cfg(feature = "cluster")]
397    Cluster {
398        outcomes: &'a laminar_core::cluster::control::LeaderLeaseStore,
399        capsules: &'a laminar_core::checkpoint_decision::CheckpointDecisionStore,
400    },
401}
402
403#[derive(Debug, Clone, Copy)]
404struct ClusterPreparedDominance {
405    highest_terminal_epoch: u64,
406    highest_terminal_checkpoint_id: u64,
407}
408
409#[cfg(feature = "cluster")]
410fn validate_cluster_candidate_manifest_binding(
411    manifest: &CheckpointManifest,
412    participant: &laminar_core::checkpoint::ParticipantRecoveryRef,
413    outcome: &CheckpointOutcome,
414    expected_pipeline_identity: &PipelineIdentity,
415    expected_deployment_id: &str,
416    expected_portable_state_sha256: &str,
417) -> Result<(), String> {
418    if manifest.epoch != outcome.epoch
419        || manifest.checkpoint_id != outcome.checkpoint_id
420        || manifest.participant_id != participant.participant_id
421        || manifest.pipeline_identity != *expected_pipeline_identity
422        || manifest.deployment_id != expected_deployment_id
423    {
424        return Err("manifest does not identify the committed runtime cut".into());
425    }
426    let (manifest_sha256, portable_state_sha256) =
427        crate::cluster_recovery_capsule::manifest_digests(manifest)
428            .map_err(|error| format!("manifest is not portable: {error}"))?;
429    if manifest_sha256 != participant.manifest_sha256
430        || portable_state_sha256 != participant.portable_state_sha256
431        || portable_state_sha256 != expected_portable_state_sha256
432    {
433        return Err("manifest digest does not match the recovery capsule".into());
434    }
435    Ok(())
436}
437
438#[cfg(feature = "cluster")]
439fn declared_sidecar_len(manifest: &CheckpointManifest) -> Result<Option<u64>, String> {
440    let mut ranges = manifest
441        .operator_states
442        .iter()
443        .filter(|(_, state)| state.external)
444        .map(|(name, state)| {
445            let end = state
446                .external_offset
447                .checked_add(state.external_length)
448                .ok_or_else(|| format!("operator '{name}' sidecar range overflows"))?;
449            if state.external_length == 0 {
450                return Err(format!(
451                    "operator '{name}' has an empty external sidecar range"
452                ));
453            }
454            Ok((state.external_offset, end, name.as_str()))
455        })
456        .collect::<Result<Vec<_>, String>>()?;
457    if ranges.is_empty() {
458        if manifest.operator_states.is_empty() && manifest.state_checksum.is_some() {
459            return Err("sidecar checksum has no external operator ranges".into());
460        }
461        return Ok(None);
462    }
463    ranges.sort_unstable();
464    let mut expected_offset = 0;
465    for (start, end, name) in ranges {
466        if start != expected_offset {
467            return Err(format!(
468                "operator '{name}' sidecar range starts at {start}, expected {expected_offset}"
469            ));
470        }
471        expected_offset = end;
472    }
473    Ok(Some(expected_offset))
474}
475
476impl ClusterPreparedDominance {
477    #[cfg(feature = "cluster")]
478    fn from_outcomes(outcomes: &[CheckpointOutcome]) -> Option<Self> {
479        let terminal = outcomes.last()?;
480        Some(Self {
481            highest_terminal_epoch: terminal.epoch,
482            highest_terminal_checkpoint_id: terminal.checkpoint_id,
483        })
484    }
485}
486
487impl RecoveryOutcomeAuthority<'_> {
488    const fn scope(self) -> CheckpointScope {
489        match self {
490            Self::Local(_) => CheckpointScope::Local,
491            #[cfg(feature = "cluster")]
492            Self::Cluster { .. } => CheckpointScope::Cluster,
493        }
494    }
495
496    async fn outcome(self, epoch: u64) -> Result<Option<CheckpointOutcome>, DbError> {
497        match self {
498            Self::Local(store) => store
499                .outcome(epoch)
500                .await
501                .map_err(|error| DbError::Checkpoint(error.to_string())),
502            #[cfg(feature = "cluster")]
503            Self::Cluster { outcomes, .. } => outcomes
504                .cluster_outcome(epoch)
505                .await
506                .map_err(|error| DbError::Checkpoint(error.to_string())),
507        }
508    }
509
510    async fn outcomes(self) -> Result<Vec<CheckpointOutcome>, DbError> {
511        match self {
512            Self::Local(store) => store
513                .outcomes()
514                .await
515                .map_err(|error| DbError::Checkpoint(error.to_string())),
516            #[cfg(feature = "cluster")]
517            Self::Cluster { outcomes, .. } => outcomes
518                .cluster_outcome_inventory()
519                .await
520                .map(|inventory| inventory.outcomes)
521                .map_err(|error| DbError::Checkpoint(error.to_string())),
522        }
523    }
524}
525
526#[cfg(feature = "cluster")]
527impl<'a> RecoveryOutcomeAuthority<'a> {
528    const fn capsule_store(
529        self,
530    ) -> Option<&'a laminar_core::checkpoint_decision::CheckpointDecisionStore> {
531        match self {
532            Self::Local(_) => None,
533            Self::Cluster { capsules, .. } => Some(capsules),
534        }
535    }
536}
537
538impl<'a> RecoveryManager<'a> {
539    /// Create a recovery manager.
540    #[must_use]
541    pub fn new(store: &'a dyn CheckpointStore) -> Self {
542        Self {
543            store,
544            expected_pipeline_identity: PipelineIdentity::empty(),
545            expected_deployment_id: String::new(),
546            expected_outcome_scope: CheckpointScope::Local,
547        }
548    }
549
550    /// Require an exact logical topology/state-ABI identity during recovery.
551    #[must_use]
552    pub fn with_pipeline_identity(mut self, identity: &PipelineIdentity) -> Self {
553        self.expected_pipeline_identity.clone_from(identity);
554        self
555    }
556
557    /// Require the create-once checkpoint namespace incarnation during recovery.
558    #[must_use]
559    pub fn with_deployment_id(mut self, deployment_id: &str) -> Self {
560        deployment_id.clone_into(&mut self.expected_deployment_id);
561        self
562    }
563
564    /// Require durable outcomes to belong to the active local or cluster recovery domain.
565    #[must_use]
566    pub(crate) fn with_outcome_scope(mut self, scope: CheckpointScope) -> Self {
567        self.expected_outcome_scope = scope;
568        self
569    }
570
571    async fn restore_committed_outcome(
572        &self,
573        outcome: &CheckpointOutcome,
574        authority: RecoveryOutcomeAuthority<'_>,
575    ) -> Result<Option<RecoveredState>, DbError> {
576        if outcome.scope != self.expected_outcome_scope {
577            return Err(DbError::Checkpoint(format!(
578                "[LDB-6041] durable outcome scope {:?} does not match active runtime scope {:?}",
579                outcome.scope, self.expected_outcome_scope
580            )));
581        }
582        if outcome.deployment_id != self.expected_deployment_id {
583            return Err(DbError::Checkpoint(format!(
584                "[LDB-6043] durable outcome deployment '{}' does not match runtime deployment '{}'",
585                outcome.deployment_id, self.expected_deployment_id
586            )));
587        }
588        match &outcome.verdict {
589            CheckpointVerdict::Commit => {}
590            CheckpointVerdict::Abort => {
591                return Err(DbError::Checkpoint(format!(
592                    "[LDB-6041] epoch {} checkpoint {} has an Abort outcome and is not a recovery cut",
593                    outcome.epoch, outcome.checkpoint_id
594                )));
595            }
596        }
597
598        if outcome.scope == CheckpointScope::Cluster {
599            #[cfg(feature = "cluster")]
600            {
601                return self
602                    .load_cluster_committed_outcome(
603                        outcome,
604                        authority.capsule_store().ok_or_else(|| {
605                            DbError::Checkpoint(
606                                "[LDB-6041] cluster recovery has no recovery capsule store".into(),
607                            )
608                        })?,
609                        true,
610                    )
611                    .await;
612            }
613            #[cfg(not(feature = "cluster"))]
614            {
615                return Err(DbError::Checkpoint(
616                    "[LDB-6041] cluster recovery requires the 'cluster' feature".into(),
617                ));
618            }
619        }
620
621        let local_participant = self.store.participant_id();
622        if local_participant != 0 {
623            return Err(DbError::Checkpoint(format!(
624                "[LDB-6041] local outcome recovery requires participant 0, but the checkpoint store is participant {local_participant}"
625            )));
626        }
627        if outcome.recovery_capsule.is_some() {
628            return Err(DbError::Checkpoint(format!(
629                "[LDB-6041] local epoch {} checkpoint {} unexpectedly binds a cluster recovery capsule",
630                outcome.epoch, outcome.checkpoint_id
631            )));
632        }
633
634        let artifacts = self
635            .store
636            .load_checkpoint_artifacts_for_participant(0, outcome.checkpoint_id)
637            .await
638            .map_err(|error| {
639                DbError::Checkpoint(format!(
640                    "[LDB-6041] committed epoch {} checkpoint {} participant {} artifacts are unreadable: {error}",
641                    outcome.epoch, outcome.checkpoint_id, local_participant
642                ))
643            })?
644            .ok_or_else(|| {
645                DbError::Checkpoint(format!(
646                    "[LDB-6041] committed checkpoint {} is absent from participant \
647                     {local_participant} recovery inventory",
648                    outcome.checkpoint_id
649                ))
650            })?;
651        let manifest = &artifacts.manifest;
652        if manifest.epoch != outcome.epoch || manifest.checkpoint_id != outcome.checkpoint_id {
653            return Err(DbError::Checkpoint(format!(
654                "[LDB-6041] outcome epoch {} checkpoint {} does not match participant \
655                 {local_participant} manifest epoch {} checkpoint {}",
656                outcome.epoch, outcome.checkpoint_id, manifest.epoch, manifest.checkpoint_id
657            )));
658        }
659        self.try_restore(
660            outcome.checkpoint_id,
661            artifacts,
662            Some(authority),
663            0,
664            Some(outcome),
665        )
666        .await
667    }
668
669    #[cfg(feature = "cluster")]
670    async fn load_cluster_committed_outcome(
671        &self,
672        outcome: &CheckpointOutcome,
673        decision_store: &laminar_core::checkpoint_decision::CheckpointDecisionStore,
674        finalize_local_manifest: bool,
675    ) -> Result<Option<RecoveredState>, DbError> {
676        let reference = outcome.recovery_capsule.as_ref().ok_or_else(|| {
677            DbError::Checkpoint(format!(
678                "[LDB-6041] cluster epoch {} checkpoint {} has no recovery capsule",
679                outcome.epoch, outcome.checkpoint_id
680            ))
681        })?;
682        let capsule = decision_store
683            .load_recovery_capsule(reference)
684            .await
685            .map_err(|error| {
686                DbError::Checkpoint(format!(
687                    "[LDB-6041] cluster epoch {} checkpoint {} recovery capsule is unreadable: {error}",
688                    outcome.epoch, outcome.checkpoint_id
689                ))
690            })?;
691        let expected_attempt = CheckpointAttempt::new(outcome.epoch, outcome.checkpoint_id);
692        if capsule.attempt != expected_attempt
693            || capsule.deployment_id != outcome.deployment_id
694            || capsule.deployment_id != self.expected_deployment_id
695            || capsule.pipeline_identity != self.expected_pipeline_identity
696            || outcome.assignment_fence.as_ref() != Some(&capsule.assignment_fence)
697        {
698            return Err(DbError::Checkpoint(format!(
699                "[LDB-6041] cluster epoch {} checkpoint {} recovery capsule does not match the active attempt, deployment, pipeline identity, and assignment fence",
700                outcome.epoch, outcome.checkpoint_id
701            )));
702        }
703
704        let local_participant = self.store.participant_id();
705        let mut candidates = capsule.participants.iter().collect::<Vec<_>>();
706        if let Some(local_index) = candidates
707            .iter()
708            .position(|participant| participant.participant_id == local_participant)
709        {
710            candidates[..=local_index].rotate_right(1);
711        }
712
713        let mut rejected = 0_usize;
714        let mut first_failure = None;
715        let mut last_failure = None;
716        let mut reject_candidate = |failure: String| {
717            rejected += 1;
718            if first_failure.is_none() {
719                first_failure = Some(failure.clone());
720            }
721            last_failure = Some(failure);
722        };
723        for participant in candidates {
724            let storage_participant = participant.participant_id;
725            info!(
726                local_participant,
727                storage_participant,
728                epoch = outcome.epoch,
729                checkpoint_id = outcome.checkpoint_id,
730                assignment_version = capsule.assignment_fence.assignment_version,
731                "cluster recovery: validating participant checkpoint artifacts"
732            );
733
734            let artifacts = match self
735                .store
736                .load_checkpoint_artifacts_for_participant(
737                    storage_participant,
738                    outcome.checkpoint_id,
739                )
740                .await
741            {
742                Ok(Some(artifacts)) => artifacts,
743                Ok(None) => {
744                    reject_candidate(format!(
745                        "participant {storage_participant} manifest is absent"
746                    ));
747                    warn!(
748                        storage_participant,
749                        checkpoint_id = outcome.checkpoint_id,
750                        "cluster recovery candidate manifest is absent"
751                    );
752                    continue;
753                }
754                Err(error) => {
755                    reject_candidate(format!(
756                        "participant {storage_participant} artifacts are unreadable: {error}"
757                    ));
758                    warn!(
759                        storage_participant,
760                        checkpoint_id = outcome.checkpoint_id,
761                        %error,
762                        "cluster recovery candidate artifacts are unreadable"
763                    );
764                    continue;
765                }
766            };
767
768            let manifest = &artifacts.manifest;
769            if let Err(failure) = validate_cluster_candidate_manifest_binding(
770                manifest,
771                participant,
772                outcome,
773                &self.expected_pipeline_identity,
774                &self.expected_deployment_id,
775                &capsule.portable_state_sha256,
776            ) {
777                reject_candidate(format!("participant {storage_participant} {failure}"));
778                warn!(
779                    storage_participant,
780                    checkpoint_id = outcome.checkpoint_id,
781                    %failure,
782                    "cluster recovery candidate manifest binding is invalid"
783                );
784                continue;
785            }
786            let (artifacts, validation) = artifacts
787                .validate(
788                    outcome.checkpoint_id,
789                    storage_participant,
790                    self.store.key_group_count(),
791                    self.store.max_state_data_bytes(),
792                )
793                .await
794                .map_err(DbError::from)?;
795            if !validation.valid {
796                reject_candidate(format!(
797                    "participant {storage_participant} artifact integrity failed: {:?}",
798                    validation.issues
799                ));
800                warn!(
801                    storage_participant,
802                    checkpoint_id = outcome.checkpoint_id,
803                    issues = ?validation.issues,
804                    "cluster recovery candidate artifact integrity failed"
805                );
806                continue;
807            }
808
809            let mut recovered = match Self::restore_from(artifacts) {
810                Ok(recovered) => recovered,
811                Err(error) => {
812                    reject_candidate(format!(
813                        "participant {storage_participant} sidecar cannot be resolved: {error}"
814                    ));
815                    warn!(
816                        storage_participant,
817                        checkpoint_id = outcome.checkpoint_id,
818                        %error,
819                        "cluster recovery candidate sidecar cannot be resolved"
820                    );
821                    continue;
822                }
823            };
824
825            recovered.manifest.source_offsets = capsule
826                .source_offsets
827                .iter()
828                .map(|(source, offsets)| {
829                    let metadata = capsule
830                        .source_metadata
831                        .get(source)
832                        .expect("validated recovery capsule has matching source metadata");
833                    (
834                        source.clone(),
835                        ConnectorCheckpoint {
836                            offsets: offsets.clone().into_iter().collect(),
837                            metadata: metadata.clone().into_iter().collect(),
838                            source_assignment_version: capsule
839                                .source_assignment_versions
840                                .get(source)
841                                .copied(),
842                        },
843                    )
844                })
845                .collect();
846            recovered.manifest.source_names = capsule.source_offsets.keys().cloned().collect();
847            recovered.manifest.source_watermarks =
848                capsule.source_watermarks.clone().into_iter().collect();
849            recovered.manifest.watermark = capsule.recovery_watermark_frontier;
850            let mut recovered = if finalize_local_manifest {
851                self.complete_restore(
852                    recovered,
853                    Some(outcome.clone()),
854                    outcome.checkpoint_id,
855                    storage_participant,
856                )
857                .await?
858            } else {
859                recovered.outcome = Some(outcome.clone());
860                recovered
861            };
862            recovered.set_cluster_capsule(capsule);
863            return Ok(Some(recovered));
864        }
865
866        let failure_summary = match (first_failure, last_failure) {
867            (Some(first), Some(last)) if first != last => {
868                format!("; first failure: {first}; last failure: {last}")
869            }
870            (Some(failure), _) => format!("; failure: {failure}"),
871            _ => String::new(),
872        };
873        Err(DbError::Checkpoint(format!(
874            "[LDB-6041] committed cluster checkpoint {} has no usable participant artifact replica; {rejected} candidate(s) rejected{}",
875            outcome.checkpoint_id,
876            failure_summary
877        )))
878    }
879
880    #[cfg(feature = "cluster")]
881    pub(crate) async fn preflight_cluster_committed_outcome(
882        &self,
883        outcome: &CheckpointOutcome,
884        decision_store: &laminar_core::checkpoint_decision::CheckpointDecisionStore,
885    ) -> Result<RecoveredState, DbError> {
886        self.load_cluster_committed_outcome(outcome, decision_store, false)
887            .await?
888            .ok_or_else(|| {
889                DbError::Checkpoint(format!(
890                    "[LDB-6041] cluster checkpoint {} has no recoverable participant artifacts",
891                    outcome.checkpoint_id
892                ))
893            })
894    }
895
896    #[cfg(feature = "cluster")]
897    async fn preflight_cluster_candidate_metadata(
898        &self,
899        participant: &laminar_core::checkpoint::ParticipantRecoveryRef,
900        outcome: &CheckpointOutcome,
901        capsule: &ClusterRecoveryCapsule,
902    ) -> Result<(), String> {
903        let participant_id = participant.participant_id;
904        let manifest = self
905            .store
906            .load_manifest_for_participant(participant_id, outcome.checkpoint_id)
907            .await
908            .map_err(|error| {
909                format!("participant {participant_id} manifest is unreadable: {error}")
910            })?
911            .ok_or_else(|| format!("participant {participant_id} manifest is absent"))?;
912        let validation = manifest.validate(self.store.key_group_count());
913        if let Some(failure) = validation.first() {
914            return Err(format!(
915                "participant {participant_id} manifest metadata is invalid: {failure}"
916            ));
917        }
918        validate_cluster_candidate_manifest_binding(
919            &manifest,
920            participant,
921            outcome,
922            &self.expected_pipeline_identity,
923            &self.expected_deployment_id,
924            &capsule.portable_state_sha256,
925        )
926        .map_err(|failure| format!("participant {participant_id} {failure}"))?;
927
928        let Some(expected_len) = declared_sidecar_len(&manifest).map_err(|failure| {
929            format!("participant {participant_id} manifest sidecar shape is invalid: {failure}")
930        })?
931        else {
932            return Ok(());
933        };
934        match self
935            .store
936            .state_data_len_for_participant(participant_id, outcome.checkpoint_id)
937            .await
938            .map_err(|error| {
939                format!("participant {participant_id} sidecar metadata is unreadable: {error}")
940            })?
941        {
942            Some(actual_len) if actual_len == expected_len => Ok(()),
943            Some(actual_len) => Err(format!(
944                "participant {participant_id} sidecar is {actual_len} bytes; expected {expected_len}"
945            )),
946            None => Err(format!("participant {participant_id} sidecar is absent")),
947        }
948    }
949
950    #[cfg(feature = "cluster")]
951    /// Audit durable recovery metadata bound to a committed cluster capsule without reading
952    /// state-sidecar or vnode payload bodies. Recovery validates those bodies before use.
953    pub(crate) async fn preflight_cluster_committed_metadata(
954        &self,
955        outcome: &CheckpointOutcome,
956        capsule: &ClusterRecoveryCapsule,
957    ) -> Result<(), DbError> {
958        capsule
959            .validate()
960            .map_err(|error| DbError::Checkpoint(format!("[LDB-6041] {error}")))?;
961        let expected_attempt = CheckpointAttempt::new(outcome.epoch, outcome.checkpoint_id);
962        if !outcome.is_commit()
963            || outcome.scope != CheckpointScope::Cluster
964            || capsule.attempt != expected_attempt
965            || capsule.deployment_id != outcome.deployment_id
966            || capsule.deployment_id != self.expected_deployment_id
967            || capsule.pipeline_identity != self.expected_pipeline_identity
968            || outcome.assignment_fence.as_ref() != Some(&capsule.assignment_fence)
969        {
970            return Err(DbError::Checkpoint(format!(
971                "[LDB-6041] cluster epoch {} checkpoint {} manifest preflight does not match the committed runtime cut",
972                outcome.epoch, outcome.checkpoint_id
973            )));
974        }
975
976        let local_participant = self.store.participant_id();
977        let mut candidates = capsule.participants.iter().collect::<Vec<_>>();
978        if let Some(local_index) = candidates
979            .iter()
980            .position(|participant| participant.participant_id == local_participant)
981        {
982            candidates[..=local_index].rotate_right(1);
983        }
984        let mut rejected = 0_usize;
985        let mut last_failure = None;
986        for participant in candidates {
987            match self
988                .preflight_cluster_candidate_metadata(participant, outcome, capsule)
989                .await
990            {
991                Ok(()) => return Ok(()),
992                Err(failure) => {
993                    rejected += 1;
994                    last_failure = Some(failure);
995                }
996            }
997        }
998
999        let failure = last_failure.map_or_else(String::new, |failure| format!("; {failure}"));
1000        Err(DbError::Checkpoint(format!(
1001            "[LDB-6041] committed cluster checkpoint {} has no usable participant manifest metadata; {rejected} candidate(s) rejected{failure}",
1002            outcome.checkpoint_id
1003        )))
1004    }
1005
1006    #[cfg(feature = "cluster")]
1007    async fn validated_cluster_recovery_outcomes(
1008        &self,
1009        authority: &laminar_core::cluster::control::LeaderLeaseStore,
1010        capsule_store: &laminar_core::checkpoint_decision::CheckpointDecisionStore,
1011    ) -> Result<Vec<CheckpointOutcome>, DbError> {
1012        let inventory = authority
1013            .validated_cluster_outcome_inventory(|outcome| async move {
1014                self.preflight_cluster_committed_outcome(&outcome, capsule_store)
1015                    .await
1016                    .map(drop)
1017                    .map_err(|error| error.to_string())
1018            })
1019            .await
1020            .map_err(|error| DbError::Checkpoint(error.to_string()))?;
1021        Ok(inventory.outcomes)
1022    }
1023
1024    async fn settle_local_prepared_attempts(
1025        &self,
1026        decision_store: &laminar_core::checkpoint_decision::CheckpointDecisionStore,
1027        outcomes: &[CheckpointOutcome],
1028    ) -> Result<(), DbError> {
1029        let authority_deployment = decision_store
1030            .load_or_create_deployment_id()
1031            .await
1032            .map_err(|error| DbError::Checkpoint(error.to_string()))?;
1033        if !self.expected_deployment_id.is_empty()
1034            && self.expected_deployment_id != authority_deployment
1035        {
1036            return Err(DbError::Checkpoint(format!(
1037                "[LDB-6043] local decision authority deployment '{}' does not match runtime deployment '{}'",
1038                authority_deployment, self.expected_deployment_id
1039            )));
1040        }
1041        let floor = decision_store
1042            .outcome_retention_boundary()
1043            .await
1044            .map_err(|error| DbError::Checkpoint(error.to_string()))?
1045            .before_epoch;
1046        let checkpoint_ids = self.store.list_ids().await.map_err(DbError::from)?;
1047        for checkpoint_id in checkpoint_ids {
1048            if outcomes
1049                .iter()
1050                .any(|outcome| outcome.checkpoint_id == checkpoint_id)
1051            {
1052                continue;
1053            }
1054            let manifest = self
1055                .store
1056                .load_by_id(checkpoint_id)
1057                .await
1058                .map_err(|error| {
1059                    DbError::Checkpoint(format!(
1060                        "[LDB-6041] checkpoint {checkpoint_id} recovery inventory is unreadable while settling Prepared witnesses: {error}"
1061                    ))
1062                })?
1063                .ok_or_else(|| {
1064                    DbError::Checkpoint(format!(
1065                        "[LDB-6041] checkpoint {checkpoint_id} disappeared while settling prepared recovery inventory"
1066                    ))
1067                })?;
1068            if manifest.durable_phase != DurableCheckpointPhase::Prepared || manifest.epoch < floor
1069            {
1070                continue;
1071            }
1072            if let Some(outcome) = outcomes
1073                .iter()
1074                .find(|outcome| outcome.epoch == manifest.epoch)
1075            {
1076                if outcome.checkpoint_id != manifest.checkpoint_id {
1077                    return Err(DbError::Checkpoint(format!(
1078                        "[LDB-6041] prepared checkpoint {} epoch {} conflicts with durable outcome checkpoint {}",
1079                        manifest.checkpoint_id, manifest.epoch, outcome.checkpoint_id
1080                    )));
1081                }
1082                continue;
1083            }
1084
1085            let validation = manifest.validate(self.store.key_group_count());
1086            if !validation.is_empty() {
1087                return Err(DbError::Checkpoint(format!(
1088                    "[LDB-6041] prepared checkpoint {} epoch {} is invalid and cannot be used to mint an Abort outcome: {}",
1089                    manifest.checkpoint_id,
1090                    manifest.epoch,
1091                    validation
1092                        .iter()
1093                        .map(ToString::to_string)
1094                        .collect::<Vec<_>>()
1095                        .join("; ")
1096                )));
1097            }
1098            if manifest.participant_id != 0
1099                || manifest.deployment_id != self.expected_deployment_id
1100                || manifest.pipeline_identity != self.expected_pipeline_identity
1101            {
1102                return Err(DbError::Checkpoint(format!(
1103                    "[LDB-6043] prepared checkpoint {} epoch {} does not belong to local participant 0 and the active deployment/pipeline identity",
1104                    manifest.checkpoint_id, manifest.epoch
1105                )));
1106            }
1107
1108            let settled = decision_store
1109                .record_outcome(
1110                    manifest.epoch,
1111                    manifest.checkpoint_id,
1112                    CheckpointScope::Local,
1113                    None,
1114                    None,
1115                    CheckpointVerdict::Abort,
1116                    None,
1117                )
1118                .await
1119                .map_err(|error| {
1120                    DbError::Checkpoint(format!(
1121                        "[LDB-6041] prepared checkpoint {} epoch {} could not be durably settled as Abort: {error}",
1122                        manifest.checkpoint_id, manifest.epoch
1123                    ))
1124                })?;
1125            let outcome = match settled {
1126                laminar_core::checkpoint_decision::RecordOutcomeResult::Created(outcome)
1127                | laminar_core::checkpoint_decision::RecordOutcomeResult::Unchanged(outcome) => {
1128                    outcome
1129                }
1130                laminar_core::checkpoint_decision::RecordOutcomeResult::Conflict { winner } => {
1131                    winner
1132                }
1133            };
1134            if outcome.epoch != manifest.epoch || outcome.checkpoint_id != manifest.checkpoint_id {
1135                return Err(DbError::Checkpoint(format!(
1136                    "[LDB-6041] prepared checkpoint {} epoch {} was not settled by an exact durable terminal outcome",
1137                    manifest.checkpoint_id, manifest.epoch
1138                )));
1139            }
1140        }
1141        Ok(())
1142    }
1143
1144    async fn validate_newer_durable_attempts(
1145        &self,
1146        selected: &CheckpointOutcome,
1147        outcomes: &[CheckpointOutcome],
1148        cluster_dominance: Option<ClusterPreparedDominance>,
1149    ) -> Result<(), DbError> {
1150        let checkpoint_ids = self.store.list_ids().await.map_err(DbError::from)?;
1151        for checkpoint_id in checkpoint_ids {
1152            if checkpoint_id == selected.checkpoint_id {
1153                continue;
1154            }
1155            let manifest = self
1156                .store
1157                .load_by_id(checkpoint_id)
1158                .await
1159                .map_err(DbError::from)?
1160                .ok_or_else(|| {
1161                    DbError::Checkpoint(format!(
1162                        "[LDB-6041] checkpoint {checkpoint_id} disappeared while auditing recovery inventory"
1163                    ))
1164                })?;
1165            if manifest.epoch <= selected.epoch && manifest.checkpoint_id <= selected.checkpoint_id
1166            {
1167                if cluster_dominance.is_none() {
1168                    continue;
1169                }
1170                if manifest.epoch < selected.epoch
1171                    && manifest.checkpoint_id < selected.checkpoint_id
1172                {
1173                    if manifest.durable_phase == DurableCheckpointPhase::Prepared
1174                        && !outcomes
1175                            .iter()
1176                            .any(|outcome| outcome.epoch == manifest.epoch)
1177                    {
1178                        self.validate_dominated_prepared_manifest(checkpoint_id, &manifest)?;
1179                    }
1180                    continue;
1181                }
1182            }
1183            let Some(outcome) = outcomes
1184                .iter()
1185                .find(|outcome| outcome.epoch == manifest.epoch)
1186            else {
1187                if let Some(outcome) = outcomes
1188                    .iter()
1189                    .find(|outcome| outcome.checkpoint_id == manifest.checkpoint_id)
1190                {
1191                    return Err(DbError::Checkpoint(format!(
1192                        "[LDB-6041] checkpoint {} is reused by manifest epoch {} and terminal outcome epoch {}",
1193                        manifest.checkpoint_id, manifest.epoch, outcome.epoch
1194                    )));
1195                }
1196                let dominated = cluster_dominance.is_some_and(|boundary| {
1197                    manifest.epoch < boundary.highest_terminal_epoch
1198                        && manifest.checkpoint_id < boundary.highest_terminal_checkpoint_id
1199                });
1200                if manifest.durable_phase == DurableCheckpointPhase::Prepared && dominated {
1201                    self.validate_dominated_prepared_manifest(checkpoint_id, &manifest)?;
1202                    continue;
1203                }
1204                return Err(DbError::Checkpoint(format!(
1205                    "[LDB-6041] newer {:?} checkpoint {} epoch {} is not settled by an exact terminal outcome; refusing to restore older checkpoint {} epoch {}",
1206                    manifest.durable_phase,
1207                    manifest.checkpoint_id,
1208                    manifest.epoch,
1209                    selected.checkpoint_id,
1210                    selected.epoch
1211                )));
1212            };
1213            let expected_verdict = match manifest.durable_phase {
1214                DurableCheckpointPhase::Prepared => CheckpointVerdict::Abort,
1215                DurableCheckpointPhase::Finalized => CheckpointVerdict::Commit,
1216            };
1217            if outcome.checkpoint_id != manifest.checkpoint_id
1218                || outcome.verdict != expected_verdict
1219            {
1220                return Err(DbError::Checkpoint(format!(
1221                    "[LDB-6041] newer {:?} checkpoint {} epoch {} conflicts with terminal outcome checkpoint {} {:?}",
1222                    manifest.durable_phase,
1223                    manifest.checkpoint_id,
1224                    manifest.epoch,
1225                    outcome.checkpoint_id,
1226                    outcome.verdict
1227                )));
1228            }
1229        }
1230        Ok(())
1231    }
1232
1233    fn validate_dominated_prepared_manifest(
1234        &self,
1235        storage_id: u64,
1236        manifest: &CheckpointManifest,
1237    ) -> Result<(), DbError> {
1238        if manifest.checkpoint_id != storage_id {
1239            return Err(DbError::Checkpoint(format!(
1240                "[LDB-6041] storage checkpoint {storage_id} contains dominated Prepared manifest checkpoint {}",
1241                manifest.checkpoint_id
1242            )));
1243        }
1244        let validation = manifest.validate(self.store.key_group_count());
1245        if !validation.is_empty() {
1246            return Err(DbError::Checkpoint(format!(
1247                "[LDB-6041] dominated Prepared checkpoint {} epoch {} is invalid: {}",
1248                manifest.checkpoint_id,
1249                manifest.epoch,
1250                validation
1251                    .iter()
1252                    .map(ToString::to_string)
1253                    .collect::<Vec<_>>()
1254                    .join("; ")
1255            )));
1256        }
1257        if manifest.participant_id != self.store.participant_id()
1258            || manifest.deployment_id != self.expected_deployment_id
1259            || manifest.pipeline_identity != self.expected_pipeline_identity
1260        {
1261            return Err(DbError::Checkpoint(format!(
1262                "[LDB-6043] dominated Prepared checkpoint {} epoch {} does not belong to storage participant {} and the active deployment/pipeline identity",
1263                manifest.checkpoint_id,
1264                manifest.epoch,
1265                self.store.participant_id()
1266            )));
1267        }
1268        Ok(())
1269    }
1270
1271    async fn validate_no_commit_outcome_genesis(&self) -> Result<(), DbError> {
1272        // Prepared-only inventory is normal residue from an aborted or unresolved attempt. A
1273        // Finalized manifest, torn pointer, or unreadable inventory is different: it is evidence
1274        // that authoritative Commit history was lost, so genesis would replay visible output.
1275        let published = self.store.load_latest().await.map_err(|error| {
1276            DbError::Checkpoint(format!(
1277                "[LDB-6041] checkpoint recovery pointer is invalid while no durable Commit outcome exists: {error}"
1278            ))
1279        })?;
1280        if let Some(manifest) = published {
1281            return Err(DbError::Checkpoint(format!(
1282                "[LDB-6041] published finalized checkpoint {} epoch {} exists but no durable Commit outcome exists",
1283                manifest.checkpoint_id, manifest.epoch
1284            )));
1285        }
1286        let checkpoint_ids = self.store.list_ids().await.map_err(|error| {
1287            DbError::Checkpoint(format!(
1288                "[LDB-6041] recovery inventory cannot be enumerated while no durable Commit outcome exists: {error}"
1289            ))
1290        })?;
1291        for checkpoint_id in checkpoint_ids {
1292            let manifest = self
1293                .store
1294                .load_by_id(checkpoint_id)
1295                .await
1296                .map_err(|error| {
1297                    DbError::Checkpoint(format!(
1298                        "[LDB-6041] checkpoint {checkpoint_id} recovery inventory is unreadable while no durable Commit outcome exists: {error}"
1299                    ))
1300                })?
1301                .ok_or_else(|| {
1302                    DbError::Checkpoint(format!(
1303                        "[LDB-6041] checkpoint {checkpoint_id} disappeared from recovery inventory while no durable Commit outcome exists"
1304                    ))
1305                })?;
1306            if manifest.checkpoint_id != checkpoint_id {
1307                return Err(DbError::Checkpoint(format!(
1308                    "[LDB-6041] storage checkpoint {checkpoint_id} contains manifest checkpoint {} while no durable Commit outcome exists",
1309                    manifest.checkpoint_id
1310                )));
1311            }
1312            if manifest.durable_phase == DurableCheckpointPhase::Finalized {
1313                return Err(DbError::Checkpoint(format!(
1314                    "[LDB-6041] finalized checkpoint {} epoch {} exists in recovery inventory but no durable Commit outcome exists",
1315                    manifest.checkpoint_id, manifest.epoch
1316                )));
1317            }
1318        }
1319        Ok(())
1320    }
1321
1322    /// Recover from the latest committed, structurally valid checkpoint.
1323    ///
1324    /// Returns `Ok(None)` when no committed checkpoint exists. With an outcome store, Prepared
1325    /// artifacts without a Commit outcome are explicitly ignored as abandoned attempts.
1326    ///
1327    /// # Errors
1328    ///
1329    /// Returns `DbError::Checkpoint` if the store fails or no stored checkpoint is usable.
1330    pub(crate) async fn recover(
1331        &self,
1332        decision_store: Option<&laminar_core::checkpoint_decision::CheckpointDecisionStore>,
1333    ) -> Result<Option<RecoveredState>, DbError> {
1334        self.recover_with_authority(decision_store.map(RecoveryOutcomeAuthority::Local))
1335            .await
1336    }
1337
1338    #[cfg(feature = "cluster")]
1339    pub(crate) async fn recover_cluster(
1340        &self,
1341        authority: &laminar_core::cluster::control::LeaderLeaseStore,
1342        capsule_store: &laminar_core::checkpoint_decision::CheckpointDecisionStore,
1343    ) -> Result<Option<RecoveredState>, DbError> {
1344        self.recover_with_authority(Some(RecoveryOutcomeAuthority::Cluster {
1345            outcomes: authority,
1346            capsules: capsule_store,
1347        }))
1348        .await
1349    }
1350
1351    async fn recover_with_authority(
1352        &self,
1353        authority: Option<RecoveryOutcomeAuthority<'_>>,
1354    ) -> Result<Option<RecoveredState>, DbError> {
1355        self.validate_outcome_authority(authority)?;
1356        // A Commit outcome is the irrevocable recovery frontier. Once it exists, restoring any
1357        // older manifest can replay output already visible in an exact external sink. Resolve the
1358        // highest commit first and require an exact participant-bound manifest. Corruption or
1359        // storage loss for an included participant is fatal rather than a reason to rewind.
1360        if let Some(authority) = authority {
1361            let (outcomes, cluster_dominance) = match authority {
1362                RecoveryOutcomeAuthority::Local(decision_store) => {
1363                    let mut outcomes = authority.outcomes().await?;
1364                    // Prepared is the sole local pre-outcome durable phase: state is sealed and
1365                    // coordinated sinks may have phase-1 side effects, but Finalized is written
1366                    // only after Commit. Settle every outcome-less Prepared attempt before
1367                    // choosing an older recovery cut.
1368                    self.settle_local_prepared_attempts(decision_store, &outcomes)
1369                        .await?;
1370                    outcomes = authority.outcomes().await?;
1371                    (outcomes, None)
1372                }
1373                #[cfg(feature = "cluster")]
1374                RecoveryOutcomeAuthority::Cluster {
1375                    outcomes: cluster_authority,
1376                    capsules,
1377                } => {
1378                    let outcomes = self
1379                        .validated_cluster_recovery_outcomes(cluster_authority, capsules)
1380                        .await?;
1381                    let dominance = ClusterPreparedDominance::from_outcomes(&outcomes);
1382                    (outcomes, dominance)
1383                }
1384            };
1385            let outcome = outcomes
1386                .iter()
1387                .rev()
1388                .find(|outcome| outcome.is_commit())
1389                .cloned();
1390            if let Some(outcome) = outcome {
1391                self.validate_newer_durable_attempts(&outcome, &outcomes, cluster_dominance)
1392                    .await?;
1393                return self.restore_committed_outcome(&outcome, authority).await;
1394            }
1395
1396            self.validate_no_commit_outcome_genesis().await?;
1397            info!(
1398                "no Commit outcome exists; ignoring aborted or unresolved Prepared artifacts and starting fresh"
1399            );
1400            return Ok(None);
1401        }
1402        let mut checkpoint_ids = self.store.list_ids().await.map_err(DbError::from)?;
1403        if checkpoint_ids.is_empty() {
1404            info!("checkpoint store is empty, starting fresh");
1405            return Ok(None);
1406        }
1407        checkpoint_ids.reverse();
1408        self.restore_first_for_participant(&checkpoint_ids, authority, self.store.participant_id())
1409            .await
1410    }
1411
1412    /// Recover from the newest viable checkpoint with `epoch <= target_epoch` — the
1413    /// coordinated-restart target, which may be older than this node's local latest.
1414    ///
1415    /// # Errors
1416    /// Returns `DbError::Checkpoint` if the store fails.
1417    pub(crate) async fn recover_to_epoch(
1418        &self,
1419        target_epoch: u64,
1420        decision_store: Option<&laminar_core::checkpoint_decision::CheckpointDecisionStore>,
1421    ) -> Result<Option<RecoveredState>, DbError> {
1422        self.recover_to_epoch_with_authority(
1423            target_epoch,
1424            decision_store.map(RecoveryOutcomeAuthority::Local),
1425        )
1426        .await
1427    }
1428
1429    #[cfg(feature = "cluster")]
1430    pub(crate) async fn recover_cluster_to_epoch(
1431        &self,
1432        target_epoch: u64,
1433        authority: &laminar_core::cluster::control::LeaderLeaseStore,
1434        capsule_store: &laminar_core::checkpoint_decision::CheckpointDecisionStore,
1435    ) -> Result<Option<RecoveredState>, DbError> {
1436        self.recover_to_epoch_with_authority(
1437            target_epoch,
1438            Some(RecoveryOutcomeAuthority::Cluster {
1439                outcomes: authority,
1440                capsules: capsule_store,
1441            }),
1442        )
1443        .await
1444    }
1445
1446    async fn recover_to_epoch_with_authority(
1447        &self,
1448        target_epoch: u64,
1449        authority: Option<RecoveryOutcomeAuthority<'_>>,
1450    ) -> Result<Option<RecoveredState>, DbError> {
1451        self.validate_outcome_authority(authority)?;
1452        if let Some(authority) = authority {
1453            // Audit the immutable outcome history before selecting a target. Abort closes an
1454            // attempt but never creates a recovery cut; the newest retained Commit is authoritative.
1455            let (outcomes, cluster_dominance) = match authority {
1456                RecoveryOutcomeAuthority::Local(decision_store) => {
1457                    let mut outcomes = authority.outcomes().await?;
1458                    self.settle_local_prepared_attempts(decision_store, &outcomes)
1459                        .await?;
1460                    outcomes = authority.outcomes().await?;
1461                    (outcomes, None)
1462                }
1463                #[cfg(feature = "cluster")]
1464                RecoveryOutcomeAuthority::Cluster {
1465                    outcomes: cluster_authority,
1466                    capsules,
1467                } => {
1468                    let outcomes = self
1469                        .validated_cluster_recovery_outcomes(cluster_authority, capsules)
1470                        .await?;
1471                    let dominance = ClusterPreparedDominance::from_outcomes(&outcomes);
1472                    (outcomes, dominance)
1473                }
1474            };
1475            let committed = outcomes
1476                .iter()
1477                .rev()
1478                .find(|outcome| outcome.is_commit())
1479                .cloned();
1480            return match committed {
1481                Some(outcome) if outcome.epoch == target_epoch => {
1482                    self.validate_newer_durable_attempts(
1483                        &outcome,
1484                        &outcomes,
1485                        cluster_dominance,
1486                    )
1487                        .await?;
1488                    self.restore_committed_outcome(&outcome, authority).await
1489                }
1490                Some(outcome) => Err(DbError::Checkpoint(format!(
1491                    "[LDB-6041] recovery target epoch {target_epoch} is not the highest durable Commit outcome: epoch {} checkpoint {} is authoritative",
1492                    outcome.epoch, outcome.checkpoint_id
1493                ))),
1494                None if target_epoch == 0 => {
1495                    self.validate_no_commit_outcome_genesis().await?;
1496                    Ok(None)
1497                }
1498                None => Err(DbError::Checkpoint(format!(
1499                    "[LDB-6041] recovery target epoch {target_epoch} has no Commit outcome"
1500                ))),
1501            };
1502        }
1503
1504        let checkpoint_ids = self.store.list_ids().await.map_err(DbError::from)?;
1505        if checkpoint_ids.is_empty() {
1506            return if target_epoch == 0 {
1507                Ok(None)
1508            } else {
1509                Err(DbError::Checkpoint(format!(
1510                    "[LDB-6041] recovery target epoch {target_epoch} has no checkpoint history"
1511                )))
1512            };
1513        }
1514
1515        let mut checkpoints = Vec::with_capacity(checkpoint_ids.len());
1516        for &id in &checkpoint_ids {
1517            match self.store.load_by_id(id).await {
1518                Ok(Some(manifest)) if manifest.checkpoint_id == id => {
1519                    if manifest.epoch <= target_epoch {
1520                        checkpoints.push((id, manifest.epoch));
1521                    }
1522                }
1523                Ok(Some(manifest)) => warn!(
1524                    storage_id = id,
1525                    manifest_id = manifest.checkpoint_id,
1526                    "checkpoint identity mismatch — skipping"
1527                ),
1528                Ok(None) => {
1529                    return Err(DbError::Checkpoint(format!(
1530                        "[LDB-6041] checkpoint {id} disappeared during recovery inventory"
1531                    )));
1532                }
1533                Err(CheckpointStoreError::Serde(e)) => {
1534                    warn!(checkpoint_id = id, error = %e, "corrupt checkpoint manifest — skipping");
1535                }
1536                Err(e) => return Err(DbError::from(e)),
1537            }
1538        }
1539        // Newest eligible first. The globally unique checkpoint ID is the authoritative tie-break
1540        // and prevents abandoned attempts from winning through store iteration order.
1541        checkpoints.sort_by_key(|&(id, epoch)| std::cmp::Reverse((epoch, id)));
1542        if checkpoints.is_empty() {
1543            return if target_epoch == 0 {
1544                Ok(None)
1545            } else {
1546                Err(DbError::Checkpoint(format!(
1547                    "[LDB-6041] no usable checkpoint exists at or before recovery target \
1548                     epoch {target_epoch}"
1549                )))
1550            };
1551        }
1552        let candidate_ids: Vec<u64> = checkpoints.iter().map(|&(id, _)| id).collect();
1553        self.restore_first_for_participant(&candidate_ids, authority, self.store.participant_id())
1554            .await
1555    }
1556
1557    fn validate_outcome_authority(
1558        &self,
1559        authority: Option<RecoveryOutcomeAuthority<'_>>,
1560    ) -> Result<(), DbError> {
1561        match authority {
1562            Some(authority) if authority.scope() != self.expected_outcome_scope => {
1563                Err(DbError::Checkpoint(format!(
1564                    "[LDB-6041] recovery authority scope {:?} does not match active runtime scope {:?}",
1565                    authority.scope(),
1566                    self.expected_outcome_scope
1567                )))
1568            }
1569            None if self.expected_outcome_scope == CheckpointScope::Cluster => {
1570                Err(DbError::Checkpoint(
1571                    "[LDB-6041] cluster recovery requires the exact checkpoint authority".into(),
1572                ))
1573            }
1574            _ => Ok(()),
1575        }
1576    }
1577
1578    /// Resolve external operator states from the sidecar file into inline entries.
1579    ///
1580    /// # Errors
1581    /// Returns a checkpoint error if the sidecar is unavailable, truncated, or unreadable.
1582    fn resolve_external_states(
1583        manifest: &mut CheckpointManifest,
1584        state_data: Option<&[u8]>,
1585    ) -> Result<(), DbError> {
1586        let external_ops: Vec<String> = manifest
1587            .operator_states
1588            .iter()
1589            .filter(|(_, op)| op.external)
1590            .map(|(name, _)| name.clone())
1591            .collect();
1592
1593        if external_ops.is_empty() {
1594            return Ok(());
1595        }
1596
1597        let Some(state_data) = state_data else {
1598            return Err(DbError::Checkpoint(format!(
1599                "[LDB-6010] checkpoint {} sidecar is missing for external operators \
1600                 {external_ops:?}",
1601                manifest.checkpoint_id
1602            )));
1603        };
1604
1605        for (name, op) in &mut manifest.operator_states {
1606            if op.external {
1607                // Checked arithmetic: a corrupt manifest can't overflow the length check.
1608                let range = match (
1609                    usize::try_from(op.external_offset),
1610                    usize::try_from(op.external_length),
1611                ) {
1612                    (Ok(start), Ok(len)) => start.checked_add(len).map(|end| (start, end)),
1613                    _ => None,
1614                }
1615                .filter(|&(_, end)| end <= state_data.len());
1616                if let Some((start, end)) = range {
1617                    let external_offset = op.external_offset;
1618                    let external_length = op.external_length;
1619                    let data = &state_data[start..end];
1620                    *op = laminar_core::storage::checkpoint_manifest::OperatorCheckpoint::inline(
1621                        data,
1622                    );
1623                    debug!(
1624                        operator = %name,
1625                        offset = external_offset,
1626                        length = external_length,
1627                        "resolved external operator state from sidecar"
1628                    );
1629                } else {
1630                    return Err(DbError::Checkpoint(format!(
1631                        "[LDB-6010] checkpoint {} sidecar range for operator '{name}' is invalid \
1632                         (offset {}, length {}, sidecar {})",
1633                        manifest.checkpoint_id,
1634                        op.external_offset,
1635                        op.external_length,
1636                        state_data.len()
1637                    )));
1638                }
1639            }
1640        }
1641        Ok(())
1642    }
1643
1644    /// Inner restore logic shared by the fast path and the fallback loop.
1645    fn restore_from(artifacts: CheckpointArtifacts) -> Result<RecoveredState, DbError> {
1646        let CheckpointArtifacts {
1647            mut manifest,
1648            state_data,
1649        } = artifacts;
1650        Self::resolve_external_states(&mut manifest, state_data.as_deref())?;
1651
1652        info!(
1653            checkpoint_id = manifest.checkpoint_id,
1654            epoch = manifest.epoch,
1655            "recovering from checkpoint"
1656        );
1657        Ok(RecoveredState {
1658            manifest,
1659            outcome: None,
1660            #[cfg(feature = "cluster")]
1661            cluster_capsule: None,
1662        })
1663    }
1664
1665    fn validate_outcome_manifest_binding(
1666        &self,
1667        outcome: &CheckpointOutcome,
1668        storage_id: u64,
1669        manifest: &CheckpointManifest,
1670        storage_participant: u64,
1671    ) -> Result<(), DbError> {
1672        if outcome.scope != self.expected_outcome_scope {
1673            return Err(DbError::Checkpoint(format!(
1674                "[LDB-6041] durable outcome scope {:?} does not match active runtime scope {:?}",
1675                outcome.scope, self.expected_outcome_scope
1676            )));
1677        }
1678        if outcome.checkpoint_id != storage_id {
1679            return Err(DbError::Checkpoint(format!(
1680                "[LDB-6041] epoch {} commits checkpoint {}, but storage candidate is {storage_id}",
1681                manifest.epoch, outcome.checkpoint_id
1682            )));
1683        }
1684        if outcome.deployment_id != manifest.deployment_id {
1685            return Err(DbError::Checkpoint(format!(
1686                "[LDB-6041] epoch {} checkpoint {storage_id} outcome deployment '{}' does not match manifest deployment '{}'",
1687                manifest.epoch, outcome.deployment_id, manifest.deployment_id
1688            )));
1689        }
1690        let outcome_participants = outcome.assignment_fence.as_ref().map_or_else(
1691            || vec![0],
1692            laminar_core::checkpoint::CheckpointAssignmentFence::participant_ids,
1693        );
1694        if outcome_participants
1695            .binary_search(&storage_participant)
1696            .is_err()
1697        {
1698            return Err(DbError::Checkpoint(format!(
1699                "[LDB-6041] epoch {} checkpoint {storage_id} storage participant {storage_participant} is absent from outcome participants {:?}",
1700                manifest.epoch, outcome_participants
1701            )));
1702        }
1703        if outcome.verdict != CheckpointVerdict::Commit {
1704            return Err(DbError::Checkpoint(format!(
1705                "[LDB-6041] epoch {} checkpoint {storage_id} has an Abort outcome",
1706                manifest.epoch
1707            )));
1708        }
1709        match (outcome.scope, outcome.recovery_capsule.as_ref()) {
1710            (CheckpointScope::Local, None) | (CheckpointScope::Cluster, Some(_)) => {}
1711            (CheckpointScope::Local, Some(_)) => {
1712                return Err(DbError::Checkpoint(format!(
1713                    "[LDB-6041] local epoch {} checkpoint {storage_id} unexpectedly binds a cluster recovery capsule",
1714                    manifest.epoch
1715                )));
1716            }
1717            (CheckpointScope::Cluster, None) => {
1718                return Err(DbError::Checkpoint(format!(
1719                    "[LDB-6041] cluster epoch {} checkpoint {storage_id} has no recovery capsule",
1720                    manifest.epoch
1721                )));
1722            }
1723        }
1724        Ok(())
1725    }
1726
1727    async fn complete_restore(
1728        &self,
1729        mut state: RecoveredState,
1730        outcome: Option<CheckpointOutcome>,
1731        storage_id: u64,
1732        storage_participant: u64,
1733    ) -> Result<RecoveredState, DbError> {
1734        state.outcome = outcome;
1735        if state.manifest.durable_phase == DurableCheckpointPhase::Prepared {
1736            if storage_participant == self.store.participant_id() {
1737                self.store
1738                    .finalize(storage_id)
1739                    .await
1740                    .map_err(DbError::from)?;
1741            }
1742            // A Commit outcome is authoritative. A donor manifest is not republished locally.
1743            state.manifest.durable_phase = DurableCheckpointPhase::Finalized;
1744        }
1745        Ok(state)
1746    }
1747
1748    /// Restore from `artifacts` if viable; `None` means an older checkpoint may be tried
1749    /// because this candidate is deterministically corrupt or has no durable Commit outcome.
1750    async fn try_restore(
1751        &self,
1752        storage_id: u64,
1753        artifacts: CheckpointArtifacts,
1754        authority: Option<RecoveryOutcomeAuthority<'_>>,
1755        storage_participant: u64,
1756        known_outcome: Option<&CheckpointOutcome>,
1757    ) -> Result<Option<RecoveredState>, DbError> {
1758        let manifest = &artifacts.manifest;
1759        let (checkpoint_id, epoch) = (manifest.checkpoint_id, manifest.epoch);
1760        if manifest.participant_id != storage_participant {
1761            return Err(DbError::Checkpoint(format!(
1762                "[LDB-6041] checkpoint {storage_id} manifest participant {} does not match \
1763                 storage participant {storage_participant}",
1764                manifest.participant_id
1765            )));
1766        }
1767        if manifest.pipeline_identity != self.expected_pipeline_identity {
1768            return Err(DbError::Checkpoint(format!(
1769                "[LDB-6043] checkpoint {storage_id} pipeline identity {} does not match runtime \
1770                 identity {}; explicit checkpoint reset or savepoint migration is required",
1771                manifest.pipeline_identity.sha256, self.expected_pipeline_identity.sha256
1772            )));
1773        }
1774        if manifest.deployment_id != self.expected_deployment_id {
1775            return Err(DbError::Checkpoint(format!(
1776                "[LDB-6043] checkpoint {storage_id} deployment identity '{}' does not match \
1777                 runtime identity '{}'; a partial storage reset is unsafe",
1778                manifest.deployment_id, self.expected_deployment_id
1779            )));
1780        }
1781        let outcome = match known_outcome {
1782            Some(outcome) => Some(outcome.clone()),
1783            None => match authority {
1784                Some(authority) => authority
1785                    .outcome(epoch)
1786                    .await?
1787                    .filter(CheckpointOutcome::is_commit),
1788                None => None,
1789            },
1790        };
1791        if let Some(ref outcome) = outcome {
1792            self.validate_outcome_manifest_binding(
1793                outcome,
1794                storage_id,
1795                manifest,
1796                storage_participant,
1797            )?;
1798        }
1799
1800        // Prepared inventory is never a recovery cut on its own. When an exact outcome store is
1801        // configured, it is authoritative for Finalized manifests too: the manifest phase is a
1802        // publication optimization, not a second commit oracle. At-least-once runtimes without a
1803        // outcome store may recover only an integrity-valid Finalized manifest.
1804        if outcome.is_none()
1805            && (manifest.durable_phase == DurableCheckpointPhase::Prepared || authority.is_some())
1806        {
1807            warn!(
1808                checkpoint_id,
1809                epoch,
1810                phase = ?manifest.durable_phase,
1811                "checkpoint has no exact Commit outcome; trying older"
1812            );
1813            return Ok(None);
1814        }
1815
1816        let (artifacts, validation) = artifacts
1817            .validate(
1818                storage_id,
1819                storage_participant,
1820                self.store.key_group_count(),
1821                self.store.max_state_data_bytes(),
1822            )
1823            .await
1824            .map_err(DbError::from)?;
1825        if !validation.valid {
1826            error!(
1827                checkpoint_id,
1828                issues = ?validation.issues,
1829                "[LDB-6010] checkpoint integrity check failed"
1830            );
1831            warn!(
1832                checkpoint_id,
1833                epoch, "[LDB-6010] checkpoint corrupt, trying older"
1834            );
1835            if outcome.is_some() {
1836                return Err(DbError::Checkpoint(format!(
1837                    "[LDB-6041] committed checkpoint {storage_id} is corrupt"
1838                )));
1839            }
1840            return Ok(None);
1841        }
1842        let state = Self::restore_from(artifacts)?;
1843        Ok(Some(
1844            self.complete_restore(state, outcome, storage_id, storage_participant)
1845                .await?,
1846        ))
1847    }
1848
1849    /// Restore from the first viable checkpoint ID in try order.
1850    async fn restore_first_for_participant(
1851        &self,
1852        candidates: &[u64],
1853        authority: Option<RecoveryOutcomeAuthority<'_>>,
1854        storage_participant: u64,
1855    ) -> Result<Option<RecoveredState>, DbError> {
1856        for &checkpoint_id in candidates {
1857            match self
1858                .store
1859                .load_checkpoint_artifacts_for_participant(storage_participant, checkpoint_id)
1860                .await
1861            {
1862                Ok(Some(artifacts)) => {
1863                    if let Some(state) = self
1864                        .try_restore(
1865                            checkpoint_id,
1866                            artifacts,
1867                            authority,
1868                            storage_participant,
1869                            None,
1870                        )
1871                        .await?
1872                    {
1873                        return Ok(Some(state));
1874                    }
1875                }
1876                Ok(None) => {
1877                    return Err(DbError::Checkpoint(format!(
1878                        "[LDB-6041] checkpoint {checkpoint_id} is absent from participant \
1879                         {storage_participant} recovery inventory"
1880                    )));
1881                }
1882                Err(CheckpointStoreError::Serde(e)) => {
1883                    warn!(checkpoint_id, error = %e, "corrupt checkpoint manifest, trying older");
1884                }
1885                Err(e) => return Err(DbError::from(e)),
1886            }
1887        }
1888        Err(Self::no_usable_checkpoint_error(candidates))
1889    }
1890
1891    fn no_usable_checkpoint_error(checkpoint_ids: &[u64]) -> DbError {
1892        DbError::Checkpoint(format!(
1893            "[LDB-6041] checkpoint history exists but none is usable; \
1894             refusing to start with empty state (checkpoint ids: {checkpoint_ids:?})"
1895        ))
1896    }
1897}
1898
1899#[cfg(test)]
1900mod tests;
1901
1902#[cfg(test)]
1903mod rehydration_tests;