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 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
144/// Cascade from a child: forward the bundle to direct children.
145/// No-op when the bundle is empty.
146pub async fn nonroot_cascade_state(bundle: &StateBundle) -> Result<(), Error> {
147    OpsError::deny_root()?;
148
149    // update local state
150    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
177/// Save state locally on a child canister.
178fn save_state(bundle: &StateBundle) -> Result<(), Error> {
179    OpsError::deny_root()?;
180
181    // states
182    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    // directories
190    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
200/// Low-level bundle sender.
201async 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}