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}