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 icydb_diagnostic_code::{Diagnostic, DiagnosticFactTag, MAX_PUBLIC_DIAGNOSTIC_FACTS};
11use serde::Deserialize;
12
13use crate::{db::StoreRegistry, error::InternalError, traits::CanisterKind};
14
15/// Current generated-database startup readiness.
16#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
17pub enum DatabaseStartupState {
18    /// Recovery controls are complete and the generated schema is reconciled.
19    Ready,
20    /// Dedicated replicated startup work remains.
21    Recovering,
22}
23
24/// One bounded outcome from the hidden replicated startup coordinator.
25#[doc(hidden)]
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum GeneratedStartupDriverStep {
28    /// Startup is already ready or has one durably observable terminal failure.
29    Terminal,
30    /// Recovery or optional derived-evidence work remains after one bounded page.
31    Recovering,
32    /// Recovery is complete and generated schema reconciliation must run now.
33    ApplyGeneratedSchema,
34}
35
36/// Closed owner of one terminal startup failure.
37#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
38pub enum StartupFailureKind {
39    /// Database boot, incarnation, or commit-control failure.
40    DatabaseControl,
41    /// Journal-tail or fold-continuation recovery failure.
42    JournalRecovery,
43    /// Accepted-schema reconciliation or runtime preparation failure.
44    SchemaReconciliation,
45}
46
47/// Internal bounded startup failure projected by the public facade.
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct StartupFailure {
50    kind: StartupFailureKind,
51    diagnostic: Diagnostic,
52    facts: Vec<(DiagnosticFactTag, u64)>,
53}
54
55impl StartupFailure {
56    pub(in crate::db) fn from_internal(kind: StartupFailureKind, error: &InternalError) -> Self {
57        Self::new(kind, error.diagnostic(), error.diagnostic_facts())
58    }
59
60    pub(in crate::db) fn new(
61        kind: StartupFailureKind,
62        diagnostic: Diagnostic,
63        facts: Vec<(DiagnosticFactTag, u64)>,
64    ) -> Self {
65        debug_assert!(facts.len() <= MAX_PUBLIC_DIAGNOSTIC_FACTS);
66        Self {
67            kind,
68            diagnostic,
69            facts,
70        }
71    }
72
73    /// Return the subsystem that owns this terminal startup failure.
74    #[must_use]
75    pub const fn kind(&self) -> StartupFailureKind {
76        self.kind
77    }
78
79    /// Return its compact diagnostic identity.
80    #[must_use]
81    pub const fn diagnostic(&self) -> &Diagnostic {
82        &self.diagnostic
83    }
84
85    /// Borrow its bounded numeric diagnostic facts.
86    #[must_use]
87    pub const fn facts(&self) -> &[(DiagnosticFactTag, u64)] {
88        self.facts.as_slice()
89    }
90}
91
92pub(in crate::db) fn classify_terminal_failure(
93    kind: StartupFailureKind,
94    error: &InternalError,
95) -> Option<StartupFailure> {
96    terminal_code_for_kind(kind, error.diagnostic().error_code())
97        .then(|| StartupFailure::from_internal(kind, error))
98}
99
100pub(in crate::db) fn classify_terminal_failure_parts(
101    kind: StartupFailureKind,
102    diagnostic: Diagnostic,
103    facts: Vec<(DiagnosticFactTag, u64)>,
104) -> Option<StartupFailure> {
105    terminal_code_for_kind(kind, diagnostic.error_code())
106        .then(|| StartupFailure::new(kind, diagnostic, facts))
107}
108
109fn terminal_code_for_kind(
110    kind: StartupFailureKind,
111    code: icydb_diagnostic_code::ErrorCode,
112) -> bool {
113    use icydb_diagnostic_code::ErrorCode;
114
115    let persisted_failure = code == ErrorCode::STORE_CORRUPTION
116        || code == ErrorCode::STORE_INVARIANT_VIOLATION
117        || code == ErrorCode::RUNTIME_CORRUPTION
118        || code == ErrorCode::RUNTIME_INCOMPATIBLE_PERSISTED_FORMAT
119        || code == ErrorCode::RUNTIME_INVARIANT_VIOLATION
120        || code == ErrorCode::RUNTIME_BOUNDARY_PERSISTED_ROW_LAYOUT_OUTSIDE_ACCEPTED_WINDOW
121        || code == ErrorCode::RUNTIME_BOUNDARY_PERSISTED_ROW_SLOT_COUNT_MISMATCH
122        || code == ErrorCode::RUNTIME_BOUNDARY_ACCEPTED_ROW_CONSTRAINT_PROGRAM_CORRUPT;
123    persisted_failure
124        || match kind {
125            StartupFailureKind::DatabaseControl => false,
126            StartupFailureKind::JournalRecovery => {
127                code == ErrorCode::RUNTIME_BOUNDARY_JOURNAL_MUTATION_REVISION_EXHAUSTED
128            }
129            StartupFailureKind::SchemaReconciliation => {
130                code == ErrorCode::SCHEMA_DDL_ADMISSION
131                    || code == ErrorCode::RUNTIME_CONFLICT
132                    || code == ErrorCode::RUNTIME_UNSUPPORTED
133                    || code == ErrorCode::RUNTIME_BOUNDARY_GENERATED_FIELD_AFTER_DDL_FIELD
134                    || code == ErrorCode::RUNTIME_BOUNDARY_CONSTRAINT_VIOLATION
135                    || code == ErrorCode::RUNTIME_BOUNDARY_GENERATED_CONSTRAINT_ACTIVATION_STALE
136            }
137        }
138}
139
140/// Observe one generated database without opening a request or advancing recovery.
141pub fn observe_generated_startup_state<C: CanisterKind>(
142    stores: &'static std::thread::LocalKey<StoreRegistry>,
143    submission_key: &str,
144) -> Result<DatabaseStartupState, StartupFailure> {
145    observe::observe::<C>(stores, submission_key)
146}
147
148/// Run at most one bounded recovery page without admitting ordinary work.
149#[doc(hidden)]
150pub fn drive_generated_startup_recovery_page<C: CanisterKind>(
151    session: &crate::db::DbSession<C>,
152    stores: &'static std::thread::LocalKey<StoreRegistry>,
153    submission_key: &str,
154) -> Result<GeneratedStartupDriverStep, InternalError> {
155    driver::drive_recovery_page(session, stores, submission_key)
156}
157
158/// Persist one deterministic generated-schema failure against fresh authority.
159#[doc(hidden)]
160pub fn record_generated_schema_startup_failure<C: CanisterKind>(
161    stores: &'static std::thread::LocalKey<StoreRegistry>,
162    submission_key: &str,
163    diagnostic: Diagnostic,
164    facts: Vec<(DiagnosticFactTag, u64)>,
165) -> Result<bool, InternalError> {
166    driver::record_schema_failure::<C>(stores, submission_key, diagnostic, facts)
167}
168
169/// Clear a stale startup failure after an authoritative successful handoff.
170#[doc(hidden)]
171pub fn clear_generated_startup_failure<C: CanisterKind>() -> Result<bool, InternalError> {
172    receipt::clear::<C>()
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::{
179        db::{
180            DataStore, IndexStore, StoreAllocationIdentities, StoreRuntimeStorageCapabilities,
181            commit::{
182                CommitMarker, begin_commit, commit_memory_handle, current_commit_memory_allocation,
183                database_incarnation_id, finish_commit, mark_startup_recovery_complete_for_tests,
184                persist_raw_commit_marker_for_tests, select_commit_memory_allocation,
185            },
186            database_format::{
187                ensure_database_format_admitted, initialize_current_database_control_for_tests,
188            },
189            journal::{
190                JournalBatch, JournalRecord, JournalSequence, JournalTailStore,
191                encode_journal_batch,
192            },
193            registry::StoreAllocationIdentity,
194            resource::request::RequestExecutionRoot,
195            schema::{
196                ConstraintId, SchemaApplicationRecord, SchemaApplicationRecordOp,
197                SchemaChangeOutcome, SchemaChangeReceipt, SchemaStore,
198                apply_schema_application_record_op, corrupt_schema_control_header_for_tests,
199                generated_schema_authority, load_schema_application_record_read_only,
200            },
201        },
202        testing::test_memory,
203        traits::Path,
204        types::EntityTag,
205    };
206    use ic_memory::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.as_ref().grow(1), Ok(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 non_virgin_control_page_fails_observation_without_writes_or_retry() {
585        const SUBMISSION: &str = "generated/0123456789abcdef";
586        select_commit_memory_allocation(
587            CorruptCanister::commit_memory_id().unwrap(),
588            CorruptCanister::COMMIT_STABLE_KEY,
589        );
590        let memory = commit_memory_handle(current_commit_memory_allocation().unwrap()).unwrap();
591        assert_eq!(memory.as_ref().grow(1), Ok(0));
592        assert_eq!(
593            observe_generated_startup_state::<CorruptCanister>(&CORRUPT_STORES, SUBMISSION),
594            Ok(DatabaseStartupState::Recovering),
595        );
596        for (offset, bytes) in [
597            (0, &b"SCL"[..]),
598            (
599                crate::db::database_format::DATABASE_BOOT_RECORD_BYTES as u64,
600                &[1][..],
601            ),
602            (65_535, &[1][..]),
603        ] {
604            memory.write(offset, bytes);
605            let mut before = vec![0; 65_536];
606            memory.read(0, &mut before);
607            for _ in 0..2 {
608                let failure =
609                    observe_generated_startup_state::<CorruptCanister>(&CORRUPT_STORES, SUBMISSION)
610                        .expect_err(
611                            "non-virgin control memory must fail before recovery is scheduled",
612                        );
613                assert_eq!(failure.kind(), StartupFailureKind::DatabaseControl);
614                assert_eq!(
615                    failure.diagnostic().class(),
616                    icydb_diagnostic_code::ErrorClass::IncompatiblePersistedFormat
617                );
618            }
619            let mut after = vec![0; 65_536];
620            memory.read(0, &mut after);
621            assert_eq!(before, after);
622            assert_eq!(memory.size(), 1);
623            assert_eq!(
624                receipt::startup_memory::<CorruptCanister>().unwrap().size(),
625                0
626            );
627            memory.write(offset, &[0; 3][..bytes.len()]);
628        }
629    }
630
631    #[test]
632    fn heap_only_malformed_marker_becomes_a_durable_database_control_failure() {
633        const SUBMISSION: &str = "generated/89abcdef01234567";
634
635        select_commit_memory_allocation(
636            HeapRecoveryFailureCanister::commit_memory_id().expect("test allocation"),
637            HeapRecoveryFailureCanister::COMMIT_STABLE_KEY,
638        );
639        let memory = commit_memory_handle(
640            current_commit_memory_allocation().expect("commit allocation should resolve"),
641        )
642        .expect("commit memory should open");
643        initialize_current_database_control_for_tests(&memory);
644        persist_raw_commit_marker_for_tests(vec![0xff])
645            .expect("malformed marker payload should persist inside valid control authority");
646
647        assert_eq!(
648            observe_generated_startup_state::<HeapRecoveryFailureCanister>(
649                &HEAP_RECOVERY_FAILURE_STORES,
650                SUBMISSION,
651            ),
652            Ok(DatabaseStartupState::Recovering),
653            "bounded observation must not decode marker payloads",
654        );
655
656        let request_root = RequestExecutionRoot::__new_runtime_root();
657        let session = crate::db::DbSession::<HeapRecoveryFailureCanister>::new(
658            &HEAP_RECOVERY_FAILURE_STORES,
659            &request_root,
660        );
661        assert_eq!(
662            drive_generated_startup_recovery_page(
663                &session,
664                &HEAP_RECOVERY_FAILURE_STORES,
665                SUBMISSION,
666            )
667            .expect("terminal corruption should publish one durable receipt"),
668            GeneratedStartupDriverStep::Terminal,
669        );
670
671        let failure = observe_generated_startup_state::<HeapRecoveryFailureCanister>(
672            &HEAP_RECOVERY_FAILURE_STORES,
673            SUBMISSION,
674        )
675        .expect_err("the durable database-control failure should replace blind recovery retries");
676        assert_eq!(failure.kind(), StartupFailureKind::DatabaseControl);
677        assert_eq!(
678            failure.diagnostic().error_code(),
679            ErrorCode::RUNTIME_CORRUPTION
680        );
681        assert_eq!(
682            drive_generated_startup_recovery_page(
683                &session,
684                &HEAP_RECOVERY_FAILURE_STORES,
685                SUBMISSION,
686            )
687            .expect("exact terminal replay should not attempt recovery again"),
688            GeneratedStartupDriverStep::Terminal,
689        );
690    }
691
692    #[test]
693    fn heap_only_schema_control_corruption_becomes_a_durable_database_control_failure() {
694        const SUBMISSION: &str = "generated/76543210fedcba98";
695
696        select_commit_memory_allocation(
697            HeapCheckpointFailureCanister::commit_memory_id().expect("test allocation"),
698            HeapCheckpointFailureCanister::COMMIT_STABLE_KEY,
699        );
700        let memory = commit_memory_handle(
701            current_commit_memory_allocation().expect("commit allocation should resolve"),
702        )
703        .expect("commit memory should open");
704        initialize_current_database_control_for_tests(&memory);
705        corrupt_schema_control_header_for_tests()
706            .expect("schema-control authority should admit focused corruption");
707
708        let request_root = RequestExecutionRoot::__new_runtime_root();
709        let session = crate::db::DbSession::<HeapCheckpointFailureCanister>::new(
710            &HEAP_CHECKPOINT_FAILURE_STORES,
711            &request_root,
712        );
713        assert_eq!(
714            drive_generated_startup_recovery_page(
715                &session,
716                &HEAP_CHECKPOINT_FAILURE_STORES,
717                SUBMISSION,
718            )
719            .expect("checkpoint corruption should publish one durable receipt"),
720            GeneratedStartupDriverStep::Terminal,
721        );
722
723        let failure = observe_generated_startup_state::<HeapCheckpointFailureCanister>(
724            &HEAP_CHECKPOINT_FAILURE_STORES,
725            SUBMISSION,
726        )
727        .expect_err("the checkpoint failure should remain visible after the timer returns");
728        assert_eq!(failure.kind(), StartupFailureKind::DatabaseControl);
729        assert_eq!(
730            failure.diagnostic().error_code(),
731            ErrorCode::RUNTIME_CORRUPTION
732        );
733    }
734
735    #[test]
736    fn persisted_journal_record_corruption_becomes_a_durable_journal_failure() {
737        const SUBMISSION: &str = "generated/2280bad0bad0bad0";
738
739        select_commit_memory_allocation(
740            JournalRecoveryFailureCanister::commit_memory_id().expect("test allocation"),
741            JournalRecoveryFailureCanister::COMMIT_STABLE_KEY,
742        );
743        let memory = commit_memory_handle(
744            current_commit_memory_allocation().expect("commit allocation should resolve"),
745        )
746        .expect("commit memory should open");
747        initialize_current_database_control_for_tests(&memory);
748        let format_root = RequestExecutionRoot::__new_runtime_root();
749        let format_database = crate::db::Db::<JournalRecoveryFailureCanister>::new(
750            &JOURNAL_RECOVERY_FAILURE_STORES,
751            format_root.scope(),
752        );
753        ensure_database_format_admitted(&format_database)
754            .expect("current journal registry should initialize before corruption injection");
755
756        let record = JournalRecord::constraint_validation_job_delete(
757            JOURNAL_RECOVERY_FAILURE_STORE_PATH,
758            EntityTag::new(1),
759            ConstraintId::new(1).unwrap(),
760        )
761        .expect("syntactically bounded control record should build");
762        let batch = JournalBatch::new(
763            [0x22; 16],
764            [0x28; 16],
765            JournalSequence::new(1),
766            vec![record],
767        )
768        .expect("syntactically current journal batch should build");
769        JOURNAL_RECOVERY_FAILURE_TAIL.with(|tail| {
770            tail.borrow_mut()
771                .append_batch(&batch)
772                .expect("persisted semantic-corruption fixture should insert");
773        });
774
775        assert_eq!(
776            observe_generated_startup_state::<JournalRecoveryFailureCanister>(
777                &JOURNAL_RECOVERY_FAILURE_STORES,
778                SUBMISSION,
779            ),
780            Ok(DatabaseStartupState::Recovering),
781        );
782        let request_root = RequestExecutionRoot::__new_runtime_root();
783        let session = crate::db::DbSession::<JournalRecoveryFailureCanister>::new(
784            &JOURNAL_RECOVERY_FAILURE_STORES,
785            &request_root,
786        );
787        let drive = || {
788            drive_generated_startup_recovery_page(
789                &session,
790                &JOURNAL_RECOVERY_FAILURE_STORES,
791                SUBMISSION,
792            )
793            .expect("the driver should progress or persist a durable failure")
794        };
795        assert_eq!(drive(), GeneratedStartupDriverStep::Recovering);
796        assert_eq!(drive(), GeneratedStartupDriverStep::Terminal);
797
798        let failure = observe_generated_startup_state::<JournalRecoveryFailureCanister>(
799            &JOURNAL_RECOVERY_FAILURE_STORES,
800            SUBMISSION,
801        )
802        .expect_err("the durable journal failure should replace blind recovery retries");
803        assert_eq!(failure.kind(), StartupFailureKind::JournalRecovery);
804        assert_eq!(
805            failure.diagnostic().error_code(),
806            ErrorCode::STORE_CORRUPTION,
807        );
808        assert_eq!(drive(), GeneratedStartupDriverStep::Terminal);
809
810        let changed_record = JournalRecord::constraint_validation_job_delete(
811            JOURNAL_RECOVERY_FAILURE_STORE_PATH,
812            EntityTag::new(2),
813            ConstraintId::new(1).unwrap(),
814        )
815        .expect("changed semantic-corruption record should build");
816        let changed_batch = JournalBatch::new(
817            [0x23; 16],
818            [0x29; 16],
819            JournalSequence::new(2),
820            vec![changed_record],
821        )
822        .expect("changed journal batch should build");
823        let changed_encoded =
824            encode_journal_batch(&changed_batch).expect("changed journal batch should encode");
825        JOURNAL_RECOVERY_FAILURE_TAIL.with(|tail| {
826            tail.borrow_mut()
827                .insert_raw_batch_for_tests(JournalSequence::new(2), changed_encoded)
828                .expect("changed journal authority should insert");
829        });
830        assert_eq!(
831            observe_generated_startup_state::<JournalRecoveryFailureCanister>(
832                &JOURNAL_RECOVERY_FAILURE_STORES,
833                SUBMISSION,
834            ),
835            Ok(DatabaseStartupState::Recovering),
836            "a receipt bound to the predecessor tail proof must become stale",
837        );
838    }
839
840    #[test]
841    #[expect(
842        clippy::too_many_lines,
843        reason = "one lifecycle test keeps pending, ready, marker, and receipt precedence in one scenario"
844    )]
845    fn completed_recovery_stays_recovering_until_exact_generated_schema_receipt_then_is_ready() {
846        const SUBMISSION: &str = "generated/0123456789abcdef";
847
848        select_commit_memory_allocation(
849            CurrentCanister::commit_memory_id().expect("test allocation"),
850            CurrentCanister::COMMIT_STABLE_KEY,
851        );
852        let memory = commit_memory_handle(
853            current_commit_memory_allocation().expect("commit allocation should resolve"),
854        )
855        .expect("commit memory should open");
856        initialize_current_database_control_for_tests(&memory);
857        let incarnation = database_incarnation_id().expect("control should initialize");
858        mark_startup_recovery_complete_for_tests(&CURRENT_STORES)
859            .expect("recovery witness should publish");
860
861        assert_eq!(
862            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
863            Ok(DatabaseStartupState::Recovering),
864        );
865
866        let (database_identity, accepted_head) =
867            generated_schema_authority(&CURRENT_STORES, incarnation)
868                .expect("schema authority should resolve");
869        let submission_key =
870            SchemaSubmissionKey::try_new(SUBMISSION).expect("submission should admit");
871        let receipt = SchemaChangeReceipt::new(
872            database_identity,
873            submission_key.clone(),
874            SchemaProposalDigest::from_bytes([1; 32]),
875            accepted_head.clone(),
876            SchemaChangeOutcome::NoOp {
877                accepted_head: accepted_head.clone(),
878            },
879        )
880        .expect("terminal schema receipt should admit");
881        let record = SchemaApplicationRecord::new(receipt, Vec::new())
882            .expect("terminal schema record should admit");
883        apply_schema_application_record_op(
884            &SchemaApplicationRecordOp::insert(&record)
885                .expect("schema record operation should admit"),
886        )
887        .expect("schema record should publish");
888        let before = load_schema_application_record_read_only(database_identity, &submission_key)
889            .expect("record should load");
890
891        assert_eq!(
892            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
893            Ok(DatabaseStartupState::Ready),
894        );
895        assert_eq!(
896            load_schema_application_record_read_only(database_identity, &submission_key)
897                .expect("record should reload"),
898            before,
899            "pure readiness observation must not rewrite schema application state",
900        );
901        assert_eq!(
902            receipt::startup_memory::<CurrentCanister>()
903                .expect("startup memory should open")
904                .size(),
905            0,
906            "readiness without a failure must not allocate the receipt cell",
907        );
908
909        let marker = CommitMarker::from_parts([0x5a; 16], Vec::new())
910            .expect("empty marker should admit for control observation");
911        let interrupted = begin_commit(&marker).expect("marker should persist");
912        assert_eq!(
913            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
914            Ok(DatabaseStartupState::Recovering),
915            "a marker must take precedence over a completed volatile witness",
916        );
917        finish_commit(interrupted, |_| Ok(())).expect("empty marker should clear");
918        assert_eq!(
919            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
920            Ok(DatabaseStartupState::Ready),
921        );
922
923        let accepted_head_binding = match accepted_head {
924            icydb_schema::ExpectedAcceptedHead::Empty => receipt::AcceptedHeadBinding::Empty,
925            icydb_schema::ExpectedAcceptedHead::Exact {
926                revision,
927                fingerprint,
928            } => receipt::AcceptedHeadBinding::Exact {
929                revision,
930                fingerprint: fingerprint.to_bytes(),
931            },
932        };
933        let terminal_failure = StartupFailure::new(
934            StartupFailureKind::SchemaReconciliation,
935            ErrorCode::RUNTIME_CONFLICT.diagnostic(DiagnosticOrigin::Recovery),
936            Vec::new(),
937        );
938        let memoized = receipt::StartupFailureReceipt::new(
939            terminal_failure.clone(),
940            receipt::StartupFailureBinding::SchemaReconciliation {
941                incarnation,
942                submission_key: SUBMISSION.to_string(),
943                accepted_head: accepted_head_binding,
944            },
945        )
946        .expect("memoized schema failure should admit");
947        assert!(
948            receipt::publish::<CurrentCanister>(&memoized)
949                .expect("memoized failure should publish")
950        );
951        assert_eq!(
952            observe_generated_startup_state::<CurrentCanister>(&CURRENT_STORES, SUBMISSION),
953            Err(terminal_failure),
954            "one exact matching failure receipt has priority over Ready evidence",
955        );
956        assert_eq!(
957            observe_generated_startup_state::<CurrentCanister>(
958                &CURRENT_STORES,
959                "generated/fedcba9876543210",
960            ),
961            Ok(DatabaseStartupState::Recovering),
962            "a receipt bound to another generated submission must be stale",
963        );
964        assert!(receipt::clear::<CurrentCanister>().expect("test receipt should clear"));
965    }
966
967    #[test]
968    fn driver_completes_one_recovery_page_then_memoizes_only_terminal_schema_failure() {
969        const SUBMISSION: &str = "generated/0011223344556677";
970
971        select_commit_memory_allocation(
972            DriverCanister::commit_memory_id().expect("test allocation"),
973            DriverCanister::COMMIT_STABLE_KEY,
974        );
975        let memory = commit_memory_handle(
976            current_commit_memory_allocation().expect("commit allocation should resolve"),
977        )
978        .expect("commit memory should open");
979        initialize_current_database_control_for_tests(&memory);
980        let request_root = RequestExecutionRoot::__new_runtime_root();
981        let session = crate::db::DbSession::<DriverCanister>::new(&DRIVER_STORES, &request_root);
982
983        assert_eq!(
984            drive_generated_startup_recovery_page(&session, &DRIVER_STORES, SUBMISSION)
985                .expect("empty recovery page should complete"),
986            GeneratedStartupDriverStep::ApplyGeneratedSchema,
987        );
988        assert_eq!(
989            observe_generated_startup_state::<DriverCanister>(&DRIVER_STORES, SUBMISSION),
990            Ok(DatabaseStartupState::Recovering),
991            "recovery completion alone must not claim generated reconciliation",
992        );
993
994        let retryable = InternalError::recovery_pending();
995        assert!(
996            !record_generated_schema_startup_failure::<DriverCanister>(
997                &DRIVER_STORES,
998                SUBMISSION,
999                retryable.diagnostic(),
1000                retryable.diagnostic_facts(),
1001            )
1002            .expect("retryable classification should complete without publication")
1003        );
1004        assert_eq!(
1005            receipt::startup_memory::<DriverCanister>()
1006                .expect("startup memory should open")
1007                .size(),
1008            0,
1009            "retryable failure must not allocate the receipt cell",
1010        );
1011
1012        let terminal = InternalError::store_corruption();
1013        let marker = CommitMarker::from_parts([0x7b; 16], Vec::new())
1014            .expect("empty marker should admit for receipt priority");
1015        let interrupted = begin_commit(&marker).expect("marker should persist");
1016        assert!(
1017            record_generated_schema_startup_failure::<DriverCanister>(
1018                &DRIVER_STORES,
1019                SUBMISSION,
1020                terminal.diagnostic(),
1021                terminal.diagnostic_facts(),
1022            )
1023            .expect("terminal failure should publish")
1024        );
1025        let observed =
1026            observe_generated_startup_state::<DriverCanister>(&DRIVER_STORES, SUBMISSION)
1027                .expect_err("matching terminal receipt should surface");
1028        assert_eq!(observed.kind(), StartupFailureKind::SchemaReconciliation);
1029        assert_eq!(
1030            observed.diagnostic().error_code(),
1031            ErrorCode::STORE_CORRUPTION
1032        );
1033        finish_commit(interrupted, |_| Ok(())).expect("test marker should clear");
1034        assert!(
1035            clear_generated_startup_failure::<DriverCanister>()
1036                .expect("authoritative correction should clear the receipt")
1037        );
1038    }
1039
1040    #[test]
1041    fn ready_startup_driver_publishes_empty_cardinality_then_quiesces() {
1042        const SUBMISSION: &str = "generated/cardinality-driver";
1043
1044        select_commit_memory_allocation(
1045            CardinalityDriverCanister::commit_memory_id().expect("test allocation"),
1046            CardinalityDriverCanister::COMMIT_STABLE_KEY,
1047        );
1048        let memory = commit_memory_handle(
1049            current_commit_memory_allocation().expect("commit allocation should resolve"),
1050        )
1051        .expect("commit memory should open");
1052        initialize_current_database_control_for_tests(&memory);
1053        let request_root = RequestExecutionRoot::__new_runtime_root();
1054        let database = crate::db::Db::<CardinalityDriverCanister>::new(
1055            &CARDINALITY_DRIVER_STORES,
1056            request_root.scope(),
1057        );
1058        ensure_database_format_admitted(&database)
1059            .expect("current store registry should initialize");
1060        mark_startup_recovery_complete_for_tests(&CARDINALITY_DRIVER_STORES)
1061            .expect("recovery witness should publish");
1062        let incarnation = database_incarnation_id().expect("incarnation should resolve");
1063        let (database_identity, accepted_head) =
1064            generated_schema_authority(&CARDINALITY_DRIVER_STORES, incarnation)
1065                .expect("empty generated authority should resolve");
1066        let submission_key =
1067            SchemaSubmissionKey::try_new(SUBMISSION).expect("submission should admit");
1068        let receipt = SchemaChangeReceipt::new(
1069            database_identity,
1070            submission_key,
1071            SchemaProposalDigest::from_bytes([2; 32]),
1072            accepted_head.clone(),
1073            SchemaChangeOutcome::NoOp { accepted_head },
1074        )
1075        .expect("terminal schema receipt should admit");
1076        let record = SchemaApplicationRecord::new(receipt, Vec::new())
1077            .expect("terminal schema record should admit");
1078        apply_schema_application_record_op(
1079            &SchemaApplicationRecordOp::insert(&record)
1080                .expect("schema record operation should admit"),
1081        )
1082        .expect("schema receipt should publish");
1083        assert_eq!(
1084            observe_generated_startup_state::<CardinalityDriverCanister>(
1085                &CARDINALITY_DRIVER_STORES,
1086                SUBMISSION,
1087            ),
1088            Ok(DatabaseStartupState::Ready),
1089        );
1090
1091        let session = crate::db::DbSession::<CardinalityDriverCanister>::new(
1092            &CARDINALITY_DRIVER_STORES,
1093            &request_root,
1094        );
1095        assert_eq!(
1096            drive_generated_startup_recovery_page(
1097                &session,
1098                &CARDINALITY_DRIVER_STORES,
1099                SUBMISSION,
1100            )
1101            .expect("empty cardinality publication should use the existing driver"),
1102            GeneratedStartupDriverStep::Recovering,
1103        );
1104        CARDINALITY_DRIVER_SCHEMA.with_borrow(|schema| {
1105            let header = schema
1106                .cardinality_generation_header()
1107                .expect("cardinality header should decode")
1108                .expect("cardinality header should publish");
1109            assert_eq!(
1110                header.state(),
1111                crate::db::schema::cardinality_generation::CardinalityGenerationState::Ready,
1112            );
1113        });
1114        assert_eq!(
1115            drive_generated_startup_recovery_page(
1116                &session,
1117                &CARDINALITY_DRIVER_STORES,
1118                SUBMISSION,
1119            )
1120            .expect("current cardinality evidence should quiesce"),
1121            GeneratedStartupDriverStep::Terminal,
1122        );
1123    }
1124}