Skip to content

Commit b9e4937

Browse files
ulanlwshang
andauthored
feat: Add wasm_memory_limit to canister settings (#548)
* feat: Add `wasm_memory_limit` to canister settings Part of https://dfinity.atlassian.net/browse/SDK-1588 * use grouping --------- Co-authored-by: Linwei Shang <[email protected]>
1 parent 7840c34 commit b9e4937

File tree

7 files changed

+241
-1
lines changed

7 files changed

+241
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
regardless the Agent level configuration from `AgentBuilder::with_verify_query_signatures`.
2121
* Function `Agent::fetch_api_boundary_nodes()` is split into two functions: `fetch_api_boundary_nodes_by_canister_id()` and `fetch_api_boundary_nodes_by_subnet_id()`.
2222
* `ReqwestTransport` and `HyperTransport` structures storing the trait object `route_provider: Box<dyn RouteProvider>` have been modified to allow for shared ownership via `Arc<dyn RouteProvider>`.
23+
* Added `wasm_memory_limit` to canister creation and canister setting update options.
2324

2425
## [0.34.0] - 2024-03-18
2526

ic-utils/src/interfaces/management_canister.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ pub struct DefiniteCanisterSettings {
157157
pub freezing_threshold: Nat,
158158
/// The upper limit of the canister's reserved cycles balance.
159159
pub reserved_cycles_limit: Option<Nat>,
160+
/// A soft limit on the Wasm memory usage of the canister in bytes (up to 256TiB).
161+
pub wasm_memory_limit: Option<Nat>,
160162
}
161163

162164
impl std::fmt::Display for StatusCallResult {

ic-utils/src/interfaces/management_canister/attributes.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,53 @@ try_from_reserved_cycles_limit_decl!(i64);
207207
try_from_reserved_cycles_limit_decl!(i128);
208208
try_from_reserved_cycles_limit_decl!(u128);
209209

210+
/// An error encountered when attempting to construct a [`WasmMemoryLimit`].
211+
#[derive(Error, Debug)]
212+
pub enum WasmMemoryLimitError {
213+
/// The provided value was not in the range [0, 2^48] (i.e. 256 TiB).
214+
#[error("Wasm memory limit must be between 0 and 2^48 (i.e 256TiB), inclusively. Got {0}.")]
215+
InvalidMemoryLimit(i64),
216+
}
217+
218+
/// A soft limit on the Wasm memory usage of the canister. Update calls,
219+
/// timers, heartbeats, install, and post-upgrade fail if the Wasm memory
220+
/// usage exceeds this limit. The main purpose of this field is to protect
221+
/// against the case when the canister reaches the hard 4GiB limit.
222+
/// Must be a number between 0 and 2^48^ (i.e 256TB), inclusively.
223+
#[derive(Copy, Clone, Debug)]
224+
pub struct WasmMemoryLimit(u64);
225+
226+
impl std::convert::From<WasmMemoryLimit> for u64 {
227+
fn from(wasm_memory_limit: WasmMemoryLimit) -> Self {
228+
wasm_memory_limit.0
229+
}
230+
}
231+
232+
macro_rules! try_from_wasm_memory_limit_decl {
233+
( $t: ty ) => {
234+
impl std::convert::TryFrom<$t> for WasmMemoryLimit {
235+
type Error = WasmMemoryLimitError;
236+
237+
fn try_from(value: $t) -> Result<Self, Self::Error> {
238+
if (value as i64) < 0 || (value as i64) > (1i64 << 48) {
239+
Err(Self::Error::InvalidMemoryLimit(value as i64))
240+
} else {
241+
Ok(Self(value as u64))
242+
}
243+
}
244+
}
245+
};
246+
}
247+
248+
try_from_wasm_memory_limit_decl!(u8);
249+
try_from_wasm_memory_limit_decl!(u16);
250+
try_from_wasm_memory_limit_decl!(u32);
251+
try_from_wasm_memory_limit_decl!(u64);
252+
try_from_wasm_memory_limit_decl!(i8);
253+
try_from_wasm_memory_limit_decl!(i16);
254+
try_from_wasm_memory_limit_decl!(i32);
255+
try_from_wasm_memory_limit_decl!(i64);
256+
210257
#[test]
211258
#[allow(clippy::useless_conversion)]
212259
fn can_convert_compute_allocation() {
@@ -296,3 +343,32 @@ fn can_convert_reserved_cycles_limit() {
296343
let ft = ReservedCyclesLimit(100);
297344
let _ft_ft: ReservedCyclesLimit = ReservedCyclesLimit::try_from(ft).unwrap();
298345
}
346+
347+
#[test]
348+
#[allow(clippy::useless_conversion)]
349+
fn can_convert_wasm_memory_limit() {
350+
use std::convert::{TryFrom, TryInto};
351+
352+
// This is more of a compiler test than an actual test.
353+
let _ma_u8: WasmMemoryLimit = 1u8.try_into().unwrap();
354+
let _ma_u16: WasmMemoryLimit = 1u16.try_into().unwrap();
355+
let _ma_u32: WasmMemoryLimit = 1u32.try_into().unwrap();
356+
let _ma_u64: WasmMemoryLimit = 1u64.try_into().unwrap();
357+
let _ma_i8: WasmMemoryLimit = 1i8.try_into().unwrap();
358+
let _ma_i16: WasmMemoryLimit = 1i16.try_into().unwrap();
359+
let _ma_i32: WasmMemoryLimit = 1i32.try_into().unwrap();
360+
let _ma_i64: WasmMemoryLimit = 1i64.try_into().unwrap();
361+
362+
let ma = WasmMemoryLimit(100);
363+
let _ma_ma: WasmMemoryLimit = WasmMemoryLimit::try_from(ma).unwrap();
364+
365+
assert!(matches!(
366+
WasmMemoryLimit::try_from(-4).unwrap_err(),
367+
WasmMemoryLimitError::InvalidMemoryLimit(-4)
368+
));
369+
370+
assert!(matches!(
371+
WasmMemoryLimit::try_from(562949953421312_u64).unwrap_err(),
372+
WasmMemoryLimitError::InvalidMemoryLimit(562949953421312)
373+
));
374+
}

ic-utils/src/interfaces/management_canister/builders.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
#[doc(inline)]
44
pub use super::attributes::{
5-
ComputeAllocation, FreezingThreshold, MemoryAllocation, ReservedCyclesLimit,
5+
ComputeAllocation, FreezingThreshold, MemoryAllocation, ReservedCyclesLimit, WasmMemoryLimit,
66
};
77
use super::{ChunkHash, ManagementCanister};
88
use crate::{
@@ -62,6 +62,16 @@ pub struct CanisterSettings {
6262
/// If set to 0, disables the reservation mechanism for the canister.
6363
/// Doing so will cause the canister to trap when it tries to allocate storage, if the subnet's usage exceeds 450 GiB.
6464
pub reserved_cycles_limit: Option<Nat>,
65+
66+
/// A soft limit on the Wasm memory usage of the canister.
67+
///
68+
/// Update calls, timers, heartbeats, install, and post-upgrade fail if the
69+
/// Wasm memory usage exceeds this limit. The main purpose of this field is
70+
/// to protect against the case when the canister reaches the hard 4GiB
71+
/// limit.
72+
///
73+
/// Must be a number between 0 and 2^48^ (i.e 256TB), inclusively.
74+
pub wasm_memory_limit: Option<Nat>,
6575
}
6676

6777
/// A builder for a `create_canister` call.
@@ -74,6 +84,7 @@ pub struct CreateCanisterBuilder<'agent, 'canister: 'agent> {
7484
memory_allocation: Option<Result<MemoryAllocation, AgentError>>,
7585
freezing_threshold: Option<Result<FreezingThreshold, AgentError>>,
7686
reserved_cycles_limit: Option<Result<ReservedCyclesLimit, AgentError>>,
87+
wasm_memory_limit: Option<Result<WasmMemoryLimit, AgentError>>,
7788
is_provisional_create: bool,
7889
amount: Option<u128>,
7990
specified_id: Option<Principal>,
@@ -90,6 +101,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
90101
memory_allocation: None,
91102
freezing_threshold: None,
92103
reserved_cycles_limit: None,
104+
wasm_memory_limit: None,
93105
is_provisional_create: false,
94106
amount: None,
95107
specified_id: None,
@@ -278,6 +290,32 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
278290
}
279291
}
280292

293+
/// Pass in a Wasm memory limit value for the canister.
294+
pub fn with_wasm_memory_limit<C, E>(self, wasm_memory_limit: C) -> Self
295+
where
296+
E: std::fmt::Display,
297+
C: TryInto<WasmMemoryLimit, Error = E>,
298+
{
299+
self.with_optional_wasm_memory_limit(Some(wasm_memory_limit))
300+
}
301+
302+
/// Pass in a Wasm memory limit optional value for the canister. If this is [None],
303+
/// it will revert the Wasm memory limit to default.
304+
pub fn with_optional_wasm_memory_limit<E, C>(self, wasm_memory_limit: Option<C>) -> Self
305+
where
306+
E: std::fmt::Display,
307+
C: TryInto<WasmMemoryLimit, Error = E>,
308+
{
309+
Self {
310+
wasm_memory_limit: wasm_memory_limit.map(|limit| {
311+
limit
312+
.try_into()
313+
.map_err(|e| AgentError::MessageError(format!("{}", e)))
314+
}),
315+
..self
316+
}
317+
}
318+
281319
/// Create an [AsyncCall] implementation that, when called, will create a
282320
/// canister.
283321
pub fn build(self) -> Result<impl 'agent + AsyncCall<(Principal,)>, AgentError> {
@@ -306,6 +344,11 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
306344
Some(Ok(x)) => Some(Nat::from(u128::from(x))),
307345
None => None,
308346
};
347+
let wasm_memory_limit = match self.wasm_memory_limit {
348+
Some(Err(x)) => return Err(AgentError::MessageError(format!("{}", x))),
349+
Some(Ok(x)) => Some(Nat::from(u64::from(x))),
350+
None => None,
351+
};
309352

310353
#[derive(Deserialize, CandidType)]
311354
struct Out {
@@ -327,6 +370,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
327370
memory_allocation,
328371
freezing_threshold,
329372
reserved_cycles_limit,
373+
wasm_memory_limit,
330374
},
331375
specified_id: self.specified_id,
332376
};
@@ -343,6 +387,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
343387
memory_allocation,
344388
freezing_threshold,
345389
reserved_cycles_limit,
390+
wasm_memory_limit,
346391
})
347392
.with_effective_canister_id(self.effective_canister_id)
348393
};
@@ -808,6 +853,7 @@ pub struct UpdateCanisterBuilder<'agent, 'canister: 'agent> {
808853
memory_allocation: Option<Result<MemoryAllocation, AgentError>>,
809854
freezing_threshold: Option<Result<FreezingThreshold, AgentError>>,
810855
reserved_cycles_limit: Option<Result<ReservedCyclesLimit, AgentError>>,
856+
wasm_memory_limit: Option<Result<WasmMemoryLimit, AgentError>>,
811857
}
812858

