canic_core/ops/sync/
state.rs1use crate::{
7 Error,
8 log::Topic,
9 ops::{
10 OpsError,
11 model::memory::{
12 directory::{AppDirectoryOps, DirectoryView, SubnetDirectoryOps},
13 state::{AppStateData, AppStateOps, SubnetStateData, SubnetStateOps},
14 topology::subnet::{SubnetCanisterChildrenOps, SubnetCanisterRegistryOps},
15 },
16 prelude::*,
17 },
18};
19
20#[derive(CandidType, Clone, Debug, Default, Deserialize)]
26pub struct StateBundle {
27 pub app_state: Option<AppStateData>,
29 pub subnet_state: Option<SubnetStateData>,
30
31 pub app_directory: Option<DirectoryView>,
33 pub subnet_directory: Option<DirectoryView>,
34}
35
36impl StateBundle {
37 #[must_use]
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 #[must_use]
44 pub fn root() -> Self {
45 Self {
46 app_state: Some(AppStateOps::export()),
47 subnet_state: Some(SubnetStateOps::export()),
48 app_directory: Some(AppDirectoryOps::export()),
49 subnet_directory: Some(SubnetDirectoryOps::export()),
50 }
51 }
52
53 #[must_use]
54 pub fn with_app_state(mut self) -> Self {
55 self.app_state = Some(AppStateOps::export());
56 self
57 }
58
59 #[must_use]
60 pub fn with_subnet_state(mut self) -> Self {
61 self.subnet_state = Some(SubnetStateOps::export());
62 self
63 }
64
65 #[must_use]
66 pub fn with_app_directory(mut self) -> Self {
67 self.app_directory = Some(AppDirectoryOps::export());
68 self
69 }
70
71 #[must_use]
72 pub fn with_subnet_directory(mut self) -> Self {
73 self.subnet_directory = Some(SubnetDirectoryOps::export());
74 self
75 }
76
77 #[must_use]
80 pub fn debug(&self) -> String {
81 const fn fmt(present: bool, code: &str) -> &str {
82 if present { code } else { ".." }
83 }
84
85 format!(
86 "[{} {} {} {}]",
87 fmt(self.app_state.is_some(), "as"),
88 fmt(self.subnet_state.is_some(), "ss"),
89 fmt(self.app_directory.is_some(), "ad"),
90 fmt(self.subnet_directory.is_some(), "sd"),
91 )
92 }
93
94 #[must_use]
96 pub const fn is_empty(&self) -> bool {
97 self.app_state.is_none()
98 && self.subnet_state.is_none()
99 && self.app_directory.is_none()
100 && self.subnet_directory.is_none()
101 }
102}
103
104pub async fn root_cascade_state(bundle: StateBundle) -> Result<(), Error> {
107 OpsError::require_root()?;
108
109 if bundle.is_empty() {
110 log!(
111 Topic::Sync,
112 Info,
113 "π¦ sync.state: root_cascade skipped (empty bundle)"
114 );
115 return Ok(());
116 }
117
118 let root_pid = canister_self();
119 let children = SubnetCanisterRegistryOps::children(root_pid);
120 let child_count = children.len();
121 if child_count > 10 {
122 log!(
123 Topic::Sync,
124 Warn,
125 "π¦ sync.state: large root cascade to {child_count} children"
126 );
127 }
128
129 let mut failures = 0;
130 for child in children {
131 if let Err(err) = send_bundle(&child.pid, &bundle).await {
132 failures += 1;
133 log!(
134 Topic::Sync,
135 Warn,
136 "π¦ sync.state: failed to cascade to {}: {}",
137 child.pid,
138 err
139 );
140 }
141 }
142
143 if failures > 0 {
144 log!(
145 Topic::Sync,
146 Warn,
147 "π¦ sync.state: {failures} child cascade(s) failed; continuing"
148 );
149 }
150
151 Ok(())
152}
153
154pub async fn nonroot_cascade_state(bundle: &StateBundle) -> Result<(), Error> {
157 OpsError::deny_root()?;
158
159 save_state(bundle)?;
161
162 let children = SubnetCanisterChildrenOps::export();
163 let child_count = children.len();
164 if child_count > 10 {
165 log!(
166 Topic::Sync,
167 Warn,
168 "π¦ sync.state: large nonroot cascade to {child_count} children"
169 );
170 }
171
172 let mut failures = 0;
173 for child in children {
174 if let Err(err) = send_bundle(&child.pid, bundle).await {
175 failures += 1;
176 log!(
177 Topic::Sync,
178 Warn,
179 "π¦ sync.state: failed to cascade to {}: {}",
180 child.pid,
181 err
182 );
183 }
184 }
185
186 if failures > 0 {
187 log!(
188 Topic::Sync,
189 Warn,
190 "π¦ sync.state: {failures} child cascade(s) failed; continuing"
191 );
192 }
193
194 Ok(())
195}
196
197fn save_state(bundle: &StateBundle) -> Result<(), Error> {
199 OpsError::deny_root()?;
200
201 if let Some(state) = bundle.app_state {
203 AppStateOps::import(state);
204 }
205 if let Some(state) = bundle.subnet_state {
206 SubnetStateOps::import(state);
207 }
208
209 if let Some(dir) = &bundle.app_directory {
211 AppDirectoryOps::import(dir.clone());
212 }
213 if let Some(dir) = &bundle.subnet_directory {
214 SubnetDirectoryOps::import(dir.clone());
215 }
216
217 Ok(())
218}
219
220async fn send_bundle(pid: &Principal, bundle: &StateBundle) -> Result<(), Error> {
222 let debug = bundle.debug();
223 log!(Topic::Sync, Info, "π¦ sync.state: {debug} -> {pid}");
224
225 call_and_decode::<Result<(), Error>>(*pid, "canic_sync_state", bundle).await?
226}