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