canic_core/ops/sync/
topology.rs

1//! Topology synchronization helpers.
2//!
3//! Captures subsets of the canister graph (subtree and parent chain) and
4//! propagates them down the hierarchy so every node maintains an up-to-date
5//! view of its surroundings.
6
7use crate::{
8    Error,
9    log::Topic,
10    model::memory::CanisterSummary,
11    ops::{
12        OpsError,
13        model::memory::topology::subnet::{SubnetCanisterChildrenOps, SubnetCanisterRegistryOps},
14        prelude::*,
15        sync::SyncOpsError,
16    },
17};
18use std::collections::HashMap;
19
20///
21/// TopologyBundle
22/// Snapshot describing a canister’s view of the topology
23///
24
25#[derive(CandidType, Clone, Debug, Default, Deserialize)]
26pub struct TopologyBundle {
27    pub subtree: Vec<CanisterSummary>,
28    pub parents: Vec<CanisterSummary>,
29}
30
31impl TopologyBundle {
32    /// Construct a bundle rooted at the actual root canister.
33    pub fn root() -> Result<Self, Error> {
34        let root = SubnetCanisterRegistryOps::get_type(&CanisterRole::ROOT)
35            .ok_or(SyncOpsError::RootNotFound)?;
36
37        Ok(Self {
38            subtree: SubnetCanisterRegistryOps::subtree(root.pid), // subtree rooted at the actual root PID
39            parents: vec![root.into()],
40        })
41    }
42
43    /// Build a new bundle for a given child, rooted at `child_pid`.
44    #[must_use]
45    pub fn for_child(
46        parent_pid: Principal,
47        child_pid: Principal,
48        subtree: &[CanisterSummary],
49        base: &Self,
50    ) -> Self {
51        let index = SubtreeIndex::new(subtree);
52        Self::for_child_indexed(parent_pid, child_pid, base, &index)
53    }
54
55    #[must_use]
56    pub fn for_child_indexed(
57        parent_pid: Principal,
58        child_pid: Principal,
59        base: &Self,
60        index: &SubtreeIndex,
61    ) -> Self {
62        let child_subtree = collect_child_subtree(child_pid, index);
63
64        // Parents = whatever base had, plus parent
65        let mut new_parents = base.parents.clone();
66
67        if let Some(parent_entry) = index.by_pid.get(&parent_pid).cloned() {
68            new_parents.push(parent_entry);
69        }
70
71        Self {
72            subtree: child_subtree,
73            parents: new_parents,
74        }
75    }
76
77    /// Simple debug string for logging
78    #[must_use]
79    pub fn debug(&self) -> String {
80        format!(
81            "subtree:{} parents:{}",
82            self.subtree.len(),
83            self.parents.len(),
84        )
85    }
86}
87
88/// Cascade from root: build fresh bundles per direct child from the registry.
89pub async fn root_cascade_topology() -> Result<(), Error> {
90    OpsError::require_root()?;
91
92    let root_pid = canister_self();
93    let bundle = TopologyBundle::root()?;
94    let index = SubtreeIndex::new(&bundle.subtree);
95
96    let mut failures = 0;
97    for child in SubnetCanisterRegistryOps::children(root_pid) {
98        let child_bundle = TopologyBundle::for_child_indexed(root_pid, child.pid, &bundle, &index);
99        if let Err(err) = send_bundle(&child.pid, &child_bundle).await {
100            failures += 1;
101
102            log!(
103                Topic::Sync,
104                Warn,
105                "💦 sync.topology: failed to cascade to {}: {}",
106                child.pid,
107                err
108            );
109        }
110    }
111
112    if failures > 0 {
113        log!(
114            Topic::Sync,
115            Warn,
116            "💦 sync.topology: {failures} child cascade(s) failed; continuing"
117        );
118    }
119
120    Ok(())
121}
122
123/// Cascade from a child: trim bundle to the child’s subtree and forward.
124pub async fn nonroot_cascade_topology(bundle: &TopologyBundle) -> Result<(), Error> {
125    OpsError::deny_root()?;
126
127    // save local topology
128    save_topology(bundle)?;
129
130    // Direct children of self (freshly imported during save_state)
131    let self_pid = canister_self();
132    let index = SubtreeIndex::new(&bundle.subtree);
133    let mut failures = 0;
134    for child in SubnetCanisterChildrenOps::export() {
135        let child_bundle = TopologyBundle::for_child_indexed(self_pid, child.pid, bundle, &index);
136
137        if let Err(err) = send_bundle(&child.pid, &child_bundle).await {
138            failures += 1;
139
140            log!(
141                Topic::Sync,
142                Warn,
143                "💦 sync.topology: failed to cascade to {}: {}",
144                child.pid,
145                err
146            );
147        }
148    }
149
150    if failures > 0 {
151        log!(
152            Topic::Sync,
153            Warn,
154            "💦 sync.topology: {failures} child cascade(s) failed; continuing"
155        );
156    }
157
158    Ok(())
159}
160
161/// private function to save local state
162fn save_topology(bundle: &TopologyBundle) -> Result<(), Error> {
163    OpsError::deny_root()?;
164
165    // subnet canister children
166    let self_pid = canister_self();
167    let direct_children: Vec<_> = bundle
168        .subtree
169        .iter()
170        .filter(|entry| entry.parent_pid == Some(self_pid))
171        .cloned()
172        .collect();
173    SubnetCanisterChildrenOps::import(direct_children);
174
175    Ok(())
176}
177
178/// Low-level bundle sender used by cascade helpers.
179async fn send_bundle(pid: &Principal, bundle: &TopologyBundle) -> Result<(), Error> {
180    // let debug = bundle.debug();
181    //   log!(Topic::Sync, Info, "💦 sync.topology: [{debug}] -> {pid}");
182
183    call_and_decode::<Result<(), Error>>(*pid, "canic_sync_topology", bundle).await?
184}
185
186///
187/// SubtreeIndex
188///
189
190pub struct SubtreeIndex {
191    by_pid: HashMap<Principal, CanisterSummary>,
192    children_by_parent: HashMap<Principal, Vec<Principal>>,
193}
194
195impl SubtreeIndex {
196    fn new(subtree: &[CanisterSummary]) -> Self {
197        let mut by_pid = HashMap::new();
198        let mut children_by_parent: HashMap<Principal, Vec<Principal>> = HashMap::new();
199
200        for entry in subtree {
201            by_pid.insert(entry.pid, entry.clone());
202
203            if let Some(parent) = entry.parent_pid {
204                children_by_parent
205                    .entry(parent)
206                    .or_default()
207                    .push(entry.pid);
208            }
209        }
210
211        Self {
212            by_pid,
213            children_by_parent,
214        }
215    }
216}
217
218fn collect_child_subtree(child_pid: Principal, index: &SubtreeIndex) -> Vec<CanisterSummary> {
219    let mut result = Vec::new();
220    let mut stack = vec![child_pid];
221
222    while let Some(current) = stack.pop() {
223        if let Some(entry) = index.by_pid.get(&current) {
224            result.push(entry.clone());
225        }
226
227        if let Some(children) = index.children_by_parent.get(&current) {
228            stack.extend(children.iter().copied());
229        }
230    }
231
232    result
233}
234
235///
236/// TESTS
237///
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use crate::ids::CanisterRole;
243
244    fn p(id: u8) -> Principal {
245        Principal::from_slice(&[id; 29])
246    }
247
248    fn summary(pid: Principal, parent_pid: Option<Principal>) -> CanisterSummary {
249        CanisterSummary {
250            pid,
251            ty: CanisterRole::new("test"),
252            parent_pid,
253        }
254    }
255
256    #[test]
257    fn build_child_subtree_returns_only_descendants() {
258        let root = p(1);
259        let alpha = p(2);
260        let beta = p(3);
261        let alpha_a = p(4);
262        let alpha_b = p(5);
263        let alpha_b_child = p(6);
264
265        let subtree = vec![
266            summary(root, None),
267            summary(alpha, Some(root)),
268            summary(beta, Some(root)),
269            summary(alpha_a, Some(alpha)),
270            summary(alpha_b, Some(alpha)),
271            summary(alpha_b_child, Some(alpha_b)),
272        ];
273
274        let index = SubtreeIndex::new(&subtree);
275        let mut child_subtree = collect_child_subtree(alpha, &index);
276        child_subtree.sort_by(|a, b| a.pid.as_slice().cmp(b.pid.as_slice()));
277
278        let expected: Vec<Principal> = vec![alpha, alpha_a, alpha_b, alpha_b_child];
279        let actual: Vec<Principal> = child_subtree.into_iter().map(|e| e.pid).collect();
280
281        assert_eq!(expected, actual);
282    }
283}