Skip to main content

lenso_kernel/
kernel.rs

1use super::{
2    ActivateContext, AppAdmission, AppReadyGate, BTreeMap, CancellationToken, Cell,
3    DeactivationReason, DriverControl, ExecutionAdapterCatalog, ManagedResourceScope,
4    ManagedTaskScope, NativeApp, NativeAppRuntime, NativeBindingTable, NativeEndpointBinding,
5    NativeEndpointState, NativeEndpointStateTable, NativeEventBindingTable,
6    NativeEventEndpointStateTable, NativeExecutionAdapter, NativePluginGeneration,
7    NativePluginRuntime, NativeStreamBindingTable, NativeStreamEndpointBinding,
8    NativeStreamEndpointState, NativeStreamEndpointStateTable, PlanResolutionError,
9    PluginDependencies, PluginDependency, PluginDependencyHandle, PluginEventDependencyHandle,
10    PluginStreamDependencyHandle, PrepareContext, PreparedBinding, PreparedEventBinding,
11    PreparedNativeApp, PreparedNativePlugin, PreparedStreamBinding, Rc, RefCell, RequestAdmission,
12    ResolvedAppPlan, RuntimeDiagnostics, RuntimeDriver, RuntimeFailure, ShutdownCoordinator, Weak,
13    begin_plugin_supervision, deactivate_in_reverse, event, handle_supervision_schedule_failure,
14    plugin_supervision, schedule_plugin_supervision, validate_native_endpoint_set,
15};
16
17/// A reason the Kernel rejected a Resolved App Plan before boot.
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub enum PlanValidationError {
20    /// The Plan schema cannot be executed by this Kernel version.
21    UnsupportedSchemaVersion { expected: u32, actual: u32 },
22    /// The Plan graph is structurally invalid and cannot be booted.
23    InvalidResolvedPlan { detail: String },
24}
25
26impl std::fmt::Display for PlanValidationError {
27    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::UnsupportedSchemaVersion { expected, actual } => write!(
30                formatter,
31                "unsupported Resolved App Plan schema {actual}; expected {expected}"
32            ),
33            Self::InvalidResolvedPlan { detail } => {
34                write!(formatter, "invalid Resolved App Plan: {detail}")
35            }
36        }
37    }
38}
39
40impl std::error::Error for PlanValidationError {}
41
42/// The portable App execution engine.
43#[derive(Debug)]
44pub struct Kernel;
45
46impl Kernel {
47    /// Starts one App backed by a single statically linked native Adapter package.
48    pub async fn start_native<D: RuntimeDriver, A: NativeExecutionAdapter>(
49        plan: ResolvedAppPlan,
50        driver: D,
51        adapter: A,
52    ) -> Result<NativeApp, RuntimeFailure> {
53        Self::start_native_with_diagnostics(plan, driver, adapter, RuntimeDiagnostics::new()).await
54    }
55
56    /// Starts one native Adapter package with an opt-in Runtime Diagnostics port.
57    pub async fn start_native_with_diagnostics<D: RuntimeDriver, A: NativeExecutionAdapter>(
58        plan: ResolvedAppPlan,
59        driver: D,
60        adapter: A,
61        diagnostics: RuntimeDiagnostics,
62    ) -> Result<NativeApp, RuntimeFailure> {
63        Self::start_with_diagnostics(
64            plan,
65            driver,
66            ExecutionAdapterCatalog::single(adapter),
67            diagnostics,
68        )
69        .await
70    }
71
72    /// Starts Plugin Instances through the Adapter catalog assembled by the Runner.
73    pub async fn start<D: RuntimeDriver>(
74        plan: ResolvedAppPlan,
75        driver: D,
76        adapters: ExecutionAdapterCatalog,
77    ) -> Result<NativeApp, RuntimeFailure> {
78        Self::start_with_diagnostics(plan, driver, adapters, RuntimeDiagnostics::new()).await
79    }
80
81    /// Starts an App with an opt-in Runtime Diagnostics port.
82    #[allow(
83        clippy::too_many_lines,
84        reason = "startup remains linear so validation, preparation, and activation fail closed in order"
85    )]
86    pub async fn start_with_diagnostics<D: RuntimeDriver>(
87        plan: ResolvedAppPlan,
88        driver: D,
89        adapters: ExecutionAdapterCatalog,
90        diagnostics: RuntimeDiagnostics,
91    ) -> Result<NativeApp, RuntimeFailure> {
92        if plan
93            .plugin_instances()
94            .iter()
95            .any(|instance| instance.authoring_version() == 2)
96        {
97            return Self::start_controlled(
98                plan,
99                driver,
100                adapters,
101                diagnostics,
102                super::InvocationContext::new(0, None, CancellationToken::new()),
103                super::startup::DEFAULT_STARTUP_CLEANUP_TIMEOUT,
104            )
105            .await;
106        }
107        Self::start_owned(plan, driver, adapters, diagnostics, None, None).await
108    }
109
110    /// Starts through a Driver-owned attempt with explicit cancellation,
111    /// monotonic startup deadline, and one late-result cleanup budget.
112    pub async fn start_controlled<D: RuntimeDriver>(
113        plan: ResolvedAppPlan,
114        driver: D,
115        adapters: ExecutionAdapterCatalog,
116        diagnostics: RuntimeDiagnostics,
117        context: super::InvocationContext,
118        cleanup_timeout: std::time::Duration,
119    ) -> Result<NativeApp, RuntimeFailure> {
120        super::startup::start(
121            plan,
122            driver,
123            adapters,
124            diagnostics,
125            context,
126            cleanup_timeout,
127        )
128        .await
129    }
130
131    #[allow(
132        clippy::too_many_lines,
133        reason = "startup remains linear so validation, preparation and activation fail closed"
134    )]
135    pub(super) async fn start_owned<D: RuntimeDriver>(
136        plan: ResolvedAppPlan,
137        driver: D,
138        adapters: ExecutionAdapterCatalog,
139        diagnostics: RuntimeDiagnostics,
140        startup_context: Option<super::InvocationContext>,
141        startup_cleanup: Option<super::cleanup::StartupCleanupBudget>,
142    ) -> Result<NativeApp, RuntimeFailure> {
143        if let Err(error) = plan.validate() {
144            let error = runtime_plan_error(&error);
145            diagnostics.emit_runtime_failure(driver.now(), None, &error);
146            return Err(error);
147        }
148
149        let activation_order = match plan.activation_order() {
150            Ok(order) => order,
151            Err(error) => {
152                let error = runtime_plan_error(&error);
153                diagnostics.emit_runtime_failure(driver.now(), None, &error);
154                return Err(error);
155            }
156        };
157        let adapters = Rc::new(adapters);
158        let PreparedNativeApp {
159            bindings: prepared_bindings,
160            stream_bindings: prepared_stream_bindings,
161            event_bindings: prepared_event_bindings,
162            generations,
163        } = match adapters.prepare(&plan) {
164            Ok(prepared) => prepared,
165            Err(error) => {
166                diagnostics.emit_runtime_failure(driver.now(), None, &error);
167                return Err(error);
168            }
169        };
170        if let Err(error) = validate_prepared_native_app(
171            &plan,
172            &prepared_bindings,
173            &prepared_stream_bindings,
174            &prepared_event_bindings,
175            &generations,
176        ) {
177            diagnostics.emit_runtime_failure(driver.now(), None, &error);
178            return Err(error);
179        }
180        let (bindings, endpoint_states) = native_bindings(&plan, &prepared_bindings);
181        let (stream_bindings, stream_endpoint_states) =
182            native_stream_bindings(&plan, &prepared_stream_bindings);
183        let (event_bindings, event_endpoint_states) =
184            native_event_bindings(&plan, &prepared_event_bindings);
185        let runtime_link = Rc::new(RefCell::new(Weak::new()));
186        let dependencies = plugin_dependencies(
187            &plan,
188            &bindings,
189            &stream_bindings,
190            &event_bindings,
191            &runtime_link,
192        );
193        let driver_control = DriverControl::new(&driver);
194        let admission = AppAdmission::new();
195        let plugin_runtimes = native_plugin_runtimes(&plan, &driver, generations);
196        let ready_gate = AppReadyGate::new();
197        let supervision = plugin_supervision(&plan);
198        let cleanup_timeout = startup_cleanup
199            .as_ref()
200            .map(super::cleanup::StartupCleanupBudget::timeout);
201        let runtime = Rc::new(NativeAppRuntime {
202            startup_context: RefCell::new(startup_context),
203            startup_cleanup,
204            cleanup_timeout,
205            executions: Rc::default(),
206            plan,
207            adapters,
208            plugins: plugin_runtimes,
209            dependencies,
210            endpoint_states,
211            stream_endpoint_states,
212            event_endpoint_states,
213            supervision: RefCell::new(supervision),
214            supervision_tasks: RefCell::new(BTreeMap::new()),
215            activation_order,
216            ready_gate,
217            admission,
218            driver: driver_control,
219            diagnostics: diagnostics.clone(),
220            request_ids: Rc::new(Cell::new(1)),
221            supervision_cancellation: CancellationToken::new(),
222            shutdown_started: Cell::new(false),
223            shutdown: ShutdownCoordinator::default(),
224            shutdown_task: RefCell::new(None),
225            terminal_failure: RefCell::new(None),
226        });
227        runtime_link.replace(Rc::downgrade(&runtime));
228        attach_managed_task_failure_handlers(&runtime);
229        runtime.diagnostics.emit(
230            super::DiagnosticSource::Lifecycle,
231            (runtime.driver.now)(),
232            |_| super::DiagnosticEvent::AppStarted {
233                plugin_count: runtime.plan.plugin_instances().len(),
234            },
235        );
236        let prepared_instances = prepare_native_plugins(&runtime).await?;
237        if let Err(error) = construct_native_plugins(&runtime).await {
238            let cleanup_error = deactivate_in_reverse(
239                &runtime.plugins,
240                &runtime.dependencies,
241                &prepared_instances,
242                DeactivationReason::StartupRollback,
243                &runtime.admission,
244                &runtime.diagnostics,
245                &runtime.driver,
246                runtime
247                    .startup_cleanup
248                    .as_ref()
249                    .map(super::cleanup::StartupCleanupBudget::establish),
250            )
251            .await;
252            retain_unsafe_startup(&runtime, cleanup_error.as_ref());
253            runtime
254                .diagnostics
255                .emit_runtime_failure((runtime.driver.now)(), None, &error);
256            return Err(error);
257        }
258        if let Err(error) = activate_native_plugins(&runtime).await {
259            let cleanup_error = deactivate_in_reverse(
260                &runtime.plugins,
261                &runtime.dependencies,
262                &prepared_instances,
263                DeactivationReason::StartupRollback,
264                &runtime.admission,
265                &runtime.diagnostics,
266                &runtime.driver,
267                runtime
268                    .startup_cleanup
269                    .as_ref()
270                    .map(super::cleanup::StartupCleanupBudget::establish),
271            )
272            .await;
273            retain_unsafe_startup(&runtime, cleanup_error.as_ref());
274            runtime
275                .diagnostics
276                .emit_runtime_failure((runtime.driver.now)(), None, &error);
277            return Err(error);
278        }
279        open_native_readiness(&runtime).await;
280        Ok(NativeApp {
281            bindings,
282            stream_bindings,
283            event_bindings,
284            diagnostics,
285            runtime,
286        })
287    }
288}
289
290pub(super) fn attach_managed_task_failure_handlers(runtime: &Rc<NativeAppRuntime>) {
291    for (instance_key, plugin) in &runtime.plugins {
292        let Some((_, tasks, _)) = plugin.generation_parts() else {
293            continue;
294        };
295        attach_managed_task_failure_handler(runtime, instance_key, &tasks);
296    }
297}
298
299fn startup_active(runtime: &NativeAppRuntime) -> Result<(), RuntimeFailure> {
300    let result = runtime
301        .startup_context
302        .borrow()
303        .as_ref()
304        .map_or(Ok(()), |context| {
305            super::ensure_context_active(&runtime.driver, context)
306        });
307    if result.is_err()
308        && let Some(cleanup) = &runtime.startup_cleanup
309    {
310        let now = (runtime.driver.now)();
311        let cleanup_started_at = runtime
312            .startup_context
313            .borrow()
314            .as_ref()
315            .and_then(super::InvocationContext::deadline)
316            .filter(|deadline| now >= *deadline)
317            .unwrap_or(now);
318        cleanup.establish_at(cleanup_started_at);
319    }
320    result
321}
322
323fn lifecycle_cancellation(
324    runtime: &NativeAppRuntime,
325    tasks: &ManagedTaskScope,
326) -> CancellationToken {
327    runtime.startup_context.borrow().as_ref().map_or_else(
328        || tasks.cancellation(),
329        super::InvocationContext::cancellation,
330    )
331}
332
333pub(super) fn attach_managed_task_failure_handler(
334    runtime: &Rc<NativeAppRuntime>,
335    instance_key: &str,
336    tasks: &ManagedTaskScope,
337) {
338    let task_runtime = Rc::downgrade(runtime);
339    let task_instance_key = instance_key.to_owned();
340    let handler: Rc<dyn Fn()> = Rc::new(move || {
341        let Some(runtime) = task_runtime.upgrade() else {
342            return;
343        };
344        if begin_plugin_supervision(&runtime, &task_instance_key).unwrap_or(false)
345            && let Err(error) = schedule_plugin_supervision(&runtime, &task_instance_key)
346        {
347            let _ = handle_supervision_schedule_failure(&runtime, &task_instance_key, error);
348        }
349    });
350    tasks.set_failure_handler(&handler);
351}
352
353pub(super) fn runtime_plan_error(error: &PlanResolutionError) -> RuntimeFailure {
354    RuntimeFailure::InvalidResolvedPlan {
355        detail: error.to_string(),
356    }
357}
358
359#[allow(
360    clippy::too_many_lines,
361    reason = "one fail-closed pass keeps request, stream, event, and generation validation aligned"
362)]
363pub(super) fn validate_prepared_native_app(
364    plan: &ResolvedAppPlan,
365    bindings: &[PreparedBinding],
366    stream_bindings: &[PreparedStreamBinding],
367    event_bindings: &[PreparedEventBinding],
368    generations: &BTreeMap<String, PreparedNativePlugin>,
369) -> Result<(), RuntimeFailure> {
370    if generations.len() != plan.plugin_instances().len() {
371        return Err(RuntimeFailure::InvalidResolvedPlan {
372            detail: format!(
373                "Execution Adapters prepared {} Plugin generations; expected {}",
374                generations.len(),
375                plan.plugin_instances().len()
376            ),
377        });
378    }
379    for instance in plan.plugin_instances() {
380        let generation = generations.get(instance.instance_key()).ok_or_else(|| {
381            RuntimeFailure::InvalidResolvedPlan {
382                detail: format!(
383                    "Execution Adapters did not prepare Plugin Instance `{}`",
384                    instance.instance_key()
385                ),
386            }
387        })?;
388        validate_native_endpoint_set(
389            instance.instance_key(),
390            instance,
391            generation.endpoints(),
392            generation.stream_endpoints(),
393            generation.event_endpoints(),
394        )?;
395    }
396    if let Some(instance_key) = generations.keys().find(|instance_key| {
397        !plan
398            .plugin_instances()
399            .iter()
400            .any(|instance| instance.instance_key() == instance_key.as_str())
401    }) {
402        return Err(RuntimeFailure::InvalidResolvedPlan {
403            detail: format!("Execution Adapter prepared unknown Plugin Instance `{instance_key}`"),
404        });
405    }
406
407    let expected_request_bindings = plan
408        .capability_bindings()
409        .iter()
410        .filter(|binding| {
411            plan.plugin_instance(binding.provider_instance())
412                .and_then(|provider| {
413                    provider
414                        .provided_capabilities()
415                        .iter()
416                        .find(|endpoint| endpoint.capability_id() == binding.capability_id())
417                })
418                .is_some_and(|endpoint| !endpoint.request_operations().is_empty())
419        })
420        .count();
421    let expected_stream_bindings = plan
422        .capability_bindings()
423        .iter()
424        .filter(|binding| {
425            plan.plugin_instance(binding.provider_instance())
426                .and_then(|provider| {
427                    provider
428                        .provided_capabilities()
429                        .iter()
430                        .find(|endpoint| endpoint.capability_id() == binding.capability_id())
431                })
432                .is_some_and(|endpoint| !endpoint.stream_operations().is_empty())
433        })
434        .count();
435    let expected_event_bindings = plan
436        .capability_bindings()
437        .iter()
438        .filter(|binding| {
439            plan.plugin_instance(binding.provider_instance())
440                .and_then(|provider| {
441                    provider
442                        .provided_capabilities()
443                        .iter()
444                        .find(|endpoint| endpoint.capability_id() == binding.capability_id())
445                })
446                .is_some_and(|endpoint| !endpoint.event_operations().is_empty())
447        })
448        .count();
449    if bindings.len() != expected_request_bindings {
450        return Err(RuntimeFailure::InvalidResolvedPlan {
451            detail: if expected_stream_bindings == 0 && stream_bindings.is_empty() {
452                format!(
453                    "Execution Adapters prepared {} bindings; expected {}",
454                    bindings.len(),
455                    expected_request_bindings
456                )
457            } else {
458                format!(
459                    "Execution Adapters prepared {} request bindings; expected {}",
460                    bindings.len(),
461                    expected_request_bindings
462                )
463            },
464        });
465    }
466    if stream_bindings.len() != expected_stream_bindings {
467        return Err(RuntimeFailure::InvalidResolvedPlan {
468            detail: format!(
469                "Execution Adapters prepared {} stream bindings; expected {}",
470                stream_bindings.len(),
471                expected_stream_bindings
472            ),
473        });
474    }
475    if event_bindings.len() != expected_event_bindings {
476        return Err(RuntimeFailure::InvalidResolvedPlan {
477            detail: format!(
478                "Execution Adapters prepared {} Event bindings; expected {}",
479                event_bindings.len(),
480                expected_event_bindings
481            ),
482        });
483    }
484    for planned in plan.capability_bindings() {
485        let provider = generations
486            .get(planned.provider_instance())
487            .expect("the resolved Plan references one validated provider generation");
488        let descriptor = plan
489            .plugin_instance(planned.provider_instance())
490            .and_then(|provider| {
491                provider
492                    .provided_capabilities()
493                    .iter()
494                    .find(|endpoint| endpoint.capability_id() == planned.capability_id())
495            })
496            .expect("the resolved Plan references one validated provider endpoint");
497        if !descriptor.request_operations().is_empty() {
498            let matching: Vec<_> = bindings
499                .iter()
500                .filter(|prepared| {
501                    prepared.requirement_id() == planned.requirement_id()
502                        && prepared.consumer_instance == planned.consumer_instance()
503                        && prepared.provider_instance == planned.provider_instance()
504                        && prepared.endpoint.capability_id() == planned.capability_id()
505                        && prepared.endpoint.descriptor_version() == planned.descriptor_version()
506                })
507                .collect();
508            if matching.len() != 1 {
509                return Err(RuntimeFailure::InvalidResolvedPlan {
510                    detail: format!(
511                        "Execution Adapters prepared {} request bindings for `{}:{}:{}`; expected 1",
512                        matching.len(),
513                        planned.consumer_instance(),
514                        planned.capability_id(),
515                        planned.provider_instance()
516                    ),
517                });
518            }
519            if !provider
520                .endpoints()
521                .iter()
522                .any(|endpoint| Rc::ptr_eq(endpoint, &matching[0].endpoint))
523            {
524                return Err(RuntimeFailure::InvalidResolvedPlan {
525                    detail: format!(
526                        "request binding `{}:{}:{}` does not reference its provider generation endpoint",
527                        planned.consumer_instance(),
528                        planned.capability_id(),
529                        planned.provider_instance()
530                    ),
531                });
532            }
533        }
534        if !descriptor.stream_operations().is_empty() {
535            let matching: Vec<_> = stream_bindings
536                .iter()
537                .filter(|prepared| {
538                    prepared.requirement_id() == planned.requirement_id()
539                        && prepared.consumer_instance == planned.consumer_instance()
540                        && prepared.provider_instance == planned.provider_instance()
541                        && prepared.endpoint.capability_id() == planned.capability_id()
542                        && prepared.endpoint.descriptor_version() == planned.descriptor_version()
543                })
544                .collect();
545            if matching.len() != 1 {
546                return Err(RuntimeFailure::InvalidResolvedPlan {
547                    detail: format!(
548                        "Execution Adapters prepared {} stream bindings for `{}:{}:{}`; expected 1",
549                        matching.len(),
550                        planned.consumer_instance(),
551                        planned.capability_id(),
552                        planned.provider_instance()
553                    ),
554                });
555            }
556            if !provider
557                .stream_endpoints()
558                .iter()
559                .any(|endpoint| Rc::ptr_eq(endpoint, &matching[0].endpoint))
560            {
561                return Err(RuntimeFailure::InvalidResolvedPlan {
562                    detail: format!(
563                        "stream binding `{}:{}:{}` does not reference its provider generation endpoint",
564                        planned.consumer_instance(),
565                        planned.capability_id(),
566                        planned.provider_instance()
567                    ),
568                });
569            }
570        }
571        if !descriptor.event_operations().is_empty() {
572            let matching: Vec<_> = event_bindings
573                .iter()
574                .filter(|prepared| {
575                    prepared.requirement_id() == planned.requirement_id()
576                        && prepared.consumer_instance == planned.consumer_instance()
577                        && prepared.provider_instance == planned.provider_instance()
578                        && prepared.endpoint.capability_id() == planned.capability_id()
579                        && prepared.endpoint.descriptor_version() == planned.descriptor_version()
580                })
581                .collect();
582            if matching.len() != 1 {
583                return Err(RuntimeFailure::InvalidResolvedPlan {
584                    detail: format!(
585                        "Execution Adapters prepared {} Event bindings for `{}:{}:{}`; expected 1",
586                        matching.len(),
587                        planned.consumer_instance(),
588                        planned.capability_id(),
589                        planned.provider_instance()
590                    ),
591                });
592            }
593            if !provider
594                .event_endpoints()
595                .iter()
596                .any(|endpoint| Rc::ptr_eq(endpoint, &matching[0].endpoint))
597            {
598                return Err(RuntimeFailure::InvalidResolvedPlan {
599                    detail: format!(
600                        "Event binding `{}:{}:{}` does not reference its provider generation endpoint",
601                        planned.consumer_instance(),
602                        planned.capability_id(),
603                        planned.provider_instance()
604                    ),
605                });
606            }
607        }
608    }
609    Ok(())
610}
611
612pub(super) fn native_plugin_runtimes<D: RuntimeDriver>(
613    plan: &ResolvedAppPlan,
614    driver: &D,
615    mut generations: BTreeMap<String, PreparedNativePlugin>,
616) -> BTreeMap<String, NativePluginRuntime> {
617    let mut runtimes = BTreeMap::new();
618    for instance in plan.plugin_instances() {
619        let lifecycle = generations
620            .remove(instance.instance_key())
621            .map(|generation| generation.lifecycle())
622            .expect("prepared App validation requires one generation per planned Instance");
623        runtimes.insert(
624            instance.instance_key().to_owned(),
625            NativePluginRuntime {
626                generation: RefCell::new(Some(NativePluginGeneration {
627                    lifecycle,
628                    tasks: ManagedTaskScope::new(driver),
629                    resources: ManagedResourceScope::new(),
630                    stop_attempted: false,
631                    cleanup_timed_out: false,
632                })),
633            },
634        );
635    }
636    runtimes
637}
638
639pub(super) async fn prepare_native_plugins(
640    runtime: &Rc<NativeAppRuntime>,
641) -> Result<Vec<String>, RuntimeFailure> {
642    let mut prepared_instances = Vec::with_capacity(runtime.activation_order.len());
643    for instance_key in &runtime.activation_order {
644        startup_active(runtime)?;
645        let instance = runtime
646            .plan
647            .plugin_instances()
648            .iter()
649            .find(|instance| instance.instance_key() == instance_key)
650            .expect("activation order only contains planned Plugin Instances");
651        let plugin = runtime
652            .plugins
653            .get(instance_key)
654            .expect("activation order only contains planned Plugin Instances");
655        let (lifecycle, tasks, resources) = plugin
656            .generation_parts()
657            .expect("every startup Plugin Instance has a generation");
658        let cancellation = lifecycle_cancellation(runtime, &tasks);
659        prepared_instances.push(instance_key.clone());
660        let started_at = (runtime.driver.now)();
661        runtime
662            .diagnostics
663            .emit(super::DiagnosticSource::Lifecycle, started_at, |_| {
664                super::DiagnosticEvent::LifecycleStarted {
665                    instance: instance_key.clone(),
666                    generation: 1,
667                    phase: super::PluginLifecyclePhase::Prepare,
668                }
669            });
670        let context = PrepareContext {
671            instance_key: instance_key.clone(),
672            entrypoint: instance.entrypoint().to_owned(),
673            configuration: instance.configuration().to_owned(),
674            dependencies: runtime
675                .dependencies
676                .get(instance_key)
677                .cloned()
678                .unwrap_or_default(),
679            resources,
680            cancellation,
681            admission: runtime.admission.clone(),
682        };
683        let result = lifecycle
684            .prepare(context)
685            .await
686            .and_then(|()| startup_active(runtime));
687        let outcome = result.as_ref().map_or_else(
688            |error| super::DiagnosticOutcome::RuntimeFailure(error.into()),
689            |()| super::DiagnosticOutcome::Succeeded,
690        );
691        runtime.diagnostics.emit(
692            super::DiagnosticSource::Lifecycle,
693            (runtime.driver.now)(),
694            |_| super::DiagnosticEvent::LifecycleCompleted {
695                instance: instance_key.clone(),
696                generation: 1,
697                phase: super::PluginLifecyclePhase::Prepare,
698                outcome,
699                elapsed: (runtime.driver.now)().saturating_sub(started_at),
700            },
701        );
702        if let Err(error) = result {
703            let cleanup_error = deactivate_in_reverse(
704                &runtime.plugins,
705                &runtime.dependencies,
706                &prepared_instances,
707                DeactivationReason::StartupRollback,
708                &runtime.admission,
709                &runtime.diagnostics,
710                &runtime.driver,
711                runtime
712                    .startup_cleanup
713                    .as_ref()
714                    .map(super::cleanup::StartupCleanupBudget::establish),
715            )
716            .await;
717            retain_unsafe_startup(runtime, cleanup_error.as_ref());
718            runtime.diagnostics.emit_runtime_failure(
719                (runtime.driver.now)(),
720                Some(instance_key),
721                &error,
722            );
723            return Err(error);
724        }
725    }
726    Ok(prepared_instances)
727}
728
729fn retain_unsafe_startup(runtime: &Rc<NativeAppRuntime>, cleanup_error: Option<&RuntimeFailure>) {
730    if matches!(cleanup_error, Some(RuntimeFailure::DeadlineExceeded { .. })) {
731        // Native code cannot be preempted safely. With no App handle to carry
732        // this failed startup generation, retain ownership until the embedding
733        // Host escalates by terminating the containing process.
734        std::mem::forget(runtime.clone());
735    }
736}
737
738pub(super) async fn activate_native_plugins(
739    runtime: &Rc<NativeAppRuntime>,
740) -> Result<(), RuntimeFailure> {
741    for instance_key in &runtime.activation_order {
742        startup_active(runtime)?;
743        let plugin = runtime
744            .plugins
745            .get(instance_key)
746            .expect("activation order only contains planned Plugin Instances");
747        let (lifecycle, tasks, resources) = plugin
748            .generation_parts()
749            .expect("every startup Plugin Instance has a generation");
750        let cancellation = lifecycle_cancellation(runtime, &tasks);
751        let started_at = (runtime.driver.now)();
752        runtime
753            .diagnostics
754            .emit(super::DiagnosticSource::Lifecycle, started_at, |_| {
755                super::DiagnosticEvent::LifecycleStarted {
756                    instance: instance_key.clone(),
757                    generation: 1,
758                    phase: super::PluginLifecyclePhase::Activate,
759                }
760            });
761        let context = ActivateContext {
762            instance_key: instance_key.clone(),
763            dependencies: runtime
764                .dependencies
765                .get(instance_key)
766                .cloned()
767                .unwrap_or_default(),
768            ready_gate: runtime.ready_gate.clone(),
769            tasks,
770            resources,
771            cancellation,
772            admission: runtime.admission.clone(),
773        };
774        let result = lifecycle
775            .activate(context)
776            .await
777            .and_then(|()| startup_active(runtime));
778        let outcome = result.as_ref().map_or_else(
779            |error| super::DiagnosticOutcome::RuntimeFailure(error.into()),
780            |()| super::DiagnosticOutcome::Succeeded,
781        );
782        runtime.diagnostics.emit(
783            super::DiagnosticSource::Lifecycle,
784            (runtime.driver.now)(),
785            |_| super::DiagnosticEvent::LifecycleCompleted {
786                instance: instance_key.clone(),
787                generation: 1,
788                phase: super::PluginLifecyclePhase::Activate,
789                outcome,
790                elapsed: (runtime.driver.now)().saturating_sub(started_at),
791            },
792        );
793        if let Err(error) = result {
794            runtime.diagnostics.emit_runtime_failure(
795                (runtime.driver.now)(),
796                Some(instance_key),
797                &error,
798            );
799            return Err(error);
800        }
801    }
802    Ok(())
803}
804
805pub(super) async fn construct_native_plugins(
806    runtime: &Rc<NativeAppRuntime>,
807) -> Result<(), RuntimeFailure> {
808    for instance_key in &runtime.activation_order {
809        let instance = runtime
810            .plan
811            .plugin_instance(instance_key)
812            .expect("construction order contains planned Instances");
813        if instance.authoring_version() == 1 {
814            continue;
815        }
816        startup_active(runtime)?;
817        let plugin = runtime
818            .plugins
819            .get(instance_key)
820            .expect("construction order contains planned Instances");
821        let (lifecycle, tasks, resources) = plugin
822            .generation_parts()
823            .expect("startup generation exists");
824        let started_at = (runtime.driver.now)();
825        runtime
826            .diagnostics
827            .emit(super::DiagnosticSource::Lifecycle, started_at, |_| {
828                super::DiagnosticEvent::LifecycleStarted {
829                    instance: instance_key.clone(),
830                    generation: 1,
831                    phase: super::PluginLifecyclePhase::Construct,
832                }
833            });
834        let result = lifecycle
835            .construct(ActivateContext {
836                instance_key: instance_key.clone(),
837                dependencies: runtime
838                    .dependencies
839                    .get(instance_key)
840                    .cloned()
841                    .unwrap_or_default(),
842                ready_gate: runtime.ready_gate.clone(),
843                tasks: tasks.clone(),
844                resources,
845                cancellation: lifecycle_cancellation(runtime, &tasks),
846                admission: runtime.admission.clone(),
847            })
848            .await
849            .and_then(|()| startup_active(runtime));
850        let outcome = result.as_ref().map_or_else(
851            |error| super::DiagnosticOutcome::RuntimeFailure(error.into()),
852            |()| super::DiagnosticOutcome::Succeeded,
853        );
854        runtime.diagnostics.emit(
855            super::DiagnosticSource::Lifecycle,
856            (runtime.driver.now)(),
857            |_| super::DiagnosticEvent::LifecycleCompleted {
858                instance: instance_key.clone(),
859                generation: 1,
860                phase: super::PluginLifecyclePhase::Construct,
861                outcome,
862                elapsed: (runtime.driver.now)().saturating_sub(started_at),
863            },
864        );
865        result?;
866    }
867    Ok(())
868}
869
870pub(super) async fn open_native_readiness(runtime: &Rc<NativeAppRuntime>) {
871    runtime.startup_context.borrow_mut().take();
872    runtime.ready_gate.open();
873    runtime.admission.open();
874    runtime.diagnostics.emit(
875        super::DiagnosticSource::Lifecycle,
876        (runtime.driver.now)(),
877        |_| super::DiagnosticEvent::AppReady,
878    );
879    (runtime.driver.yield_now)().await;
880}
881
882pub(super) fn native_bindings(
883    plan: &ResolvedAppPlan,
884    prepared: &[PreparedBinding],
885) -> (NativeBindingTable, NativeEndpointStateTable) {
886    let mut bindings = BTreeMap::new();
887    let mut endpoint_states = BTreeMap::new();
888    for binding in plan.capability_bindings() {
889        let Some(descriptor) =
890            plan.plugin_instance(binding.provider_instance())
891                .and_then(|provider| {
892                    provider
893                        .provided_capabilities()
894                        .iter()
895                        .find(|endpoint| endpoint.capability_id() == binding.capability_id())
896                })
897        else {
898            continue;
899        };
900        if descriptor.request_operations().is_empty() {
901            continue;
902        }
903        let Some(endpoint) = prepared.iter().find_map(|prepared| {
904            (prepared.requirement_id() == binding.requirement_id()
905                && prepared.consumer_instance == binding.consumer_instance()
906                && prepared.provider_instance == binding.provider_instance()
907                && prepared.endpoint.capability_id() == binding.capability_id())
908            .then_some(&prepared.endpoint)
909        }) else {
910            continue;
911        };
912        let state = endpoint_states
913            .entry((
914                binding.provider_instance().to_owned(),
915                endpoint.capability_id().to_owned(),
916            ))
917            .or_insert_with(|| Rc::new(NativeEndpointState::new(endpoint.clone(), 1)))
918            .clone();
919        let admissions = endpoint
920            .operations()
921            .iter()
922            .map(|operation| {
923                (
924                    (*operation).to_owned(),
925                    RequestAdmission::new(plan.request_admission_for(binding, operation)),
926                )
927            })
928            .collect();
929        bindings
930            .entry((
931                binding.consumer_instance().to_owned(),
932                endpoint.capability_id(),
933            ))
934            .or_insert_with(Vec::new)
935            .push(NativeEndpointBinding {
936                requirement_id: binding.requirement_id().to_owned(),
937                plugin_instance: binding.provider_instance().to_owned(),
938                state,
939                admissions,
940            });
941    }
942    (bindings, endpoint_states)
943}
944
945pub(super) fn native_stream_bindings(
946    plan: &ResolvedAppPlan,
947    prepared: &[PreparedStreamBinding],
948) -> (NativeStreamBindingTable, NativeStreamEndpointStateTable) {
949    let mut bindings = BTreeMap::new();
950    let mut endpoint_states = BTreeMap::new();
951    for binding in plan.capability_bindings() {
952        let Some(descriptor) =
953            plan.plugin_instance(binding.provider_instance())
954                .and_then(|provider| {
955                    provider
956                        .provided_capabilities()
957                        .iter()
958                        .find(|endpoint| endpoint.capability_id() == binding.capability_id())
959                })
960        else {
961            continue;
962        };
963        if descriptor.stream_operations().is_empty() {
964            continue;
965        }
966        let Some(endpoint) = prepared.iter().find_map(|prepared| {
967            (prepared.requirement_id() == binding.requirement_id()
968                && prepared.consumer_instance == binding.consumer_instance()
969                && prepared.provider_instance == binding.provider_instance()
970                && prepared.endpoint.capability_id() == binding.capability_id())
971            .then_some(&prepared.endpoint)
972        }) else {
973            continue;
974        };
975        let state = endpoint_states
976            .entry((
977                binding.provider_instance().to_owned(),
978                endpoint.capability_id().to_owned(),
979            ))
980            .or_insert_with(|| Rc::new(NativeStreamEndpointState::new(endpoint.clone(), 1)))
981            .clone();
982        let admissions = endpoint
983            .operations()
984            .iter()
985            .map(|operation| {
986                (
987                    (*operation).to_owned(),
988                    RequestAdmission::new(plan.request_admission_for(binding, operation)),
989                )
990            })
991            .collect();
992        bindings
993            .entry((
994                binding.consumer_instance().to_owned(),
995                endpoint.capability_id(),
996            ))
997            .or_insert_with(Vec::new)
998            .push(NativeStreamEndpointBinding {
999                requirement_id: binding.requirement_id().to_owned(),
1000                plugin_instance: binding.provider_instance().to_owned(),
1001                state,
1002                admissions,
1003            });
1004    }
1005    (bindings, endpoint_states)
1006}
1007
1008pub(super) fn native_event_bindings(
1009    plan: &ResolvedAppPlan,
1010    prepared: &[PreparedEventBinding],
1011) -> (NativeEventBindingTable, NativeEventEndpointStateTable) {
1012    let mut bindings = BTreeMap::new();
1013    let mut endpoint_states = BTreeMap::new();
1014    for binding in plan.capability_bindings() {
1015        let Some(descriptor) =
1016            plan.plugin_instance(binding.provider_instance())
1017                .and_then(|provider| {
1018                    provider
1019                        .provided_capabilities()
1020                        .iter()
1021                        .find(|endpoint| endpoint.capability_id() == binding.capability_id())
1022                })
1023        else {
1024            continue;
1025        };
1026        if descriptor.event_operations().is_empty() {
1027            continue;
1028        }
1029        let Some(endpoint) = prepared.iter().find_map(|prepared| {
1030            (prepared.requirement_id() == binding.requirement_id()
1031                && prepared.consumer_instance == binding.consumer_instance()
1032                && prepared.provider_instance == binding.provider_instance()
1033                && prepared.endpoint.capability_id() == binding.capability_id())
1034            .then_some(&prepared.endpoint)
1035        }) else {
1036            continue;
1037        };
1038        let state = endpoint_states
1039            .entry((
1040                binding.provider_instance().to_owned(),
1041                endpoint.capability_id().to_owned(),
1042            ))
1043            .or_insert_with(|| Rc::new(event::NativeEventEndpointState::new(endpoint.clone(), 1)))
1044            .clone();
1045        let queue = event::NativeEventQueue::new(plan.event_admission_for(binding));
1046        state.register_queue(&queue);
1047        bindings
1048            .entry((
1049                binding.consumer_instance().to_owned(),
1050                endpoint.capability_id(),
1051            ))
1052            .or_insert_with(Vec::new)
1053            .push(event::NativeEventEndpointBinding {
1054                requirement_id: binding.requirement_id().to_owned(),
1055                plugin_instance: binding.provider_instance().to_owned(),
1056                state,
1057                queue,
1058            });
1059    }
1060    (bindings, endpoint_states)
1061}
1062
1063pub(super) fn plugin_dependencies(
1064    plan: &ResolvedAppPlan,
1065    endpoints: &BTreeMap<(String, &'static str), Vec<NativeEndpointBinding>>,
1066    stream_endpoints: &NativeStreamBindingTable,
1067    event_endpoints: &NativeEventBindingTable,
1068    runtime: &Rc<RefCell<Weak<NativeAppRuntime>>>,
1069) -> BTreeMap<String, PluginDependencies> {
1070    let mut dependencies: BTreeMap<String, PluginDependencies> = plan
1071        .plugin_instances()
1072        .iter()
1073        .map(|instance| {
1074            (
1075                instance.instance_key().to_owned(),
1076                PluginDependencies::new(
1077                    instance.instance_key(),
1078                    runtime.clone(),
1079                    instance.required_capabilities().to_vec(),
1080                ),
1081            )
1082        })
1083        .collect();
1084    for binding in plan.capability_bindings() {
1085        dependencies
1086            .get_mut(binding.consumer_instance())
1087            .expect("every resolved binding consumer has Plugin dependencies")
1088            .bindings
1089            .push(PluginDependency::new(
1090                binding.requirement_id(),
1091                binding.capability_id(),
1092                binding.provider_instance(),
1093                binding.provider_order(),
1094                endpoints
1095                    .iter()
1096                    .find(|((consumer, capability), _)| {
1097                        consumer == binding.consumer_instance()
1098                            && *capability == binding.capability_id()
1099                    })
1100                    .and_then(|(_, endpoints)| {
1101                        endpoints.iter().find(|endpoint| {
1102                            endpoint.requirement_id == binding.requirement_id()
1103                                && endpoint.plugin_instance == binding.provider_instance()
1104                        })
1105                    })
1106                    .map(|endpoint| PluginDependencyHandle {
1107                        binding: endpoint.clone(),
1108                        caller_instance: binding.consumer_instance().to_owned(),
1109                        runtime: runtime.clone(),
1110                    }),
1111                stream_endpoints
1112                    .iter()
1113                    .find(|((consumer, capability), _)| {
1114                        consumer == binding.consumer_instance()
1115                            && *capability == binding.capability_id()
1116                    })
1117                    .and_then(|(_, endpoints)| {
1118                        endpoints.iter().find(|endpoint| {
1119                            endpoint.requirement_id == binding.requirement_id()
1120                                && endpoint.plugin_instance == binding.provider_instance()
1121                        })
1122                    })
1123                    .map(|endpoint| PluginStreamDependencyHandle {
1124                        binding: endpoint.clone(),
1125                        caller_instance: binding.consumer_instance().to_owned(),
1126                        runtime: runtime.clone(),
1127                    }),
1128                event_endpoints
1129                    .iter()
1130                    .find(|((consumer, capability), _)| {
1131                        consumer == binding.consumer_instance()
1132                            && *capability == binding.capability_id()
1133                    })
1134                    .and_then(|(_, endpoints)| {
1135                        endpoints.iter().find(|endpoint| {
1136                            endpoint.requirement_id == binding.requirement_id()
1137                                && endpoint.plugin_instance == binding.provider_instance()
1138                        })
1139                    })
1140                    .map(|endpoint| PluginEventDependencyHandle {
1141                        binding: endpoint.clone(),
1142                        caller_instance: binding.consumer_instance().to_owned(),
1143                        runtime: runtime.clone(),
1144                    }),
1145            ));
1146    }
1147    dependencies
1148}