Skip to main content

lenso_kernel/
event.rs

1use std::{
2    any::Any,
3    cell::{Cell, RefCell},
4    collections::VecDeque,
5    fmt,
6    marker::PhantomData,
7    panic::AssertUnwindSafe,
8    rc::{Rc, Weak},
9};
10
11use futures::{FutureExt, future::LocalBoxFuture};
12
13use super::{
14    CancellationToken, DiagnosticAdmission, DiagnosticEvent, DiagnosticSource, EventAdmissionPlan,
15    InvocationContext, NativeAppRuntime, RuntimeFailure, await_with_generation_context,
16    diagnostics::diagnostic_operation, ensure_context_active,
17    schedule_module_supervision_after_failure,
18};
19
20/// Static identity and Rust value types generated for one ephemeral Event Capability.
21pub trait EventCapability: 'static {
22    /// Typed value published to each explicitly bound subscriber.
23    type Event: Clone + 'static;
24    /// Stable Capability series identity.
25    const ID: &'static str;
26    /// Exact generated Descriptor version.
27    const DESCRIPTOR_VERSION: &'static str;
28}
29
30/// The admission result for one publisher-to-subscriber binding.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum EventAdmission {
33    /// The event entered that subscriber's volatile queue.
34    Accepted,
35    /// The subscriber binding is currently unavailable.
36    Unavailable,
37    /// The subscriber's bounded admission is full.
38    Exhausted,
39}
40
41/// Alias emphasizing the result's publication context.
42pub type EventPublishStatus = EventAdmission;
43
44/// One deterministic result for one explicit subscriber binding.
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct EventPublishResult {
47    subscriber_instance: String,
48    admission: EventAdmission,
49}
50
51impl EventPublishResult {
52    pub(crate) fn new(subscriber_instance: String, admission: EventAdmission) -> Self {
53        Self {
54            subscriber_instance,
55            admission,
56        }
57    }
58
59    /// Returns the App-local subscriber Instance key.
60    pub fn subscriber_instance(&self) -> &str {
61        &self.subscriber_instance
62    }
63
64    /// Returns the provider Instance key used by the explicit binding.
65    pub fn provider_instance(&self) -> &str {
66        self.subscriber_instance()
67    }
68
69    /// Returns the independent admission result for this binding.
70    pub const fn admission(&self) -> EventAdmission {
71        self.admission
72    }
73
74    /// Returns the status using publication-oriented terminology.
75    pub const fn status(&self) -> EventPublishStatus {
76        self.admission
77    }
78}
79
80/// Adapter-facing endpoint for one or more ephemeral Event Operations.
81pub trait NativeEventEndpoint: fmt::Debug {
82    /// Stable Capability series identity.
83    fn capability_id(&self) -> &'static str;
84    /// Exact Descriptor version implemented by this endpoint.
85    fn descriptor_version(&self) -> &'static str;
86    /// Exact stable Event Operation names implemented by this endpoint.
87    fn operations(&self) -> &'static [&'static str];
88    /// Returns whether the Adapter owns the subscriber's admission queue.
89    ///
90    /// Native endpoints use the Kernel mailbox by default. An out-of-process
91    /// Adapter may return `true` when its transport has already admitted the
92    /// value into the subscriber's own bounded queue before this call returns.
93    fn owns_event_admission(&self) -> bool {
94        false
95    }
96    /// Publishes one value after the endpoint has admitted it.
97    ///
98    /// Implementations must return once the value is accepted into their own
99    /// bounded volatile queue. Subscriber handler failures after that point
100    /// are deliberately outside the publisher's response path.
101    fn publish(
102        &self,
103        operation: &str,
104        event: Box<dyn Any>,
105        context: InvocationContext,
106    ) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
107}
108
109struct QueuedEvent {
110    operation: String,
111    event: Box<dyn Any>,
112    context: InvocationContext,
113    snapshot: NativeEventEndpointSnapshot,
114}
115
116impl fmt::Debug for QueuedEvent {
117    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118        formatter
119            .debug_struct("QueuedEvent")
120            .field("operation", &self.operation)
121            .field("request_id", &self.context.request_id())
122            .field("generation", &self.snapshot.generation)
123            .finish_non_exhaustive()
124    }
125}
126
127#[derive(Debug, Default)]
128struct NativeEventQueueState {
129    pending: VecDeque<QueuedEvent>,
130    admitted: usize,
131    draining: bool,
132}
133
134/// One bounded FIFO mailbox owned by one publisher-to-subscriber binding.
135#[derive(Debug)]
136pub(crate) struct NativeEventQueue {
137    capacity: usize,
138    state: RefCell<NativeEventQueueState>,
139}
140
141impl NativeEventQueue {
142    pub(crate) fn new(admission: EventAdmissionPlan) -> Rc<Self> {
143        Self {
144            capacity: admission.capacity(),
145            state: RefCell::new(NativeEventQueueState::default()),
146        }
147        .into()
148    }
149
150    fn try_enqueue(&self, event: QueuedEvent) -> Option<bool> {
151        let mut state = self.state.borrow_mut();
152        if state.admitted >= self.capacity {
153            return None;
154        }
155        state.admitted += 1;
156        state.pending.push_back(event);
157        if state.draining {
158            Some(false)
159        } else {
160            state.draining = true;
161            Some(true)
162        }
163    }
164
165    fn pop(&self) -> Option<QueuedEvent> {
166        self.state.borrow_mut().pending.pop_front()
167    }
168
169    fn complete(&self) {
170        let mut state = self.state.borrow_mut();
171        state.admitted = state.admitted.saturating_sub(1);
172        if state.pending.is_empty() {
173            state.draining = false;
174        }
175    }
176
177    fn abort(&self) {
178        let mut state = self.state.borrow_mut();
179        state.pending.clear();
180        state.admitted = 0;
181        state.draining = false;
182    }
183}
184
185#[derive(Clone, Debug)]
186pub(crate) struct NativeEventEndpointSnapshot {
187    pub(crate) endpoint: Rc<dyn NativeEventEndpoint>,
188    pub(crate) generation: u64,
189    pub(crate) cancellation: CancellationToken,
190}
191
192#[derive(Debug)]
193pub(crate) struct NativeEventEndpointState {
194    pub(crate) capability_id: &'static str,
195    pub(crate) descriptor_version: &'static str,
196    pub(crate) operations: &'static [&'static str],
197    endpoint: RefCell<Option<Rc<dyn NativeEventEndpoint>>>,
198    generation: Cell<u64>,
199    cancellation: RefCell<CancellationToken>,
200    queues: RefCell<Vec<Weak<NativeEventQueue>>>,
201}
202
203impl NativeEventEndpointState {
204    pub(crate) fn new(endpoint: Rc<dyn NativeEventEndpoint>, generation: u64) -> Self {
205        Self {
206            capability_id: endpoint.capability_id(),
207            descriptor_version: endpoint.descriptor_version(),
208            operations: endpoint.operations(),
209            endpoint: RefCell::new(Some(endpoint)),
210            generation: Cell::new(generation),
211            cancellation: RefCell::new(CancellationToken::new()),
212            queues: RefCell::new(Vec::new()),
213        }
214    }
215
216    pub(crate) fn snapshot(&self) -> Option<NativeEventEndpointSnapshot> {
217        self.endpoint
218            .borrow()
219            .clone()
220            .map(|endpoint| NativeEventEndpointSnapshot {
221                endpoint,
222                generation: self.generation.get(),
223                cancellation: self.cancellation.borrow().clone(),
224            })
225    }
226
227    pub(crate) fn mark_unavailable(&self) {
228        self.cancellation.borrow().cancel();
229        self.endpoint.borrow_mut().take();
230        self.reset_queues();
231    }
232
233    pub(crate) fn install(&self, endpoint: Rc<dyn NativeEventEndpoint>, generation: u64) {
234        self.generation.set(generation);
235        self.cancellation.replace(CancellationToken::new());
236        self.endpoint.replace(Some(endpoint));
237    }
238
239    pub(crate) fn is_current(&self, generation: u64) -> bool {
240        self.generation.get() == generation && self.endpoint.borrow().is_some()
241    }
242
243    pub(crate) fn register_queue(&self, queue: &Rc<NativeEventQueue>) {
244        self.queues.borrow_mut().push(Rc::downgrade(queue));
245    }
246
247    fn reset_queues(&self) {
248        self.queues.borrow_mut().retain(|queue| {
249            let Some(queue) = queue.upgrade() else {
250                return false;
251            };
252            queue.abort();
253            true
254        });
255    }
256}
257
258#[derive(Clone, Debug)]
259pub(crate) struct NativeEventEndpointBinding {
260    pub(crate) module_instance: String,
261    pub(crate) state: Rc<NativeEventEndpointState>,
262    pub(crate) queue: Rc<NativeEventQueue>,
263}
264
265/// An opaque Event endpoint passed to Module lifecycle code.
266#[derive(Clone, Debug)]
267pub struct ModuleEventDependencyHandle {
268    pub(crate) binding: NativeEventEndpointBinding,
269    pub(crate) caller_instance: String,
270    pub(crate) runtime: Rc<RefCell<std::rc::Weak<NativeAppRuntime>>>,
271}
272
273impl ModuleEventDependencyHandle {
274    /// Returns the Capability implemented by this handle.
275    pub fn capability_id(&self) -> &'static str {
276        self.binding.state.capability_id
277    }
278
279    /// Returns the exact Descriptor version implemented by this handle.
280    pub fn descriptor_version(&self) -> &'static str {
281        self.binding.state.descriptor_version
282    }
283
284    /// Returns the exact Event Operation table implemented by this handle.
285    pub fn operations(&self) -> &'static [&'static str] {
286        self.binding.state.operations
287    }
288
289    /// Converts this resolved dependency into its generated typed Event handle.
290    pub fn typed<C: EventCapability>(&self) -> Result<NativeEventHandle<C>, RuntimeFailure> {
291        if self.capability_id() != C::ID || self.descriptor_version() != C::DESCRIPTOR_VERSION {
292            return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
293        }
294        let runtime = self
295            .runtime
296            .borrow()
297            .upgrade()
298            .ok_or(RuntimeFailure::AdmissionClosed)?;
299        Ok(NativeEventHandle::from_endpoints(
300            std::slice::from_ref(&self.binding),
301            runtime,
302            &self.caller_instance,
303            true,
304        ))
305    }
306}
307
308/// Typed, immutable Event endpoints materialized before App boot completes.
309#[derive(Debug)]
310pub struct NativeEventHandle<C: EventCapability> {
311    endpoints: Vec<NativeEventEndpointBinding>,
312    runtime: Rc<NativeAppRuntime>,
313    caller_instance: String,
314    allow_before_ready: bool,
315    capability: PhantomData<fn() -> C>,
316}
317
318impl<C: EventCapability> NativeEventHandle<C> {
319    pub(crate) fn from_endpoints(
320        endpoints: &[NativeEventEndpointBinding],
321        runtime: Rc<NativeAppRuntime>,
322        caller_instance: &str,
323        allow_before_ready: bool,
324    ) -> Self {
325        Self {
326            endpoints: endpoints.to_vec(),
327            runtime,
328            caller_instance: caller_instance.to_owned(),
329            allow_before_ready,
330            capability: PhantomData,
331        }
332    }
333
334    /// Returns the number of explicitly bound subscriber endpoints.
335    pub fn binding_count(&self) -> usize {
336        self.endpoints.len()
337    }
338
339    /// Publishes to every bound subscriber in deterministic order.
340    ///
341    /// An empty binding set succeeds with an empty result. Each result is
342    /// independent: one exhausted or unavailable subscriber does not prevent
343    /// the remaining bindings from being attempted.
344    pub async fn publish(&self, operation: &str, event: C::Event) -> Vec<EventPublishResult> {
345        self.publish_with_context(operation, self.next_context(), event)
346            .await
347    }
348
349    /// Publishes with one propagated Invocation Context.
350    pub async fn publish_with_context(
351        &self,
352        operation: &str,
353        context: InvocationContext,
354        event: C::Event,
355    ) -> Vec<EventPublishResult> {
356        let context = context
357            .with_caller_instance(self.caller_instance.clone())
358            .for_target(C::ID, operation);
359        futures::future::join_all(self.endpoints.iter().map(|endpoint| {
360            self.publish_to_endpoint(endpoint, operation, context.clone(), event.clone())
361        }))
362        .await
363    }
364
365    async fn publish_to_endpoint(
366        &self,
367        endpoint: &NativeEventEndpointBinding,
368        operation: &str,
369        context: InvocationContext,
370        event: C::Event,
371    ) -> EventPublishResult {
372        let operation_name = diagnostic_operation(endpoint.state.operations, operation);
373        let was_closed = self.runtime.shutdown_started.get()
374            || (!self.allow_before_ready && self.runtime.admission.is_closed());
375        let result = self
376            .publish_to_endpoint_inner(endpoint, operation, context.clone(), event)
377            .await;
378        let outcome = match result.admission() {
379            EventAdmission::Accepted => DiagnosticAdmission::Accepted,
380            EventAdmission::Unavailable if was_closed => DiagnosticAdmission::Closed,
381            EventAdmission::Unavailable => DiagnosticAdmission::Unavailable,
382            EventAdmission::Exhausted => DiagnosticAdmission::Exhausted,
383        };
384        self.runtime.diagnostics.emit(
385            DiagnosticSource::Admission,
386            (self.runtime.driver.now)(),
387            |_| DiagnosticEvent::EventAdmission {
388                request_id: context.request_id(),
389                publisher_instance: self.caller_instance.clone(),
390                subscriber_instance: endpoint.module_instance.clone(),
391                capability: C::ID,
392                operation: operation_name,
393                outcome,
394            },
395        );
396        result
397    }
398
399    async fn publish_to_endpoint_inner(
400        &self,
401        endpoint: &NativeEventEndpointBinding,
402        operation: &str,
403        context: InvocationContext,
404        event: C::Event,
405    ) -> EventPublishResult {
406        let subscriber = endpoint.module_instance.clone();
407        let unavailable =
408            || EventPublishResult::new(subscriber.clone(), EventAdmission::Unavailable);
409        if self.runtime.shutdown_started.get()
410            || (!self.allow_before_ready && self.runtime.admission.is_closed())
411        {
412            return unavailable();
413        }
414        let Some(snapshot) = endpoint.state.snapshot() else {
415            return unavailable();
416        };
417        if !endpoint.state.operations.contains(&operation) {
418            return unavailable();
419        }
420        let queue = &endpoint.queue;
421        if !endpoint.state.is_current(snapshot.generation)
422            || ensure_context_active(&self.runtime.driver, &context).is_err()
423        {
424            return unavailable();
425        }
426        if snapshot.endpoint.owns_event_admission() {
427            // Once an out-of-process Adapter starts admission, wait for its
428            // commit acknowledgement. Racing that acknowledgement against the
429            // caller deadline can report unavailable after the subscriber has
430            // already accepted the Event.
431            let result = snapshot
432                .endpoint
433                .publish(operation, Box::new(event), context.clone())
434                .await;
435            return match result {
436                Ok(()) => EventPublishResult::new(subscriber, EventAdmission::Accepted),
437                Err(error) => {
438                    let error = schedule_module_supervision_after_failure(
439                        &self.runtime,
440                        &endpoint.module_instance,
441                        error,
442                    );
443                    self.runtime.diagnostics.emit_runtime_failure(
444                        (self.runtime.driver.now)(),
445                        Some(&endpoint.module_instance),
446                        &error,
447                    );
448                    let admission = if matches!(error, RuntimeFailure::ResourceExhausted { .. }) {
449                        EventAdmission::Exhausted
450                    } else {
451                        EventAdmission::Unavailable
452                    };
453                    EventPublishResult::new(subscriber, admission)
454                }
455            };
456        }
457        let queued = QueuedEvent {
458            operation: operation.to_owned(),
459            event: Box::new(event),
460            context,
461            snapshot,
462        };
463        let Some(should_start) = queue.try_enqueue(queued) else {
464            return EventPublishResult::new(subscriber, EventAdmission::Exhausted);
465        };
466        if should_start {
467            let Some(tasks) = self
468                .runtime
469                .modules
470                .get(&endpoint.module_instance)
471                .and_then(|module| module.generation_parts().map(|(_, tasks, _)| tasks))
472            else {
473                queue.abort();
474                return unavailable();
475            };
476            let drain = drain_event_queue(
477                queue.clone(),
478                self.runtime.clone(),
479                endpoint.module_instance.clone(),
480                C::ID,
481            );
482            if tasks.spawn_local(Box::pin(drain)).is_err() {
483                queue.abort();
484                return unavailable();
485            }
486        }
487        EventPublishResult::new(subscriber, EventAdmission::Accepted)
488    }
489
490    fn next_context(&self) -> InvocationContext {
491        InvocationContext::new(self.next_request_id(), None, CancellationToken::new())
492            .with_caller_instance(self.caller_instance.clone())
493    }
494
495    fn next_request_id(&self) -> super::RequestId {
496        let request_id = self.runtime.request_ids.get();
497        self.runtime.request_ids.set(request_id.saturating_add(1));
498        request_id
499    }
500}
501
502async fn drain_event_queue(
503    queue: Rc<NativeEventQueue>,
504    runtime: Rc<NativeAppRuntime>,
505    module_instance: String,
506    capability: &'static str,
507) {
508    while let Some(queued) = queue.pop() {
509        let result = AssertUnwindSafe(await_with_generation_context(
510            &runtime.driver,
511            &queued.context,
512            queued.snapshot.cancellation,
513            capability,
514            queued.snapshot.endpoint.publish(
515                &queued.operation,
516                queued.event,
517                queued.context.clone(),
518            ),
519        ))
520        .catch_unwind()
521        .await;
522        match result {
523            Ok(Ok(Ok(()))) => {}
524            Ok(Ok(Err(error)) | Err(error)) => {
525                runtime.diagnostics.emit_runtime_failure(
526                    (runtime.driver.now)(),
527                    Some(&module_instance),
528                    &error,
529                );
530                let _ =
531                    schedule_module_supervision_after_failure(&runtime, &module_instance, error);
532            }
533            Err(_) => {
534                let error = RuntimeFailure::ModuleFailure {
535                    detail: format!("native Event subscriber `{module_instance}` panicked"),
536                };
537                runtime.diagnostics.emit_runtime_failure(
538                    (runtime.driver.now)(),
539                    Some(&module_instance),
540                    &error,
541                );
542                let _ =
543                    schedule_module_supervision_after_failure(&runtime, &module_instance, error);
544            }
545        }
546        queue.complete();
547    }
548}