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 mut failures = 0;
120 for child in SubnetCanisterRegistryOps::children(root_pid) {
121 if let Err(err) = send_bundle(&child.pid, &bundle).await {
122 failures += 1;
123 log!(
124 Topic::Sync,
125 Warn,
126 "💦 sync.state: failed to cascade to {}: {}",
127 child.pid,
128 err
129 );
130 }
131 }
132
133 if failures > 0 {
134 log!(
135 Topic::Sync,
136 Warn,
137 "💦 sync.state: {failures} child cascade(s) failed; continuing"
138 );
139 }
140
141 Ok(())
142}
143
144pub async fn nonroot_cascade_state(bundle: &StateBundle) -> Result<(), Error> {
147 OpsError::deny_root()?;
148
149 save_state(bundle)?;
151
152 let mut failures = 0;
153 for child in SubnetCanisterChildrenOps::export() {
154 if let Err(err) = send_bundle(&child.pid, bundle).await {
155 failures += 1;
156 log!(
157 Topic::Sync,
158 Warn,
159 "💦 sync.state: failed to cascade to {}: {}",
160 child.pid,
161 err
162 );
163 }
164 }
165
166 if failures > 0 {
167 log!(
168 Topic::Sync,
169 Warn,
170 "💦 sync.state: {failures} child cascade(s) failed; continuing"
171 );
172 }
173
174 Ok(())
175}
176
177fn save_state(bundle: &StateBundle) -> Result<(), Error> {
179 OpsError::deny_root()?;
180
181 if let Some(state) = bundle.app_state {
183 AppStateOps::import(state);
184 }
185 if let Some(state) = bundle.subnet_state {
186 SubnetStateOps::import(state);
187 }
188
189 if let Some(dir) = &bundle.app_directory {
191 AppDirectoryOps::import(dir.clone());
192 }
193 if let Some(dir) = &bundle.subnet_directory {
194 SubnetDirectoryOps::import(dir.clone());
195 }
196
197 Ok(())
198}
199
200async fn send_bundle(pid: &Principal, bundle: &StateBundle) -> Result<(), Error> {
202 let debug = bundle.debug();
203 log!(Topic::Sync, Info, "💦 sync.state: {debug} -> {pid}");
204
205 call_and_decode::<Result<(), Error>>(*pid, "canic_sync_state", bundle).await?
206}