canic_core/ops/sync/
topology.rs

1//! Topology synchronization helpers.
2//!
3//! Extracts relevant topology subsets and propagates them so each canister
4//! maintains an up-to-date view of its children and parent chain.
5
6use crate::{
7    Error,
8    log::Topic,
9    model::memory::CanisterSummary,
10    ops::{
11        OpsError,
12        model::memory::topology::subnet::{SubnetCanisterChildrenOps, SubnetCanisterRegistryOps},
13        prelude::*,
14        sync::SyncOpsError,
15    },
16};
17use std::collections::HashMap;
18
19//
20// ===========================================================================
21//  BUNDLES
22// ===========================================================================
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    pub fn root() -> Result<Self, Error> {
33        let root = SubnetCanisterRegistryOps::get_type(&CanisterRole::ROOT)
34            .ok_or(SyncOpsError::RootNotFound)?;
35        Ok(Self {
36            subtree: SubnetCanisterRegistryOps::subtree(root.pid),
37            parents: vec![root.into()],
38        })
39    }
40
41    #[must_use]
42    pub fn for_child(
43        parent_pid: Principal,
44        child_pid: Principal,
45        base: &Self,
46        index: &SubtreeIndex,
47    ) -> Self {
48        let mut parents = base.parents.clone();
49        if let Some(parent) = index.by_pid.get(&parent_pid) {
50            parents.push(parent.clone());
51        }
52        Self {
53            subtree: collect_child_subtree(child_pid, index),
54            parents,
55        }
56    }
57}
58
59//
60// ===========================================================================
61//  ROOT CASCADES
62// ===========================================================================
63//
64
65pub async fn root_cascade_topology() -> Result<(), Error> {
66    OpsError::require_root()?;
67
68    let root_pid = canister_self();
69    let bundle = TopologyBundle::root()?;
70    let index = SubtreeIndex::new(&bundle.subtree);
71
72    let children = SubnetCanisterRegistryOps::children(root_pid);
73    warn_large(children.len(), "root");
74
75    cascade_children(root_pid, &bundle, &index, children).await
76}
77
78pub async fn root_cascade_topology_for_pid(target_pid: Principal) -> Result<(), Error> {
79    OpsError::require_root()?;
80
81    let root_pid = canister_self();
82    let bundle = TopologyBundle::root()?;
83    let index = SubtreeIndex::new(&bundle.subtree);
84
85    // Upward path: [target, parent, ..., root_child, root]
86    let path = collect_branch_path(target_pid, &index, root_pid);
87    if path.is_empty() {
88        log!(
89            Topic::Sync,
90            Warn,
91            "sync.topology: no branch path to {target_pid}, skipping targeted cascade"
92        );
93        return Ok(());
94    }
95
96    // Descending: [root, root_child, ..., target]
97    let mut down: Vec<_> = path.into_iter().rev().collect();
98
99    // Must begin with root
100    if down.first().copied() != Some(root_pid) {
101        log!(
102            Topic::Sync,
103            Warn,
104            "sync.topology: branch path for {target_pid} does not start at root, skipping targeted cascade"
105        );
106        return Ok(());
107    }
108
109    // Skip root itself
110    down.remove(0);
111    if down.is_empty() {
112        return Ok(());
113    }
114
115    // Hand off to the first child; that branch will cascade onward via non-root logic.
116    let first_child = down.remove(0);
117    let child_bundle = TopologyBundle::for_child(root_pid, first_child, &bundle, &index);
118    if let Err(err) = send_bundle(&first_child, &child_bundle).await {
119        log!(
120            Topic::Sync,
121            Warn,
122            "sync.topology: failed targeted cascade to first child {first_child}: {err}"
123        );
124    } else {
125        log!(
126            Topic::Sync,
127            Info,
128            "sync.topology: delegated targeted cascade to {first_child} (depth={})",
129            down.len() + 1
130        );
131    }
132
133    Ok(())
134}
135
136//
137// ===========================================================================
138//  NON-ROOT CASCADES
139// ===========================================================================
140//
141
142pub async fn nonroot_cascade_topology(bundle: &TopologyBundle) -> Result<(), Error> {
143    OpsError::deny_root()?;
144    save_topology(bundle);
145
146    let self_pid = canister_self();
147    let index = SubtreeIndex::new(&bundle.subtree);
148    let children = SubnetCanisterChildrenOps::export();
149    warn_large(children.len(), "nonroot");
150
151    cascade_children(self_pid, bundle, &index, children).await
152}
153
154//
155// ===========================================================================
156//  HELPERS
157// ===========================================================================
158//
159
160async fn cascade_children(
161    parent_pid: Principal,
162    bundle: &TopologyBundle,
163    index: &SubtreeIndex,
164    children: Vec<CanisterSummary>,
165) -> Result<(), Error> {
166    let mut failures = 0;
167
168    for child in children {
169        let child_bundle = TopologyBundle::for_child(parent_pid, child.pid, bundle, index);
170        if let Err(err) = send_bundle(&child.pid, &child_bundle).await {
171            failures += 1;
172            log!(
173                Topic::Sync,
174                Warn,
175                "sync.topology: failed cascade to {}: {}",
176                child.pid,
177                err
178            );
179        }
180    }
181
182    if failures > 0 {
183        log!(
184            Topic::Sync,
185            Warn,
186            "sync.topology: {failures} cascade(s) failed"
187        );
188    }
189
190    Ok(())
191}
192
193fn warn_large(n: usize, label: &str) {
194    if n > 10 {
195        log!(
196            Topic::Sync,
197            Warn,
198            "sync.topology: large {label} fanout: {n}"
199        );
200    }
201}
202
203fn save_topology(bundle: &TopologyBundle) {
204    let self_pid = canister_self();
205    let direct: Vec<_> = bundle
206        .subtree
207        .iter()
208        .filter(|e| e.parent_pid == Some(self_pid))
209        .cloned()
210        .collect();
211
212    SubnetCanisterChildrenOps::import(direct);
213}
214
215async fn send_bundle(pid: &Principal, bundle: &TopologyBundle) -> Result<(), Error> {
216    call_and_decode::<Result<(), Error>>(*pid, "canic_sync_topology", bundle).await?
217}
218
219//
220// ===========================================================================
221//  INDEX + TRAVERSAL
222// ===========================================================================
223//
224
225pub struct SubtreeIndex {
226    by_pid: HashMap<Principal, CanisterSummary>,
227    children_by_parent: HashMap<Principal, Vec<Principal>>,
228}
229
230impl SubtreeIndex {
231    fn new(subtree: &[CanisterSummary]) -> Self {
232        let mut by_pid: HashMap<Principal, CanisterSummary> = HashMap::new();
233        let mut children_by_parent: HashMap<Principal, Vec<Principal>> = HashMap::new();
234
235        for entry in subtree {
236            by_pid.insert(entry.pid, entry.clone());
237            if let Some(p) = entry.parent_pid {
238                children_by_parent.entry(p).or_default().push(entry.pid);
239            }
240        }
241
242        Self {
243            by_pid,
244            children_by_parent,
245        }
246    }
247
248    fn parent_of(&self, pid: Principal) -> Option<Principal> {
249        self.by_pid.get(&pid).and_then(|e| e.parent_pid)
250    }
251}
252
253fn collect_child_subtree(root: Principal, index: &SubtreeIndex) -> Vec<CanisterSummary> {
254    let mut result = Vec::new();
255    let mut stack = vec![root];
256
257    while let Some(pid) = stack.pop() {
258        if let Some(entry) = index.by_pid.get(&pid) {
259            result.push(entry.clone());
260        }
261        if let Some(children) = index.children_by_parent.get(&pid) {
262            stack.extend(children.iter().copied());
263        }
264    }
265
266    result
267}
268
269fn collect_branch_path(
270    mut pid: Principal,
271    index: &SubtreeIndex,
272    root_pid: Principal,
273) -> Vec<Principal> {
274    let mut path = vec![pid];
275    loop {
276        let Some(p) = index.parent_of(pid) else {
277            return Vec::new();
278        };
279        path.push(p);
280        if p == root_pid {
281            break;
282        }
283        pid = p;
284    }
285    path
286}
287
288//
289// ===========================================================================
290//  TESTS
291// ===========================================================================
292//
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::ids::CanisterRole;
298
299    fn p(id: u8) -> Principal {
300        Principal::from_slice(&[id; 29])
301    }
302
303    fn s(pid: Principal, parent_pid: Option<Principal>) -> CanisterSummary {
304        CanisterSummary {
305            pid,
306            ty: CanisterRole::new("test"),
307            parent_pid,
308        }
309    }
310
311    #[test]
312    fn subtree_descendants() {
313        let root = p(1);
314        let a = p(2);
315        let b = p(3);
316        let a1 = p(4);
317        let a2 = p(5);
318        let a2c = p(6);
319
320        let st = vec![
321            s(root, None),
322            s(a, Some(root)),
323            s(b, Some(root)),
324            s(a1, Some(a)),
325            s(a2, Some(a)),
326            s(a2c, Some(a2)),
327        ];
328
329        let index = SubtreeIndex::new(&st);
330        let mut out = collect_child_subtree(a, &index);
331        out.sort_by_key(|e| e.pid.as_slice().to_vec());
332
333        assert_eq!(
334            out.into_iter().map(|e| e.pid).collect::<Vec<_>>(),
335            vec![a, a1, a2, a2c]
336        );
337    }
338
339    #[test]
340    fn branch_path() {
341        let root = p(1);
342        let hub = p(2);
343        let inst = p(3);
344        let ledg = p(4);
345
346        let st = vec![
347            s(root, None),
348            s(hub, Some(root)),
349            s(inst, Some(hub)),
350            s(ledg, Some(inst)),
351        ];
352
353        let index = SubtreeIndex::new(&st);
354        assert_eq!(
355            collect_branch_path(ledg, &index, root),
356            vec![ledg, inst, hub, root]
357        );
358    }
359}