1mod 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#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
17pub enum DatabaseStartupState {
18 Ready,
20 Recovering,
22}
23
24#[doc(hidden)]
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum GeneratedStartupDriverStep {
28 Terminal,
30 Recovering,
32 ApplyGeneratedSchema,
34}
35
36#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
38pub enum StartupFailureKind {
39 DatabaseControl,
41 JournalRecovery,
43 SchemaReconciliation,
45}
46
47#[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 #[must_use]
75 pub const fn kind(&self) -> StartupFailureKind {
76 self.kind
77 }
78
79 #[must_use]
81 pub const fn diagnostic(&self) -> &Diagnostic {
82 &self.diagnostic
83 }
84
85 #[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
140pub 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#[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#[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#[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 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}