1use 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#[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
59pub 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 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 let mut down: Vec<_> = path.into_iter().rev().collect();
98
99 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 down.remove(0);
111 if down.is_empty() {
112 return Ok(());
113 }
114
115 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
136pub 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
154async 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
219pub 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#[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}