813859
impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
@@ -821,6 +867,7 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
821867
memory_allocation: None,
822868
freezing_threshold: None,
823869
reserved_cycles_limit: None,
870+
wasm_memory_limit: None,
824871
}
825872
}
826873

@@ -963,6 +1010,32 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
9631010
}
9641011
}
9651012

1013+
/// Pass in a Wasm memory limit value for the canister.
1014+
pub fn with_wasm_memory_limit<C, E>(self, wasm_memory_limit: C) -> Self
1015+
where
1016+
E: std::fmt::Display,
1017+
C: TryInto<WasmMemoryLimit, Error = E>,
1018+
{
1019+
self.with_optional_wasm_memory_limit(Some(wasm_memory_limit))
1020+
}
1021+
1022+
/// Pass in a Wasm memory limit optional value for the canister. If this is [None],
1023+
/// leaves the Wasm memory limit unchanged.
1024+
pub fn with_optional_wasm_memory_limit<E, C>(self, wasm_memory_limit: Option<C>) -> Self
1025+
where
1026+
E: std::fmt::Display,
1027+
C: TryInto<WasmMemoryLimit, Error = E>,
1028+
{
1029+
Self {
1030+
wasm_memory_limit: wasm_memory_limit.map(|limit| {
1031+
limit
1032+
.try_into()
1033+
.map_err(|e| AgentError::MessageError(format!("{}", e)))
1034+
}),
1035+
..self
1036+
}
1037+
}
1038+
9661039
/// Create an [AsyncCall] implementation that, when called, will update a
9671040
/// canisters settings.
9681041
pub fn build(self) -> Result<impl 'agent + AsyncCall<()>, AgentError> {
@@ -997,6 +1070,11 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
9971070
Some(Ok(x)) => Some(Nat::from(u128::from(x))),
9981071
None => None,
9991072
};
1073+
let wasm_memory_limit = match self.wasm_memory_limit {
1074+
Some(Err(x)) => return Err(AgentError::MessageError(format!("{}", x))),
1075+
Some(Ok(x)) => Some(Nat::from(u64::from(x))),
1076+
None => None,
1077+
};
10001078

