1use std::collections::BTreeMap;
2
3use crate::pocket_ic::{ClientError, PocketIc};
4use ic_platform_types::principal::ExternalPrincipal as Principal;
5
6mod transfer;
7
8#[derive(Clone, Debug, Eq, PartialEq)]
9struct ControllerSnapshot {
10 snapshot_id: Vec<u8>,
11 sender: Option<Principal>,
12}
13
14#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct ControllerSnapshots(BTreeMap<Principal, ControllerSnapshot>);
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct CanisterSnapshotTarget {
21 pub canister_id: Principal,
23 pub sender: Option<Principal>,
25}
26
27#[non_exhaustive]
29#[derive(Debug)]
30pub struct SnapshotCleanupFailure {
31 pub canister_id: Principal,
33 pub sender: Option<Principal>,
35 pub snapshot_id: Vec<u8>,
37 pub source: Box<ClientError>,
39}
40
41#[non_exhaustive]
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum SnapshotRestoreFunding {
45 Preserve,
47 TopUpTo {
49 minimum_cycles: u128,
51 },
52}
53
54#[non_exhaustive]
56#[derive(Debug, thiserror::Error)]
57pub enum ControllerSnapshotError {
58 #[error("a cached baseline requires at least one canister")]
60 EmptyCanisterSet,
61 #[error("duplicate canister id in snapshot set: {canister_id}")]
63 DuplicateCanisterId {
64 canister_id: Principal,
66 },
67 #[error("invalid snapshot for {canister_id}: {message}; {} partial snapshots could not be cleaned up", .cleanup_failures.len())]
69 InvalidSnapshot {
70 canister_id: Principal,
72 message: &'static str,
74 cleanup_failures: Vec<SnapshotCleanupFailure>,
76 },
77 #[error("snapshot codec failed for {canister_id}: {source}")]
79 SnapshotCodec {
80 canister_id: Principal,
82 source: ic_platform::candid::contract::Error,
84 cleanup_failures: Vec<SnapshotCleanupFailure>,
86 },
87 #[error("failed to capture snapshot for {canister_id} with sender {sender:?}: {source}; {} partial snapshots could not be cleaned up", .cleanup_failures.len())]
89 CaptureFailed {
90 canister_id: Principal,
92 sender: Option<Principal>,
94 source: Box<ClientError>,
96 cleanup_failures: Vec<SnapshotCleanupFailure>,
98 },
99 #[error("failed to restore snapshot for {canister_id} with sender {sender:?}: {source}")]
101 RestoreFailed {
102 canister_id: Principal,
104 sender: Option<Principal>,
106 source: Box<ClientError>,
108 },
109}
110
111pub trait PocketIcSnapshotExt {
113 fn capture_snapshots<I>(
130 &self,
131 targets: I,
132 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
133 where
134 I: IntoIterator<Item = CanisterSnapshotTarget>;
135
136 fn restore_snapshots(
151 &self,
152 snapshots: &ControllerSnapshots,
153 funding: SnapshotRestoreFunding,
154 ) -> Result<(), ControllerSnapshotError>;
155}
156
157impl PocketIcSnapshotExt for PocketIc {
158 fn capture_snapshots<I>(
159 &self,
160 targets: I,
161 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
162 where
163 I: IntoIterator<Item = CanisterSnapshotTarget>,
164 {
165 capture_snapshot_set(self, targets)
166 }
167
168 fn restore_snapshots(
169 &self,
170 snapshots: &ControllerSnapshots,
171 funding: SnapshotRestoreFunding,
172 ) -> Result<(), ControllerSnapshotError> {
173 for (canister_id, snapshot_id, sender) in snapshots.iter() {
174 restore_controller_snapshot(self, canister_id, snapshot_id, funding, sender)?;
175 }
176 Ok(())
177 }
178}
179
180fn capture_snapshot_set<I>(
181 pocket_ic: &PocketIc,
182 targets: I,
183) -> Result<ControllerSnapshots, ControllerSnapshotError>
184where
185 I: IntoIterator<Item = CanisterSnapshotTarget>,
186{
187 let mut ordered_targets = BTreeMap::new();
190 for target in targets {
191 if ordered_targets
192 .insert(target.canister_id, target.sender)
193 .is_some()
194 {
195 return Err(ControllerSnapshotError::DuplicateCanisterId {
196 canister_id: target.canister_id,
197 });
198 }
199 }
200 let mut snapshots = BTreeMap::new();
201 for (canister_id, sender) in ordered_targets {
202 let mut pending = Vec::new();
203 let failure = match try_take_snapshot(pocket_ic, canister_id, sender, &mut pending) {
204 Ok(snapshot) => {
205 snapshots.insert(canister_id, snapshot);
206 continue;
207 }
208 Err(failure) => failure,
209 };
210 let cleanup_failures = cleanup_captured_snapshots(
211 pending
212 .into_iter()
213 .map(|snapshot_id| {
214 (
215 canister_id,
216 ControllerSnapshot {
217 snapshot_id,
218 sender,
219 },
220 )
221 })
222 .chain(snapshots),
223 |canister_id, sender, snapshot_id| {
224 pocket_ic.delete_canister_snapshot(canister_id, sender, snapshot_id)
225 },
226 );
227 return Err(match failure {
228 transfer::Failure::Call(failure) => ControllerSnapshotError::CaptureFailed {
229 canister_id,
230 sender,
231 source: Box::new(failure),
232 cleanup_failures,
233 },
234 transfer::Failure::Invalid(message) => ControllerSnapshotError::InvalidSnapshot {
235 canister_id,
236 message,
237 cleanup_failures,
238 },
239 transfer::Failure::Codec(source) => ControllerSnapshotError::SnapshotCodec {
240 canister_id,
241 source,
242 cleanup_failures,
243 },
244 });
245 }
246 Ok(ControllerSnapshots(snapshots))
247}
248
249impl ControllerSnapshots {
250 #[must_use]
252 pub fn len(&self) -> usize {
253 self.0.len()
254 }
255
256 #[must_use]
258 pub fn is_empty(&self) -> bool {
259 self.0.is_empty()
260 }
261
262 pub fn canister_ids(&self) -> impl Iterator<Item = Principal> + '_ {
264 self.0.keys().copied()
265 }
266
267 pub(super) fn iter(&self) -> impl Iterator<Item = (Principal, &[u8], Option<Principal>)> + '_ {
268 self.0.iter().map(|(canister_id, snapshot)| {
269 (
270 *canister_id,
271 snapshot.snapshot_id.as_slice(),
272 snapshot.sender,
273 )
274 })
275 }
276}
277
278fn try_take_snapshot(
279 pocket_ic: &PocketIc,
280 canister_id: Principal,
281 sender: Option<Principal>,
282 pending: &mut Vec<Vec<u8>>,
283) -> Result<ControllerSnapshot, transfer::Failure> {
284 let snapshot = pocket_ic.take_canister_snapshot(canister_id, sender, None)?;
285 pending.push(snapshot.id.clone());
286 let uploaded = transfer::upload(pocket_ic, canister_id, sender, &snapshot.id, pending)?;
287 pocket_ic.delete_canister_snapshot(canister_id, sender, snapshot.id)?;
288 pending.clear();
289 Ok(ControllerSnapshot {
290 snapshot_id: uploaded,
291 sender,
292 })
293}
294
295fn cleanup_captured_snapshots(
296 snapshots: impl IntoIterator<Item = (Principal, ControllerSnapshot)>,
297 mut remove: impl FnMut(Principal, Option<Principal>, Vec<u8>) -> Result<(), ClientError>,
298) -> Vec<SnapshotCleanupFailure> {
299 let mut failures = Vec::new();
300 for (canister_id, snapshot) in snapshots {
301 let cleanup = remove(canister_id, snapshot.sender, snapshot.snapshot_id.clone());
302 if let Err(source) = cleanup {
303 failures.push(SnapshotCleanupFailure {
304 canister_id,
305 sender: snapshot.sender,
306 snapshot_id: snapshot.snapshot_id,
307 source: Box::new(source),
308 });
309 }
310 }
311 failures
312}
313
314fn restore_controller_snapshot(
315 pocket_ic: &PocketIc,
316 canister_id: Principal,
317 snapshot_id: &[u8],
318 funding: SnapshotRestoreFunding,
319 sender: Option<Principal>,
320) -> Result<(), ControllerSnapshotError> {
321 apply_snapshot_restore_funding(pocket_ic, canister_id, funding)
322 .and_then(|()| pocket_ic.load_canister_snapshot(canister_id, sender, snapshot_id.to_vec()))
323 .map_err(|source| ControllerSnapshotError::RestoreFailed {
324 canister_id,
325 sender,
326 source: Box::new(source),
327 })
328}
329
330fn apply_snapshot_restore_funding(
331 pocket_ic: &PocketIc,
332 canister_id: Principal,
333 funding: SnapshotRestoreFunding,
334) -> Result<(), ClientError> {
335 match funding {
336 SnapshotRestoreFunding::Preserve => {}
337 SnapshotRestoreFunding::TopUpTo { minimum_cycles } => {
338 let top_up = minimum_cycles.saturating_sub(pocket_ic.cycle_balance(canister_id)?);
339 if top_up > 0 {
340 pocket_ic.add_cycles(canister_id, top_up)?;
341 }
342 }
343 }
344 Ok(())
345}
346
347#[cfg(test)]
348mod tests;