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