1use std::{
2 collections::{BTreeMap, BTreeSet},
3 panic::{AssertUnwindSafe, catch_unwind},
4};
5
6use candid::Principal;
7use pocket_ic::{PocketIc, RejectResponse};
8
9use super::startup;
10
11const SNAPSHOT_RESTORE_MIN_CYCLES: u128 = 200_000_000_000_000;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14struct ControllerSnapshot {
15 snapshot_id: Vec<u8>,
16 sender: Option<Principal>,
17}
18
19#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct ControllerSnapshots(BTreeMap<Principal, ControllerSnapshot>);
22
23#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct SnapshotAttemptFailure {
26 sender: Option<Principal>,
27 response: RejectResponse,
28}
29
30#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct SnapshotCleanupFailure {
33 canister_id: Principal,
34 sender: Option<Principal>,
35 response: Option<Box<RejectResponse>>,
36 panic_message: Option<String>,
37}
38
39#[non_exhaustive]
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum ControllerSnapshotError {
43 DuplicateCanisterId {
44 canister_id: Principal,
45 },
46 CaptureFailed {
47 canister_id: Principal,
48 attempts: Vec<SnapshotAttemptFailure>,
49 cleanup_failures: Vec<SnapshotCleanupFailure>,
50 },
51 CapturePanicked {
52 canister_id: Principal,
53 message: String,
54 cleanup_failures: Vec<SnapshotCleanupFailure>,
55 },
56 RestoreFailed {
57 canister_id: Principal,
58 attempts: Vec<SnapshotAttemptFailure>,
59 },
60 RestorePanicked {
61 canister_id: Principal,
62 message: String,
63 },
64}
65
66enum SnapshotCaptureFailure {
67 Rejected(Vec<SnapshotAttemptFailure>),
68 Panicked(String),
69}
70
71pub trait PocketIcSnapshotExt {
73 fn capture_controller_snapshots<I>(
79 &self,
80 controller_id: Principal,
81 canister_ids: I,
82 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
83 where
84 I: IntoIterator<Item = Principal>;
85
86 fn restore_controller_snapshots(
88 &self,
89 controller_id: Principal,
90 snapshots: &ControllerSnapshots,
91 ) -> Result<(), ControllerSnapshotError>;
92}
93
94impl PocketIcSnapshotExt for PocketIc {
95 fn capture_controller_snapshots<I>(
96 &self,
97 controller_id: Principal,
98 canister_ids: I,
99 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
100 where
101 I: IntoIterator<Item = Principal>,
102 {
103 let canister_ids = ordered_unique_canister_ids(canister_ids)?;
104 let mut snapshots = BTreeMap::new();
105
106 for canister_id in canister_ids {
107 match try_take_controller_snapshot(self, controller_id, canister_id) {
108 Ok(snapshot) => {
109 snapshots.insert(canister_id, snapshot);
110 }
111 Err(SnapshotCaptureFailure::Rejected(attempts)) => {
112 let cleanup_failures = cleanup_captured_snapshots(self, &snapshots);
113 return Err(ControllerSnapshotError::CaptureFailed {
114 canister_id,
115 attempts,
116 cleanup_failures,
117 });
118 }
119 Err(SnapshotCaptureFailure::Panicked(message)) => {
120 let cleanup_failures = cleanup_captured_snapshots(self, &snapshots);
121 return Err(ControllerSnapshotError::CapturePanicked {
122 canister_id,
123 message,
124 cleanup_failures,
125 });
126 }
127 }
128 }
129
130 Ok(ControllerSnapshots(snapshots))
131 }
132
133 fn restore_controller_snapshots(
134 &self,
135 controller_id: Principal,
136 snapshots: &ControllerSnapshots,
137 ) -> Result<(), ControllerSnapshotError> {
138 for (canister_id, snapshot_id, sender) in snapshots.iter() {
139 restore_controller_snapshot(self, controller_id, canister_id, sender, snapshot_id)?;
140 }
141 Ok(())
142 }
143}
144
145impl ControllerSnapshots {
146 #[must_use]
148 pub fn len(&self) -> usize {
149 self.0.len()
150 }
151
152 #[must_use]
154 pub fn is_empty(&self) -> bool {
155 self.0.is_empty()
156 }
157
158 pub fn canister_ids(&self) -> impl Iterator<Item = Principal> + '_ {
160 self.0.keys().copied()
161 }
162
163 pub(super) fn iter(&self) -> impl Iterator<Item = (Principal, &[u8], Option<Principal>)> + '_ {
164 self.0.iter().map(|(canister_id, snapshot)| {
165 (
166 *canister_id,
167 snapshot.snapshot_id.as_slice(),
168 snapshot.sender,
169 )
170 })
171 }
172}
173
174impl SnapshotAttemptFailure {
175 #[must_use]
177 pub const fn sender(&self) -> Option<Principal> {
178 self.sender
179 }
180
181 #[must_use]
183 pub const fn response(&self) -> &RejectResponse {
184 &self.response
185 }
186}
187
188impl SnapshotCleanupFailure {
189 #[must_use]
191 pub const fn canister_id(&self) -> Principal {
192 self.canister_id
193 }
194
195 #[must_use]
197 pub const fn sender(&self) -> Option<Principal> {
198 self.sender
199 }
200
201 #[must_use]
203 pub fn response(&self) -> Option<&RejectResponse> {
204 self.response.as_deref()
205 }
206
207 #[must_use]
209 pub fn panic_message(&self) -> Option<&str> {
210 self.panic_message.as_deref()
211 }
212}
213
214impl std::fmt::Display for ControllerSnapshotError {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 match self {
217 Self::DuplicateCanisterId { canister_id } => {
218 write!(f, "duplicate canister id in snapshot set: {canister_id}")
219 }
220 Self::CaptureFailed {
221 canister_id,
222 attempts,
223 cleanup_failures,
224 } => write!(
225 f,
226 "failed to capture snapshot for {canister_id} after {} sender attempts; {} partial snapshots could not be cleaned up",
227 attempts.len(),
228 cleanup_failures.len()
229 ),
230 Self::CapturePanicked {
231 canister_id,
232 message,
233 cleanup_failures,
234 } => write!(
235 f,
236 "snapshot capture panicked for {canister_id}: {message}; {} partial snapshots could not be cleaned up",
237 cleanup_failures.len()
238 ),
239 Self::RestoreFailed {
240 canister_id,
241 attempts,
242 } => write!(
243 f,
244 "failed to restore snapshot for {canister_id} after {} sender attempts",
245 attempts.len()
246 ),
247 Self::RestorePanicked {
248 canister_id,
249 message,
250 } => write!(f, "snapshot restore panicked for {canister_id}: {message}"),
251 }
252 }
253}
254
255impl std::error::Error for ControllerSnapshotError {}
256
257fn ordered_unique_canister_ids<I>(
258 canister_ids: I,
259) -> Result<Vec<Principal>, ControllerSnapshotError>
260where
261 I: IntoIterator<Item = Principal>,
262{
263 let mut unique = BTreeSet::new();
264 for canister_id in canister_ids {
265 if !unique.insert(canister_id) {
266 return Err(ControllerSnapshotError::DuplicateCanisterId { canister_id });
267 }
268 }
269 Ok(unique.into_iter().collect())
270}
271
272fn try_take_controller_snapshot(
273 pocket_ic: &PocketIc,
274 controller_id: Principal,
275 canister_id: Principal,
276) -> Result<ControllerSnapshot, SnapshotCaptureFailure> {
277 let candidates = controller_sender_candidates(controller_id, canister_id);
278 let mut attempts = Vec::new();
279
280 for sender in candidates {
281 let capture = catch_unwind(AssertUnwindSafe(|| {
282 pocket_ic.take_canister_snapshot(canister_id, sender, None)
283 }));
284 match capture {
285 Err(payload) => {
286 return Err(SnapshotCaptureFailure::Panicked(
287 startup::panic_payload_to_string(payload.as_ref()),
288 ));
289 }
290 Ok(snapshot) => match snapshot {
291 Ok(snapshot) => {
292 return Ok(ControllerSnapshot {
293 snapshot_id: snapshot.id,
294 sender,
295 });
296 }
297 Err(response) => attempts.push(SnapshotAttemptFailure { sender, response }),
298 },
299 }
300 }
301
302 Err(SnapshotCaptureFailure::Rejected(attempts))
303}
304
305fn cleanup_captured_snapshots(
306 pocket_ic: &PocketIc,
307 snapshots: &BTreeMap<Principal, ControllerSnapshot>,
308) -> Vec<SnapshotCleanupFailure> {
309 let mut failures = Vec::new();
310 for (canister_id, snapshot) in snapshots {
311 let cleanup = catch_unwind(AssertUnwindSafe(|| {
312 pocket_ic.delete_canister_snapshot(
313 *canister_id,
314 snapshot.sender,
315 snapshot.snapshot_id.clone(),
316 )
317 }));
318 match cleanup {
319 Ok(Ok(())) => {}
320 Ok(Err(response)) => failures.push(SnapshotCleanupFailure {
321 canister_id: *canister_id,
322 sender: snapshot.sender,
323 response: Some(Box::new(response)),
324 panic_message: None,
325 }),
326 Err(payload) => failures.push(SnapshotCleanupFailure {
327 canister_id: *canister_id,
328 sender: snapshot.sender,
329 response: None,
330 panic_message: Some(startup::panic_payload_to_string(payload.as_ref())),
331 }),
332 }
333 }
334 failures
335}
336
337fn restore_controller_snapshot(
338 pocket_ic: &PocketIc,
339 controller_id: Principal,
340 canister_id: Principal,
341 snapshot_sender: Option<Principal>,
342 snapshot_id: &[u8],
343) -> Result<(), ControllerSnapshotError> {
344 let fallback_sender = if snapshot_sender.is_some() {
345 None
346 } else {
347 Some(controller_id)
348 };
349 let candidates = [snapshot_sender, fallback_sender];
350 let mut attempts = Vec::new();
351
352 for sender in candidates {
353 let restore = catch_unwind(AssertUnwindSafe(|| {
354 ensure_snapshot_restore_cycles(pocket_ic, canister_id);
355 pocket_ic.load_canister_snapshot(canister_id, sender, snapshot_id.to_vec())
356 }));
357 match restore {
358 Err(payload) => {
359 return Err(ControllerSnapshotError::RestorePanicked {
360 canister_id,
361 message: startup::panic_payload_to_string(payload.as_ref()),
362 });
363 }
364 Ok(Ok(())) => return Ok(()),
365 Ok(Err(response)) => attempts.push(SnapshotAttemptFailure { sender, response }),
366 }
367 }
368
369 Err(ControllerSnapshotError::RestoreFailed {
370 canister_id,
371 attempts,
372 })
373}
374
375fn ensure_snapshot_restore_cycles(pocket_ic: &PocketIc, canister_id: Principal) {
376 let balance = pocket_ic.cycle_balance(canister_id);
377 if balance < SNAPSHOT_RESTORE_MIN_CYCLES {
378 let top_up = SNAPSHOT_RESTORE_MIN_CYCLES - balance;
379 let _ = pocket_ic.add_cycles(canister_id, top_up);
380 }
381}
382
383fn controller_sender_candidates(
384 controller_id: Principal,
385 canister_id: Principal,
386) -> [Option<Principal>; 2] {
387 if canister_id == controller_id {
388 [None, Some(controller_id)]
389 } else {
390 [Some(controller_id), None]
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use candid::Principal;
397
398 use super::{ControllerSnapshotError, ordered_unique_canister_ids};
399
400 #[test]
401 fn duplicate_canister_ids_are_rejected_before_capture() {
402 let canister_id = Principal::from_slice(&[1]);
403 let error = ordered_unique_canister_ids([canister_id, canister_id]).unwrap_err();
404
405 assert_eq!(
406 error,
407 ControllerSnapshotError::DuplicateCanisterId { canister_id }
408 );
409 }
410
411 #[test]
412 fn canister_ids_are_sorted_deterministically() {
413 let first = Principal::from_slice(&[1]);
414 let second = Principal::from_slice(&[2]);
415
416 assert_eq!(
417 ordered_unique_canister_ids([second, first]).unwrap(),
418 vec![first, second]
419 );
420 }
421}