1use 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#[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> {
34 let root = SubnetCanisterRegistryOps::get_type(&CanisterRole::ROOT)
35 .ok_or(SyncOpsError::RootNotFound)?;
36
37 Ok(Self {
38 subtree: SubnetCanisterRegistryOps::subtree(root.pid), parents: vec![root.into()],
40 })
41 }
42
43 #[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 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 #[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
88pub 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
123pub async fn nonroot_cascade_topology(bundle: &TopologyBundle) -> Result<(), Error> {
125 OpsError::deny_root()?;
126
127 save_topology(bundle)?;
129
130 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
161fn save_topology(bundle: &TopologyBundle) -> Result<(), Error> {
163 OpsError::deny_root()?;
164
165 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
178async fn send_bundle(pid: &Principal, bundle: &TopologyBundle) -> Result<(), Error> {
180 call_and_decode::<Result<(), Error>>(*pid, "canic_sync_topology", bundle).await?
184}
185
186pub 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(¤t) {
224 result.push(entry.clone());
225 }
226
227 if let Some(children) = index.children_by_parent.get(¤t) {
228 stack.extend(children.iter().copied());
229 }
230 }
231
232 result
233}
234
235#[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}