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