Skip to main content

arkhe_kernel/runtime/
apply.rs

1//! Stage application — runtime-side `apply_stage`.
2//!
3//! Lives in `runtime/` so the layer DAG `abi → state → runtime → persist`
4//! is preserved (R4-X) — placing it on `Instance` would force `state` to
5//! import `runtime::StepStage`.
6//!
7//! `apply_stage` borrows the stage `&mut` and drains the buckets it commits,
8//! so the kernel can reuse one `StepStage` scratch across actions. The
9//! rollback path needs no separate function: the kernel simply skips
10//! `apply_stage` and `clear()`s the scratch before the next action.
11//!
12//! Application is panic-free (totality contract): every operation
13//! uses saturating arithmetic and `if let Some(...)` style guards.
14
15use super::stage::{LedgerOp, PendingSignal, ScheduledEntryDelta, StagedStateDelta, StepStage};
16use crate::state::Instance;
17
18/// Apply a `StepStage` to `instance` in canonical order:
19///
20/// 1. `id_counters`
21/// 2. `state_ops`
22/// 3. `ledger_delta`
23/// 4. `schedule_deltas`
24/// 5. `wall_remainder` and `local_tick`
25///
26/// Kernel-level buckets `pending_signals` / `events` / `observer_eviction`
27/// are drained by Kernel post-commit (chunks 3b/c) — left untouched here.
28///
29/// `stage` is borrowed `&mut` (not consumed): the kernel owns one
30/// `StepStage` scratch and reuses it across actions, so this function drains
31/// the buckets it applies (retaining their capacity) and leaves the
32/// kernel-level buckets (`events`, `observer_eviction_pending`) for the
33/// caller. The caller `clear()`s the scratch before the next action.
34///
35/// Returns the drained outbound `pending_signals` so the kernel router can
36/// resolve targets and deliver them into per-route inboxes (routing needs
37/// the cross-instance map, which lives on `Kernel`, not `Instance` — so it
38/// cannot happen here without violating the single-`iter_mut` borrow).
39pub(crate) fn apply_stage(instance: &mut Instance, stage: &mut StepStage) -> Vec<PendingSignal> {
40    // 1. id_counters — monotonic; no preconditions.
41    {
42        let c = instance.id_counters_mut();
43        c.next_entity = c
44            .next_entity
45            .saturating_add(stage.id_counters.next_entity_advance);
46        c.next_scheduled = c
47            .next_scheduled
48            .saturating_add(stage.id_counters.next_scheduled_advance);
49        c.next_source_seq = c
50            .next_source_seq
51            .saturating_add(stage.id_counters.next_source_seq_advance);
52    }
53
54    // 2. state_ops — entity/component mutation.
55    for op in stage.state_ops.drain(..) {
56        match op {
57            StagedStateDelta::SpawnEntity { id, meta } => {
58                instance.insert_entity(id, meta);
59            }
60            StagedStateDelta::DespawnEntity { id } => {
61                instance.remove_entity(id);
62            }
63            StagedStateDelta::SetComponent {
64                entity,
65                type_code,
66                bytes,
67                ..
68            } => {
69                // Gate the component store on entity existence, mirroring
70                // `ResourceLedger::add_component` (which rejects an unknown
71                // entity). Without this guard the component map and the
72                // ledger diverge: the bytes would be stored but unaccounted,
73                // leaving them invisible to `memory_budget_bytes` and to the
74                // ledger baseline (a cross-step budget bypass and unbounded
75                // growth). An in-stage `SpawnEntity` for the same entity is
76                // already applied earlier in this `state_ops` loop, so a
77                // spawn-then-set within one step still stores correctly.
78                if instance.entity_meta(entity).is_some() {
79                    instance.insert_component((entity, type_code), bytes);
80                }
81            }
82            StagedStateDelta::RemoveComponent {
83                entity, type_code, ..
84            } => {
85                instance.remove_component((entity, type_code));
86            }
87        }
88    }
89
90    // 3. ledger_delta — accounting follows entity/component apply.
91    {
92        let ledger = instance.ledger_mut();
93        for lop in stage.ledger_delta.ops.drain(..) {
94            match lop {
95                LedgerOp::AddEntity(id) => {
96                    let _ = ledger.add_entity(id);
97                }
98                LedgerOp::RemoveEntity(id) => {
99                    let _ = ledger.remove_entity(id);
100                }
101                LedgerOp::AddComponent {
102                    entity,
103                    type_code,
104                    size,
105                } => {
106                    let _ = ledger.add_component(entity, type_code, size);
107                }
108                LedgerOp::RemoveComponent {
109                    entity,
110                    type_code,
111                    size,
112                } => {
113                    let _ = ledger.remove_component(entity, type_code, size);
114                }
115            }
116        }
117    }
118
119    // 4. schedule_deltas — scheduler mutation. The Add path commits each
120    // staged entry under its OWN pre-allocated id (dispatch minted it
121    // deterministically from the instance's id counter), so the id is decided
122    // once and reproduced verbatim by re-execution on replay.
123    {
124        let scheduler = instance.scheduler_mut();
125        for sd in stage.schedule_deltas.drain(..) {
126            match sd {
127                ScheduledEntryDelta::Add(entry) => {
128                    // Commit the staged entry under its OWN pre-allocated id
129                    // (dispatch minted it deterministically) — never re-mint a
130                    // fresh id here, so the id that dispatch decided is the id
131                    // the scheduler holds end-to-end (replay reproduces it by
132                    // re-execution).
133                    scheduler.schedule_with_id(
134                        entry.id,
135                        entry.at,
136                        entry.actor,
137                        entry.principal,
138                        entry.caps_ceiling,
139                        entry.action_type_code,
140                        entry.action_bytes,
141                    );
142                }
143                ScheduledEntryDelta::Remove(id) => {
144                    let _ = scheduler.cancel(id);
145                }
146            }
147        }
148    }
149
150    // 8. wall_remainder + local_tick advance.
151    instance.advance_wall_remainder(stage.wall_remainder_delta);
152    instance.advance_local_tick(stage.local_tick_delta);
153
154    // 6/7/9. events / observer_eviction_pending stay for the kernel to
155    // read post-commit (then `clear()`). pending_signals are DRAINED and
156    // returned for the kernel router (delivery + refcount + events happen
157    // there, where the cross-instance map is reachable).
158    stage.pending_signals.drain(..).collect()
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use bytes::Bytes;
165
166    use crate::abi::{CapabilityMask, EntityId, InstanceId, Principal, Tick, TypeCode};
167    use crate::runtime::stage::{IdCountersDelta, LedgerOp, ScheduledEntryDelta, StagedStateDelta};
168    use crate::state::{
169        EntityMeta, Instance, InstanceConfig, QuotaReductionPolicy, ScheduledActionId,
170        ScheduledEntry,
171    };
172
173    fn id(n: u64) -> InstanceId {
174        InstanceId::new(n).unwrap()
175    }
176    fn entity(n: u64) -> EntityId {
177        EntityId::new(n).unwrap()
178    }
179    fn cfg() -> InstanceConfig {
180        InstanceConfig {
181            default_caps: CapabilityMask::default(),
182            max_entities: 100,
183            max_scheduled: 1000,
184            max_inbox_per_route: 16,
185            memory_budget_bytes: 1 << 20,
186            parent: None,
187            quota_reduction: QuotaReductionPolicy::default(),
188        }
189    }
190
191    #[test]
192    fn apply_empty_stage_leaves_instance_untouched() {
193        let mut inst = Instance::new(id(1), cfg());
194        apply_stage(&mut inst, &mut StepStage::default());
195        assert_eq!(inst.entities_len(), 0);
196        assert_eq!(inst.components_len(), 0);
197        assert_eq!(inst.local_tick(), 0);
198        assert_eq!(inst.wall_remainder(), 0);
199        assert_eq!(inst.ledger().total_entities(), 0);
200        assert_eq!(inst.id_counters().next_entity, 0);
201    }
202
203    #[test]
204    fn apply_spawn_entity() {
205        let mut inst = Instance::new(id(1), cfg());
206        let mut stage = StepStage::default();
207        stage.state_ops.push(StagedStateDelta::SpawnEntity {
208            id: entity(1),
209            meta: EntityMeta {
210                owner: Principal::System,
211                created: Tick(0),
212            },
213        });
214        apply_stage(&mut inst, &mut stage);
215        assert_eq!(inst.entities_len(), 1);
216    }
217
218    #[test]
219    fn apply_despawn_entity() {
220        let mut inst = Instance::new(id(1), cfg());
221        let mut spawn = StepStage::default();
222        spawn.state_ops.push(StagedStateDelta::SpawnEntity {
223            id: entity(1),
224            meta: EntityMeta {
225                owner: Principal::System,
226                created: Tick(0),
227            },
228        });
229        apply_stage(&mut inst, &mut spawn);
230        assert_eq!(inst.entities_len(), 1);
231
232        let mut despawn = StepStage::default();
233        despawn
234            .state_ops
235            .push(StagedStateDelta::DespawnEntity { id: entity(1) });
236        apply_stage(&mut inst, &mut despawn);
237        assert_eq!(inst.entities_len(), 0);
238    }
239
240    #[test]
241    fn apply_set_and_remove_component() {
242        let mut inst = Instance::new(id(1), cfg());
243        // Spawn the entity first: SetComponent on a known entity is the
244        // valid history (an unknown-entity set is a no-op — see
245        // `apply_set_component_on_unknown_entity_is_noop`).
246        let mut spawn = StepStage::default();
247        spawn.state_ops.push(StagedStateDelta::SpawnEntity {
248            id: entity(1),
249            meta: EntityMeta {
250                owner: Principal::System,
251                created: Tick(0),
252            },
253        });
254        apply_stage(&mut inst, &mut spawn);
255
256        let mut stage = StepStage::default();
257        stage.state_ops.push(StagedStateDelta::SetComponent {
258            entity: entity(1),
259            type_code: TypeCode(7),
260            bytes: Bytes::from_static(b"data"),
261            size: 4,
262        });
263        apply_stage(&mut inst, &mut stage);
264        assert_eq!(inst.components_len(), 1);
265
266        let mut rm = StepStage::default();
267        rm.state_ops.push(StagedStateDelta::RemoveComponent {
268            entity: entity(1),
269            type_code: TypeCode(7),
270            size: 4,
271        });
272        apply_stage(&mut inst, &mut rm);
273        assert_eq!(inst.components_len(), 0);
274    }
275
276    #[test]
277    fn apply_set_component_on_unknown_entity_is_noop() {
278        // Regression: a SetComponent on an entity that was never spawned must
279        // NOT store the component, so the component map and the ledger stay
280        // consistent (the ledger already rejects the unknown entity). Storing
281        // it would leave unaccounted bytes that bypass `memory_budget_bytes`.
282        let mut inst = Instance::new(id(1), cfg());
283        let mut stage = StepStage::default();
284        stage.state_ops.push(StagedStateDelta::SetComponent {
285            entity: entity(999),
286            type_code: TypeCode(7),
287            bytes: Bytes::from_static(b"orphan"),
288            size: 6,
289        });
290        stage.ledger_delta.ops.push(LedgerOp::AddComponent {
291            entity: entity(999),
292            type_code: TypeCode(7),
293            size: 6,
294        });
295        apply_stage(&mut inst, &mut stage);
296        assert_eq!(inst.components_len(), 0, "orphan component must not be stored");
297        assert_eq!(
298            inst.ledger().total_bytes(),
299            0,
300            "ledger must not account an unknown-entity component"
301        );
302        assert!(
303            inst.component(entity(999), TypeCode(7)).is_none(),
304            "InstanceView must not surface the orphan component"
305        );
306    }
307
308    #[test]
309    fn apply_spawn_then_set_in_same_stage_stores_component() {
310        // The in-stage spawn is applied before the set in the state_ops loop,
311        // so a spawn-then-set within one step stores correctly (the guard
312        // sees the just-spawned entity).
313        let mut inst = Instance::new(id(1), cfg());
314        let mut stage = StepStage::default();
315        stage.state_ops.push(StagedStateDelta::SpawnEntity {
316            id: entity(5),
317            meta: EntityMeta {
318                owner: Principal::System,
319                created: Tick(0),
320            },
321        });
322        stage.state_ops.push(StagedStateDelta::SetComponent {
323            entity: entity(5),
324            type_code: TypeCode(7),
325            bytes: Bytes::from_static(b"data"),
326            size: 4,
327        });
328        stage.ledger_delta.ops.push(LedgerOp::AddEntity(entity(5)));
329        stage.ledger_delta.ops.push(LedgerOp::AddComponent {
330            entity: entity(5),
331            type_code: TypeCode(7),
332            size: 4,
333        });
334        apply_stage(&mut inst, &mut stage);
335        assert_eq!(inst.components_len(), 1);
336        assert_eq!(inst.ledger().total_bytes(), 4);
337    }
338
339    #[test]
340    fn apply_ledger_delta_balanced() {
341        let mut inst = Instance::new(id(1), cfg());
342        let mut stage = StepStage::default();
343        stage.ledger_delta.ops.push(LedgerOp::AddEntity(entity(1)));
344        stage.ledger_delta.ops.push(LedgerOp::AddComponent {
345            entity: entity(1),
346            type_code: TypeCode(1),
347            size: 100,
348        });
349        apply_stage(&mut inst, &mut stage);
350        assert_eq!(inst.ledger().total_entities(), 1);
351        assert_eq!(inst.ledger().total_bytes(), 100);
352        assert_eq!(inst.ledger().entity_bytes(entity(1)), 100);
353    }
354
355    #[test]
356    fn apply_id_counters_advance() {
357        let mut inst = Instance::new(id(1), cfg());
358        let mut stage = StepStage {
359            id_counters: IdCountersDelta {
360                next_entity_advance: 5,
361                next_scheduled_advance: 3,
362                next_source_seq_advance: 7,
363            },
364            ..Default::default()
365        };
366        apply_stage(&mut inst, &mut stage);
367        assert_eq!(inst.id_counters().next_entity, 5);
368        assert_eq!(inst.id_counters().next_scheduled, 3);
369        assert_eq!(inst.id_counters().next_source_seq, 7);
370    }
371
372    #[test]
373    fn apply_wall_and_local_tick_advance() {
374        let mut inst = Instance::new(id(1), cfg());
375        let mut stage = StepStage {
376            wall_remainder_delta: 12345,
377            local_tick_delta: 7,
378            ..Default::default()
379        };
380        apply_stage(&mut inst, &mut stage);
381        assert_eq!(inst.wall_remainder(), 12345);
382        assert_eq!(inst.local_tick(), 7);
383    }
384
385    #[test]
386    fn apply_schedule_delta_add_inserts_into_scheduler() {
387        let mut inst = Instance::new(id(1), cfg());
388        let mut stage = StepStage {
389            schedule_deltas: vec![ScheduledEntryDelta::Add(ScheduledEntry {
390                id: ScheduledActionId::new(1).unwrap(),
391                at: Tick(10),
392                actor: None,
393                principal: Principal::System,
394                action_type_code: TypeCode(0),
395                action_bytes: vec![1, 2, 3],
396                caps_ceiling: CapabilityMask::all(),
397            })],
398            ..Default::default()
399        };
400        apply_stage(&mut inst, &mut stage);
401        assert_eq!(inst.scheduler().len(), 1);
402    }
403
404    #[test]
405    fn apply_then_clear_reuse_is_state_clean() {
406        // Scratch-reuse contract: applying one stage, then clear()ing it and
407        // applying a second through the SAME StepStage, must commit the second
408        // without any residue from the first (drains + clear leave no leak).
409        let mut inst = Instance::new(id(1), cfg());
410        let mut scratch = StepStage::default();
411        scratch.state_ops.push(StagedStateDelta::SpawnEntity {
412            id: entity(1),
413            meta: EntityMeta {
414                owner: Principal::System,
415                created: Tick(0),
416            },
417        });
418        scratch.ledger_delta.ops.push(LedgerOp::AddEntity(entity(1)));
419        scratch.local_tick_delta = 1;
420        apply_stage(&mut inst, &mut scratch);
421        scratch.clear();
422
423        scratch.state_ops.push(StagedStateDelta::SpawnEntity {
424            id: entity(2),
425            meta: EntityMeta {
426                owner: Principal::System,
427                created: Tick(0),
428            },
429        });
430        scratch.ledger_delta.ops.push(LedgerOp::AddEntity(entity(2)));
431        scratch.local_tick_delta = 1;
432        apply_stage(&mut inst, &mut scratch);
433
434        // Exactly the two spawned entities and two tick advances — no residue.
435        assert_eq!(inst.entities_len(), 2);
436        assert_eq!(inst.ledger().total_entities(), 2);
437        assert_eq!(inst.local_tick(), 2);
438    }
439}