Skip to main content

icydb_core/db/startup/
mod.rs

1//! Module: db::startup
2//! Responsibility: derive bounded generated-database startup readiness.
3//! Does not own: recovery execution, watchdog registration, or ordinary-operation admission.
4//! Boundary: fixed durable controls plus runtime recovery witness -> readiness or typed failure.
5
6mod driver;
7mod observe;
8pub(in crate::db) mod receipt;
9
10use candid::CandidType;
11use icydb_diagnostic_code::{Diagnostic, DiagnosticFactTag, MAX_PUBLIC_DIAGNOSTIC_FACTS};
12use serde::Deserialize;
13
14use crate::{db::StoreRegistry, error::InternalError, traits::CanisterKind};
15
16/// Current generated-database startup readiness.
17#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
18pub enum DatabaseStartupState {
19    /// Recovery controls are complete and the generated schema is reconciled.
20    Ready,
21    /// Dedicated replicated startup work remains.
22    Recovering,
23}
24
25/// One bounded outcome from the hidden replicated startup coordinator.
26#[doc(hidden)]
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum GeneratedStartupDriverStep {
29    /// Startup is already ready or has one durably observable terminal failure.
30    Terminal,
31    /// Recovery or optional derived-evidence work remains after one bounded page.
32    Recovering,
33    /// Recovery is complete and generated schema reconciliation must run now.
34    ApplyGeneratedSchema,
35}
36
37/// Closed owner of one terminal startup failure.
38#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
39pub enum StartupFailureKind {
40    /// Database boot, incarnation, or commit-control failure.
41    DatabaseControl,
42    /// Journal-tail or fold-continuation recovery failure.
43    JournalRecovery,
44    /// Accepted-schema reconciliation or runtime preparation failure.
45    SchemaReconciliation,
46}
47
48/// Internal bounded startup failure projected by the public facade.
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct StartupFailure {
51    kind: StartupFailureKind,
52    diagnostic: Diagnostic,
53    facts: Vec<(DiagnosticFactTag, u64)>,
54}
55
56impl StartupFailure {
57    pub(in crate::db) fn from_internal(kind: StartupFailureKind, error: &InternalError) -> Self {
58        Self::new(kind, error.diagnostic(), error.diagnostic_facts())
59    }
60
61    pub(in crate::db) fn new(
62        kind: StartupFailureKind,
63        diagnostic: Diagnostic,
64        facts: Vec<(DiagnosticFactTag, u64)>,
65    ) -> Self {
66        debug_assert!(facts.len() <= MAX_PUBLIC_DIAGNOSTIC_FACTS);
67        Self {
68            kind,
69            diagnostic,
70            facts,
71        }
72    }
73
74    /// Return the subsystem that owns this terminal startup failure.
75    #[must_use]
76    pub const fn kind(&self) -> StartupFailureKind {
77        self.kind
78    }
79
80    /// Return its compact diagnostic identity.
81    #[must_use]
82    pub const fn diagnostic(&self) -> &Diagnostic {
83        &self.diagnostic
84    }
85
86    /// Borrow its bounded numeric diagnostic facts.
87    #[must_use]
88    pub const fn facts(&self) -> &[(DiagnosticFactTag, u64)] {
89        self.facts.as_slice()
90    }
91}
92
93pub(in crate::db) fn classify_terminal_failure(
94    kind: StartupFailureKind,
95    error: &InternalError,
96) -> Option<StartupFailure> {
97    terminal_code_for_kind(kind, error.diagnostic().error_code())
98        .then(|| StartupFailure::from_internal(kind, error))
99}
100
101pub(in crate::db) fn classify_terminal_failure_parts(
102    kind: StartupFailureKind,
103    diagnostic: Diagnostic,
104    facts: Vec<(DiagnosticFactTag, u64)>,
105) -> Option<StartupFailure> {
106    terminal_code_for_kind(kind, diagnostic.error_code())
107        .then(|| StartupFailure::new(kind, diagnostic, facts))
108}
109
110fn terminal_code_for_kind(
111    kind: StartupFailureKind,
112    code: icydb_diagnostic_code::ErrorCode,
113) -> bool {
114    use icydb_diagnostic_code::ErrorCode;
115
116    let persisted_failure = code == ErrorCode::STORE_CORRUPTION
117        || code == ErrorCode::STORE_INVARIANT_VIOLATION
118        || code == ErrorCode::RUNTIME_CORRUPTION
119        || code == ErrorCode::RUNTIME_INCOMPATIBLE_PERSISTED_FORMAT
120        || code == ErrorCode::RUNTIME_INVARIANT_VIOLATION
121        || code == ErrorCode::RUNTIME_BOUNDARY_PERSISTED_ROW_LAYOUT_OUTSIDE_ACCEPTED_WINDOW
122        || code == ErrorCode::RUNTIME_BOUNDARY_PERSISTED_ROW_SLOT_COUNT_MISMATCH
123        || code == ErrorCode::RUNTIME_BOUNDARY_ACCEPTED_ROW_CONSTRAINT_PROGRAM_CORRUPT;
124    persisted_failure
125        || match kind {
126            StartupFailureKind::DatabaseControl => false,
127            StartupFailureKind::JournalRecovery => {
128                code == ErrorCode::RUNTIME_BOUNDARY_JOURNAL_MUTATION_REVISION_EXHAUSTED
129            }
130            StartupFailureKind::SchemaReconciliation => {
131                code == ErrorCode::SCHEMA_DDL_ADMISSION
132                    || code == ErrorCode::RUNTIME_CONFLICT
133                    || code == ErrorCode::RUNTIME_UNSUPPORTED
134                    || code == ErrorCode::RUNTIME_BOUNDARY_GENERATED_FIELD_AFTER_DDL_FIELD
135                    || code == ErrorCode::RUNTIME_BOUNDARY_CONSTRAINT_VIOLATION
136                    || code == ErrorCode::RUNTIME_BOUNDARY_GENERATED_CONSTRAINT_ACTIVATION_STALE
137            }
138        }
139}
140
141/// Observe one generated database without opening a request or advancing recovery.
142pub fn observe_generated_startup_state<C: CanisterKind>(
143    stores: &'static std::thread::LocalKey<StoreRegistry>,
144    submission_key: &str,
145) -> Result<DatabaseStartupState, StartupFailure> {
146    observe::observe::<C>(stores, submission_key)
147}
148
149/// Run at most one bounded recovery page without admitting ordinary work.
150#[doc(hidden)]
151pub fn drive_generated_startup_recovery_page<C: CanisterKind>(
152    session: &crate::db::DbSession<C>,
153    stores: &'static std::thread::LocalKey<StoreRegistry>,
154    submission_key: &str,
155) -> Result<GeneratedStartupDriverStep, InternalError> {
156    driver::drive_recovery_page(session, stores, submission_key)
157}
158
159/// Persist one deterministic generated-schema failure against fresh authority.
160#[doc(hidden)]
161pub fn record_generated_schema_startup_failure<C: CanisterKind>(
162    stores: &'static std::thread::LocalKey<StoreRegistry>,
163    submission_key: &str,
164    diagnostic: Diagnostic,
165    facts: Vec<(DiagnosticFactTag, u64)>,
166) -> Result<bool, InternalError> {
167    driver::record_schema_failure::<C>(stores, submission_key, diagnostic, facts)
168}
169
170/// Clear a stale startup failure after an authoritative successful handoff.
171#[doc(hidden)]
172pub fn clear_generated_startup_failure<C: CanisterKind>() -> Result<bool, InternalError> {
173    receipt::clear::<C>()
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::{
180        db::{
181            DataStore, IndexStore, StoreAllocationIdentities, StoreRuntimeStorageCapabilities,
182            commit::{
183                CommitMarker, begin_commit, commit_memory_handle, current_commit_memory_allocation,
184                database_incarnation_id, finish_commit, mark_startup_recovery_complete_for_tests,
185                persist_raw_commit_marker_for_tests, select_commit_memory_allocation,
186            },
187            database_format::{
188                ensure_database_format_admitted, initialize_current_database_control_for_tests,
189            },
190            journal::{
191                JournalBatch, JournalRecord, JournalSequence, JournalTailStore,
192                encode_journal_batch,
193            },
194            registry::StoreAllocationIdentity,
195            schema::{
196                SchemaApplicationRecord, SchemaApplicationRecordOp, SchemaChangeOutcome,
197                SchemaChangeReceipt, SchemaStore, apply_schema_application_record_op,
198                corrupt_schema_control_header_for_tests, generated_schema_authority,
199                load_schema_application_record_read_only,
200            },
201            session::RequestExecutionRoot,
202        },
203        testing::test_memory,
204        traits::Path,
205    };
206    use ic_memory::ic_stable_structures::Memory;
207    use icydb_diagnostic_code::{ErrorCode, ErrorOrigin as DiagnosticOrigin};
208    use icydb_schema::{SchemaProposalDigest, SchemaSubmissionKey};
209    use std::cell::RefCell;
210
211    struct FreshCanister;
212
213    impl Path for FreshCanister {
214        const PATH: &'static str = "startup_tests::FreshCanister";
215    }
216
217    impl CanisterKind for FreshCanister {
218        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
219            Ok(232)
220        }
221        const COMMIT_STABLE_KEY: &'static str = "icydb.test.startup_fresh.commit.v1";
222        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
223            Ok(233)
224        }
225        const STARTUP_STABLE_KEY: &'static str = "icydb.test.startup_fresh.control.v1";
226        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
227            Ok(234)
228        }
229        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str = "icydb.test.startup_fresh.integrity.v1";
230    }
231
232    thread_local! {
233        static FRESH_STORES: StoreRegistry = StoreRegistry::new();
234        static CURRENT_STORES: StoreRegistry = StoreRegistry::new();
235    }
236
237    struct CurrentCanister;
238
239    impl Path for CurrentCanister {
240        const PATH: &'static str = "startup_tests::CurrentCanister";
241    }
242
243    impl CanisterKind for CurrentCanister {
244        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
245            Ok(228)
246        }
247        const COMMIT_STABLE_KEY: &'static str = "icydb.test.startup_current.commit.v1";
248        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
249            Ok(229)
250        }
251        const STARTUP_STABLE_KEY: &'static str = "icydb.test.startup_current.control.v1";
252        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
253            Ok(230)
254        }
255        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
256            "icydb.test.startup_current.integrity.v1";
257    }
258
259    struct CorruptCanister;
260
261    impl Path for CorruptCanister {
262        const PATH: &'static str = "startup_tests::CorruptCanister";
263    }
264
265    impl CanisterKind for CorruptCanister {
266        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
267            Ok(224)
268        }
269        const COMMIT_STABLE_KEY: &'static str = "icydb.test.startup_corrupt.commit.v1";
270        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
271            Ok(225)
272        }
273        const STARTUP_STABLE_KEY: &'static str = "icydb.test.startup_corrupt.control.v1";
274        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
275            Ok(226)
276        }
277        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
278            "icydb.test.startup_corrupt.integrity.v1";
279    }
280
281    thread_local! {
282        static CORRUPT_STORES: StoreRegistry = StoreRegistry::new();
283    }
284
285    struct DriverCanister;
286
287    impl Path for DriverCanister {
288        const PATH: &'static str = "startup_tests::DriverCanister";
289    }
290
291    impl CanisterKind for DriverCanister {
292        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
293            Ok(248)
294        }
295        const COMMIT_STABLE_KEY: &'static str = "icydb.test.startup_driver.commit.v1";
296        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
297            Ok(249)
298        }
299        const STARTUP_STABLE_KEY: &'static str = "icydb.test.startup_driver.control.v1";
300        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
301            Ok(250)
302        }
303        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
304            "icydb.test.startup_driver.integrity.v1";
305    }
306
307    thread_local! {
308        static DRIVER_STORES: StoreRegistry = StoreRegistry::new();
309    }
310
311    struct CardinalityDriverCanister;
312
313    impl Path for CardinalityDriverCanister {
314        const PATH: &'static str = "startup_tests::CardinalityDriverCanister";
315    }
316
317    impl CanisterKind for CardinalityDriverCanister {
318        // Keep this test-only control triplet distinct from the migration
319        // fixtures that coexist in the all-feature libtest process.
320        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
321            Ok(217)
322        }
323        const COMMIT_STABLE_KEY: &'static str = "icydb.test.startup.cardinality.driver.commit.v1";
324        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
325            Ok(218)
326        }
327        const STARTUP_STABLE_KEY: &'static str = "icydb.test.startup.cardinality.driver.control.v1";
328        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
329            Ok(219)
330        }
331        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
332            "icydb.test.startup.cardinality.driver.integrity.v1";
333    }
334
335    thread_local! {
336        static CARDINALITY_DRIVER_DATA: RefCell<DataStore> =
337            RefCell::new(DataStore::init_journaled(test_memory(210)));
338        static CARDINALITY_DRIVER_INDEX: RefCell<IndexStore> =
339            RefCell::new(IndexStore::init_journaled(test_memory(211)));
340        static CARDINALITY_DRIVER_SCHEMA: RefCell<SchemaStore> =
341            RefCell::new(SchemaStore::init_journaled(test_memory(212)));
342        static CARDINALITY_DRIVER_TAIL: RefCell<JournalTailStore> =
343            RefCell::new(JournalTailStore::init(test_memory(213)));
344        static CARDINALITY_DRIVER_STORES: StoreRegistry = {
345            let mut registry = StoreRegistry::new();
346            registry.register_journaled_store(
347                "startup_tests::CardinalityDriverStore",
348                &CARDINALITY_DRIVER_DATA,
349                &CARDINALITY_DRIVER_INDEX,
350                &CARDINALITY_DRIVER_SCHEMA,
351                &CARDINALITY_DRIVER_TAIL,
352                StoreAllocationIdentities::new_journaled(
353                    StoreAllocationIdentity::new(210, "icydb.test.cardinality.driver.data.v1"),
354                    StoreAllocationIdentity::new(211, "icydb.test.cardinality.driver.index.v1"),
355                    StoreAllocationIdentity::new(212, "icydb.test.cardinality.driver.schema.v1"),
356                    StoreAllocationIdentity::new(213, "icydb.test.cardinality.driver.journal.v1"),
357                ),
358                StoreRuntimeStorageCapabilities::journaled(),
359            ).expect("cardinality driver store should register");
360            registry
361        };
362    }
363
364    struct HeapRecoveryFailureCanister;
365
366    impl Path for HeapRecoveryFailureCanister {
367        const PATH: &'static str = "startup_tests::HeapRecoveryFailureCanister";
368    }
369
370    impl CanisterKind for HeapRecoveryFailureCanister {
371        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
372            Ok(251)
373        }
374        const COMMIT_STABLE_KEY: &'static str =
375            "icydb.test.startup.heap.recovery.failure.commit.v1";
376        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
377            Ok(245)
378        }
379        const STARTUP_STABLE_KEY: &'static str =
380            "icydb.test.startup.heap.recovery.failure.control.v1";
381        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
382            Ok(246)
383        }
384        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
385            "icydb.test.startup.heap.recovery.failure.integrity.v1";
386    }
387
388    thread_local! {
389        static HEAP_RECOVERY_FAILURE_STORES: StoreRegistry = StoreRegistry::new();
390    }
391
392    struct HeapCheckpointFailureCanister;
393
394    impl Path for HeapCheckpointFailureCanister {
395        const PATH: &'static str = "startup_tests::HeapCheckpointFailureCanister";
396    }
397
398    impl CanisterKind for HeapCheckpointFailureCanister {
399        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
400            Ok(254)
401        }
402        const COMMIT_STABLE_KEY: &'static str =
403            "icydb.test.startup.heap.checkpoint.failure.commit.v1";
404        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
405            Ok(221)
406        }
407        const STARTUP_STABLE_KEY: &'static str =
408            "icydb.test.startup.heap.checkpoint.failure.control.v1";
409        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
410            Ok(222)
411        }
412        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
413            "icydb.test.startup.heap.checkpoint.failure.integrity.v1";
414    }
415
416    thread_local! {
417        static HEAP_CHECKPOINT_FAILURE_DATA: RefCell<DataStore> =
418            const { RefCell::new(DataStore::init_heap()) };
419        static HEAP_CHECKPOINT_FAILURE_INDEX: RefCell<IndexStore> =
420            const { RefCell::new(IndexStore::init_heap()) };
421        static HEAP_CHECKPOINT_FAILURE_SCHEMA: RefCell<SchemaStore> =
422            const { RefCell::new(SchemaStore::init_heap()) };
423        static HEAP_CHECKPOINT_FAILURE_STORES: StoreRegistry = {
424            let mut registry = StoreRegistry::new();
425            registry.register_store(
426                "startup_tests::HeapCheckpointFailureStore",
427                &HEAP_CHECKPOINT_FAILURE_DATA,
428                &HEAP_CHECKPOINT_FAILURE_INDEX,
429                &HEAP_CHECKPOINT_FAILURE_SCHEMA,
430                StoreAllocationIdentities::absent(),
431                StoreRuntimeStorageCapabilities::heap(),
432            ).expect("heap checkpoint failure store should register");
433            registry
434        };
435    }
436
437    const JOURNAL_RECOVERY_FAILURE_STORE_PATH: &str = "startup_tests::JournalRecoveryFailureStore";
438
439    struct JournalRecoveryFailureCanister;
440
441    impl Path for JournalRecoveryFailureCanister {
442        const PATH: &'static str = "startup_tests::JournalRecoveryFailureCanister";
443    }
444
445    impl CanisterKind for JournalRecoveryFailureCanister {
446        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
447            Ok(185)
448        }
449        const COMMIT_STABLE_KEY: &'static str =
450            "icydb.test.startup.journal.recovery.failure.commit.v1";
451        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
452            Ok(186)
453        }
454        const STARTUP_STABLE_KEY: &'static str =
455            "icydb.test.startup.journal.recovery.failure.control.v1";
456        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
457            Ok(187)
458        }
459        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
460            "icydb.test.startup.journal.recovery.failure.integrity.v1";
461    }
462
463    thread_local! {
464        static JOURNAL_RECOVERY_FAILURE_DATA: RefCell<DataStore> =
465            RefCell::new(DataStore::init_journaled(test_memory(181)));
466        static JOURNAL_RECOVERY_FAILURE_INDEX: RefCell<IndexStore> =
467            RefCell::new(IndexStore::init_journaled(test_memory(182)));
468        static JOURNAL_RECOVERY_FAILURE_SCHEMA: RefCell<SchemaStore> =
469            RefCell::new(SchemaStore::init_journaled(test_memory(183)));
470        static JOURNAL_RECOVERY_FAILURE_TAIL: RefCell<JournalTailStore> =
471            RefCell::new(JournalTailStore::init(test_memory(184)));
472        static JOURNAL_RECOVERY_FAILURE_STORES: StoreRegistry = {
473            let mut registry = StoreRegistry::new();
474            registry.register_journaled_store(
475                JOURNAL_RECOVERY_FAILURE_STORE_PATH,
476                &JOURNAL_RECOVERY_FAILURE_DATA,
477                &JOURNAL_RECOVERY_FAILURE_INDEX,
478                &JOURNAL_RECOVERY_FAILURE_SCHEMA,
479                &JOURNAL_RECOVERY_FAILURE_TAIL,
480                StoreAllocationIdentities::new_journaled(
481                    StoreAllocationIdentity::new(
482                        181,
483                        "icydb.test.startup.journal.recovery.failure.data.v1",
484                    ),
485                    StoreAllocationIdentity::new(
486                        182,
487                        "icydb.test.startup.journal.recovery.failure.index.v1",
488                    ),
489                    StoreAllocationIdentity::new(
490                        183,
491                        "icydb.test.startup.journal.recovery.failure.schema.v1",
492                    ),
493                    StoreAllocationIdentity::new(
494                        184,
495                        "icydb.test.startup.journal.recovery.failure.journal.v1",
496                    ),
497                ),
498                StoreRuntimeStorageCapabilities::journaled(),
499            ).expect("journal recovery failure store should register");
500            registry
501        };
502    }
503
504    #[test]
505    fn fresh_observation_is_recovering_and_performs_no_stable_write() {
506        assert_eq!(
507            observe_generated_startup_state::<FreshCanister>(
508                &FRESH_STORES,
509                "generated/0123456789abcdef",
510            ),
511            Ok(DatabaseStartupState::Recovering)
512        );
513        let commit = commit_memory_handle(
514            current_commit_memory_allocation().expect("commit allocation should configure"),
515        )
516        .expect("commit memory should reopen");
517        let startup =
518            receipt::startup_memory::<FreshCanister>().expect("startup memory should reopen");
519        assert_eq!(commit.size(), 0);
520        assert_eq!(startup.size(), 0);
521    }
522
523    #[test]
524    fn terminal_classification_is_typed_and_pending_or_internal_failures_remain_retryable() {
525        let corruption = InternalError::store_corruption();
526        assert!(
527            classify_terminal_failure(StartupFailureKind::JournalRecovery, &corruption).is_some()
528        );
529        let pending = InternalError::recovery_pending();
530        assert!(classify_terminal_failure(StartupFailureKind::JournalRecovery, &pending).is_none());
531        let transient = InternalError::recovery_database_format_control_unavailable();
532        assert!(
533            classify_terminal_failure(StartupFailureKind::DatabaseControl, &transient).is_none()
534        );
535        let migration_pending = InternalError::schema_migration(
536            icydb_diagnostic_code::SchemaMigrationCode::MigrationInProgress,
537        );
538        assert!(
539            classify_terminal_failure(StartupFailureKind::SchemaReconciliation, &migration_pending)
540                .is_none()
541        );
542        assert!(
543            classify_terminal_failure(
544                StartupFailureKind::SchemaReconciliation,
545                &InternalError::store_unsupported(),
546            )
547            .is_some(),
548            "genuinely unsupported schema application remains terminal",
549        );
550    }
551
552    #[test]
553    fn non_current_fixed_boot_magic_surfaces_directly_without_a_failure_receipt() {
554        select_commit_memory_allocation(
555            CorruptCanister::commit_memory_id().expect("test allocation"),
556            CorruptCanister::COMMIT_STABLE_KEY,
557        );
558        let memory = commit_memory_handle(
559            current_commit_memory_allocation().expect("commit allocation should resolve"),
560        )
561        .expect("commit memory should open");
562        assert_eq!(memory.grow(1), 0);
563        memory.write(0, b"NOTICYDBCONTROL");
564
565        let failure = observe_generated_startup_state::<CorruptCanister>(
566            &CORRUPT_STORES,
567            "generated/0123456789abcdef",
568        )
569        .expect_err("non-current boot magic must fail directly");
570        assert_eq!(failure.kind(), StartupFailureKind::DatabaseControl);
571        assert_eq!(
572            failure.diagnostic().class(),
573            icydb_diagnostic_code::ErrorClass::IncompatiblePersistedFormat,
574        );
575        assert_eq!(
576            receipt::startup_memory::<CorruptCanister>()
577                .expect("startup memory should open")
578                .size(),
579            0,
580        );
581    }
582
583    #[test]
584    fn heap_only_malformed_marker_becomes_a_durable_database_control_failure() {
585        const SUBMISSION: &str = "generated/89abcdef01234567";
586
587        select_commit_memory_allocation(
588            HeapRecoveryFailureCanister::commit_memory_id().expect("test allocation"),
589            HeapRecoveryFailureCanister::COMMIT_STABLE_KEY,
590        );
591        let memory = commit_memory_handle(
592            current_commit_memory_allocation().expect("commit allocation should resolve"),
593        )
594        .expect("commit memory should open");
595        initialize_current_database_control_for_tests(&memory);
596        persist_raw_commit_marker_for_tests(vec![0xff])
597            .expect("malformed marker payload should persist inside valid control authority");
598
599        assert_eq!(
600            observe_generated_startup_state::<HeapRecoveryFailureCanister>(
601                &HEAP_RECOVERY_FAILURE_STORES,
602                SUBMISSION,
603            ),
604            Ok(DatabaseStartupState::Recovering),
605            "bounded observation must not decode marker payloads",
606        );
607
608        let request_root = RequestExecutionRoot::__new_runtime_root();
609        let session = crate::db::DbSession::<HeapRecoveryFailureCanister>::new(
610            &HEAP_RECOVERY_FAILURE_STORES,
611            &request_root,
612        );
613        assert_eq!(
614            drive_generated_startup_recovery_page(
615                &session,
616                &HEAP_RECOVERY_FAILURE_STORES,
617                SUBMISSION,
618            )
619            .expect("terminal corruption should publish one durable receipt"),
620            GeneratedStartupDriverStep::Terminal,
621        );
622
623        let failure = observe_generated_startup_state::<HeapRecoveryFailureCanister>(
624            &HEAP_RECOVERY_FAILURE_STORES,
625            SUBMISSION,
626        )
627        .expect_err("the durable database-control failure should replace blind recovery retries");
628        assert_eq!(failure.kind(), StartupFailureKind::DatabaseControl);
629        assert_eq!(
630            failure.diagnostic().error_code(),
631            ErrorCode::RUNTIME_CORRUPTION
632        );
633        assert_eq!(
634            drive_generated_startup_recovery_page(
635                &session,
636                &HEAP_RECOVERY_FAILURE_STORES,
637                SUBMISSION,
638            )
639            .expect("exact terminal replay should not attempt recovery again"),
640            GeneratedStartupDriverStep::Terminal,
641        );
642    }
643
644    #[test]
645    fn heap_only_schema_control_corruption_becomes_a_durable_database_control_failure() {
646        const SUBMISSION: &str = "generated/76543210fedcba98";
647
648        select_commit_memory_allocation(
649            HeapCheckpointFailureCanister::commit_memory_id().expect("test allocation"),
650            HeapCheckpointFailureCanister::COMMIT_STABLE_KEY,
651        );
652        let memory = commit_memory_handle(
653            current_commit_memory_allocation().expect("commit allocation should resolve"),
654        )
655        .expect("commit memory should open");
656        initialize_current_database_control_for_tests(&memory);
657        corrupt_schema_control_header_for_tests()
658            .expect("schema-control authority should admit focused corruption");
659
660        let request_root = RequestExecutionRoot::__new_runtime_root();
661        let session = crate::db::DbSession::<HeapCheckpointFailureCanister>::new(
662            &HEAP_CHECKPOINT_FAILURE_STORES,
663            &request_root,
664        );
665        assert_eq!(
666            drive_generated_startup_recovery_page(
667                &session,
668                &HEAP_CHECKPOINT_FAILURE_STORES,
669                SUBMISSION,
670            )
671            .expect("checkpoint corruption should publish one durable receipt"),
672            GeneratedStartupDriverStep::Terminal,
673        );
674
675        let failure = observe_generated_startup_state::<HeapCheckpointFailureCanister>(
676            &HEAP_CHECKPOINT_FAILURE_STORES,
677            SUBMISSION,
678        )
679        .expect_err("the checkpoint failure should remain visible after the timer returns");
680        assert_eq!(failure.kind(), StartupFailureKind::DatabaseControl);
681        assert_eq!(
682            failure.diagnostic().error_code(),
683            ErrorCode::RUNTIME_CORRUPTION
684        );
685    }
686
687    #[test]
688    fn persisted_journal_record_corruption_becomes_a_durable_journal_failure() {
689        const SUBMISSION: &str = "generated/2280bad0bad0bad0";
690
691        select_commit_memory_allocation(
692            JournalRecoveryFailureCanister::commit_memory_id().expect("test allocation"),
693            JournalRecoveryFailureCanister::COMMIT_STABLE_KEY,
694        );
695        let memory = commit_memory_handle(
696            current_commit_memory_allocation().expect("commit allocation should resolve"),
697        )
698        .expect("commit memory should open");
699        initialize_current_database_control_for_tests(&memory);
700        let format_root = RequestExecutionRoot::__new_runtime_root();
701        let format_database = crate::db::Db::<JournalRecoveryFailureCanister>::new(
702            &JOURNAL_RECOVERY_FAILURE_STORES,
703            format_root.scope(),
704        );
705        ensure_database_format_admitted(&format_database)
706            .expect("current journal registry should initialize before corruption injection");
707
708        let record = JournalRecord::schema_put(JOURNAL_RECOVERY_FAILURE_STORE_PATH, vec![0xff; 8])
709            .expect("syntactically bounded schema record should build");
710        let batch = JournalBatch::new(
711            [0x22; 16],
712            [0x28; 16],
713            JournalSequence::new(1),
714            vec![record],
715        )
716        .expect("syntactically current journal batch should build");
717        JOURNAL_RECOVERY_FAILURE_TAIL.with(|tail| {
718            tail.borrow_mut()
719                .append_batch(&batch)
720                .expect("persisted semantic-corruption fixture should insert");
721        });
722
723        assert_eq!(
724            observe_generated_startup_state::<JournalRecoveryFailureCanister>(
725                &JOURNAL_RECOVERY_FAILURE_STORES,
726                SUBMISSION,
727            ),
728            Ok(DatabaseStartupState::Recovering),
729        );
730        let request_root = RequestExecutionRoot::__new_runtime_root();
731        let session = crate::db::DbSession::<JournalRecoveryFailureCanister>::new(
732            &JOURNAL_RECOVERY_FAILURE_STORES,
733            &request_root,
734        );
735        let drive = || {
736            drive_generated_startup_recovery_page(
737                &session,
738                &JOURNAL_RECOVERY_FAILURE_STORES,
739                SUBMISSION,
740            )
741            .expect("the driver should progress or persist a durable failure")
742        };
743        assert_eq!(drive(), GeneratedStartupDriverStep::Recovering);
744        assert_eq!(drive(), GeneratedStartupDriverStep::Terminal);
745
746        let failure = observe_generated_startup_state::<JournalRecoveryFailureCanister>(
747            &JOURNAL_RECOVERY_FAILURE_STORES,
748            SUBMISSION,
749        )
750        .expect_err("the durable journal failure should replace blind recovery retries");
751        assert_eq!(failure.kind(), StartupFailureKind::JournalRecovery);
752        assert_eq!(
753            failure.diagnostic().error_code(),
754            ErrorCode::STORE_CORRUPTION,
755        );
756        assert_eq!(drive(), GeneratedStartupDriverStep::Terminal);
757
758        let changed_record =
759            JournalRecord::schema_put(JOURNAL_RECOVERY_FAILURE_STORE_PATH, vec![0xfe; 8])
760                .expect("changed semantic-corruption record should build");
761        let changed_batch = JournalBatch::new(
762            [0x23; 16],
763            [0x29; 16],
764            JournalSequence::new(2),
765            vec![changed_record],
766        )
767        .expect("changed journal batch should build");
768        let changed_encoded =
769            encode_journal_batch(&changed_batch).expect("changed journal batch should encode");
770        JOURNAL_RECOVERY_FAILURE_TAIL.with(|tail| {
771            tail.borrow_mut()
772                .insert_raw_batch_for_tests(JournalSequence::new(2), changed_encoded)
773                .expect("changed journal authority should insert");
774        });
775        assert_eq!(
776            observe_generated_startup_state::<JournalRecoveryFailureCanister>(
777                &JOURNAL_RECOVERY_FAILURE_STORES,
778                SUBMISSION,
779            ),
780            Ok(DatabaseStartupState::Recovering),
781            "a receipt bound to the predecessor tail proof must become stale",
782        );
783    }
784
785    #[test]
786    #[expect(
787        clippy::too_many_lines,
788        reason = "one lifecycle test keeps pending, ready, marker, and receipt precedence in one scenario"
789    )]
790    fn completed_recovery_stays_recovering_until_exact_generated_schema_receipt_then_is_ready() {
791        const SUBMISSION: &str = "generated/0123456789abcdef";
792
793        select_commit_memory_allocation(
794            CurrentCanister::commit_memory_id().expect("test allocation"),
795            CurrentCanister::COMMIT_STABLE_KEY,
796        );
797        let memory = commit_memory_handle(
798            current_commit_memory_allocation().expect("commit allocation should resolve"),
799        )
800        .expect("commit memory should open");
801        initialize_current_database_control_for_tests(&memory);
802        let incarnation = database_incarnation_id().expect("control should initialize");
803        mark_startup_recovery_complete_for_tests(&CURRENT_STORES)
804            .expect("recovery witness should publish");
805
806        assert_eq!(
807            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
808            Ok(DatabaseStartupState::Recovering),
809        );
810
811        let (database_identity, accepted_head) =
812            generated_schema_authority(&CURRENT_STORES, incarnation)
813                .expect("schema authority should resolve");
814        let submission_key =
815            SchemaSubmissionKey::try_new(SUBMISSION).expect("submission should admit");
816        let receipt = SchemaChangeReceipt::new(
817            database_identity,
818            submission_key.clone(),
819            SchemaProposalDigest::from_bytes([1; 32]),
820            accepted_head.clone(),
821            SchemaChangeOutcome::NoOp {
822                accepted_head: accepted_head.clone(),
823            },
824        )
825        .expect("terminal schema receipt should admit");
826        let record = SchemaApplicationRecord::new(receipt, Vec::new())
827            .expect("terminal schema record should admit");
828        apply_schema_application_record_op(
829            &SchemaApplicationRecordOp::insert(&record)
830                .expect("schema record operation should admit"),
831        )
832        .expect("schema record should publish");
833        let before = load_schema_application_record_read_only(database_identity, &submission_key)
834            .expect("record should load");
835
836        assert_eq!(
837            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
838            Ok(DatabaseStartupState::Ready),
839        );
840        assert_eq!(
841            load_schema_application_record_read_only(database_identity, &submission_key)
842                .expect("record should reload"),
843            before,
844            "pure readiness observation must not rewrite schema application state",
845        );
846        assert_eq!(
847            receipt::startup_memory::<CurrentCanister>()
848                .expect("startup memory should open")
849                .size(),
850            0,
851            "readiness without a failure must not allocate the receipt cell",
852        );
853
854        let marker = CommitMarker::from_parts([0x5a; 16], Vec::new())
855            .expect("empty marker should admit for control observation");
856        let interrupted = begin_commit(&marker).expect("marker should persist");
857        assert_eq!(
858            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
859            Ok(DatabaseStartupState::Recovering),
860            "a marker must take precedence over a completed volatile witness",
861        );
862        finish_commit(interrupted, |_| Ok(())).expect("empty marker should clear");
863        assert_eq!(
864            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
865            Ok(DatabaseStartupState::Ready),
866        );
867
868        let accepted_head_binding = match accepted_head {
869            icydb_schema::ExpectedAcceptedHead::Empty => receipt::AcceptedHeadBinding::Empty,
870            icydb_schema::ExpectedAcceptedHead::Exact {
871                revision,
872                fingerprint,
873            } => receipt::AcceptedHeadBinding::Exact {
874                revision,
875                fingerprint: fingerprint.to_bytes(),
876            },
877        };
878        let terminal_failure = StartupFailure::new(
879            StartupFailureKind::SchemaReconciliation,
880            ErrorCode::RUNTIME_CONFLICT.diagnostic(DiagnosticOrigin::Recovery),
881            Vec::new(),
882        );
883        let memoized = receipt::StartupFailureReceipt::new(
884            terminal_failure.clone(),
885            receipt::StartupFailureBinding::SchemaReconciliation {
886                incarnation,
887                submission_key: SUBMISSION.to_string(),
888                accepted_head: accepted_head_binding,
889            },
890        )
891        .expect("memoized schema failure should admit");
892        assert!(
893            receipt::publish::<CurrentCanister>(&memoized)
894                .expect("memoized failure should publish")
895        );
896        assert_eq!(
897            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
898            Err(terminal_failure),
899            "one exact matching failure receipt has priority over Ready evidence",
900        );
901        assert_eq!(
902            observe_generated_startup_state::<CurrentCanister>(
903                &CURRENT_STORES,
904                "generated/fedcba9876543210",
905            ),
906            Ok(DatabaseStartupState::Recovering),
907            "a receipt bound to another generated submission must be stale",
908        );
909        assert!(receipt::clear::<CurrentCanister>().expect("test receipt should clear"));
910    }
911
912    #[test]
913    fn driver_completes_one_recovery_page_then_memoizes_only_terminal_schema_failure() {
914        const SUBMISSION: &str = "generated/0011223344556677";
915
916        select_commit_memory_allocation(
917            DriverCanister::commit_memory_id().expect("test allocation"),
918            DriverCanister::COMMIT_STABLE_KEY,
919        );
920        let memory = commit_memory_handle(
921            current_commit_memory_allocation().expect("commit allocation should resolve"),
922        )
923        .expect("commit memory should open");
924        initialize_current_database_control_for_tests(&memory);
925        let request_root = RequestExecutionRoot::__new_runtime_root();
926        let session = crate::db::DbSession::<DriverCanister>::new(&DRIVER_STORES, &request_root);
927
928        assert_eq!(
929            drive_generated_startup_recovery_page(&session, &DRIVER_STORES, SUBMISSION)
930                .expect("empty recovery page should complete"),
931            GeneratedStartupDriverStep::ApplyGeneratedSchema,
932        );
933        assert_eq!(
934            observe_generated_startup_state::<DriverCanister>(&DRIVER_STORES, SUBMISSION),
935            Ok(DatabaseStartupState::Recovering),
936            "recovery completion alone must not claim generated reconciliation",
937        );
938
939        let retryable = InternalError::recovery_pending();
940        assert!(
941            !record_generated_schema_startup_failure::<DriverCanister>(
942                &DRIVER_STORES,
943                SUBMISSION,
944                retryable.diagnostic(),
945                retryable.diagnostic_facts(),
946            )
947            .expect("retryable classification should complete without publication")
948        );
949        assert_eq!(
950            receipt::startup_memory::<DriverCanister>()
951                .expect("startup memory should open")
952                .size(),
953            0,
954            "retryable failure must not allocate the receipt cell",
955        );
956
957        let terminal = InternalError::store_corruption();
958        let marker = CommitMarker::from_parts([0x7b; 16], Vec::new())
959            .expect("empty marker should admit for receipt priority");
960        let interrupted = begin_commit(&marker).expect("marker should persist");
961        assert!(
962            record_generated_schema_startup_failure::<DriverCanister>(
963                &DRIVER_STORES,
964                SUBMISSION,
965                terminal.diagnostic(),
966                terminal.diagnostic_facts(),
967            )
968            .expect("terminal failure should publish")
969        );
970        let observed =
971            observe_generated_startup_state::<DriverCanister>(&DRIVER_STORES, SUBMISSION)
972                .expect_err("matching terminal receipt should surface");
973        assert_eq!(observed.kind(), StartupFailureKind::SchemaReconciliation);
974        assert_eq!(
975            observed.diagnostic().error_code(),
976            ErrorCode::STORE_CORRUPTION
977        );
978        finish_commit(interrupted, |_| Ok(())).expect("test marker should clear");
979        assert!(
980            clear_generated_startup_failure::<DriverCanister>()
981                .expect("authoritative correction should clear the receipt")
982        );
983    }
984
985    #[test]
986    fn ready_startup_driver_publishes_empty_cardinality_then_quiesces() {
987        const SUBMISSION: &str = "generated/cardinality-driver";
988
989        select_commit_memory_allocation(
990            CardinalityDriverCanister::commit_memory_id().expect("test allocation"),
991            CardinalityDriverCanister::COMMIT_STABLE_KEY,
992        );
993        let memory = commit_memory_handle(
994            current_commit_memory_allocation().expect("commit allocation should resolve"),
995        )
996        .expect("commit memory should open");
997        initialize_current_database_control_for_tests(&memory);
998        let request_root = RequestExecutionRoot::__new_runtime_root();
999        let database = crate::db::Db::<CardinalityDriverCanister>::new(
1000            &CARDINALITY_DRIVER_STORES,
1001            request_root.scope(),
1002        );
1003        ensure_database_format_admitted(&database)
1004            .expect("current store registry should initialize");
1005        mark_startup_recovery_complete_for_tests(&CARDINALITY_DRIVER_STORES)
1006            .expect("recovery witness should publish");
1007        let incarnation = database_incarnation_id().expect("incarnation should resolve");
1008        let (database_identity, accepted_head) =
1009            generated_schema_authority(&CARDINALITY_DRIVER_STORES, incarnation)
1010                .expect("empty generated authority should resolve");
1011        let submission_key =
1012            SchemaSubmissionKey::try_new(SUBMISSION).expect("submission should admit");
1013        let receipt = SchemaChangeReceipt::new(
1014            database_identity,
1015            submission_key,
1016            SchemaProposalDigest::from_bytes([2; 32]),
1017            accepted_head.clone(),
1018            SchemaChangeOutcome::NoOp { accepted_head },
1019        )
1020        .expect("terminal schema receipt should admit");
1021        let record = SchemaApplicationRecord::new(receipt, Vec::new())
1022            .expect("terminal schema record should admit");
1023        apply_schema_application_record_op(
1024            &SchemaApplicationRecordOp::insert(&record)
1025                .expect("schema record operation should admit"),
1026        )
1027        .expect("schema receipt should publish");
1028        assert_eq!(
1029            observe_generated_startup_state::<CardinalityDriverCanister>(
1030                &CARDINALITY_DRIVER_STORES,
1031                SUBMISSION,
1032            ),
1033            Ok(DatabaseStartupState::Ready),
1034        );
1035
1036        let session = crate::db::DbSession::<CardinalityDriverCanister>::new(
1037            &CARDINALITY_DRIVER_STORES,
1038            &request_root,
1039        );
1040        assert_eq!(
1041            drive_generated_startup_recovery_page(
1042                &session,
1043                &CARDINALITY_DRIVER_STORES,
1044                SUBMISSION,
1045            )
1046            .expect("empty cardinality publication should use the existing driver"),
1047            GeneratedStartupDriverStep::Recovering,
1048        );
1049        CARDINALITY_DRIVER_SCHEMA.with_borrow(|schema| {
1050            let header = schema
1051                .cardinality_generation_header()
1052                .expect("cardinality header should decode")
1053                .expect("cardinality header should publish");
1054            assert_eq!(
1055                header.state(),
1056                crate::db::schema::cardinality_generation::CardinalityGenerationState::Ready,
1057            );
1058        });
1059        assert_eq!(
1060            drive_generated_startup_recovery_page(
1061                &session,
1062                &CARDINALITY_DRIVER_STORES,
1063                SUBMISSION,
1064            )
1065            .expect("current cardinality evidence should quiesce"),
1066            GeneratedStartupDriverStep::Terminal,
1067        );
1068    }
1069}