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