Skip to main content

lenso_kernel/
runtime.rs

1use super::{
2    AppAdmission, AppReadyGate, BTreeMap, CancellationToken, Cell, DiagnosticEvent,
3    DiagnosticShutdownOutcome, DiagnosticSource, DriverControl, DriverTask, Duration,
4    EventCapability, ExecutionAdapterCatalog, InvocationContext, LocalBoxFuture,
5    ManagedResourceScope, ManagedTask, ManagedTaskScope, NativeEventBindingTable,
6    NativeEventEndpointStateTable, NativeEventHandle, NativeRequestEndpoint, NativeRequestHandle,
7    NativeStreamBindingTable, NativeStreamEndpoint, NativeStreamEndpointStateTable,
8    NativeStreamHandle, PluginCriticality, PluginDependencies, PluginLifecycle, Rc, RefCell,
9    RequestAdmission, RequestCapability, RequestId, ResolvedAppPlan, RestartPolicy,
10    RuntimeDiagnostics, RuntimeFailure, ShutdownOutcome, StreamCapability,
11    begin_plugin_supervision, event, handle_supervision_schedule_failure, oneshot,
12    schedule_plugin_supervision, shutdown_native_plugins,
13};
14
15#[derive(Clone, Debug)]
16pub(super) struct NativeEndpointSnapshot {
17    pub(super) endpoint: Rc<dyn NativeRequestEndpoint>,
18    pub(super) generation: u64,
19    pub(super) cancellation: CancellationToken,
20}
21
22#[derive(Debug)]
23pub(super) struct NativeEndpointState {
24    pub(super) capability_id: &'static str,
25    pub(super) descriptor_version: &'static str,
26    pub(super) operations: &'static [&'static str],
27    pub(super) endpoint: RefCell<Option<Rc<dyn NativeRequestEndpoint>>>,
28    pub(super) generation: Cell<u64>,
29    pub(super) cancellation: RefCell<CancellationToken>,
30}
31
32#[derive(Clone, Debug)]
33pub(crate) struct NativeStreamEndpointSnapshot {
34    pub(crate) endpoint: Rc<dyn NativeStreamEndpoint>,
35    pub(crate) generation: u64,
36    pub(crate) cancellation: CancellationToken,
37}
38
39#[derive(Debug)]
40pub(crate) struct NativeStreamEndpointState {
41    pub(super) capability_id: &'static str,
42    pub(super) descriptor_version: &'static str,
43    pub(super) operations: &'static [&'static str],
44    pub(super) endpoint: RefCell<Option<Rc<dyn NativeStreamEndpoint>>>,
45    pub(super) generation: Cell<u64>,
46    pub(super) cancellation: RefCell<CancellationToken>,
47}
48
49impl NativeStreamEndpointState {
50    pub(crate) fn new(endpoint: Rc<dyn NativeStreamEndpoint>, generation: u64) -> Self {
51        Self {
52            capability_id: endpoint.capability_id(),
53            descriptor_version: endpoint.descriptor_version(),
54            operations: endpoint.operations(),
55            endpoint: RefCell::new(Some(endpoint)),
56            generation: Cell::new(generation),
57            cancellation: RefCell::new(CancellationToken::new()),
58        }
59    }
60
61    pub(crate) fn snapshot(&self) -> Option<NativeStreamEndpointSnapshot> {
62        self.endpoint
63            .borrow()
64            .clone()
65            .map(|endpoint| NativeStreamEndpointSnapshot {
66                endpoint,
67                generation: self.generation.get(),
68                cancellation: self.cancellation.borrow().clone(),
69            })
70    }
71
72    pub(crate) fn mark_unavailable(&self) {
73        self.cancellation.borrow().cancel();
74        self.endpoint.borrow_mut().take();
75    }
76
77    pub(crate) fn install(&self, endpoint: Rc<dyn NativeStreamEndpoint>, generation: u64) {
78        self.generation.set(generation);
79        self.cancellation.replace(CancellationToken::new());
80        self.endpoint.replace(Some(endpoint));
81    }
82
83    pub(crate) fn is_current(&self, generation: u64) -> bool {
84        self.generation.get() == generation && self.endpoint.borrow().is_some()
85    }
86}
87
88impl NativeEndpointState {
89    pub(super) fn new(endpoint: Rc<dyn NativeRequestEndpoint>, generation: u64) -> Self {
90        Self {
91            capability_id: endpoint.capability_id(),
92            descriptor_version: endpoint.descriptor_version(),
93            operations: endpoint.operations(),
94            endpoint: RefCell::new(Some(endpoint)),
95            generation: Cell::new(generation),
96            cancellation: RefCell::new(CancellationToken::new()),
97        }
98    }
99
100    pub(super) fn snapshot(&self) -> Option<NativeEndpointSnapshot> {
101        self.endpoint
102            .borrow()
103            .clone()
104            .map(|endpoint| NativeEndpointSnapshot {
105                endpoint,
106                generation: self.generation.get(),
107                cancellation: self.cancellation.borrow().clone(),
108            })
109    }
110
111    pub(super) fn mark_unavailable(&self) {
112        self.cancellation.borrow().cancel();
113        self.endpoint.borrow_mut().take();
114    }
115
116    pub(super) fn install(&self, endpoint: Rc<dyn NativeRequestEndpoint>, generation: u64) {
117        self.generation.set(generation);
118        self.cancellation.replace(CancellationToken::new());
119        self.endpoint.replace(Some(endpoint));
120    }
121
122    pub(super) fn is_current(&self, generation: u64) -> bool {
123        self.generation.get() == generation && self.endpoint.borrow().is_some()
124    }
125}
126
127#[derive(Clone, Debug)]
128pub(super) struct NativeEndpointBinding {
129    pub(super) requirement_id: String,
130    pub(super) plugin_instance: String,
131    pub(super) state: Rc<NativeEndpointState>,
132    pub(super) admissions: BTreeMap<String, RequestAdmission>,
133}
134
135impl NativeEndpointBinding {
136    pub(super) fn admission(&self, operation: &str) -> Option<&RequestAdmission> {
137        self.admissions.get(operation)
138    }
139}
140
141#[derive(Clone, Debug)]
142pub(crate) struct NativeStreamEndpointBinding {
143    pub(super) requirement_id: String,
144    pub(crate) plugin_instance: String,
145    pub(crate) state: Rc<NativeStreamEndpointState>,
146    pub(super) admissions: BTreeMap<String, RequestAdmission>,
147}
148
149impl NativeStreamEndpointBinding {
150    pub(crate) fn admission(&self, operation: &str) -> Option<&RequestAdmission> {
151        self.admissions.get(operation)
152    }
153}
154
155#[derive(Debug)]
156pub(super) struct NativePluginGeneration {
157    pub(super) lifecycle: Rc<dyn PluginLifecycle>,
158    pub(super) tasks: ManagedTaskScope,
159    pub(super) resources: ManagedResourceScope,
160    pub(super) stop_attempted: bool,
161    pub(super) cleanup_timed_out: bool,
162}
163
164pub(super) enum GenerationPreparationFailure {
165    Lifecycle,
166    Cleanup { primary: RuntimeFailure },
167}
168
169#[derive(Debug)]
170pub(super) struct NativePluginRuntime {
171    pub(super) generation: RefCell<Option<NativePluginGeneration>>,
172}
173
174impl NativePluginRuntime {
175    pub(super) fn take_generation(&self) -> Option<NativePluginGeneration> {
176        self.generation.borrow_mut().take()
177    }
178
179    pub(super) fn install_generation(&self, generation: NativePluginGeneration) {
180        debug_assert!(self.generation.borrow().is_none());
181        self.generation.replace(Some(generation));
182    }
183
184    pub(super) fn generation_parts(
185        &self,
186    ) -> Option<(
187        Rc<dyn PluginLifecycle>,
188        ManagedTaskScope,
189        ManagedResourceScope,
190    )> {
191        self.generation.borrow().as_ref().map(|generation| {
192            (
193                generation.lifecycle.clone(),
194                generation.tasks.clone(),
195                generation.resources.clone(),
196            )
197        })
198    }
199}
200
201#[derive(Clone, Debug)]
202pub(super) struct PluginSupervision {
203    pub(super) policy: RestartPolicy,
204    pub(super) criticality: PluginCriticality,
205    pub(super) required_path: bool,
206    pub(super) generation: u64,
207    pub(super) attempts: Vec<Duration>,
208    pub(super) stable_since: Option<Duration>,
209    pub(super) restarting: bool,
210}
211
212#[derive(Debug, Default)]
213pub(super) struct ShutdownCoordinator {
214    pub(super) started: Cell<bool>,
215    pub(super) cleanup_started_at: Cell<Option<Duration>>,
216    pub(super) completed: Cell<bool>,
217    pub(super) outcome: RefCell<Option<ShutdownOutcome>>,
218    pub(super) waiters: RefCell<Vec<oneshot::Sender<ShutdownOutcome>>>,
219}
220
221impl ShutdownCoordinator {
222    pub(super) fn start(&self, started_at: Duration) -> bool {
223        if self.started.replace(true) {
224            return false;
225        }
226        self.cleanup_started_at.set(Some(started_at));
227        true
228    }
229
230    pub(super) fn begin_completion(&self) -> bool {
231        !self.completed.replace(true)
232    }
233
234    pub(super) fn publish(&self, outcome: &ShutdownOutcome) {
235        self.outcome.replace(Some(outcome.clone()));
236        for waiter in self.waiters.borrow_mut().drain(..) {
237            let _ = waiter.send(outcome.clone());
238        }
239    }
240
241    pub(super) fn wait(&self) -> LocalBoxFuture<'static, ShutdownOutcome> {
242        if let Some(outcome) = self.outcome.borrow().clone() {
243            return Box::pin(futures::future::ready(outcome));
244        }
245        let (complete, waiter) = oneshot::channel();
246        self.waiters.borrow_mut().push(complete);
247        Box::pin(async move {
248            waiter.await.unwrap_or(ShutdownOutcome::RuntimeFailure {
249                error: RuntimeFailure::Internal {
250                    detail: "shutdown coordinator terminated before publishing an outcome"
251                        .to_owned(),
252                },
253            })
254        })
255    }
256}
257
258pub(super) struct NativeAppRuntime {
259    pub(super) startup_context: RefCell<Option<InvocationContext>>,
260    pub(super) startup_cleanup: Option<super::cleanup::StartupCleanupBudget>,
261    pub(super) cleanup_timeout: Option<Duration>,
262    pub(super) executions: Rc<super::settlement::ExecutionLedger>,
263    pub(super) plan: ResolvedAppPlan,
264    pub(super) adapters: Rc<ExecutionAdapterCatalog>,
265    pub(super) plugins: BTreeMap<String, NativePluginRuntime>,
266    pub(super) dependencies: BTreeMap<String, PluginDependencies>,
267    pub(super) endpoint_states: BTreeMap<(String, String), Rc<NativeEndpointState>>,
268    pub(super) stream_endpoint_states: NativeStreamEndpointStateTable,
269    pub(super) event_endpoint_states: NativeEventEndpointStateTable,
270    pub(super) supervision: RefCell<BTreeMap<String, PluginSupervision>>,
271    pub(super) supervision_tasks: RefCell<BTreeMap<String, ManagedTask>>,
272    pub(super) activation_order: Vec<String>,
273    pub(super) ready_gate: AppReadyGate,
274    pub(super) admission: AppAdmission,
275    pub(super) driver: DriverControl,
276    pub(super) diagnostics: RuntimeDiagnostics,
277    pub(super) request_ids: Rc<Cell<RequestId>>,
278    pub(super) supervision_cancellation: CancellationToken,
279    pub(super) shutdown_started: Cell<bool>,
280    pub(super) shutdown: ShutdownCoordinator,
281    pub(super) shutdown_task: RefCell<Option<DriverTask>>,
282    pub(super) terminal_failure: RefCell<Option<RuntimeFailure>>,
283}
284
285impl std::fmt::Debug for NativeAppRuntime {
286    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        formatter
288            .debug_struct("NativeAppRuntime")
289            .field("plugin_count", &self.plugins.len())
290            .field("endpoint_count", &self.endpoint_states.len())
291            .field("stream_endpoint_count", &self.stream_endpoint_states.len())
292            .field("event_endpoint_count", &self.event_endpoint_states.len())
293            .field("ready", &self.ready_gate.is_open())
294            .field("accepting", &self.admission.is_open())
295            .field("next_request_id", &self.request_ids.get())
296            .field("shutdown_started", &self.shutdown_started.get())
297            .field("cleanup_started", &self.shutdown.started.get())
298            .field("cleanup_completed", &self.shutdown.completed.get())
299            .field(
300                "terminal_failure",
301                &self.terminal_failure.borrow().is_some(),
302            )
303            .finish_non_exhaustive()
304    }
305}
306
307impl NativeAppRuntime {
308    pub(super) fn begin_shutdown(&self) {
309        let admission_closed_at = (self.driver.now)();
310        if self.shutdown_started.replace(true) {
311            return;
312        }
313        self.admission.close();
314        self.supervision_cancellation.cancel();
315        for endpoint in self.endpoint_states.values() {
316            endpoint.mark_unavailable();
317        }
318        for endpoint in self.stream_endpoint_states.values() {
319            endpoint.mark_unavailable();
320        }
321        for endpoint in self.event_endpoint_states.values() {
322            endpoint.mark_unavailable();
323        }
324        for plugin in self.plugins.values() {
325            if let Some((_, tasks, resources)) = plugin.generation_parts() {
326                tasks.close();
327                resources.close();
328            }
329        }
330        self.diagnostics
331            .emit(DiagnosticSource::Shutdown, admission_closed_at, |_| {
332                DiagnosticEvent::ShutdownAdmissionClosed
333            });
334    }
335
336    pub(super) fn complete_shutdown(&self, outcome: &ShutdownOutcome) {
337        if !self.shutdown.begin_completion() {
338            return;
339        }
340        let completed_at = (self.driver.now)();
341        let started_at = self
342            .shutdown
343            .cleanup_started_at
344            .get()
345            .unwrap_or(completed_at);
346        let diagnostic_outcome = match outcome {
347            ShutdownOutcome::Clean => DiagnosticShutdownOutcome::Clean,
348            ShutdownOutcome::RuntimeFailure { .. } => DiagnosticShutdownOutcome::RuntimeFailure,
349            ShutdownOutcome::Timeout => DiagnosticShutdownOutcome::Timeout,
350        };
351        self.diagnostics
352            .emit(DiagnosticSource::Shutdown, completed_at, |_| {
353                DiagnosticEvent::ShutdownCompleted {
354                    outcome: diagnostic_outcome,
355                    elapsed: completed_at.saturating_sub(started_at),
356                }
357            });
358        if let ShutdownOutcome::RuntimeFailure { error } = outcome {
359            self.diagnostics
360                .emit_runtime_failure(completed_at, None, error);
361        }
362        self.shutdown.publish(outcome);
363    }
364}
365
366/// A started native App whose generated clients can invoke resolved bindings.
367#[derive(Clone, Debug)]
368pub struct NativeApp {
369    pub(super) bindings: BTreeMap<(String, &'static str), Vec<NativeEndpointBinding>>,
370    pub(super) stream_bindings: NativeStreamBindingTable,
371    pub(super) event_bindings: NativeEventBindingTable,
372    pub(super) diagnostics: RuntimeDiagnostics,
373    pub(super) runtime: Rc<NativeAppRuntime>,
374}
375
376impl NativeApp {
377    fn diagnostic_failure<T>(
378        &self,
379        instance_key: Option<&str>,
380        error: RuntimeFailure,
381    ) -> Result<T, RuntimeFailure> {
382        let instance_key = instance_key
383            .filter(|instance_key| self.runtime.plan.plugin_instance(instance_key).is_some());
384        self.runtime.diagnostics.emit_runtime_failure(
385            (self.runtime.driver.now)(),
386            instance_key,
387            &error,
388        );
389        Err(error)
390    }
391
392    /// Confirms that a generated client has one resolved binding before use.
393    pub fn ensure_binding<C: RequestCapability>(
394        &self,
395        caller_instance: &str,
396    ) -> Result<(), RuntimeFailure> {
397        if self.runtime.admission.is_closed() {
398            return self.diagnostic_failure(Some(caller_instance), RuntimeFailure::AdmissionClosed);
399        }
400        if self
401            .endpoints::<C>(caller_instance)
402            .is_some_and(|endpoints| !endpoints.is_empty())
403        {
404            return Ok(());
405        }
406        self.diagnostic_failure(
407            Some(caller_instance),
408            RuntimeFailure::Unavailable { capability: C::ID },
409        )
410    }
411
412    /// Materializes one typed handle from the immutable binding selected by the Plan.
413    pub fn handle<C: RequestCapability>(
414        &self,
415        caller_instance: &str,
416    ) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
417        self.validate_requirement_lookup(caller_instance, C::ID)?;
418        if self.runtime.admission.is_closed() {
419            return self.diagnostic_failure(Some(caller_instance), RuntimeFailure::AdmissionClosed);
420        }
421        let Some(endpoints) = self
422            .endpoints::<C>(caller_instance)
423            .filter(|endpoints| !endpoints.is_empty())
424        else {
425            return self.diagnostic_failure(
426                Some(caller_instance),
427                RuntimeFailure::Unavailable { capability: C::ID },
428            );
429        };
430        Ok(NativeRequestHandle::from_endpoints(
431            endpoints,
432            self.runtime.clone(),
433            caller_instance,
434            false,
435        ))
436    }
437
438    /// Materializes an optional typed handle; an absent binding remains `None`.
439    pub fn optional_handle<C: RequestCapability>(
440        &self,
441        caller_instance: &str,
442    ) -> Option<NativeRequestHandle<C>> {
443        self.validate_requirement_lookup(caller_instance, C::ID)
444            .ok()?;
445        let caller_instance = caller_instance.to_owned();
446        self.endpoints::<C>(&caller_instance)
447            .filter(|endpoints| !endpoints.is_empty())
448            .map(|endpoints| {
449                NativeRequestHandle::from_endpoints(
450                    endpoints,
451                    self.runtime.clone(),
452                    &caller_instance,
453                    false,
454                )
455            })
456    }
457
458    /// Materializes a typed handle whose endpoints may be empty for a `many` requirement.
459    pub fn many_handle<C: RequestCapability>(
460        &self,
461        caller_instance: &str,
462    ) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
463        self.validate_requirement_lookup(caller_instance, C::ID)?;
464        if self.runtime.admission.is_closed() {
465            return self.diagnostic_failure(Some(caller_instance), RuntimeFailure::AdmissionClosed);
466        }
467        let endpoints = self.endpoints::<C>(caller_instance).unwrap_or(&[]);
468        Ok(NativeRequestHandle::from_endpoints(
469            endpoints,
470            self.runtime.clone(),
471            caller_instance,
472            false,
473        ))
474    }
475
476    /// Returns the number of immutable provider endpoints bound to one requirement.
477    pub fn binding_count<C: RequestCapability>(&self, caller_instance: &str) -> usize {
478        self.endpoints::<C>(caller_instance).map_or(0, <[_]>::len)
479    }
480
481    /// Returns whether every declared Plugin has completed activation.
482    pub fn is_ready(&self) -> bool {
483        self.runtime.ready_gate.is_open()
484    }
485
486    /// Returns the App-wide readiness signal observed by Plugin tasks.
487    pub fn ready_gate(&self) -> AppReadyGate {
488        self.runtime.ready_gate.clone()
489    }
490
491    /// Returns whether new externally triggered work may be admitted.
492    pub fn is_accepting(&self) -> bool {
493        self.runtime.admission.is_open()
494    }
495
496    /// Returns the App-wide admission state.
497    pub fn admission(&self) -> AppAdmission {
498        self.runtime.admission.clone()
499    }
500
501    /// Returns the opt-in Runtime Diagnostics port for this App.
502    pub fn diagnostics(&self) -> RuntimeDiagnostics {
503        self.diagnostics.clone()
504    }
505
506    /// Returns the exact resolved Capability dependencies for one Plugin Instance.
507    ///
508    /// Runners use this host-facing view when they must preserve provider identity
509    /// while transferring one binding across an Execution Lane. Ordinary Plugin
510    /// code receives the same Interface through its lifecycle context.
511    pub fn dependencies(
512        &self,
513        caller_instance: &str,
514    ) -> Result<PluginDependencies, RuntimeFailure> {
515        if self.runtime.admission.is_closed() {
516            return self.diagnostic_failure(Some(caller_instance), RuntimeFailure::AdmissionClosed);
517        }
518        self.runtime
519            .dependencies
520            .get(caller_instance)
521            .cloned()
522            .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
523                detail: format!(
524                    "Plugin Instance `{caller_instance}` has no resolved dependency table"
525                ),
526            })
527    }
528
529    /// Returns current bounded request queue depths grouped by provider Instance.
530    ///
531    /// This structural snapshot contains no request payloads and is intended for
532    /// Runner-owned placement diagnostics.
533    pub fn instance_queue_depths(&self) -> BTreeMap<String, usize> {
534        let mut depths = BTreeMap::new();
535        for endpoints in self.bindings.values() {
536            for endpoint in endpoints {
537                let depth = endpoint
538                    .admissions
539                    .values()
540                    .map(RequestAdmission::queue_depth)
541                    .sum::<usize>();
542                *depths.entry(endpoint.plugin_instance.clone()).or_insert(0) += depth;
543            }
544        }
545        depths
546    }
547
548    /// Returns the terminal supervision failure, when a critical App path exhausted its budget.
549    pub fn terminal_failure(&self) -> Option<RuntimeFailure> {
550        self.runtime.terminal_failure.borrow().clone()
551    }
552
553    /// Returns whether supervision has produced a terminal App failure.
554    pub fn is_failed(&self) -> bool {
555        self.runtime.terminal_failure.borrow().is_some()
556    }
557
558    /// Returns the current ready generation for one Plugin Instance, when it is available.
559    pub fn plugin_generation(&self, instance_key: &str) -> Option<u64> {
560        self.runtime
561            .supervision
562            .borrow()
563            .get(instance_key)
564            .and_then(|state| {
565                let request_current =
566                    self.runtime
567                        .endpoint_states
568                        .iter()
569                        .any(|((plugin, _), endpoint)| {
570                            plugin == instance_key && endpoint.is_current(state.generation)
571                        });
572                let stream_current =
573                    self.runtime
574                        .stream_endpoint_states
575                        .iter()
576                        .any(|((plugin, _), endpoint)| {
577                            plugin == instance_key && endpoint.is_current(state.generation)
578                        });
579                let event_current =
580                    self.runtime
581                        .event_endpoint_states
582                        .iter()
583                        .any(|((plugin, _), endpoint)| {
584                            plugin == instance_key && endpoint.is_current(state.generation)
585                        });
586                (request_current || stream_current || event_current).then_some(state.generation)
587            })
588    }
589
590    /// Reports a Plugin Instance failure and schedules its finite supervision policy.
591    pub fn report_plugin_failure(&self, instance_key: &str) -> Result<(), RuntimeFailure> {
592        if !begin_plugin_supervision(&self.runtime, instance_key)? {
593            return Ok(());
594        }
595        schedule_plugin_supervision(&self.runtime, instance_key).map_err(|error| {
596            handle_supervision_schedule_failure(&self.runtime, instance_key, error)
597        })
598    }
599
600    /// Starts shutdown admission closure and cooperative cancellation.
601    pub fn request_shutdown(&self) {
602        self.runtime.begin_shutdown();
603    }
604
605    /// Performs bounded graceful shutdown using one global deadline.
606    pub async fn shutdown(&self, timeout: Duration) -> ShutdownOutcome {
607        self.shutdown_with_budget(super::cleanup::CleanupBudget::after(
608            &self.runtime.driver,
609            timeout,
610        ))
611        .await
612    }
613
614    pub(super) async fn shutdown_with_budget(
615        &self,
616        budget: super::cleanup::CleanupBudget,
617    ) -> ShutdownOutcome {
618        self.runtime.begin_shutdown();
619        let cleanup_started_at = (self.runtime.driver.now)();
620        if self.runtime.shutdown.start(cleanup_started_at) {
621            let timeout = budget.remaining();
622            self.runtime
623                .diagnostics
624                .emit(DiagnosticSource::Shutdown, cleanup_started_at, |_| {
625                    DiagnosticEvent::ShutdownCleanupStarted { timeout }
626                });
627            let runtime = self.runtime.clone();
628            let worker_runtime = runtime.clone();
629            match (runtime.driver.spawn_local)(Box::pin(async move {
630                let outcome = shutdown_native_plugins(&worker_runtime, budget).await;
631                worker_runtime.complete_shutdown(&outcome);
632            })) {
633                Ok(task) => {
634                    runtime.shutdown_task.replace(Some(task));
635                }
636                Err(error) => {
637                    runtime.complete_shutdown(&ShutdownOutcome::RuntimeFailure {
638                        error: RuntimeFailure::Internal {
639                            detail: format!("failed to schedule App shutdown: {error:?}"),
640                        },
641                    });
642                }
643            }
644        }
645        self.runtime.shutdown.wait().await
646    }
647
648    /// Invokes a generated request Operation through the caller's resolved binding.
649    pub async fn invoke<C: RequestCapability>(
650        &self,
651        caller_instance: &str,
652        operation: &str,
653        request: C::Request,
654    ) -> Result<Result<C::Response, C::DomainError>, RuntimeFailure> {
655        self.handle::<C>(caller_instance)?
656            .invoke(operation, request)
657            .await
658    }
659
660    /// Creates a request context with a fresh Kernel Request ID.
661    ///
662    /// `deadline` is an absolute instant returned by the selected
663    /// [`RuntimeDriver`]'s monotonic clock.
664    pub fn invocation_context(
665        &self,
666        deadline: Option<Duration>,
667        cancellation: CancellationToken,
668    ) -> InvocationContext {
669        InvocationContext::new(self.next_request_id(), deadline, cancellation)
670    }
671
672    /// Creates a request context whose deadline is relative to the Driver's clock.
673    pub fn invocation_context_after(
674        &self,
675        timeout: Duration,
676        cancellation: CancellationToken,
677    ) -> InvocationContext {
678        self.invocation_context(
679            Some((self.runtime.driver.now)().saturating_add(timeout)),
680            cancellation,
681        )
682    }
683
684    /// Invokes a request with an explicit propagated Invocation Context.
685    pub async fn invoke_with_context<C: RequestCapability>(
686        &self,
687        caller_instance: &str,
688        operation: &str,
689        context: InvocationContext,
690        request: C::Request,
691    ) -> Result<Result<C::Response, C::DomainError>, RuntimeFailure> {
692        self.handle::<C>(caller_instance)?
693            .invoke_with_context(operation, context, request)
694            .await
695    }
696
697    /// Materializes one typed bidirectional stream handle from the resolved Plan.
698    pub fn stream_handle<C: StreamCapability>(
699        &self,
700        caller_instance: &str,
701    ) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
702        self.validate_requirement_lookup(caller_instance, C::ID)?;
703        if self.runtime.admission.is_closed() {
704            return self.diagnostic_failure(Some(caller_instance), RuntimeFailure::AdmissionClosed);
705        }
706        let Some(endpoints) = self
707            .stream_endpoints::<C>(caller_instance)
708            .filter(|endpoints| !endpoints.is_empty())
709        else {
710            return self.diagnostic_failure(
711                Some(caller_instance),
712                RuntimeFailure::Unavailable { capability: C::ID },
713            );
714        };
715        Ok(NativeStreamHandle::from_endpoints(
716            endpoints,
717            self.runtime.clone(),
718            caller_instance,
719            false,
720        ))
721    }
722
723    /// Materializes an optional typed bidirectional stream handle.
724    pub fn optional_stream_handle<C: StreamCapability>(
725        &self,
726        caller_instance: &str,
727    ) -> Option<NativeStreamHandle<C>> {
728        self.validate_requirement_lookup(caller_instance, C::ID)
729            .ok()?;
730        let caller_instance = caller_instance.to_owned();
731        self.stream_endpoints::<C>(&caller_instance)
732            .filter(|endpoints| !endpoints.is_empty())
733            .map(|endpoints| {
734                NativeStreamHandle::from_endpoints(
735                    endpoints,
736                    self.runtime.clone(),
737                    &caller_instance,
738                    false,
739                )
740            })
741    }
742
743    /// Returns the number of immutable stream endpoints bound to one requirement.
744    pub fn stream_binding_count<C: StreamCapability>(&self, caller_instance: &str) -> usize {
745        self.stream_endpoints::<C>(caller_instance)
746            .map_or(0, <[_]>::len)
747    }
748
749    /// Materializes a typed Event handle and requires at least one subscriber.
750    pub fn event_handle<C: EventCapability>(
751        &self,
752        caller_instance: &str,
753    ) -> Result<NativeEventHandle<C>, RuntimeFailure> {
754        self.validate_requirement_lookup(caller_instance, C::ID)?;
755        if self.runtime.admission.is_closed() {
756            return self.diagnostic_failure(Some(caller_instance), RuntimeFailure::AdmissionClosed);
757        }
758        let Some(endpoints) = self
759            .event_endpoints::<C>(caller_instance)
760            .filter(|endpoints| !endpoints.is_empty())
761        else {
762            return self.diagnostic_failure(
763                Some(caller_instance),
764                RuntimeFailure::Unavailable { capability: C::ID },
765            );
766        };
767        Ok(NativeEventHandle::from_endpoints(
768            endpoints,
769            self.runtime.clone(),
770            caller_instance,
771            false,
772        ))
773    }
774
775    /// Materializes an optional typed Event handle.
776    pub fn optional_event_handle<C: EventCapability>(
777        &self,
778        caller_instance: &str,
779    ) -> Option<NativeEventHandle<C>> {
780        self.validate_requirement_lookup(caller_instance, C::ID)
781            .ok()?;
782        let caller_instance = caller_instance.to_owned();
783        self.event_endpoints::<C>(&caller_instance)
784            .filter(|endpoints| !endpoints.is_empty())
785            .map(|endpoints| {
786                NativeEventHandle::from_endpoints(
787                    endpoints,
788                    self.runtime.clone(),
789                    &caller_instance,
790                    false,
791                )
792            })
793    }
794
795    /// Materializes a typed Event handle whose endpoint set may be empty.
796    pub fn many_event_handle<C: EventCapability>(
797        &self,
798        caller_instance: &str,
799    ) -> Result<NativeEventHandle<C>, RuntimeFailure> {
800        self.validate_requirement_lookup(caller_instance, C::ID)?;
801        if self.runtime.admission.is_closed() {
802            return self.diagnostic_failure(Some(caller_instance), RuntimeFailure::AdmissionClosed);
803        }
804        let endpoints = self.event_endpoints::<C>(caller_instance).unwrap_or(&[]);
805        Ok(NativeEventHandle::from_endpoints(
806            endpoints,
807            self.runtime.clone(),
808            caller_instance,
809            false,
810        ))
811    }
812
813    /// Returns the number of immutable Event subscriber endpoints bound to a requirement.
814    pub fn event_binding_count<C: EventCapability>(&self, caller_instance: &str) -> usize {
815        self.event_endpoints::<C>(caller_instance)
816            .map_or(0, <[_]>::len)
817    }
818
819    fn validate_requirement_lookup(
820        &self,
821        caller: &str,
822        capability: &'static str,
823    ) -> Result<(), RuntimeFailure> {
824        let declarations = self
825            .runtime
826            .plan
827            .plugin_instance(caller)
828            .map_or(0, |instance| {
829                instance
830                    .required_capabilities()
831                    .iter()
832                    .filter(|requirement| requirement.capability_id() == capability)
833                    .count()
834            });
835        if declarations > 1 {
836            return Err(RuntimeFailure::AmbiguousBinding {
837                capability,
838                providers: declarations,
839            });
840        }
841        Ok(())
842    }
843
844    pub(super) fn next_request_id(&self) -> RequestId {
845        let request_id = self.runtime.request_ids.get();
846        self.runtime.request_ids.set(request_id.saturating_add(1));
847        request_id
848    }
849
850    pub(super) fn endpoints<C: RequestCapability>(
851        &self,
852        caller_instance: &str,
853    ) -> Option<&[NativeEndpointBinding]> {
854        self.bindings
855            .get(&(caller_instance.to_owned(), C::ID))
856            .map(Vec::as_slice)
857    }
858
859    pub(super) fn stream_endpoints<C: StreamCapability>(
860        &self,
861        caller_instance: &str,
862    ) -> Option<&[NativeStreamEndpointBinding]> {
863        self.stream_bindings
864            .get(&(caller_instance.to_owned(), C::ID))
865            .map(Vec::as_slice)
866    }
867
868    pub(super) fn event_endpoints<C: EventCapability>(
869        &self,
870        caller_instance: &str,
871    ) -> Option<&[event::NativeEventEndpointBinding]> {
872        self.event_bindings
873            .get(&(caller_instance.to_owned(), C::ID))
874            .map(Vec::as_slice)
875    }
876}