Skip to content

Commit 54b7f0e

Browse files
authored
refactor: Add memory usage checks when loading a snapshot for consistency (dfinity#3789)
This PR extracts some memory related checks in a common function that can be used across both `take_canister_snapshot` and `load_canister_snapshot` for consistency.
1 parent 89702c7 commit 54b7f0e

File tree

2 files changed

+130
-95
lines changed

2 files changed

+130
-95
lines changed

rs/execution_environment/src/canister_manager.rs

Lines changed: 125 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ use ic_types::{
6060
use ic_wasm_transform::Module;
6161
use ic_wasm_types::{doc_ref, AsErrorHelp, CanisterModule, ErrorHelp, WasmHash};
6262
use num_traits::cast::ToPrimitive;
63-
use num_traits::SaturatingAdd;
63+
use num_traits::{SaturatingAdd, SaturatingSub};
6464
use prometheus::IntCounter;
6565
use serde::{Deserialize, Serialize};
6666
use std::path::PathBuf;
@@ -1790,6 +1790,94 @@ impl CanisterManager {
17901790
Ok(StoredChunksReply(keys))
17911791
}
17921792

1793+
// Runs the following checks on memory usage and return an error
1794+
// if any fails:
1795+
// 1. Check new usage will not freeze canister
1796+
// 2. Check subnet has available memory
1797+
1798+
// Additionally calculates if any cycles will need to be reserved.
1799+
//
1800+
// This is to be used when taking or loading a canister snapshot
1801+
// to ensure consistency in checks.
1802+
fn memory_usage_checks(
1803+
&self,
1804+
subnet_size: usize,
1805+
canister: &mut CanisterState,
1806+
round_limits: &RoundLimits,
1807+
new_memory_usage: NumBytes,
1808+
old_memory_usage: NumBytes,
1809+
resource_saturation: &ResourceSaturation,
1810+
) -> Result<(), CanisterManagerError> {
1811+
let memory_increase = new_memory_usage.saturating_sub(&old_memory_usage);
1812+
1813+
let reservation_cycles = self.cycles_account_manager.storage_reservation_cycles(
1814+
memory_increase,
1815+
resource_saturation,
1816+
subnet_size,
1817+
);
1818+
1819+
// Check that the canister does not exceed its freezing threshold based
1820+
// on the new memory usage.
1821+
let threshold = self.cycles_account_manager.freeze_threshold_cycles(
1822+
canister.system_state.freeze_threshold,
1823+
canister.memory_allocation(),
1824+
new_memory_usage,
1825+
canister.message_memory_usage(),
1826+
canister.compute_allocation(),
1827+
subnet_size,
1828+
canister.system_state.reserved_balance(),
1829+
);
1830+
1831+
if canister.system_state.balance() < threshold + reservation_cycles {
1832+
return Err(CanisterManagerError::InsufficientCyclesInMemoryGrow {
1833+
bytes: memory_increase,
1834+
available: canister.system_state.balance(),
1835+
required: threshold + reservation_cycles,
1836+
});
1837+
}
1838+
1839+
// Verify that the subnet has enough memory available to satisfy the
1840+
// requested change by the canister.
1841+
round_limits
1842+
.subnet_available_memory
1843+
.check_available_memory(memory_increase, NumBytes::from(0), NumBytes::from(0))
1844+
.map_err(
1845+
|_| CanisterManagerError::SubnetMemoryCapacityOverSubscribed {
1846+
requested: memory_increase,
1847+
available: NumBytes::from(
1848+
round_limits
1849+
.subnet_available_memory
1850+
.get_execution_memory()
1851+
.max(0) as u64,
1852+
),
1853+
},
1854+
)?;
1855+
1856+
// Reserve needed cycles if the subnet is becoming saturated.
1857+
canister
1858+
.system_state
1859+
.reserve_cycles(reservation_cycles)
1860+
.map_err(|err| match err {
1861+
ReservationError::InsufficientCycles {
1862+
requested,
1863+
available,
1864+
} => CanisterManagerError::InsufficientCyclesInMemoryGrow {
1865+
bytes: memory_increase,
1866+
available,
1867+
required: requested,
1868+
},
1869+
ReservationError::ReservedLimitExceed { requested, limit } => {
1870+
CanisterManagerError::ReservedCyclesLimitExceededInMemoryGrow {
1871+
bytes: memory_increase,
1872+
requested,
1873+
limit,
1874+
}
1875+
}
1876+
})?;
1877+
1878+
Ok(())
1879+
}
1880+
17931881
/// Creates a new canister snapshot.
17941882
///
17951883
/// A canister snapshot can only be initiated by the controllers.
@@ -1880,98 +1968,21 @@ impl CanisterManager {
18801968
}
18811969

18821970
let new_snapshot_size = canister.snapshot_size_bytes();
1883-
let new_snapshot_increase = NumBytes::from(
1884-
new_snapshot_size
1885-
.get()
1886-
.saturating_sub(replace_snapshot_size.get()),
1887-
);
1888-
let new_memory_usage = NumBytes::from(
1889-
canister
1890-
.memory_usage()
1891-
.get()
1892-
.saturating_add(new_snapshot_size.get())
1893-
.saturating_sub(replace_snapshot_size.get()),
1894-
);
18951971

1896-
{
1897-
// Run the following checks on memory usage and return an error
1898-
// if any fails:
1899-
// 1. Check new usage will not freeze canister
1900-
// 2. Check subnet has available memory
1901-
// 3. Reserve cycles on canister
1902-
// 4. Actually deduct memory from subnet (asserting it won't fail)
1903-
1904-
// Calculate if any cycles will need to be reserved.
1905-
let reservation_cycles = self.cycles_account_manager.storage_reservation_cycles(
1906-
new_snapshot_increase,
1907-
resource_saturation,
1908-
subnet_size,
1909-
);
1910-
1911-
// Memory usage will increase by the snapshot size.
1912-
// Check that it doesn't bump the canister over the freezing threshold.
1913-
let threshold = self.cycles_account_manager.freeze_threshold_cycles(
1914-
canister.system_state.freeze_threshold,
1915-
canister.memory_allocation(),
1916-
new_memory_usage,
1917-
canister.message_memory_usage(),
1918-
canister.compute_allocation(),
1919-
subnet_size,
1920-
canister.system_state.reserved_balance(),
1921-
);
1922-
1923-
if canister.system_state.balance() < threshold + reservation_cycles {
1924-
return (
1925-
Err(CanisterManagerError::InsufficientCyclesInMemoryGrow {
1926-
bytes: new_snapshot_increase,
1927-
available: canister.system_state.balance(),
1928-
required: threshold + reservation_cycles,
1929-
}),
1930-
NumInstructions::new(0),
1931-
);
1932-
}
1933-
// Verify that the subnet has enough memory for a new snapshot.
1934-
if let Err(err) = round_limits
1935-
.subnet_available_memory
1936-
.check_available_memory(new_snapshot_increase, NumBytes::from(0), NumBytes::from(0))
1937-
.map_err(
1938-
|_| CanisterManagerError::SubnetMemoryCapacityOverSubscribed {
1939-
requested: new_snapshot_increase,
1940-
available: NumBytes::from(
1941-
round_limits
1942-
.subnet_available_memory
1943-
.get_execution_memory()
1944-
.max(0) as u64,
1945-
),
1946-
},
1947-
)
1948-
{
1949-
return (Err(err), NumInstructions::new(0));
1950-
};
1951-
// Reserve needed cycles if the subnet is becoming saturated.
1952-
if let Err(err) = canister
1953-
.system_state
1954-
.reserve_cycles(reservation_cycles)
1955-
.map_err(|err| match err {
1956-
ReservationError::InsufficientCycles {
1957-
requested,
1958-
available,
1959-
} => CanisterManagerError::InsufficientCyclesInMemoryGrow {
1960-
bytes: new_snapshot_increase,
1961-
available,
1962-
required: requested,
1963-
},
1964-
ReservationError::ReservedLimitExceed { requested, limit } => {
1965-
CanisterManagerError::ReservedCyclesLimitExceededInMemoryGrow {
1966-
bytes: new_snapshot_increase,
1967-
requested,
1968-
limit,
1969-
}
1970-
}
1971-
})
1972-
{
1973-
return (Err(err), NumInstructions::new(0));
1974-
};
1972+
let old_memory_usage = canister.memory_usage();
1973+
let new_memory_usage = canister
1974+
.memory_usage()
1975+
.saturating_add(&new_snapshot_size)
1976+
.saturating_sub(&replace_snapshot_size);
1977+
if let Err(err) = self.memory_usage_checks(
1978+
subnet_size,
1979+
canister,
1980+
round_limits,
1981+
new_memory_usage,
1982+
old_memory_usage,
1983+
resource_saturation,
1984+
) {
1985+
return (Err(err), NumInstructions::from(0));
19751986
}
19761987

19771988
// Charge for taking a snapshot of the canister.
@@ -2068,11 +2079,12 @@ impl CanisterManager {
20682079
&self,
20692080
subnet_size: usize,
20702081
sender: PrincipalId,
2071-
canister: &CanisterState,
2082+
canister: &mut CanisterState,
20722083
snapshot_id: SnapshotId,
20732084
state: &mut ReplicatedState,
20742085
round_limits: &mut RoundLimits,
20752086
origin: CanisterChangeOrigin,
2087+
resource_saturation: &ResourceSaturation,
20762088
long_execution_already_in_progress: &IntCounter,
20772089
) -> (Result<CanisterState, CanisterManagerError>, NumInstructions) {
20782090
let canister_id = canister.canister_id();
@@ -2140,6 +2152,7 @@ impl CanisterManager {
21402152
}
21412153

21422154
// All basic checks have passed, charge baseline instructions.
2155+
let old_memory_usage = canister.memory_usage();
21432156
let mut canister_clone = canister.clone();
21442157

21452158
if let Err(err) = self.cycles_account_manager.consume_cycles_for_instructions(
@@ -2224,6 +2237,25 @@ impl CanisterManager {
22242237
);
22252238
}
22262239

2240+
if let Err(err) = self.memory_usage_checks(
2241+
subnet_size,
2242+
canister,
2243+
round_limits,
2244+
new_memory_usage,
2245+
old_memory_usage,
2246+
resource_saturation,
2247+
) {
2248+
return (Err(err), instructions_used);
2249+
}
2250+
2251+
// Actually deduct memory from the subnet. It's safe to unwrap
2252+
// here because we already checked the available memory above.
2253+
round_limits.subnet_available_memory.try_decrement(
2254+
new_memory_usage.saturating_sub(&old_memory_usage),
2255+
NumBytes::from(0),
2256+
NumBytes::from(0),
2257+
).expect("Error: Cannot fail to decrement SubnetAvailableMemory after checking for availability");
2258+
22272259
// Charge for loading the snapshot of the canister.
22282260
if let Err(err) = self.cycles_account_manager.consume_cycles_for_instructions(
22292261
&sender,

rs/execution_environment/src/execution_environment.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,7 +2173,7 @@ impl ExecutionEnvironment {
21732173
) -> (Result<Vec<u8>, UserError>, NumInstructions) {
21742174
let canister_id = args.get_canister_id();
21752175
// Take canister out.
2176-
let old_canister = match state.take_canister_state(&canister_id) {
2176+
let mut old_canister = match state.take_canister_state(&canister_id) {
21772177
None => {
21782178
return (
21792179
Err(UserError::new(
@@ -2187,14 +2187,17 @@ impl ExecutionEnvironment {
21872187
};
21882188

21892189
let snapshot_id = args.snapshot_id();
2190+
let resource_saturation =
2191+
self.subnet_memory_saturation(&round_limits.subnet_available_memory);
21902192
let (result, instructions_used) = self.canister_manager.load_canister_snapshot(
21912193
subnet_size,
21922194
sender,
2193-
&old_canister,
2195+
&mut old_canister,
21942196
snapshot_id,
21952197
state,
21962198
round_limits,
21972199
origin,
2200+
&resource_saturation,
21982201
&self.metrics.long_execution_already_in_progress,
21992202
);
22002203

0 commit comments

Comments
 (0)