canic_core/ops/sync/
state.rs

1//! State synchronization routines shared by root and child canisters.
2//!
3//! Bundles snapshot portions of `AppState`, `SubnetState`, and the directory
4//! views, ships them across the topology, and replays them on recipients.
5
6use 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///
21/// StateBundle
22/// Snapshot of mutable state and directory sections that can be propagated to peers
23///
24
25#[derive(CandidType, Clone, Debug, Default, Deserialize)]
26pub struct StateBundle {
27    // states
28    pub app_state: Option<AppStateData>,
29    pub subnet_state: Option<SubnetStateData>,
30
31    // directories
32    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    /// Construct a bundle containing the root canister’s full state view.
43    #[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    /// Compact debug string showing which sections are present.
78    /// Example: `[as ss .. sd]`
79    #[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    /// Whether the bundle carries any sections (true when every optional field is absent).
95    #[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
104/// Cascade from root: distribute the state bundle to direct children.
105/// No-op when the bundle is empty.
106pub 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
154/// Cascade from a child: forward the bundle to direct children.
155/// No-op when the bundle is empty.
156pub async fn nonroot_cascade_state(bundle: &StateBundle) -> Result<(), Error> {
157    OpsError::deny_root()?;
158
159    // update local state
160    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
197/// Save state locally on a child canister.
198fn save_state(bundle: &StateBundle) -> Result<(), Error> {
199    OpsError::deny_root()?;
200
201    // states
202    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    // directories
210    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
220/// Low-level bundle sender.
221async 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}