Skip to main content

canic_backup/discovery/
mod.rs

1//! Module: discovery
2//!
3//! Responsibility: convert registry observations into backup target sets.
4//! Does not own: snapshot IO, manifest validation, or persisted backup state.
5//! Boundary: prepares discovery projections consumed by backup planning.
6
7#[cfg(test)]
8mod tests;
9
10use crate::registry::RegistryEntry;
11use std::collections::{BTreeMap, BTreeSet, VecDeque};
12use thiserror::Error as ThisError;
13
14///
15/// SnapshotTarget
16///
17/// Registry target selected for snapshot capture.
18/// Owned by backup discovery and consumed by snapshot planning.
19///
20
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct SnapshotTarget {
23    pub canister_id: String,
24    pub role: Option<String>,
25    pub parent_canister_id: Option<String>,
26    pub module_hash: Option<String>,
27}
28
29///
30/// DiscoveryError
31///
32/// Typed discovery failure returned while selecting backup targets.
33/// Owned by backup discovery and surfaced to snapshot and planning callers.
34///
35
36#[derive(Debug, ThisError)]
37pub enum DiscoveryError {
38    #[error("registry JSON did not contain the requested canister {0}")]
39    CanisterNotInRegistry(String),
40}
41
42/// Resolve selected target and children from registry entries.
43pub fn targets_from_registry(
44    registry: &[RegistryEntry],
45    canister_id: &str,
46    recursive: bool,
47) -> Result<Vec<SnapshotTarget>, DiscoveryError> {
48    let by_pid = registry
49        .iter()
50        .map(|entry| (entry.pid.as_str(), entry))
51        .collect::<BTreeMap<_, _>>();
52
53    let root = by_pid
54        .get(canister_id)
55        .ok_or_else(|| DiscoveryError::CanisterNotInRegistry(canister_id.to_string()))?;
56
57    let mut targets = Vec::new();
58    let mut seen = BTreeSet::new();
59    targets.push(SnapshotTarget {
60        canister_id: root.pid.clone(),
61        role: root.role.clone(),
62        parent_canister_id: root.parent_pid.clone(),
63        module_hash: root.module_hash.clone(),
64    });
65    seen.insert(root.pid.clone());
66
67    let mut queue = VecDeque::from([root.pid.clone()]);
68    while let Some(parent) = queue.pop_front() {
69        for child in registry
70            .iter()
71            .filter(|entry| entry.parent_pid.as_deref() == Some(parent.as_str()))
72        {
73            if seen.insert(child.pid.clone()) {
74                targets.push(SnapshotTarget {
75                    canister_id: child.pid.clone(),
76                    role: child.role.clone(),
77                    parent_canister_id: child.parent_pid.clone(),
78                    module_hash: child.module_hash.clone(),
79                });
80                if recursive {
81                    queue.push_back(child.pid.clone());
82                }
83            }
84        }
85    }
86
87    Ok(targets)
88}