From a8a7d4e5bd9707c69fca41986b9fe7798fa7d58f Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 8 Aug 2024 13:26:59 -0400 Subject: [PATCH] feat!: canister state snapshots (#504) * snapshot types * snapshot methods * types cont. * update pocket-ic which support nonmainnet features * e2e test * add LoadSnapshot variant to CanisterChangeDetails * changelog * fix canister_info e2e test * show canister_info after snapshot operations * ci test show output * revert canister_info.rs * revert ci.yml * canister_info changes can have load_snapshot records --- Cargo.lock | 82 +++++++++++++------ e2e-tests/Cargo.toml | 2 +- e2e-tests/canisters/management_caller.rs | 68 +++++++++++++++ e2e-tests/tests/e2e.rs | 44 +++++++--- scripts/download_pocket_ic.sh | 2 +- src/ic-cdk/CHANGELOG.md | 10 +++ .../src/api/management_canister/main/mod.rs | 54 ++++++++++++ .../src/api/management_canister/main/types.rs | 69 ++++++++++++++++ 8 files changed, 295 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3d3e46d0..686acd2b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1513,6 +1513,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.3" @@ -1524,13 +1534,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ + "hermit-abi", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1584,16 +1595,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" version = "0.32.2" @@ -1724,11 +1725,10 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "pocket-ic" -version = "3.1.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9765eeff77b8750cf6258eaeea237b96607cd770aa3d4003f021924192b7e4e" +checksum = "629f46b7ab8a8d2fee02220ef8e99ae552c7e220117efa1ce0882ff09c8fb038" dependencies = [ - "async-trait", "base64 0.13.1", "candid", "hex", @@ -1738,6 +1738,8 @@ dependencies = [ "serde", "serde_bytes", "serde_json", + "sha2", + "tokio", "tracing", "tracing-appender", "tracing-subscriber", @@ -1912,6 +1914,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", @@ -2147,9 +2150,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.201" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] @@ -2175,9 +2178,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -2270,6 +2273,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2513,18 +2525,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", - "windows-sys 0.48.0", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", ] [[package]] @@ -2742,6 +2767,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 2466e94a2..ac646bb3b 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -53,4 +53,4 @@ path = "canisters/chunk.rs" [dev-dependencies] hex.workspace = true -pocket-ic = "3" +pocket-ic = "4" diff --git a/e2e-tests/canisters/management_caller.rs b/e2e-tests/canisters/management_caller.rs index db662d6b4..6c9ee51ba 100644 --- a/e2e-tests/canisters/management_caller.rs +++ b/e2e-tests/canisters/management_caller.rs @@ -109,4 +109,72 @@ mod provisional { } } +mod snapshot { + use super::*; + use ic_cdk::api::management_canister::main::*; + + #[update] + async fn execute_snapshot_methods() { + let arg = CreateCanisterArgument::default(); + let canister_id = create_canister(arg, 2_000_000_000_000u128) + .await + .unwrap() + .0 + .canister_id; + + // Cannot take a snapshot of a canister that is empty. + // So we install a minimal wasm module. + let arg = InstallCodeArgument { + mode: CanisterInstallMode::Install, + canister_id, + // A minimal valid wasm module + // wat2wasm "(module)" + wasm_module: b"\x00asm\x01\x00\x00\x00".to_vec(), + arg: vec![], + }; + install_code(arg).await.unwrap(); + + let arg = TakeCanisterSnapshotArgs { + canister_id, + replace_snapshot: None, + }; + let snapshot = take_canister_snapshot(arg).await.unwrap().0; + + let arg = LoadCanisterSnapshotArgs { + canister_id, + snapshot_id: snapshot.id.clone(), + sender_canister_version: None, + }; + assert!(load_canister_snapshot(arg).await.is_ok()); + + let canister_id_record = CanisterIdRecord { canister_id }; + let snapshots = list_canister_snapshots(canister_id_record).await.unwrap().0; + assert_eq!(snapshots.len(), 1); + assert_eq!(snapshots[0].id, snapshot.id); + + let arg = DeleteCanisterSnapshotArgs { + canister_id, + snapshot_id: snapshot.id.clone(), + }; + assert!(delete_canister_snapshot(arg).await.is_ok()); + + let arg = CanisterInfoRequest { + canister_id, + num_requested_changes: Some(1), + }; + let canister_info_response = canister_info(arg).await.unwrap().0; + assert_eq!(canister_info_response.total_num_changes, 3); + assert_eq!(canister_info_response.recent_changes.len(), 1); + if let CanisterChange { + details: CanisterChangeDetails::LoadSnapshot(load_snapshot_record), + .. + } = &canister_info_response.recent_changes[0] + { + assert_eq!(load_snapshot_record.snapshot_id, snapshot.id); + } else { + panic!("Expected the most recent change to be LoadSnapshot"); + } + } +} + fn main() {} diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index 54a4f652c..57268ae5f 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -1,5 +1,6 @@ use std::time::Duration; use std::time::SystemTime; +use std::time::UNIX_EPOCH; use candid::utils::ArgumentDecoder; use candid::utils::ArgumentEncoder; @@ -13,6 +14,7 @@ use ic_cdk::api::management_canister::main::{ }; use ic_cdk_e2e_tests::cargo_build_canister; use pocket_ic::common::rest::RawEffectivePrincipal; +use pocket_ic::PocketIcBuilder; use pocket_ic::{call_candid_as, query_candid, CallError, ErrorCode, PocketIc, WasmResult}; use serde_bytes::ByteBuf; @@ -334,7 +336,15 @@ fn test_set_global_timers() { fn test_canister_info() { let pic = PocketIc::new(); let wasm = cargo_build_canister("canister_info"); - pic.set_time(SystemTime::UNIX_EPOCH); + // As of PocketIC server v5.0.0 and client v4.0.0, the first canister creation happens at (time0+4). + // Each operation advances the Pic by 2 nanos, except for the last operation which advances only by 1 nano. + let time0: u64 = pic + .get_time() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + .try_into() + .unwrap(); let canister_id = pic.create_canister(); pic.add_cycles(canister_id, INIT_CYCLES); pic.install_canister(canister_id, wasm, vec![], None); @@ -377,7 +387,7 @@ fn test_canister_info() { total_num_changes: 9, recent_changes: vec![ CanisterChange { - timestamp_nanos: 4, + timestamp_nanos: time0 + 4, canister_version: 0, origin: CanisterChangeOrigin::FromCanister(FromCanisterRecord { canister_id, @@ -388,7 +398,7 @@ fn test_canister_info() { }), }, CanisterChange { - timestamp_nanos: 6, + timestamp_nanos: time0 + 6, canister_version: 1, origin: CanisterChangeOrigin::FromCanister(FromCanisterRecord { canister_id, @@ -403,7 +413,7 @@ fn test_canister_info() { }), }, CanisterChange { - timestamp_nanos: 8, + timestamp_nanos: time0 + 8, canister_version: 2, origin: CanisterChangeOrigin::FromCanister(FromCanisterRecord { canister_id, @@ -412,7 +422,7 @@ fn test_canister_info() { details: CanisterChangeDetails::CodeUninstall, }, CanisterChange { - timestamp_nanos: 10, + timestamp_nanos: time0 + 10, canister_version: 3, origin: CanisterChangeOrigin::FromCanister(FromCanisterRecord { canister_id, @@ -427,7 +437,7 @@ fn test_canister_info() { }), }, CanisterChange { - timestamp_nanos: 12, + timestamp_nanos: time0 + 12, canister_version: 4, origin: CanisterChangeOrigin::FromCanister(FromCanisterRecord { canister_id, @@ -442,7 +452,7 @@ fn test_canister_info() { }), }, CanisterChange { - timestamp_nanos: 14, + timestamp_nanos: time0 + 14, canister_version: 5, origin: CanisterChangeOrigin::FromCanister(FromCanisterRecord { canister_id, @@ -457,7 +467,7 @@ fn test_canister_info() { }), }, CanisterChange { - timestamp_nanos: 16, + timestamp_nanos: time0 + 16, canister_version: 6, origin: CanisterChangeOrigin::FromCanister(FromCanisterRecord { canister_id, @@ -468,7 +478,7 @@ fn test_canister_info() { }), }, CanisterChange { - timestamp_nanos: 18, + timestamp_nanos: time0 + 18, canister_version: 7, origin: CanisterChangeOrigin::FromUser(FromUserRecord { user_id: Principal::anonymous(), @@ -476,7 +486,7 @@ fn test_canister_info() { details: CanisterChangeDetails::CodeUninstall, }, CanisterChange { - timestamp_nanos: 19, + timestamp_nanos: time0 + 19, canister_version: 8, origin: CanisterChangeOrigin::FromUser(FromUserRecord { user_id: Principal::anonymous(), @@ -543,6 +553,20 @@ fn test_call_management() { .expect("Error calling execute_provisional_methods"); } +#[test] +fn test_snapshot() { + let pic = PocketIcBuilder::new() + .with_application_subnet() + .with_nonmainnet_features(true) + .build(); + let wasm = cargo_build_canister("management_caller"); + let canister_id = pic.create_canister(); + pic.add_cycles(canister_id, 300_000_000_000_000_000_000_000_000u128); + pic.install_canister(canister_id, wasm, vec![], None); + let () = call_candid(&pic, canister_id, "execute_snapshot_methods", ()) + .expect("Error calling execute_snapshot_methods"); +} + #[test] fn test_chunk() { let pic = PocketIc::new(); diff --git a/scripts/download_pocket_ic.sh b/scripts/download_pocket_ic.sh index 545dd8306..b0191645c 100755 --- a/scripts/download_pocket_ic.sh +++ b/scripts/download_pocket_ic.sh @@ -9,7 +9,7 @@ cd "$SCRIPTS_DIR/../e2e-tests" uname_sys=$(uname -s | tr '[:upper:]' '[:lower:]') echo "uname_sys: $uname_sys" -tag="release-2024-05-22_23-01-base" +tag="release-2024-08-02_01-30-base" curl -sL "https://github.com/dfinity/ic/releases/download/$tag/pocket-ic-x86_64-$uname_sys.gz" --output pocket-ic.gz gzip -df pocket-ic.gz diff --git a/src/ic-cdk/CHANGELOG.md b/src/ic-cdk/CHANGELOG.md index 21850b17d..e5ad83969 100644 --- a/src/ic-cdk/CHANGELOG.md +++ b/src/ic-cdk/CHANGELOG.md @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +### Changed + +- BREAKING: Add the `LoadSnapshot` variant to `CanisterChangeDetails`. (#504) + +### Added + +- Support Canister State Snapshots. (#504) + - Add methods: `take_canister_snapshot`, `load_canister_snapshot`, `list_canister_snapshots`, `delete_canister_snapshot` + - Add types: `LoadSnapshotRecord`, `SnapshotId`, `Snapshot`, `TakeCanisterSnapshotArgs`, `LoadCanisterSnapshotArgs`, `DeleteCanisterSnapshotArgs` + ## [0.15.0] - 2024-07-01 ### Changed diff --git a/src/ic-cdk/src/api/management_canister/main/mod.rs b/src/ic-cdk/src/api/management_canister/main/mod.rs index d1cd9de3e..9f686c4f8 100644 --- a/src/ic-cdk/src/api/management_canister/main/mod.rs +++ b/src/ic-cdk/src/api/management_canister/main/mod.rs @@ -185,3 +185,57 @@ pub async fn raw_rand() -> CallResult<(Vec,)> { pub async fn canister_info(arg: CanisterInfoRequest) -> CallResult<(CanisterInfoResponse,)> { call(Principal::management_canister(), "canister_info", (arg,)).await } + +/// Take a snapshot of the specified canister. +/// +/// A snapshot consists of the wasm memory, stable memory, certified variables, wasm chunk store and wasm binary. +/// +/// See [IC method `take_canister_snapshot`](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-take_canister_snapshot). +pub async fn take_canister_snapshot(arg: TakeCanisterSnapshotArgs) -> CallResult<(Snapshot,)> { + call( + Principal::management_canister(), + "take_canister_snapshot", + (arg,), + ) + .await +} + +/// Load a snapshot onto the canister. +/// +/// It fails if no snapshot with the specified `snapshot_id` can be found. +/// +/// See [IC method `load_canister_snapshot`](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-load_canister_snapshot). +pub async fn load_canister_snapshot(arg: LoadCanisterSnapshotArgs) -> CallResult<()> { + call( + Principal::management_canister(), + "load_canister_snapshot", + (arg,), + ) + .await +} + +/// List the snapshots of the canister. +/// +/// See [IC method `list_canister_snapshots`](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-list_canister_snapshots). +pub async fn list_canister_snapshots(arg: CanisterIdRecord) -> CallResult<(Vec,)> { + call( + Principal::management_canister(), + "list_canister_snapshots", + (arg,), + ) + .await +} + +/// Delete a specified snapshot that belongs to an existing canister. +/// +/// An error will be returned if the snapshot is not found. +/// +/// See [IC method `delete_canister_snapshot`](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-delete_canister_snapshot). +pub async fn delete_canister_snapshot(arg: DeleteCanisterSnapshotArgs) -> CallResult<()> { + call( + Principal::management_canister(), + "delete_canister_snapshot", + (arg,), + ) + .await +} diff --git a/src/ic-cdk/src/api/management_canister/main/types.rs b/src/ic-cdk/src/api/management_canister/main/types.rs index 97c284fd3..d08ca652f 100644 --- a/src/ic-cdk/src/api/management_canister/main/types.rs +++ b/src/ic-cdk/src/api/management_canister/main/types.rs @@ -454,6 +454,19 @@ pub struct CodeDeploymentRecord { pub module_hash: Vec, } +/// Details about loading canister snapshot. +#[derive( + CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, +)] +pub struct LoadSnapshotRecord { + /// The version of the canister at the time that the snapshot was taken + pub canister_version: u64, + /// The ID of the snapshot that was loaded. + pub snapshot_id: SnapshotId, + /// The timestamp at which the snapshot was taken. + pub taken_at_timestamp: u64, +} + /// Details about updating canister controllers. #[derive( CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, @@ -477,6 +490,9 @@ pub enum CanisterChangeDetails { /// See [CodeDeploymentRecord]. #[serde(rename = "code_deployment")] CodeDeployment(CodeDeploymentRecord), + /// See [LoadSnapshotRecord]. + #[serde(rename = "load_snapshot")] + LoadSnapshot(LoadSnapshotRecord), /// See [ControllersChangeRecord]. #[serde(rename = "controllers_change")] ControllersChange(ControllersChangeRecord), @@ -526,3 +542,56 @@ pub struct CanisterInfoResponse { /// Controllers of the canister. pub controllers: Vec, } + +/// ID of a canister snapshot. +pub type SnapshotId = Vec; + +/// A snapshot of the state of the canister at a given point in time. +#[derive( + CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, +)] +pub struct Snapshot { + /// ID of the snapshot. + pub id: SnapshotId, + /// The timestamp at which the snapshot was taken. + pub taken_at_timestamp: u64, + /// The size of the snapshot in bytes. + pub total_size: u64, +} + +/// Argument type of [take_canister_snapshot](super::take_canister_snapshot). +#[derive( + CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, +)] +pub struct TakeCanisterSnapshotArgs { + /// Principal of the canister. + pub canister_id: CanisterId, + /// An optional snapshot ID to be replaced by the new snapshot. + /// + /// The snapshot identified by the specified ID will be deleted once a new snapshot has been successfully created. + pub replace_snapshot: Option, +} + +/// Argument type of [load_canister_snapshot](super::load_canister_snapshot). +#[derive( + CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, +)] +pub struct LoadCanisterSnapshotArgs { + /// Principal of the canister. + pub canister_id: CanisterId, + /// ID of the snapshot to be loaded. + pub snapshot_id: SnapshotId, + /// sender_canister_version must be set to ic_cdk::api::canister_version(). + pub sender_canister_version: Option, +} + +/// Argument type of [delete_canister_snapshot](super::delete_canister_snapshot). +#[derive( + CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, +)] +pub struct DeleteCanisterSnapshotArgs { + /// Principal of the canister. + pub canister_id: CanisterId, + /// ID of the snapshot to be deleted. + pub snapshot_id: SnapshotId, +}