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