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 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///
22/// StateBundle
23/// Snapshot of mutable state and directory sections that can be propagated to peers
24///
25
26#[derive(CandidType, Clone, Debug, Default, Deserialize)]
27pub struct StateBundle {
28    // states
29    pub app_state: Option<AppStateData>,
30    pub subnet_state: Option<SubnetStateData>,
31
32    // directories
33    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    /// Construct a bundle containing the root canister’s full state view.
44    #[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    /// Compact debug string showing which sections are present.
79    /// Example: `[as ss .. sd]`
80    #[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    /// Whether the bundle carries any sections (true when every optional field is absent).
96    #[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
105/// Cascade from root: distribute the state bundle to direct children.
106/// No-op when the bundle is empty.
107pub(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
150/// Public wrapper for root state cascades to keep the internal entrypoint crate-private.
151pub async fn cascade_root_state(bundle: StateBundle) -> Result<(), Error> {
152    root_cascade_state(bundle).await
153}
154
155/// Cascade from a child: forward the bundle to direct children.
156/// No-op when the bundle is empty.
157pub async fn nonroot_cascade_state(bundle: &StateBundle) -> Result<(), Error> {
158    OpsError::deny_root()?;
159
160    // update local state
161    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
192/// Save state locally on a child canister.
193fn save_state(bundle: &StateBundle) -> Result<(), Error> {
194    OpsError::deny_root()?;
195
196    // states
197    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    // directories
205    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
215/// Low-level bundle sender.
216async 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}