From 85f58e976a642a20879f4100983e1eb1a1feea89 Mon Sep 17 00:00:00 2001 From: AlexandraZapuc <61285418+AlexandraZapuc@users.noreply.github.com> Date: Tue, 13 Aug 2024 00:33:04 +0200 Subject: [PATCH] feat: EXC-1567: Add charging for take and load canister snapshot (#811) This PR updates the canister manager to charge canisters that are executing `take_canister_snapshot` or `load_canister_snapshot`. Closes EXC-1567 --------- Co-authored-by: Andriy Berestovskyy --- rs/config/src/subnet_config.rs | 13 ++ .../benches/lib/src/common.rs | 3 + .../src/canister_manager.rs | 197 ++++++++++++++---- .../src/canister_manager/tests.rs | 1 + .../src/execution_environment.rs | 85 +++++--- .../tests/canister_snapshots.rs | 126 ++++++++++- rs/execution_environment/src/lib.rs | 1 + .../src/scheduler/test_utilities.rs | 8 + .../src/scheduler/tests.rs | 32 ++- .../execution_environment/src/lib.rs | 4 + 10 files changed, 399 insertions(+), 71 deletions(-) diff --git a/rs/config/src/subnet_config.rs b/rs/config/src/subnet_config.rs index 02d5f813efa..c1ad0b124f6 100644 --- a/rs/config/src/subnet_config.rs +++ b/rs/config/src/subnet_config.rs @@ -161,6 +161,11 @@ const DEFAULT_RESERVED_BALANCE_LIMIT: Cycles = Cycles::new(5 * T); /// 1/10th of a round. pub const DEFAULT_UPLOAD_CHUNK_INSTRUCTIONS: NumInstructions = NumInstructions::new(200_000_000); +/// Baseline cost for creating or loading a canister snapshot (2B instructions). +/// The cost is based on the benchmarks: rs/execution_environment/benches/management_canister/ +pub const DEFAULT_CANISTERS_SNAPSHOT_BASELINE_INSTRUCTIONS: NumInstructions = + NumInstructions::new(2_000_000_000); + /// The per subnet type configuration for the scheduler component #[derive(Clone, Serialize, Deserialize)] pub struct SchedulerConfig { @@ -259,6 +264,9 @@ pub struct SchedulerConfig { /// Number of instructions to count when uploading a chunk to the wasm store. pub upload_wasm_chunk_instructions: NumInstructions, + + /// Number of instructions to count when creating or loading a canister snapshot. + pub canister_snapshot_baseline_instructions: NumInstructions, } impl SchedulerConfig { @@ -286,6 +294,8 @@ impl SchedulerConfig { dirty_page_overhead: DEFAULT_DIRTY_PAGE_OVERHEAD, accumulated_priority_reset_interval: ACCUMULATED_PRIORITY_RESET_INTERVAL, upload_wasm_chunk_instructions: DEFAULT_UPLOAD_CHUNK_INSTRUCTIONS, + canister_snapshot_baseline_instructions: + DEFAULT_CANISTERS_SNAPSHOT_BASELINE_INSTRUCTIONS, } } @@ -328,6 +338,7 @@ impl SchedulerConfig { dirty_page_overhead: SYSTEM_SUBNET_DIRTY_PAGE_OVERHEAD, accumulated_priority_reset_interval: ACCUMULATED_PRIORITY_RESET_INTERVAL, upload_wasm_chunk_instructions: NumInstructions::from(0), + canister_snapshot_baseline_instructions: NumInstructions::from(0), } } @@ -358,6 +369,8 @@ impl SchedulerConfig { dirty_page_overhead: DEFAULT_DIRTY_PAGE_OVERHEAD, accumulated_priority_reset_interval: ACCUMULATED_PRIORITY_RESET_INTERVAL, upload_wasm_chunk_instructions: DEFAULT_UPLOAD_CHUNK_INSTRUCTIONS, + canister_snapshot_baseline_instructions: + DEFAULT_CANISTERS_SNAPSHOT_BASELINE_INSTRUCTIONS, } } diff --git a/rs/execution_environment/benches/lib/src/common.rs b/rs/execution_environment/benches/lib/src/common.rs index 58c570eec2c..7cf225c5df0 100644 --- a/rs/execution_environment/benches/lib/src/common.rs +++ b/rs/execution_environment/benches/lib/src/common.rs @@ -306,6 +306,9 @@ where subnet_configs .scheduler_config .upload_wasm_chunk_instructions, + subnet_configs + .scheduler_config + .canister_snapshot_baseline_instructions, ); for Benchmark(id, wat, expected_ops) in benchmarks { run_benchmark( diff --git a/rs/execution_environment/src/canister_manager.rs b/rs/execution_environment/src/canister_manager.rs index 14af08ae1c7..5201d4aace0 100644 --- a/rs/execution_environment/src/canister_manager.rs +++ b/rs/execution_environment/src/canister_manager.rs @@ -124,6 +124,7 @@ pub(crate) struct CanisterMgrConfig { heap_delta_rate_limit: NumBytes, upload_wasm_chunk_instructions: NumInstructions, wasm_chunk_store_max_size: NumBytes, + canister_snapshot_baseline_instructions: NumInstructions, } impl CanisterMgrConfig { @@ -143,6 +144,7 @@ impl CanisterMgrConfig { heap_delta_rate_limit: NumBytes, upload_wasm_chunk_instructions: NumInstructions, wasm_chunk_store_max_size: NumBytes, + canister_snapshot_baseline_instructions: NumInstructions, ) -> Self { Self { subnet_memory_capacity, @@ -159,6 +161,7 @@ impl CanisterMgrConfig { heap_delta_rate_limit, upload_wasm_chunk_instructions, wasm_chunk_store_max_size, + canister_snapshot_baseline_instructions, } } } @@ -1767,7 +1770,7 @@ impl CanisterManager { /// and delete it before creating a new one. /// Failure to do so will result in the creation of a new snapshot being unsuccessful. /// - /// If the new snapshot cannot be created, an appropiate error will be returned. + /// If the new snapshot cannot be created, an appropriate error will be returned. pub(crate) fn take_canister_snapshot( &self, subnet_size: usize, @@ -1777,9 +1780,14 @@ impl CanisterManager { state: &mut ReplicatedState, round_limits: &mut RoundLimits, resource_saturation: &ResourceSaturation, - ) -> Result { + ) -> ( + Result, + NumInstructions, + ) { // Check sender is a controller. - validate_controller(canister, &sender)?; + if let Err(err) = validate_controller(canister, &sender) { + return (Err(err), NumInstructions::new(0)); + }; match replace_snapshot { // Check that replace snapshot ID exists if provided. @@ -1787,18 +1795,24 @@ impl CanisterManager { match state.canister_snapshots.get(replace_snapshot) { None => { // If not found, the operation fails due to invalid parameters. - return Err(CanisterManagerError::CanisterSnapshotNotFound { - canister_id: canister.canister_id(), - snapshot_id: replace_snapshot, - }); + return ( + Err(CanisterManagerError::CanisterSnapshotNotFound { + canister_id: canister.canister_id(), + snapshot_id: replace_snapshot, + }), + NumInstructions::new(0), + ); } Some(snapshot) => { // Verify the provided replacement snapshot belongs to this canister. if snapshot.canister_id() != canister.canister_id() { - return Err(CanisterManagerError::CanisterSnapshotInvalidOwnership { - canister_id: canister.canister_id(), - snapshot_id: replace_snapshot, - }); + return ( + Err(CanisterManagerError::CanisterSnapshotInvalidOwnership { + canister_id: canister.canister_id(), + snapshot_id: replace_snapshot, + }), + NumInstructions::new(0), + ); } } } @@ -1811,10 +1825,13 @@ impl CanisterManager { .snapshots_count(&canister.canister_id()) >= MAX_NUMBER_OF_SNAPSHOTS_PER_CANISTER { - return Err(CanisterManagerError::CanisterSnapshotLimitExceeded { - canister_id: canister.canister_id(), - limit: MAX_NUMBER_OF_SNAPSHOTS_PER_CANISTER, - }); + return ( + Err(CanisterManagerError::CanisterSnapshotLimitExceeded { + canister_id: canister.canister_id(), + limit: MAX_NUMBER_OF_SNAPSHOTS_PER_CANISTER, + }), + NumInstructions::new(0), + ); } } } @@ -1822,11 +1839,14 @@ impl CanisterManager { if self.config.rate_limiting_of_heap_delta == FlagStatus::Enabled && canister.scheduler_state.heap_delta_debit >= self.config.heap_delta_rate_limit { - return Err(CanisterManagerError::CanisterHeapDeltaRateLimited { - canister_id: canister.canister_id(), - value: canister.scheduler_state.heap_delta_debit, - limit: self.config.heap_delta_rate_limit, - }); + return ( + Err(CanisterManagerError::CanisterHeapDeltaRateLimited { + canister_id: canister.canister_id(), + value: canister.scheduler_state.heap_delta_debit, + limit: self.config.heap_delta_rate_limit, + }), + NumInstructions::new(0), + ); } let new_snapshot_size = canister.snapshot_size_bytes(); @@ -1859,14 +1879,17 @@ impl CanisterManager { ); if canister.system_state.balance() < threshold + reservation_cycles { - return Err(CanisterManagerError::InsufficientCyclesInMemoryGrow { - bytes: new_snapshot_size, - available: canister.system_state.balance(), - threshold, - }); + return ( + Err(CanisterManagerError::InsufficientCyclesInMemoryGrow { + bytes: new_snapshot_size, + available: canister.system_state.balance(), + threshold, + }), + NumInstructions::new(0), + ); } // Verify that the subnet has enough memory. - round_limits + if let Err(err) = round_limits .subnet_available_memory .check_available_memory(new_snapshot_size, NumBytes::from(0), NumBytes::from(0)) .map_err( @@ -1879,9 +1902,12 @@ impl CanisterManager { .max(0) as u64, ), }, - )?; + ) + { + return (Err(err), NumInstructions::new(0)); + }; // Reserve needed cycles if the subnet is becoming saturated. - canister + if let Err(err) = canister .system_state .reserve_cycles(reservation_cycles) .map_err(|err| match err { @@ -1900,7 +1926,10 @@ impl CanisterManager { limit, } } - })?; + }) + { + return (Err(err), NumInstructions::new(0)); + }; // Actually deduct memory from the subnet. It's safe to unwrap // here because we already checked the available memory above. round_limits.subnet_available_memory @@ -1908,9 +1937,53 @@ impl CanisterManager { .expect("Error: Cannot fail to decrement SubnetAvailableMemory after checking for availability"); } + let current_memory_usage = canister.memory_usage() + new_snapshot_size; + let message_memory = canister.message_memory_usage(); + let compute_allocation = canister.compute_allocation(); + let reveal_top_up = canister.controllers().contains(&sender); + let instructions = self.config.canister_snapshot_baseline_instructions + + NumInstructions::new(new_snapshot_size.get()); + + // Charge for the take snapshot of the canister. + let prepaid_cycles = match self + .cycles_account_manager + .prepay_execution_cycles( + &mut canister.system_state, + current_memory_usage, + message_memory, + compute_allocation, + instructions, + subnet_size, + reveal_top_up, + ) + .map_err(CanisterManagerError::CanisterSnapshotNotEnoughCycles) + { + Ok(c) => c, + Err(err) => return (Err(err), NumInstructions::new(0)), + }; + + // To keep the invariant that `prepay_execution_cycles` is always paired + // with `refund_unused_execution_cycles` we refund zero immediately. + self.cycles_account_manager.refund_unused_execution_cycles( + &mut canister.system_state, + NumInstructions::from(0), + instructions, + prepaid_cycles, + // This counter is incremented if we refund more + // instructions than initially charged, which is impossible + // here. + &IntCounter::new("no_op", "no_op").unwrap(), + subnet_size, + &self.log, + ); + // Create new snapshot. - let new_snapshot = CanisterSnapshot::from_canister(canister, state.time()) - .map_err(CanisterManagerError::from)?; + let new_snapshot = match CanisterSnapshot::from_canister(canister, state.time()) + .map_err(CanisterManagerError::from) + { + Ok(s) => s, + Err(err) => return (Err(err), instructions), + }; // Delete old snapshot identified by `replace_snapshot` ID. if let Some(replace_snapshot) = replace_snapshot { @@ -1943,15 +2016,19 @@ impl CanisterManager { .canister_snapshots .push(snapshot_id, Arc::new(new_snapshot)); canister.system_state.snapshots_memory_usage += new_snapshot_size; - Ok(CanisterSnapshotResponse::new( - &snapshot_id, - state.time().as_nanos_since_unix_epoch(), - new_snapshot_size, - )) + ( + Ok(CanisterSnapshotResponse::new( + &snapshot_id, + state.time().as_nanos_since_unix_epoch(), + new_snapshot_size, + )), + instructions, + ) } pub(crate) fn load_canister_snapshot( &self, + subnet_size: usize, sender: PrincipalId, canister: &CanisterState, snapshot_id: SnapshotId, @@ -2080,6 +2157,46 @@ impl CanisterManager { ); } + let compute_allocation = new_canister.compute_allocation(); + let message_memory = canister.message_memory_usage(); + let reveal_top_up = canister.controllers().contains(&sender); + let instructions = self.config.canister_snapshot_baseline_instructions + + instructions_used + + NumInstructions::new(snapshot.size().get()); + + // Charge for loading the snapshot of the canister. + let prepaid_cycles = match self.cycles_account_manager.prepay_execution_cycles( + &mut new_canister.system_state, + new_memory_usage, + message_memory, + compute_allocation, + instructions, + subnet_size, + reveal_top_up, + ) { + Ok(prepaid_cycles) => prepaid_cycles, + Err(err) => { + return ( + instructions_used, + Err(CanisterManagerError::CanisterSnapshotNotEnoughCycles(err)), + ) + } + }; + + // To keep the invariant that `prepay_execution_cycles` is always paired + // with `refund_unused_execution_cycles` we refund zero immediately. + self.cycles_account_manager.refund_unused_execution_cycles( + &mut new_canister.system_state, + NumInstructions::from(0), + instructions, + prepaid_cycles, + // This counter is incremented if we refund more + // instructions than initially charged, which is impossible + // here. + &IntCounter::new("no_op", "no_op").unwrap(), + subnet_size, + &self.log, + ); // Increment canister version. new_canister.system_state.canister_version += 1; new_canister.system_state.add_canister_change( @@ -2286,6 +2403,7 @@ pub(crate) enum CanisterManagerError { canister_id: CanisterId, limit: usize, }, + CanisterSnapshotNotEnoughCycles(CanisterOutOfCyclesError), LongExecutionAlreadyInProgress { canister_id: CanisterId, }, @@ -2334,6 +2452,7 @@ impl AsErrorHelp for CanisterManagerError { | CanisterManagerError::CanisterSnapshotInvalidOwnership { .. } | CanisterManagerError::CanisterSnapshotExecutionStateNotFound { .. } | CanisterManagerError::CanisterSnapshotLimitExceeded { .. } + | CanisterManagerError::CanisterSnapshotNotEnoughCycles { .. } | CanisterManagerError::LongExecutionAlreadyInProgress { .. } | CanisterManagerError::MissingUpgradeOptionError { .. } | CanisterManagerError::InvalidUpgradeOptionError { .. } => ErrorHelp::UserError { @@ -2633,6 +2752,12 @@ impl From for UserError { ) ) } + CanisterSnapshotNotEnoughCycles(err) => { + Self::new( + ErrorCode::CanisterOutOfCycles, + format!("Canister snapshotting failed with `{}`{additional_help}", err), + ) + } LongExecutionAlreadyInProgress { canister_id } => { Self::new( ErrorCode::CanisterRejectedMessage, diff --git a/rs/execution_environment/src/canister_manager/tests.rs b/rs/execution_environment/src/canister_manager/tests.rs index 8f6338fa114..4f04e0c31ab 100644 --- a/rs/execution_environment/src/canister_manager/tests.rs +++ b/rs/execution_environment/src/canister_manager/tests.rs @@ -297,6 +297,7 @@ fn canister_manager_config( NumBytes::from(10 * 1024 * 1024), SchedulerConfig::application_subnet().upload_wasm_chunk_instructions, ic_config::embedders::Config::default().wasm_max_size, + SchedulerConfig::application_subnet().canister_snapshot_baseline_instructions, ) } diff --git a/rs/execution_environment/src/execution_environment.rs b/rs/execution_environment/src/execution_environment.rs index 8d81e4c389a..7205a5e394f 100644 --- a/rs/execution_environment/src/execution_environment.rs +++ b/rs/execution_environment/src/execution_environment.rs @@ -371,6 +371,7 @@ impl ExecutionEnvironment { fd_factory: Arc, heap_delta_rate_limit: NumBytes, upload_wasm_chunk_instructions: NumInstructions, + canister_snapshot_baseline_instructions: NumInstructions, ) -> Self { // Assert the flag implication: DTS => sandboxing. assert!( @@ -393,6 +394,7 @@ impl ExecutionEnvironment { heap_delta_rate_limit, upload_wasm_chunk_instructions, config.embedders_config.wasm_max_size, + canister_snapshot_baseline_instructions, ); let metrics = ExecutionEnvironmentMetrics::new(metrics_registry); let canister_manager = CanisterManager::new( @@ -1374,21 +1376,29 @@ impl ExecutionEnvironment { }, Ok(Ic00Method::TakeCanisterSnapshot) => match self.config.canister_snapshots { - FlagStatus::Enabled => { - let res = TakeCanisterSnapshotArgs::decode(payload).and_then(|args| { - self.take_canister_snapshot( + FlagStatus::Enabled => match TakeCanisterSnapshotArgs::decode(payload) { + Err(err) => ExecuteSubnetMessageResult::Finished { + response: Err(err), + refund: msg.take_cycles(), + }, + Ok(args) => { + let (result, instructions_used) = self.take_canister_snapshot( *msg.sender(), &mut state, args, registry_settings.subnet_size, round_limits, - ) - }); - ExecuteSubnetMessageResult::Finished { - response: res, - refund: msg.take_cycles(), + ); + let msg_result = ExecuteSubnetMessageResult::Finished { + response: result, + refund: msg.take_cycles(), + }; + + let state = + self.finish_subnet_message_execution(state, msg, msg_result, since); + return (state, Some(instructions_used)); } - } + }, FlagStatus::Disabled => { let err = Err(UserError::new( ErrorCode::CanisterContractViolation, @@ -1409,7 +1419,8 @@ impl ExecutionEnvironment { }, Ok(args) => { let origin = msg.canister_change_origin(args.get_sender_canister_version()); - let (result, instruction_used) = self.load_canister_snapshot( + let (result, instructions_used) = self.load_canister_snapshot( + registry_settings.subnet_size, *msg.sender(), &mut state, args, @@ -1423,7 +1434,7 @@ impl ExecutionEnvironment { let state = self.finish_subnet_message_execution(state, msg, msg_result, since); - return (state, Some(instruction_used)); + return (state, Some(instructions_used)); } }, FlagStatus::Disabled => { @@ -1494,8 +1505,13 @@ impl ExecutionEnvironment { } }; - // Note that some branches above like `InstallCode` and `SignWithECDSA` - // have early returns. If you modify code below, please also update + // Note that some branches above have early returns: + // - `InstallCode` + // - `InstallChunkedCode` + // - `TakeCanisterSnapshot` + // - `LoadCanisterSnapshot` + // - `SignWithECDSA` + // If you modify code below, please also update // these cases. let state = self.finish_subnet_message_execution(state, msg, result, since); (state, Some(NumInstructions::from(0))) @@ -2039,15 +2055,18 @@ impl ExecutionEnvironment { args: TakeCanisterSnapshotArgs, subnet_size: usize, round_limits: &mut RoundLimits, - ) -> Result, UserError> { + ) -> (Result, UserError>, NumInstructions) { let canister_id = args.get_canister_id(); // Take canister out. let mut canister = match state.take_canister_state(&canister_id) { None => { - return Err(UserError::new( - ErrorCode::CanisterNotFound, - format!("Canister {} not found.", &canister_id), - )) + return ( + Err(UserError::new( + ErrorCode::CanisterNotFound, + format!("Canister {} not found.", &canister_id), + )), + NumInstructions::new(0), + ) } Some(canister) => canister, }; @@ -2055,27 +2074,28 @@ impl ExecutionEnvironment { let resource_saturation = self.subnet_memory_saturation(&round_limits.subnet_available_memory); let replace_snapshot = args.replace_snapshot(); - let result = self - .canister_manager - .take_canister_snapshot( - subnet_size, - sender, - &mut canister, - replace_snapshot, - state, - round_limits, - &resource_saturation, - ) - .map(|response| response.encode()) - .map_err(|err| err.into()); + let (result, instructions_used) = self.canister_manager.take_canister_snapshot( + subnet_size, + sender, + &mut canister, + replace_snapshot, + state, + round_limits, + &resource_saturation, + ); // Put canister back. state.put_canister_state(canister); - result + + match result { + Ok(response) => (Ok(response.encode()), instructions_used), + Err(err) => (Err(err.into()), instructions_used), + } } /// Loads a canister snapshot onto an existing canister. fn load_canister_snapshot( &self, + subnet_size: usize, sender: PrincipalId, state: &mut ReplicatedState, args: LoadCanisterSnapshotArgs, @@ -2099,6 +2119,7 @@ impl ExecutionEnvironment { let snapshot_id = args.snapshot_id(); let (instructions_used, result) = self.canister_manager.load_canister_snapshot( + subnet_size, sender, &old_canister, snapshot_id, diff --git a/rs/execution_environment/src/execution_environment/tests/canister_snapshots.rs b/rs/execution_environment/src/execution_environment/tests/canister_snapshots.rs index 49cd05f9ee0..e15c95bd39b 100644 --- a/rs/execution_environment/src/execution_environment/tests/canister_snapshots.rs +++ b/rs/execution_environment/src/execution_environment/tests/canister_snapshots.rs @@ -2,6 +2,7 @@ use assert_matches::assert_matches; use candid::{Decode, Encode}; use ic_base_types::NumBytes; use ic_config::flag_status::FlagStatus; +use ic_config::subnet_config::SubnetConfig; use ic_cycles_account_manager::ResourceSaturation; use ic_error_types::{ErrorCode, RejectCode}; use ic_management_canister_types::{ @@ -10,6 +11,7 @@ use ic_management_canister_types::{ LoadCanisterSnapshotArgs, Method, Payload as Ic00Payload, TakeCanisterSnapshotArgs, UploadChunkArgs, }; +use ic_registry_subnet_type::SubnetType; use ic_replicated_state::{ canister_snapshots::SnapshotOperation, canister_state::system_state::CyclesUseCase, }; @@ -21,7 +23,7 @@ use ic_types::{ ingress::WasmResult, messages::{Payload, RejectContext, RequestOrResponse}, time::UNIX_EPOCH, - CanisterId, Cycles, SnapshotId, + CanisterId, Cycles, NumInstructions, SnapshotId, }; use ic_universal_canister::{call_args, wasm, UNIVERSAL_CANISTER_WASM}; use more_asserts::assert_gt; @@ -779,6 +781,19 @@ fn take_canister_snapshot_fails_when_canister_would_be_frozen() { let initial_subnet_available_memory = test.subnet_available_memory(); + // Taking a snapshot of the canister will decrease the balance. + // Increase the canister balance to be able to take a new snapshot. + let scheduler_config = SubnetConfig::new(SubnetType::Application).scheduler_config; + let canister_snapshot_size = test.canister_state(canister_id).snapshot_size_bytes(); + let instructions = scheduler_config.canister_snapshot_baseline_instructions + + NumInstructions::new(canister_snapshot_size.get()); + let expected_charge = test + .cycles_account_manager() + .execution_cost(instructions, test.subnet_size()); + test.canister_state_mut(canister_id) + .system_state + .add_cycles(expected_charge, CyclesUseCase::NonConsumed); + // Take a snapshot of the canister. let args: TakeCanisterSnapshotArgs = TakeCanisterSnapshotArgs::new(canister_id, None); let error = test @@ -1494,3 +1509,112 @@ fn snapshot_is_deleted_with_canister_delete() { assert!(test.state().canister_state(&canister_id).is_none()); assert!(test.state().canister_snapshots.get(snapshot_id).is_none()); } + +#[test] +fn take_canister_snapshot_charges_canister_cycles() { + const CYCLES: Cycles = Cycles::new(1_000_000_000_000_000); + const WASM_PAGE_SIZE: u64 = 65_536; + // 7500 of stable memory pages is close to 500MB, but still leaves some room + // for Wasm memory of the universal canister. + const NUM_PAGES: u64 = 7_500; + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(1); + + let subnet_type = SubnetType::Application; + let scheduler_config = SubnetConfig::new(subnet_type).scheduler_config; + + let mut test = ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_snapshots(FlagStatus::Enabled) + .with_caller(own_subnet, caller_canister) + .build(); + + // Create canister. + let canister_id = test + .canister_from_cycles_and_binary(CYCLES, UNIVERSAL_CANISTER_WASM.into()) + .unwrap(); + test.canister_update_reserved_cycles_limit(canister_id, CYCLES) + .unwrap(); + + // Increase memory usage. + grow_stable_memory(&mut test, canister_id, WASM_PAGE_SIZE, NUM_PAGES); + let canister_snapshot_size = test.canister_state(canister_id).snapshot_size_bytes(); + + let initial_balance = test.canister_state(canister_id).system_state.balance(); + let instructions = scheduler_config.canister_snapshot_baseline_instructions + + NumInstructions::new(canister_snapshot_size.get()); + + // Take a snapshot of the canister will decrease the balance. + let expected_charge = test + .cycles_account_manager() + .execution_cost(instructions, test.subnet_size()); + + // Take a snapshot for the canister. + let args: TakeCanisterSnapshotArgs = TakeCanisterSnapshotArgs::new(canister_id, None); + let result = test.subnet_message("take_canister_snapshot", args.encode()); + let snapshot_id = CanisterSnapshotResponse::decode(&result.unwrap().bytes()) + .unwrap() + .snapshot_id(); + assert!(test.state().canister_snapshots.get(snapshot_id).is_some()); + + assert_eq!( + test.canister_state(canister_id).system_state.balance(), + initial_balance - expected_charge, + ); +} + +#[test] +fn load_canister_snapshot_charges_canister_cycles() { + const CYCLES: Cycles = Cycles::new(1_000_000_000_000_000); + const WASM_PAGE_SIZE: u64 = 65_536; + // 7500 of stable memory pages is close to 500MB, but still leaves some room + // for Wasm memory of the universal canister. + const NUM_PAGES: u64 = 500; + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(1); + + let subnet_type = SubnetType::Application; + let scheduler_config = SubnetConfig::new(subnet_type).scheduler_config; + + let mut test = ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_snapshots(FlagStatus::Enabled) + .with_caller(own_subnet, caller_canister) + .build(); + + // Create canister. + let canister_id = test + .canister_from_cycles_and_binary(CYCLES, UNIVERSAL_CANISTER_WASM.into()) + .unwrap(); + test.canister_update_reserved_cycles_limit(canister_id, CYCLES) + .unwrap(); + + // Increase memory usage. + grow_stable_memory(&mut test, canister_id, WASM_PAGE_SIZE, NUM_PAGES); + // Take a snapshot for the canister. + let args: TakeCanisterSnapshotArgs = TakeCanisterSnapshotArgs::new(canister_id, None); + let result = test.subnet_message("take_canister_snapshot", args.encode()); + let snapshot_id = CanisterSnapshotResponse::decode(&result.unwrap().bytes()) + .unwrap() + .snapshot_id(); + assert!(test.state().canister_snapshots.get(snapshot_id).is_some()); + + let canister_snapshot_size = test.canister_state(canister_id).snapshot_size_bytes(); + let initial_balance = test.canister_state(canister_id).system_state.balance(); + let instructions = scheduler_config.canister_snapshot_baseline_instructions + + NumInstructions::new(canister_snapshot_size.get()); + + // Load a snapshot of the canister will decrease the balance. + let expected_charge = test + .cycles_account_manager() + .execution_cost(instructions, test.subnet_size()); + + // Load an existing snapshot will decrease the balance. + let args: LoadCanisterSnapshotArgs = + LoadCanisterSnapshotArgs::new(canister_id, snapshot_id, None); + let result = test.subnet_message("load_canister_snapshot", args.encode()); + assert!(result.is_ok()); + assert!( + test.canister_state(canister_id).system_state.balance() < initial_balance - expected_charge + ); +} diff --git a/rs/execution_environment/src/lib.rs b/rs/execution_environment/src/lib.rs index 837804f6e87..453fc81371b 100644 --- a/rs/execution_environment/src/lib.rs +++ b/rs/execution_environment/src/lib.rs @@ -142,6 +142,7 @@ impl ExecutionServices { Arc::clone(&fd_factory), scheduler_config.heap_delta_rate_limit, scheduler_config.upload_wasm_chunk_instructions, + scheduler_config.canister_snapshot_baseline_instructions, )); let sync_query_handler = Arc::new(InternalHttpQueryHandler::new( logger.clone(), diff --git a/rs/execution_environment/src/scheduler/test_utilities.rs b/rs/execution_environment/src/scheduler/test_utilities.rs index 6aacd80b351..3d969801f16 100644 --- a/rs/execution_environment/src/scheduler/test_utilities.rs +++ b/rs/execution_environment/src/scheduler/test_utilities.rs @@ -196,6 +196,12 @@ impl SchedulerTest { ) } + pub fn execution_cost(&self, num_instructions: NumInstructions) -> Cycles { + self.scheduler + .cycles_account_manager + .execution_cost(num_instructions, self.subnet_size()) + } + /// Creates a canister with the given balance and allocations. /// The `system_task` parameter can be used to optionally enable the /// heartbeat by passing `Some(SystemMethod::CanisterHeartbeat)`. @@ -897,6 +903,8 @@ impl SchedulerTestBuilder { Arc::new(TestPageAllocatorFileDescriptorImpl::new()), self.scheduler_config.heap_delta_rate_limit, self.scheduler_config.upload_wasm_chunk_instructions, + self.scheduler_config + .canister_snapshot_baseline_instructions, ); let scheduler = SchedulerImpl::new( self.scheduler_config, diff --git a/rs/execution_environment/src/scheduler/tests.rs b/rs/execution_environment/src/scheduler/tests.rs index 66e113c2cb3..ce618562852 100644 --- a/rs/execution_environment/src/scheduler/tests.rs +++ b/rs/execution_environment/src/scheduler/tests.rs @@ -23,7 +23,7 @@ use ic_management_canister_types::{ }; use ic_registry_routing_table::CanisterIdRange; use ic_registry_subnet_type::SubnetType; -use ic_replicated_state::canister_state::system_state::PausedExecutionId; +use ic_replicated_state::canister_state::system_state::{CyclesUseCase, PausedExecutionId}; use ic_replicated_state::testing::{CanisterQueuesTesting, SystemStateTesting}; use ic_state_machine_tests::{PayloadBuilder, StateMachineBuilder}; use ic_test_utilities_metrics::{ @@ -1279,6 +1279,20 @@ fn snapshot_is_deleted_when_canister_is_out_of_cycles() { 0 ); + // Taking a snapshot of the canister will decrease the balance. + // Increase the canister balance to be able to take a new snapshot. + let subnet_type = SubnetType::Application; + let scheduler_config = SubnetConfig::new(subnet_type).scheduler_config; + let canister_snapshot_size = test.canister_state(canister_id).snapshot_size_bytes(); + let instructions = scheduler_config.canister_snapshot_baseline_instructions + + NumInstructions::new(canister_snapshot_size.get()); + let expected_charge = test.execution_cost(instructions); + test.state_mut() + .canister_state_mut(&canister_id) + .unwrap() + .system_state + .add_cycles(expected_charge, CyclesUseCase::NonConsumed); + // Take a snapshot of the canister. let args: TakeCanisterSnapshotArgs = TakeCanisterSnapshotArgs::new(canister_id, None); test.inject_call_to_ic00( @@ -1370,13 +1384,27 @@ fn snapshot_is_deleted_when_uninstalled_canister_is_out_of_cycles() { .len(), 0 ); - assert!(test .state() .canister_state(&canister_id) .unwrap() .execution_state .is_some()); + + // Taking a snapshot of the canister will decrease the balance. + // Increase the canister balance to be able to take a new snapshot. + let subnet_type = SubnetType::Application; + let scheduler_config = SubnetConfig::new(subnet_type).scheduler_config; + let canister_snapshot_size = test.canister_state(canister_id).snapshot_size_bytes(); + let instructions = scheduler_config.canister_snapshot_baseline_instructions + + NumInstructions::new(canister_snapshot_size.get()); + let expected_charge = test.execution_cost(instructions); + test.state_mut() + .canister_state_mut(&canister_id) + .unwrap() + .system_state + .add_cycles(expected_charge, CyclesUseCase::NonConsumed); + // Take a snapshot of the canister. let args: TakeCanisterSnapshotArgs = TakeCanisterSnapshotArgs::new(canister_id, None); test.inject_call_to_ic00( diff --git a/rs/test_utilities/execution_environment/src/lib.rs b/rs/test_utilities/execution_environment/src/lib.rs index 53560c24c2c..b87014bf5bd 100644 --- a/rs/test_utilities/execution_environment/src/lib.rs +++ b/rs/test_utilities/execution_environment/src/lib.rs @@ -1626,6 +1626,7 @@ pub struct ExecutionTestBuilder { resource_saturation_scaling: usize, heap_delta_rate_limit: NumBytes, upload_wasm_chunk_instructions: NumInstructions, + canister_snapshot_baseline_instructions: NumInstructions, } impl Default for ExecutionTestBuilder { @@ -1665,6 +1666,8 @@ impl Default for ExecutionTestBuilder { resource_saturation_scaling: 1, heap_delta_rate_limit: scheduler_config.heap_delta_rate_limit, upload_wasm_chunk_instructions: scheduler_config.upload_wasm_chunk_instructions, + canister_snapshot_baseline_instructions: scheduler_config + .canister_snapshot_baseline_instructions, } } } @@ -2192,6 +2195,7 @@ impl ExecutionTestBuilder { Arc::new(TestPageAllocatorFileDescriptorImpl::new()), self.heap_delta_rate_limit, self.upload_wasm_chunk_instructions, + self.canister_snapshot_baseline_instructions, ); let (query_stats_collector, _) = ic_query_stats::init_query_stats(self.log.clone(), &config, &metrics_registry);