Skip to main content

arkhe_kernel/runtime/
event.rs

1//! `KernelEvent` and supporting enums.
2//!
3//! `#[non_exhaustive]` everywhere: external matchers cannot wildcard-match,
4//! so adding a variant is not breaking for external consumers. The
5//! `clippy::wildcard_enum_match_arm = deny` lint enforces this within
6//! the crate as well.
7
8use bitflags::bitflags;
9use bytes::Bytes;
10use serde::{Deserialize, Serialize};
11
12use crate::abi::{EntityId, InstanceId, RouteId, Tick, TypeCode};
13use crate::state::ScheduledActionId;
14
15/// Top-level kernel-emitted event. Routed through observer filters and
16/// recorded in WAL (chunks 3b/c+).
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[non_exhaustive]
19pub enum KernelEvent {
20    /// An action completed dispatch successfully.
21    ActionExecuted {
22        /// Instance where the action ran.
23        instance: InstanceId,
24        /// Type code of the executed action.
25        action_type: TypeCode,
26        /// Tick at which `step()` processed the action.
27        at: Tick,
28    },
29    /// Action `compute()` failed (panic or error). `reason` is opaque bytes.
30    ActionFailed {
31        /// Instance where the action was attempted.
32        instance: InstanceId,
33        /// Type code of the failing action.
34        action_type: TypeCode,
35        /// Opaque failure reason — kernel does not interpret.
36        reason: Bytes,
37    },
38    /// Effect application failed during dispatcher.
39    EffectFailed {
40        /// Instance where the effect was being applied.
41        instance: InstanceId,
42        /// Opaque failure reason (e.g. `b"budget_exceeded"` from
43        /// memory-budget enforcement).
44        reason: Bytes,
45    },
46    /// Observer panicked during `on_event`. Bounded payload —
47    /// `observer_index` only, no panic message (covert channel closed).
48    ObserverPanic {
49        /// Index of the panicking observer in the registry.
50        observer_index: u16,
51    },
52    /// First-panic eviction (A22).
53    ObserverEvicted {
54        /// Index of the evicted observer.
55        observer_index: u16,
56        /// Sequence number of the event that triggered the panic.
57        panic_at_seq: u64,
58        /// Panic count before eviction (always `1` under the
59        /// first-panic policy).
60        panic_count_before_eviction: u32,
61    },
62    /// Cross-instance signal dropped (reserved variant for the
63    /// `SendSignal` rate-limit (deferred); constructible today for tests).
64    SignalDropped {
65        /// Target instance the signal was destined for.
66        target: InstanceId,
67        /// Route discriminant.
68        route: RouteId,
69        /// Why the kernel dropped the signal.
70        reason: SignalDropReason,
71    },
72    /// Module force-unloaded via `force_unload` cap path.
73    ModuleForceUnloaded {
74        /// Route id whose `inflight_refs` were drained.
75        route_id: RouteId,
76        /// Sum of live refs that were dropped across instances.
77        live_refs_at_unload: u32,
78    },
79    /// Action deferred to the next tick (reserved variant).
80    ActionDeferredToNextTick {
81        /// Id of the deferred scheduled action.
82        action_id: ScheduledActionId,
83        /// Why the action was deferred.
84        reason: DeferReason,
85    },
86    /// `BestEffort` durability barrier flushed pending observer events.
87    ObserversFlushed {
88        /// Caller-supplied barrier ticket.
89        barrier_ticket: u64,
90        /// Number of events drained at this barrier.
91        event_count: u32,
92    },
93    /// Domain `Op::EmitEvent` produced an event payload.
94    DomainEventEmitted {
95        /// Instance that emitted the event.
96        instance: InstanceId,
97        /// Optional originating entity.
98        actor: Option<EntityId>,
99        /// Event type discriminant.
100        event_type_code: TypeCode,
101        /// Canonical bytes of the event payload.
102        bytes: Bytes,
103    },
104    /// A cross-instance `SendSignal` was routed into the target instance's
105    /// per-route inbox (the delivery counterpart of [`SignalDropped`]).
106    SignalDelivered {
107        /// Sending instance.
108        from: InstanceId,
109        /// Target instance whose inbox received the signal.
110        target: InstanceId,
111        /// Route discriminant the signal was enqueued under.
112        route: RouteId,
113    },
114}
115
116/// Why a `SendSignal` op was dropped before delivery.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118#[non_exhaustive]
119pub enum SignalDropReason {
120    /// Target instance's IPC queue was full.
121    QueueFull,
122    /// Target instance does not exist (or has been despawned).
123    TargetNotFound,
124    /// Sender cancelled the signal before delivery.
125    Cancelled,
126}
127
128/// Why a scheduled action was deferred to the next tick.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
130#[non_exhaustive]
131pub enum DeferReason {
132    /// Per-step scheduler dispatch budget was reached.
133    SchedulerBusy,
134    /// Per-instance resource budget would be exceeded by running this
135    /// action now.
136    BudgetExceeded,
137}
138
139/// Stable observer registration handle returned by `Kernel::register_observer`.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
141#[serde(transparent)]
142pub struct ObserverHandle(
143    /// Monotonic registry index assigned at registration. A `u64` (not
144    /// `u16`) so the monotonic counter cannot realistically saturate and
145    /// alias a live handle (the registry has no unregister, so handles
146    /// only ever grow).
147    pub u64,
148);
149
150bitflags! {
151    /// Event-class filter for observer registration. One bit per
152    /// `KernelEvent` variant; an observer registered with a mask only
153    /// receives events whose variant bit is set. `EventMask::ALL`
154    /// (the `Default`) matches every variant — backward-compatible with
155    /// the unfiltered `Kernel::register_observer` path.
156    ///
157    /// Bit assignments are part of the public surface; new variants
158    /// must take the next free bit (no repurposing).
159    #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, serde::Serialize, serde::Deserialize)]
160    pub struct EventMask: u32 {
161        /// Match [`KernelEvent::ActionExecuted`].
162        const ACTION_EXECUTED       = 1 << 0;
163        /// Match [`KernelEvent::ActionFailed`].
164        const ACTION_FAILED         = 1 << 1;
165        /// Match [`KernelEvent::EffectFailed`].
166        const EFFECT_FAILED         = 1 << 2;
167        /// Match [`KernelEvent::ObserverPanic`].
168        const OBSERVER_PANIC        = 1 << 3;
169        /// Match [`KernelEvent::ObserverEvicted`].
170        const OBSERVER_EVICTED      = 1 << 4;
171        /// Match [`KernelEvent::SignalDropped`].
172        const SIGNAL_DROPPED        = 1 << 5;
173        /// Match [`KernelEvent::ModuleForceUnloaded`].
174        const MODULE_FORCE_UNLOADED = 1 << 6;
175        /// Match [`KernelEvent::ActionDeferredToNextTick`].
176        const ACTION_DEFERRED       = 1 << 7;
177        /// Match [`KernelEvent::ObserversFlushed`].
178        const OBSERVERS_FLUSHED     = 1 << 8;
179        /// Match [`KernelEvent::DomainEventEmitted`].
180        const DOMAIN_EVENT_EMITTED  = 1 << 9;
181        /// Match [`KernelEvent::SignalDelivered`].
182        const SIGNAL_DELIVERED      = 1 << 10;
183        /// Match every variant — equivalent to `Default`.
184        const ALL                   = 0x7FF;
185    }
186}
187
188impl Default for EventMask {
189    fn default() -> Self {
190        Self::ALL
191    }
192}
193
194impl EventMask {
195    /// Whether this mask wants to be notified of `event`.
196    pub(crate) fn matches(&self, event: &KernelEvent) -> bool {
197        match event {
198            KernelEvent::ActionExecuted { .. } => self.contains(Self::ACTION_EXECUTED),
199            KernelEvent::ActionFailed { .. } => self.contains(Self::ACTION_FAILED),
200            KernelEvent::EffectFailed { .. } => self.contains(Self::EFFECT_FAILED),
201            KernelEvent::ObserverPanic { .. } => self.contains(Self::OBSERVER_PANIC),
202            KernelEvent::ObserverEvicted { .. } => self.contains(Self::OBSERVER_EVICTED),
203            KernelEvent::SignalDropped { .. } => self.contains(Self::SIGNAL_DROPPED),
204            KernelEvent::ModuleForceUnloaded { .. } => self.contains(Self::MODULE_FORCE_UNLOADED),
205            KernelEvent::ActionDeferredToNextTick { .. } => self.contains(Self::ACTION_DEFERRED),
206            KernelEvent::ObserversFlushed { .. } => self.contains(Self::OBSERVERS_FLUSHED),
207            KernelEvent::DomainEventEmitted { .. } => self.contains(Self::DOMAIN_EVENT_EMITTED),
208            KernelEvent::SignalDelivered { .. } => self.contains(Self::SIGNAL_DELIVERED),
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn kernel_event_all_variants_constructible() {
219        let inst = InstanceId::new(1).unwrap();
220        let route = RouteId(1);
221        let _ = KernelEvent::ActionExecuted {
222            instance: inst,
223            action_type: TypeCode(1),
224            at: Tick(0),
225        };
226        let _ = KernelEvent::ActionFailed {
227            instance: inst,
228            action_type: TypeCode(1),
229            reason: Bytes::from_static(b"r"),
230        };
231        let _ = KernelEvent::EffectFailed {
232            instance: inst,
233            reason: Bytes::new(),
234        };
235        let _ = KernelEvent::ObserverPanic { observer_index: 0 };
236        let _ = KernelEvent::ObserverEvicted {
237            observer_index: 0,
238            panic_at_seq: 1,
239            panic_count_before_eviction: 1,
240        };
241        let _ = KernelEvent::SignalDropped {
242            target: inst,
243            route,
244            reason: SignalDropReason::QueueFull,
245        };
246        let _ = KernelEvent::ModuleForceUnloaded {
247            route_id: route,
248            live_refs_at_unload: 0,
249        };
250        let _ = KernelEvent::ActionDeferredToNextTick {
251            action_id: ScheduledActionId::new(1).unwrap(),
252            reason: DeferReason::SchedulerBusy,
253        };
254        let _ = KernelEvent::ObserversFlushed {
255            barrier_ticket: 0,
256            event_count: 0,
257        };
258    }
259
260    #[test]
261    fn signal_drop_reason_copy_eq() {
262        let r1 = SignalDropReason::QueueFull;
263        let r2 = r1;
264        assert_eq!(r1, r2);
265        assert_ne!(r1, SignalDropReason::TargetNotFound);
266    }
267
268    #[test]
269    fn defer_reason_copy_distinct() {
270        let r1 = DeferReason::SchedulerBusy;
271        let r2 = DeferReason::BudgetExceeded;
272        assert_ne!(r1, r2);
273    }
274
275    #[test]
276    fn observer_handle_total_order() {
277        let h1 = ObserverHandle(1);
278        let h2 = ObserverHandle(2);
279        assert!(h1 < h2);
280        assert_eq!(h1, ObserverHandle(1));
281    }
282}