10011079
Ok(self
10021080
.canister
@@ -1009,6 +1087,7 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
10091087
memory_allocation,
10101088
freezing_threshold,
10111089
reserved_cycles_limit,
1090+
wasm_memory_limit,
10121091
},
10131092
})
10141093
.with_effective_canister_id(self.canister_id)

ic-utils/src/interfaces/wallet.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,7 @@ impl<'agent> WalletCanister<'agent> {
658658
memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
659659
freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
660660
reserved_cycles_limit: None,
661+
wasm_memory_limit: None,
661662
};
662663

663664
self.update("wallet_create_canister")
@@ -687,6 +688,7 @@ impl<'agent> WalletCanister<'agent> {
687688
memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
688689
freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
689690
reserved_cycles_limit: None,
691+
wasm_memory_limit: None,
690692
};
691693

692694
self.update("wallet_create_canister128")
@@ -700,6 +702,10 @@ impl<'agent> WalletCanister<'agent> {
700702
/// This method does not have a `reserved_cycles_limit` parameter,
701703
/// as the wallet does not support the setting. If you need to create a canister
702704
/// with a `reserved_cycles_limit` set, use the management canister.
705+
///
706+
/// This method does not have a `wasm_memory_limit` parameter,
707+
/// as the wallet does not support the setting. If you need to create a canister
708+
/// with a `wasm_memory_limit` set, use the management canister.
703709
pub async fn wallet_create_canister(
704710
&self,
705711
cycles: u128,
@@ -810,6 +816,7 @@ impl<'agent> WalletCanister<'agent> {
810816
memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
811817
freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
812818
reserved_cycles_limit: None,
819+
wasm_memory_limit: None,
813820
};
814821

815822
self.update("wallet_create_wallet")
@@ -839,6 +846,7 @@ impl<'agent> WalletCanister<'agent> {
839846
memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
840847
freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
841848
reserved_cycles_limit: None,
849+
wasm_memory_limit: None,
842850
};
843851

844852
self.update("wallet_create_wallet128")

0 commit comments

Comments
 (0)