diff --git a/rs/state_manager/src/split/tests.rs b/rs/state_manager/src/split/tests.rs index 26aed16b288..91563d4ab33 100644 --- a/rs/state_manager/src/split/tests.rs +++ b/rs/state_manager/src/split/tests.rs @@ -5,7 +5,7 @@ use crate::{ }; use assert_matches::assert_matches; use ic_base_types::{subnet_id_try_from_protobuf, CanisterId, NumSeconds}; -use ic_config::state_manager::lsmt_storage_default; +use ic_config::{flag_status::FlagStatus, state_manager::lsmt_storage_default}; use ic_error_types::{ErrorCode, UserError}; use ic_logger::ReplicaLogger; use ic_metrics::MetricsRegistry; @@ -80,38 +80,73 @@ const SUBNET_B_RANGES: &[CanisterIdRange] = &[ /// Full list of files expected to be listed in the manifest of subnet A. /// Note that any queue files are missing as they would be empty. -const SUBNET_A_FILES: &[&str] = &[ - "canister_states/00000000000000010101/canister.pbuf", - "canister_states/00000000000000010101/wasm_chunk_store.bin", - "canister_states/00000000000000020101/canister.pbuf", - "canister_states/00000000000000020101/wasm_chunk_store.bin", - "canister_states/00000000000000030101/canister.pbuf", - "canister_states/00000000000000030101/wasm_chunk_store.bin", - INGRESS_HISTORY_FILE, - SUBNET_QUEUES_FILE, - SYSTEM_METADATA_FILE, -]; +fn subnet_a_files() -> &'static [&'static str] { + // With lsmt enabled, we do do not write empty files for the wasm chunk store. + match lsmt_storage_default() { + FlagStatus::Enabled => &[ + "canister_states/00000000000000010101/canister.pbuf", + "canister_states/00000000000000020101/canister.pbuf", + "canister_states/00000000000000030101/canister.pbuf", + INGRESS_HISTORY_FILE, + SUBNET_QUEUES_FILE, + SYSTEM_METADATA_FILE, + ], + FlagStatus::Disabled => &[ + "canister_states/00000000000000010101/canister.pbuf", + "canister_states/00000000000000010101/wasm_chunk_store.bin", + "canister_states/00000000000000020101/canister.pbuf", + "canister_states/00000000000000020101/wasm_chunk_store.bin", + "canister_states/00000000000000030101/canister.pbuf", + "canister_states/00000000000000030101/wasm_chunk_store.bin", + INGRESS_HISTORY_FILE, + SUBNET_QUEUES_FILE, + SYSTEM_METADATA_FILE, + ], + } +} /// Full list of files expected to be listed in the manifest of subnet A'. -const SUBNET_A_PRIME_FILES: &[&str] = &[ - "canister_states/00000000000000010101/canister.pbuf", - "canister_states/00000000000000010101/wasm_chunk_store.bin", - "canister_states/00000000000000030101/canister.pbuf", - "canister_states/00000000000000030101/wasm_chunk_store.bin", - INGRESS_HISTORY_FILE, - SPLIT_MARKER_FILE, - SUBNET_QUEUES_FILE, - SYSTEM_METADATA_FILE, -]; +fn subnet_a_prime_files() -> &'static [&'static str] { + match lsmt_storage_default() { + FlagStatus::Enabled => &[ + "canister_states/00000000000000010101/canister.pbuf", + "canister_states/00000000000000030101/canister.pbuf", + INGRESS_HISTORY_FILE, + SPLIT_MARKER_FILE, + SUBNET_QUEUES_FILE, + SYSTEM_METADATA_FILE, + ], + FlagStatus::Disabled => &[ + "canister_states/00000000000000010101/canister.pbuf", + "canister_states/00000000000000010101/wasm_chunk_store.bin", + "canister_states/00000000000000030101/canister.pbuf", + "canister_states/00000000000000030101/wasm_chunk_store.bin", + INGRESS_HISTORY_FILE, + SPLIT_MARKER_FILE, + SUBNET_QUEUES_FILE, + SYSTEM_METADATA_FILE, + ], + } +} /// Full list of files expected to be listed in the manifest of subnet B. -const SUBNET_B_FILES: &[&str] = &[ - "canister_states/00000000000000020101/canister.pbuf", - "canister_states/00000000000000020101/wasm_chunk_store.bin", - INGRESS_HISTORY_FILE, - SPLIT_MARKER_FILE, - SYSTEM_METADATA_FILE, -]; +fn subnet_b_files() -> &'static [&'static str] { + match lsmt_storage_default() { + FlagStatus::Enabled => &[ + "canister_states/00000000000000020101/canister.pbuf", + INGRESS_HISTORY_FILE, + SPLIT_MARKER_FILE, + SYSTEM_METADATA_FILE, + ], + FlagStatus::Disabled => &[ + "canister_states/00000000000000020101/canister.pbuf", + "canister_states/00000000000000020101/wasm_chunk_store.bin", + INGRESS_HISTORY_FILE, + SPLIT_MARKER_FILE, + SYSTEM_METADATA_FILE, + ], + } +} const HEIGHT: Height = Height::new(42); const INITIAL_CYCLES: Cycles = Cycles::new(1 << 36); @@ -176,7 +211,7 @@ fn split_subnet_a_prime() { let root = tmp.path().to_path_buf(); let (manifest_a, height_a) = compute_manifest_for_root(&root, &log); - assert_eq!(SUBNET_A_FILES, manifest_files(&manifest_a).as_slice()); + assert_eq!(subnet_a_files(), manifest_files(&manifest_a).as_slice()); split( root.clone(), @@ -190,7 +225,7 @@ fn split_subnet_a_prime() { let (manifest_a_prime, height_a_prime) = compute_manifest_for_root(&root, &log); assert_eq!( - SUBNET_A_PRIME_FILES, + subnet_a_prime_files(), manifest_files(&manifest_a_prime).as_slice() ); @@ -198,7 +233,7 @@ fn split_subnet_a_prime() { assert_eq!(height_a.increment(), height_a_prime); // Compare the 2 manifests. - for &file in SUBNET_A_PRIME_FILES { + for &file in subnet_a_prime_files() { if file == SPLIT_MARKER_FILE { assert_eq!(SUBNET_A, deserialize_split_from(&root, height_a_prime)); } else { @@ -250,7 +285,7 @@ fn split_subnet_b_helper(new_subnet_batch_time_delta: Option) { let new_subnet_batch_time = new_subnet_batch_time_delta.map(|delta| batch_time + delta); let (manifest_a, height_a) = compute_manifest_for_root(&root, &log); - assert_eq!(SUBNET_A_FILES, manifest_files(&manifest_a).as_slice()); + assert_eq!(subnet_a_files(), manifest_files(&manifest_a).as_slice()); split( root.clone(), @@ -263,7 +298,7 @@ fn split_subnet_b_helper(new_subnet_batch_time_delta: Option) { .unwrap(); let (manifest_b, height_b) = compute_manifest_for_root(&root, &log); - assert_eq!(SUBNET_B_FILES, manifest_files(&manifest_b).as_slice()); + assert_eq!(subnet_b_files(), manifest_files(&manifest_b).as_slice()); // Checkpoint heights should differ by 1. assert_eq!(height_a.increment(), height_b); @@ -278,7 +313,7 @@ fn split_subnet_b_helper(new_subnet_batch_time_delta: Option) { // // Hence, for files that we expect to be different after the split it is // safe to compare the resulding Rust structs for equality. - for &file in SUBNET_B_FILES { + for &file in subnet_b_files() { if file == SPLIT_MARKER_FILE { assert_eq!(SUBNET_A, deserialize_split_from(&root, height_b)); } else if file == SYSTEM_METADATA_FILE { diff --git a/rs/state_manager/tests/state_manager.rs b/rs/state_manager/tests/state_manager.rs index 658dfb47965..b175635ebae 100644 --- a/rs/state_manager/tests/state_manager.rs +++ b/rs/state_manager/tests/state_manager.rs @@ -1,5 +1,8 @@ use ic_certification_version::{CertificationVersion::V11, CURRENT_CERTIFICATION_VERSION}; -use ic_config::state_manager::Config; +use ic_config::{ + flag_status::FlagStatus, + state_manager::{lsmt_storage_default, Config}, +}; use ic_crypto_tree_hash::{ flatmap, sparse_labeled_tree_from_paths, Label, LabeledTree, MixedHashTree, Path as LabelPath, }; @@ -77,8 +80,46 @@ fn label>(t: T) -> Label { t.into() } +/// Combined size of wasm memory including overlays. +fn vmemory_size(canister_layout: &ic_state_layout::CanisterLayout) -> u64 { + if lsmt_storage_default() == FlagStatus::Enabled { + canister_layout + .vmemory_0_overlays() + .unwrap() + .into_iter() + .map(|p| std::fs::metadata(p).unwrap().len()) + .sum::() + + std::fs::metadata(canister_layout.vmemory_0()) + .map(|metadata| metadata.len()) + .unwrap_or(0) + } else { + std::fs::metadata(canister_layout.vmemory_0()) + .unwrap() + .len() + } +} + +/// Combined size of stable memory including overlays. +fn stable_memory_size(canister_layout: &ic_state_layout::CanisterLayout) -> u64 { + if lsmt_storage_default() == FlagStatus::Enabled { + canister_layout + .stable_memory_overlays() + .unwrap() + .into_iter() + .map(|p| std::fs::metadata(p).unwrap().len()) + .sum::() + + std::fs::metadata(canister_layout.stable_memory_blob()) + .map(|metadata| metadata.len()) + .unwrap_or(0) + } else { + std::fs::metadata(canister_layout.stable_memory_blob()) + .unwrap() + .len() + } +} + /// This is a canister that keeps a counter on the heap and allows to increment it. -/// The counter can also be read and persisted to and loaded from stable memory +/// The counter can also be read and persisted to and loaded from stable memory. const TEST_CANISTER: &str = r#" (module (import "ic0" "msg_reply" (func $msg_reply)) @@ -255,7 +296,11 @@ fn skipping_flushing_is_invisible_for_state() { env.await_state_hash() } - assert_eq!(execute(false), execute(true)); + // We only skip flushes nondetermistically when `lsmt_storage` is disabled, so this test + // makes no sense otherwise. + if lsmt_storage_default() == FlagStatus::Disabled { + assert_eq!(execute(false), execute(true)); + } } #[test] @@ -351,11 +396,14 @@ fn checkpoint_marked_ro_at_restart() { .canister(&canister_test_id(100)) .unwrap(); - let canister_100_memory = canister_100_layout.vmemory_0(); - make_mutable(&canister_100_memory).unwrap(); + // Make sure we don't do asynchronous operations with checkpoint. + state_manager.flush_tip_channel(); + let canister_100_wasm = canister_100_layout.wasm().raw_path().to_path_buf(); + make_mutable(&canister_100_wasm).unwrap(); // Check that there are mutable files before the restart... let checkpoints_path = state_manager.state_layout().checkpoints(); + assert!(std::panic::catch_unwind(|| { assert_all_files_are_readonly(&checkpoints_path); }) @@ -690,8 +738,12 @@ fn missing_wasm_chunk_store_is_handled() { let canister_layout = mutable_cp_layout.canister(&canister_test_id(100)).unwrap(); let canister_wasm_chunk_store = canister_layout.wasm_chunk_store(); - assert!(canister_wasm_chunk_store.exists()); - std::fs::remove_file(&canister_wasm_chunk_store).unwrap(); + if canister_wasm_chunk_store.exists() { + std::fs::remove_file(&canister_wasm_chunk_store).unwrap(); + } + for overlay in canister_layout.wasm_chunk_store_overlays().unwrap() { + std::fs::remove_file(&overlay).unwrap(); + } let state_manager = restart_fn(state_manager, None); let (recovered_height, recovered) = state_manager.take_tip(); @@ -2805,6 +2857,7 @@ fn can_recover_from_corruption_on_state_sync() { height(1), ) .unwrap(); + dst_state_manager.flush_tip_channel(); // There are 5 types of ways to trigger corruption recovery: // @@ -2825,7 +2878,11 @@ fn can_recover_from_corruption_on_state_sync() { // The code below prepares all 5 types of corruption. let canister_90_layout = mutable_cp_layout.canister(&canister_test_id(90)).unwrap(); - let canister_90_memory = canister_90_layout.vmemory_0(); + let canister_90_memory = if lsmt_storage_default() == FlagStatus::Enabled { + canister_90_layout.vmemory_0_overlays().unwrap().remove(0) + } else { + canister_90_layout.vmemory_0() + }; make_mutable(&canister_90_memory).unwrap(); std::fs::write(&canister_90_memory, b"Garbage").unwrap(); make_readonly(&canister_90_memory).unwrap(); @@ -2837,12 +2894,23 @@ fn can_recover_from_corruption_on_state_sync() { let canister_100_layout = mutable_cp_layout.canister(&canister_test_id(100)).unwrap(); - let canister_100_memory = canister_100_layout.vmemory_0(); + let canister_100_memory = if lsmt_storage_default() == FlagStatus::Enabled { + canister_100_layout.vmemory_0_overlays().unwrap().remove(0) + } else { + canister_100_layout.vmemory_0() + }; make_mutable(&canister_100_memory).unwrap(); write_all_at(&canister_100_memory, &[3u8; PAGE_SIZE], 4).unwrap(); make_readonly(&canister_100_memory).unwrap(); - let canister_100_stable_memory = canister_100_layout.stable_memory_blob(); + let canister_100_stable_memory = if lsmt_storage_default() == FlagStatus::Enabled { + canister_100_layout + .stable_memory_overlays() + .unwrap() + .remove(0) + } else { + canister_100_layout.stable_memory_blob() + }; make_mutable(&canister_100_stable_memory).unwrap(); write_all_at( &canister_100_stable_memory, @@ -3170,11 +3238,13 @@ fn can_reuse_chunk_hashes_when_computing_manifest() { let execution_state = canister_state.execution_state.as_mut().unwrap(); const NEW_WASM_PAGE: u64 = 300; + const WASM_PAGES: u64 = 2; execution_state.wasm_memory.page_map.update(&[ (PageIndex::new(1), &[1u8; PAGE_SIZE]), (PageIndex::new(NEW_WASM_PAGE), &[2u8; PAGE_SIZE]), ]); const NEW_STABLE_PAGE: u64 = 500; + const STABLE_PAGES: u64 = 2; execution_state.stable_memory.page_map.update(&[ (PageIndex::new(1), &[1u8; PAGE_SIZE]), (PageIndex::new(NEW_STABLE_PAGE), &[2u8; PAGE_SIZE]), @@ -3197,13 +3267,21 @@ fn can_reuse_chunk_hashes_when_computing_manifest() { state_manager.commit_and_certify(state, height(2), CertificationScope::Full); let state_2_hash = wait_for_checkpoint(&state_manager, height(2)); - // Second checkpoint can leverage heap chunks computed previously as well as the wasm binary + // Second checkpoint can leverage heap chunks computed previously as well as the wasm binary. let chunk_bytes = fetch_int_counter_vec(metrics, "state_manager_manifest_chunk_bytes"); - assert_eq!( - PAGE_SIZE as u64 * ((NEW_WASM_PAGE + 1) + (NEW_STABLE_PAGE + 1)) - + empty_wasm_size() as u64, - chunk_bytes[&reused_label] + chunk_bytes[&compared_label] - ); + if lsmt_storage_default() == FlagStatus::Enabled { + let expected_size_estimate = + PAGE_SIZE as u64 * (WASM_PAGES + STABLE_PAGES) + empty_wasm_size() as u64; + let size = chunk_bytes[&reused_label] + chunk_bytes[&compared_label]; + assert!(((expected_size_estimate as f64 * 1.1) as u64) > size); + assert!(((expected_size_estimate as f64 * 0.9) as u64) < size); + } else { + assert_eq!( + PAGE_SIZE as u64 * ((NEW_WASM_PAGE + 1) + (NEW_STABLE_PAGE + 1)) + + empty_wasm_size() as u64, + chunk_bytes[&reused_label] + chunk_bytes[&compared_label] + ); + } let checkpoint = state_manager.state_layout().checkpoint(height(2)).unwrap(); @@ -4052,6 +4130,23 @@ fn remove_too_many_diverged_state_markers() { }); } +#[test] +fn can_write_multiple_checkpoints() { + state_manager_test(|_metrics, state_manager| { + let (_height, mut state) = state_manager.take_tip(); + insert_dummy_canister(&mut state, canister_test_id(100)); + state_manager.commit_and_certify(state, height(1), CertificationScope::Full); + + for _ in 1..10 { + let (h, mut state) = state_manager.take_tip(); + insert_dummy_canister(&mut state, canister_test_id(100)); + state_manager.commit_and_certify(state, height(h.get() + 1), CertificationScope::Full); + } + + wait_for_checkpoint(&state_manager, height(10)); + }); +} + #[test] fn can_reset_memory() { state_manager_test(|metrics, state_manager| { @@ -4062,11 +4157,25 @@ fn can_reset_memory() { let canister_state = state.canister_state_mut(&canister_test_id(100)).unwrap(); let execution_state = canister_state.execution_state.as_mut().unwrap(); execution_state.wasm_memory.page_map.update(&[ + (PageIndex::new(0), &[99u8; PAGE_SIZE]), (PageIndex::new(1), &[99u8; PAGE_SIZE]), - (PageIndex::new(300), &[99u8; PAGE_SIZE]), + (PageIndex::new(2), &[99u8; PAGE_SIZE]), + (PageIndex::new(3), &[99u8; PAGE_SIZE]), + (PageIndex::new(4), &[99u8; PAGE_SIZE]), + (PageIndex::new(5), &[99u8; PAGE_SIZE]), + (PageIndex::new(6), &[99u8; PAGE_SIZE]), + (PageIndex::new(7), &[99u8; PAGE_SIZE]), ]); - state_manager.commit_and_certify(state, height(1), CertificationScope::Metadata); + state_manager.commit_and_certify(state, height(1), CertificationScope::Full); + // Check the data is written to disk. + let canister_layout = state_manager + .state_layout() + .checkpoint(height(1)) + .unwrap() + .canister(&canister_test_id(100)) + .unwrap(); + assert!(vmemory_size(&canister_layout) >= 8 * PAGE_SIZE as u64); let (_height, mut state) = state_manager.take_tip(); @@ -4075,11 +4184,11 @@ fn can_reset_memory() { let execution_state = canister_state.execution_state.as_mut().unwrap(); execution_state.wasm_memory = Memory::new(PageMap::new_for_testing(), NumWasmPages::new(0)); execution_state.wasm_memory.page_map.update(&[ + (PageIndex::new(0), &[100u8; PAGE_SIZE]), (PageIndex::new(1), &[100u8; PAGE_SIZE]), - (PageIndex::new(100), &[100u8; PAGE_SIZE]), ]); - // Check no remnants of the old data remain + // Check no remnants of the old data remain. assert_eq!( execution_state .wasm_memory @@ -4097,18 +4206,14 @@ fn can_reset_memory() { state_manager.commit_and_certify(state, height(2), CertificationScope::Full); - // Check file in checkpoint does not contain old data by checking its size - let memory_path = state_manager + // Check file in checkpoint does not contain old data by checking its size. + let canister_layout = state_manager .state_layout() .checkpoint(height(2)) .unwrap() .canister(&canister_test_id(100)) - .unwrap() - .vmemory_0(); - assert_eq!( - std::fs::metadata(memory_path).unwrap().len(), - 101 * PAGE_SIZE as u64 - ); + .unwrap(); + assert!(vmemory_size(&canister_layout) < 8 * PAGE_SIZE as u64); let (_height, mut state) = state_manager.take_tip(); let canister_state = state.canister_state_mut(&canister_test_id(100)).unwrap(); @@ -4135,15 +4240,14 @@ fn can_reset_memory() { state_manager.commit_and_certify(state, height(3), CertificationScope::Full); - // File should be empty after wiping and checkpoint - let memory_path = state_manager + // File should be empty after wiping and checkpoint. + let canister_layout = state_manager .state_layout() .checkpoint(height(3)) .unwrap() .canister(&canister_test_id(100)) - .unwrap() - .vmemory_0(); - assert_eq!(std::fs::metadata(memory_path).unwrap().len(), 0); + .unwrap(); + assert_eq!(vmemory_size(&canister_layout), 0); assert_error_counters(metrics); }); @@ -4275,19 +4379,9 @@ fn can_uninstall_code() { let canister_path = canister_layout.raw_path(); assert!(std::fs::metadata(canister_path).unwrap().is_dir()); - // WASM binary and memory stable memory should all be present - assert_ne!( - std::fs::metadata(canister_layout.vmemory_0()) - .unwrap() - .len(), - 0 - ); - assert_ne!( - std::fs::metadata(canister_layout.stable_memory_blob()) - .unwrap() - .len(), - 0 - ); + // WASM binary, WASM memory and stable memory should all be present. + assert_ne!(vmemory_size(&canister_layout), 0); + assert_ne!(stable_memory_size(&canister_layout), 0); assert!(canister_layout.wasm().raw_path().exists()); let (_height, mut state) = state_manager.take_tip(); @@ -4315,19 +4409,9 @@ fn can_uninstall_code() { assert!(canister_layout.raw_path().exists()); - // WASM and stable memory should be empty after checkpoint - assert_eq!( - std::fs::metadata(canister_layout.vmemory_0()) - .unwrap() - .len(), - 0 - ); - assert_eq!( - std::fs::metadata(canister_layout.stable_memory_blob()) - .unwrap() - .len(), - 0 - ); + // WASM and stable memory should be empty after checkpoint. + assert_eq!(vmemory_size(&canister_layout), 0); + assert_eq!(stable_memory_size(&canister_layout), 0); // WASM binary should be missing assert!(!canister_layout.wasm().raw_path().exists()); @@ -4358,19 +4442,9 @@ fn can_uninstall_code_state_machine() { .unwrap() .canister(&canister_id) .unwrap(); - assert_ne!( - std::fs::metadata(canister_layout.vmemory_0()) - .unwrap() - .len(), - 0 - ); - assert_ne!( - std::fs::metadata(canister_layout.stable_memory_blob()) - .unwrap() - .len(), - 0 - ); assert!(canister_layout.wasm().raw_path().exists()); + assert_ne!(vmemory_size(&canister_layout), 0); + assert_ne!(stable_memory_size(&canister_layout), 0); env.uninstall_code(canister_id).unwrap(); @@ -4379,19 +4453,9 @@ fn can_uninstall_code_state_machine() { .unwrap() .canister(&canister_id) .unwrap(); - assert_eq!( - std::fs::metadata(canister_layout.vmemory_0()) - .unwrap() - .len(), - 0 - ); - assert_eq!( - std::fs::metadata(canister_layout.stable_memory_blob()) - .unwrap() - .len(), - 0 - ); assert!(!canister_layout.wasm().raw_path().exists()); + assert_eq!(vmemory_size(&canister_layout), 0); + assert_eq!(stable_memory_size(&canister_layout), 0); } #[test] @@ -4430,8 +4494,14 @@ fn tip_is_initialized_correctly() { .unwrap(); assert!(!canister_layout.queues().raw_path().exists()); assert!(canister_layout.wasm().raw_path().exists()); - assert!(canister_layout.vmemory_0().exists()); - assert!(canister_layout.stable_memory_blob().exists()); + assert!( + canister_layout.vmemory_0().exists() + || canister_layout.vmemory_0_overlays().unwrap()[0].exists() + ); + assert!( + canister_layout.stable_memory_blob().exists() + || canister_layout.stable_memory_overlays().unwrap()[0].exists() + ); let (_height, state) = state_manager.take_tip(); state_manager.commit_and_certify(state, height(2), CertificationScope::Full); @@ -4448,8 +4518,14 @@ fn tip_is_initialized_correctly() { assert!(!canister_layout.queues().raw_path().exists()); // empty assert!(canister_layout.canister().raw_path().exists()); assert!(canister_layout.wasm().raw_path().exists()); - assert!(canister_layout.vmemory_0().exists()); - assert!(canister_layout.stable_memory_blob().exists()); + assert!( + canister_layout.vmemory_0().exists() + || canister_layout.vmemory_0_overlays().unwrap()[0].exists() + ); + assert!( + canister_layout.stable_memory_blob().exists() + || canister_layout.stable_memory_overlays().unwrap()[0].exists() + ); }); }