Skip to main content

arkhe_kernel/persist/
snapshot.rs

1//! `KernelSnapshot` — serializable point-in-time capture of kernel state.
2//!
3//! Snapshot is **independent** from the WAL: WAL stores history (full
4//! replay from a fresh state), snapshot stores a single state at tick
5//! N. Hybrid recovery (snapshot at N + WAL from N+1) is the future
6//! integration; they ship as orthogonal mechanisms.
7//!
8//! What snapshot includes:
9//! - All instances (entities, components, scheduler, ledger, id_counters,
10//!   inflight_refs, wall_remainder, local_tick).
11//! - Kernel-level `next_instance_id` counter.
12//!
13//! What snapshot **excludes**:
14//! - `Box<dyn KernelObserver>` (not Serialize).
15//! - `ActionRegistry` (fn pointers, not Serialize).
16//! - Attached `WalWriter` (independent persistence layer).
17//!
18//! After `Kernel::from_snapshot(...)`, the caller must re-register every
19//! Action that was active when the snapshot was taken, and re-attach
20//! observers/WAL as needed.
21//!
22//! Determinism (A1): identical kernel state produces identical snapshot
23//! bytes — postcard-canonical encoding + BTreeMap iteration (A5).
24
25use std::collections::BTreeMap;
26
27use serde::{Deserialize, Serialize};
28
29use crate::abi::InstanceId;
30use crate::state::instance::InstanceSnapshot;
31
32/// Opaque point-in-time snapshot of kernel state.
33///
34/// Pub struct, pub(crate) fields — external callers see the type at the
35/// API boundary and can hold a value, but cannot inspect its internals
36/// (use [`serialize`](Self::serialize) / [`deserialize`](Self::deserialize)
37/// for round-trip persistence). Fed to
38/// [`Kernel::from_snapshot`](crate::Kernel::from_snapshot) for restore.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct KernelSnapshot {
41    pub(crate) instances: BTreeMap<InstanceId, InstanceSnapshot>,
42    pub(crate) next_instance_id: u64,
43}
44
45impl KernelSnapshot {
46    /// Encode to canonical postcard bytes.
47    pub fn serialize(&self) -> Result<Vec<u8>, SnapshotError> {
48        postcard::to_allocvec(self).map_err(|e| SnapshotError::SerializeFailed(format!("{}", e)))
49    }
50
51    /// Decode from canonical postcard bytes.
52    pub fn deserialize(bytes: &[u8]) -> Result<Self, SnapshotError> {
53        postcard::from_bytes(bytes).map_err(|e| SnapshotError::DeserializeFailed(format!("{}", e)))
54    }
55
56    /// Number of instances captured.
57    pub fn instance_count(&self) -> usize {
58        self.instances.len()
59    }
60
61    /// Iterate captured instance ids in canonical (`InstanceId` ascending) order.
62    pub fn instance_ids(&self) -> impl Iterator<Item = InstanceId> + '_ {
63        self.instances.keys().copied()
64    }
65
66    /// Crate-internal constructor used by `Kernel::snapshot`.
67    #[doc(hidden)]
68    pub fn __construct(
69        instances: BTreeMap<InstanceId, InstanceSnapshot>,
70        next_instance_id: u64,
71    ) -> Self {
72        Self {
73            instances,
74            next_instance_id,
75        }
76    }
77
78    /// Crate-internal destructor used by `Kernel::from_snapshot`.
79    #[doc(hidden)]
80    pub fn __into_parts(self) -> (BTreeMap<InstanceId, InstanceSnapshot>, u64) {
81        (self.instances, self.next_instance_id)
82    }
83}
84
85/// Snapshot postcard round-trip failures.
86#[non_exhaustive]
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum SnapshotError {
89    /// Postcard refused to encode the snapshot.
90    SerializeFailed(String),
91    /// Postcard refused to decode the bytes.
92    DeserializeFailed(String),
93}
94
95impl core::fmt::Display for SnapshotError {
96    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
97        match self {
98            Self::SerializeFailed(m) => write!(f, "snapshot serialize failed: {}", m),
99            Self::DeserializeFailed(m) => write!(f, "snapshot deserialize failed: {}", m),
100        }
101    }
102}
103
104impl std::error::Error for SnapshotError {}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::abi::{CapabilityMask, EntityId, Principal, Tick, TypeCode};
110    use crate::state::traits::_sealed::Sealed;
111    use crate::state::{ActionCompute, ActionContext, ActionDeriv, InstanceConfig, Op};
112    use crate::Kernel;
113    use bytes::Bytes;
114    use serde::{Deserialize as De, Serialize as Ser};
115
116    /// Test action: spawn entity `id` and attach a 4-byte component
117    /// under TypeCode(7).
118    #[derive(Ser, De)]
119    struct SpawnSetAction {
120        id: u64,
121    }
122    impl Sealed for SpawnSetAction {}
123    impl ActionDeriv for SpawnSetAction {
124        const TYPE_CODE: TypeCode = TypeCode(900);
125        const SCHEMA_VERSION: u32 = 1;
126    }
127    impl ActionCompute for SpawnSetAction {
128        fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
129            let entity = EntityId::new(self.id).unwrap();
130            vec![
131                Op::SpawnEntity {
132                    id: entity,
133                    owner: Principal::System,
134                },
135                Op::SetComponent {
136                    entity,
137                    type_code: TypeCode(7),
138                    bytes: Bytes::from(vec![0xCDu8; 4]),
139                    size: 4,
140                },
141            ]
142        }
143    }
144
145    fn submit(k: &mut Kernel, inst: InstanceId, id: u64) {
146        use crate::state::Action;
147        let bytes = Action::canonical_bytes(&SpawnSetAction { id });
148        k.submit(
149            inst,
150            Principal::System,
151            None,
152            Tick(0),
153            SpawnSetAction::TYPE_CODE,
154            bytes,
155        )
156        .unwrap();
157    }
158
159    fn boot_with_state(actions: &[u64]) -> (Kernel, InstanceId) {
160        let mut k = Kernel::new();
161        k.register_action::<SpawnSetAction>();
162        let inst = k.create_instance(InstanceConfig::default());
163        for id in actions {
164            submit(&mut k, inst, *id);
165            let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
166        }
167        (k, inst)
168    }
169
170    #[test]
171    fn snapshot_empty_kernel_serdes_roundtrip() {
172        let k = Kernel::new();
173        let snap = k.snapshot();
174        assert_eq!(snap.instance_count(), 0);
175        let bytes = snap.serialize().unwrap();
176        let back = KernelSnapshot::deserialize(&bytes).unwrap();
177        assert_eq!(back.instance_count(), 0);
178    }
179
180    #[test]
181    fn snapshot_captures_instance_state() {
182        let (k, inst) = boot_with_state(&[1, 2, 3]);
183        let snap = k.snapshot();
184        assert_eq!(snap.instance_count(), 1);
185        let ids: Vec<InstanceId> = snap.instance_ids().collect();
186        assert_eq!(ids, vec![inst]);
187    }
188
189    #[test]
190    fn snapshot_preserves_entities_and_components() {
191        let (k1, inst) = boot_with_state(&[1, 2]);
192        let snap = k1.snapshot();
193        let bytes = snap.serialize().unwrap();
194        let snap2 = KernelSnapshot::deserialize(&bytes).unwrap();
195        let mut k2 = Kernel::from_snapshot(snap2);
196        // Caller MUST re-register the action types they care about; for
197        // the read assertions below this isn't required.
198
199        let v1 = k1.instance_view(inst).unwrap();
200        let v2 = k2.instance_view(inst).unwrap();
201        assert_eq!(v1.entity_count(), v2.entity_count());
202        assert_eq!(v1.component_count(), v2.component_count());
203        assert_eq!(
204            v1.component(EntityId::new(1).unwrap(), TypeCode(7)),
205            v2.component(EntityId::new(1).unwrap(), TypeCode(7)),
206        );
207        assert_eq!(
208            v1.component(EntityId::new(2).unwrap(), TypeCode(7)),
209            v2.component(EntityId::new(2).unwrap(), TypeCode(7)),
210        );
211        // After from_snapshot, k2 is mutable; verify we can re-register the
212        // action and submit on the restored kernel without panic.
213        k2.register_action::<SpawnSetAction>();
214        submit(&mut k2, inst, 3);
215        let _ = k2.step(Tick(0), CapabilityMask::SYSTEM);
216        assert_eq!(k2.instance_view(inst).unwrap().entity_count(), 3);
217    }
218
219    #[test]
220    fn snapshot_preserves_id_counters() {
221        // create_instance increments next_instance_id; verify the round
222        // trip produces a kernel that issues the same next id.
223        let mut k1 = Kernel::new();
224        let _ = k1.create_instance(InstanceConfig::default());
225        let _ = k1.create_instance(InstanceConfig::default());
226        let _ = k1.create_instance(InstanceConfig::default()); // next_instance_id = 3
227        let snap = k1.snapshot();
228        let bytes = snap.serialize().unwrap();
229        let snap2 = KernelSnapshot::deserialize(&bytes).unwrap();
230        let mut k2 = Kernel::from_snapshot(snap2);
231        // Both kernels' next create_instance returns id=4.
232        let next1 = k1.create_instance(InstanceConfig::default());
233        let next2 = k2.create_instance(InstanceConfig::default());
234        assert_eq!(next1, next2);
235        assert_eq!(next1.get(), 4);
236    }
237
238    #[test]
239    fn snapshot_preserves_local_tick_and_wall_remainder() {
240        // Round-trip a kernel that has progressed; the restored kernel's
241        // view exposes the same local_tick. (apply_stage doesn't auto-
242        // advance local_tick — both readings should be 0.)
243        let (k1, inst) = boot_with_state(&[1]);
244        let snap = k1.snapshot();
245        let bytes = snap.serialize().unwrap();
246        let k2 = Kernel::from_snapshot(KernelSnapshot::deserialize(&bytes).unwrap());
247        assert_eq!(
248            k1.instance_view(inst).unwrap().local_tick(),
249            k2.instance_view(inst).unwrap().local_tick(),
250        );
251    }
252
253    #[test]
254    fn snapshot_deserialize_fresh_kernel_no_observers_no_registry() {
255        // After from_snapshot, observer count is 0 and action registry is
256        // empty. Submitting an unknown TypeCode silently skips (per
257        // existing kernel semantics).
258        let (k1, inst) = boot_with_state(&[1]);
259        let snap = k1.snapshot();
260        let mut k2 =
261            Kernel::from_snapshot(KernelSnapshot::deserialize(&snap.serialize().unwrap()).unwrap());
262        assert_eq!(k2.stats().observer_count, 0);
263        // Action registry empty: submit + step skips the action (no
264        // deserializer registered).
265        k2.submit(
266            inst,
267            Principal::System,
268            None,
269            Tick(0),
270            TypeCode(900),
271            vec![1u8],
272        )
273        .unwrap();
274        let report = k2.step(Tick(0), CapabilityMask::SYSTEM);
275        assert_eq!(report.actions_executed, 1);
276        assert_eq!(report.effects_applied, 0);
277    }
278
279    #[test]
280    fn snapshot_deterministic_same_state_same_bytes() {
281        // A1 D1 extension: identical kernel state ⇒ identical snapshot bytes.
282        // Build two independent kernels through the same sequence of
283        // operations and compare the postcard outputs byte-for-byte.
284        let (k1, _) = boot_with_state(&[1, 2, 3]);
285        let (k2, _) = boot_with_state(&[1, 2, 3]);
286        let b1 = k1.snapshot().serialize().unwrap();
287        let b2 = k2.snapshot().serialize().unwrap();
288        assert_eq!(
289            b1, b2,
290            "identical state must produce identical snapshot bytes"
291        );
292    }
293}