Skip to main content

lenso_kernel/
lifecycle.rs

1use super::{
2    AbortHandle, AssertUnwindSafe, Cell, Context, DriverControl, DriverTask, Duration, Future,
3    FutureExt, LocalBoxFuture, LocalTask, Pin, PluginDependencies, PluginLifecyclePhase, Poll, Rc,
4    RefCell, RuntimeDriver, RuntimeFailure, SpawnError, TaskOutcome, oneshot, wait_until,
5};
6
7/// A shared App-wide signal that opens exactly once after every Plugin activates.
8#[derive(Clone, Debug)]
9pub struct AppReadyGate {
10    pub(super) state: Rc<AppReadyState>,
11}
12
13#[derive(Debug)]
14pub(super) struct AppReadyState {
15    pub(super) open: Cell<bool>,
16    pub(super) waiters: RefCell<Vec<oneshot::Sender<()>>>,
17}
18
19impl AppReadyGate {
20    /// Creates a closed App Ready Gate.
21    pub fn new() -> Self {
22        Self {
23            state: Rc::new(AppReadyState {
24                open: Cell::new(false),
25                waiters: RefCell::new(Vec::new()),
26            }),
27        }
28    }
29
30    /// Returns whether the App Ready Gate has opened.
31    pub fn is_open(&self) -> bool {
32        self.state.open.get()
33    }
34
35    /// Waits until the whole App has completed activation.
36    pub fn wait(&self) -> LocalBoxFuture<'static, ()> {
37        if self.is_open() {
38            return Box::pin(futures::future::ready(()));
39        }
40
41        let (wakeup, waiter) = oneshot::channel();
42        self.state.waiters.borrow_mut().push(wakeup);
43        Box::pin(async move {
44            let _ = waiter.await;
45        })
46    }
47
48    pub(super) fn open(&self) {
49        if self.state.open.replace(true) {
50            return;
51        }
52        for waiter in self.state.waiters.borrow_mut().drain(..) {
53            let _ = waiter.send(());
54        }
55    }
56}
57
58impl Default for AppReadyGate {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64/// App-wide admission for externally triggered work.
65#[derive(Clone, Debug)]
66pub struct AppAdmission {
67    pub(super) state: Rc<AppAdmissionState>,
68}
69
70#[derive(Debug)]
71pub(super) struct AppAdmissionState {
72    pub(super) open: Cell<bool>,
73}
74
75impl AppAdmission {
76    pub(super) fn new() -> Self {
77        Self {
78            state: Rc::new(AppAdmissionState {
79                open: Cell::new(false),
80            }),
81        }
82    }
83
84    /// Returns whether new externally triggered work may be admitted.
85    pub fn is_open(&self) -> bool {
86        self.state.open.get()
87    }
88
89    /// Returns whether new externally triggered work is rejected.
90    pub fn is_closed(&self) -> bool {
91        !self.is_open()
92    }
93
94    pub(super) fn open(&self) {
95        self.state.open.set(true);
96    }
97
98    pub(super) fn close(&self) {
99        self.state.open.set(false);
100    }
101}
102
103/// Cooperative cancellation shared by one Plugin Instance generation.
104#[derive(Clone, Debug)]
105pub struct CancellationToken {
106    pub(super) state: Rc<CancellationState>,
107}
108
109#[derive(Debug)]
110pub(super) struct CancellationState {
111    pub(super) cancelled: Cell<bool>,
112    pub(super) next_waiter_id: Cell<usize>,
113    pub(super) waiters: RefCell<Vec<(usize, oneshot::Sender<()>)>>,
114}
115
116impl CancellationToken {
117    /// Creates a token that has not been cancelled.
118    pub fn new() -> Self {
119        Self {
120            state: Rc::new(CancellationState {
121                cancelled: Cell::new(false),
122                next_waiter_id: Cell::new(0),
123                waiters: RefCell::new(Vec::new()),
124            }),
125        }
126    }
127
128    /// Returns whether cancellation has been requested.
129    pub fn is_cancelled(&self) -> bool {
130        self.state.cancelled.get()
131    }
132
133    /// Waits until cancellation is requested.
134    pub fn cancelled(&self) -> LocalBoxFuture<'static, ()> {
135        if self.is_cancelled() {
136            return Box::pin(futures::future::ready(()));
137        }
138        let (wakeup, waiter) = oneshot::channel();
139        let waiter_id = self.state.next_waiter_id.get();
140        self.state.next_waiter_id.set(waiter_id.saturating_add(1));
141        self.state.waiters.borrow_mut().push((waiter_id, wakeup));
142        Box::pin(CancellationWaiter {
143            state: self.state.clone(),
144            waiter_id,
145            receiver: waiter,
146            registered: true,
147        })
148    }
149
150    /// Requests cooperative cancellation and wakes every current waiter.
151    pub fn cancel(&self) {
152        if self.state.cancelled.replace(true) {
153            return;
154        }
155        for (_, waiter) in self.state.waiters.borrow_mut().drain(..) {
156            let _ = waiter.send(());
157        }
158    }
159}
160
161#[derive(Debug)]
162pub(super) struct CancellationWaiter {
163    pub(super) state: Rc<CancellationState>,
164    pub(super) waiter_id: usize,
165    pub(super) receiver: oneshot::Receiver<()>,
166    pub(super) registered: bool,
167}
168
169impl Future for CancellationWaiter {
170    type Output = ();
171
172    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
173        match Pin::new(&mut self.receiver).poll(context) {
174            Poll::Ready(_) => {
175                self.registered = false;
176                Poll::Ready(())
177            }
178            Poll::Pending => Poll::Pending,
179        }
180    }
181}
182
183impl Drop for CancellationWaiter {
184    fn drop(&mut self) {
185        if !self.registered {
186            return;
187        }
188        self.state
189            .waiters
190            .borrow_mut()
191            .retain(|(waiter_id, _)| *waiter_id != self.waiter_id);
192    }
193}
194
195impl Default for CancellationToken {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201/// A future used to release one Driver-backed managed resource.
202pub type ResourceFuture = LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
203
204/// A resource whose release is owned by one Plugin Instance generation.
205pub trait ManagedResource: std::fmt::Debug + 'static {
206    /// Releases the resource exactly once when its generation is cleaned up.
207    fn release(&self) -> ResourceFuture;
208}
209
210/// Error returned when a resource cannot be registered in a closed scope.
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212pub enum ResourceRegistrationError {
213    /// The Plugin generation has begun shutdown or rollback cleanup.
214    ScopeClosed,
215}
216
217pub(super) struct ManagedResourceEntry {
218    pub(super) resource: Rc<dyn ManagedResource>,
219    pub(super) release: RefCell<ManagedResourceRelease>,
220}
221
222pub(super) enum ManagedResourceRelease {
223    Pending,
224    Running(ResourceFuture),
225    Complete(Result<(), RuntimeFailure>),
226}
227
228impl std::fmt::Debug for ManagedResourceEntry {
229    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        let state = match &*self.release.borrow() {
231            ManagedResourceRelease::Pending => "pending",
232            ManagedResourceRelease::Running(_) => "running",
233            ManagedResourceRelease::Complete(Ok(())) => "released",
234            ManagedResourceRelease::Complete(Err(_)) => "failed",
235        };
236        formatter
237            .debug_struct("ManagedResourceEntry")
238            .field("release", &state)
239            .finish_non_exhaustive()
240    }
241}
242
243/// A handle that releases one managed resource at most once.
244#[derive(Clone, Debug)]
245pub struct ManagedResourceHandle {
246    pub(super) entry: Rc<ManagedResourceEntry>,
247}
248
249impl ManagedResourceHandle {
250    /// Returns whether this resource's release future completed.
251    pub fn is_released(&self) -> bool {
252        matches!(
253            &*self.entry.release.borrow(),
254            ManagedResourceRelease::Complete(_)
255        )
256    }
257
258    /// Releases this resource once; repeated calls are successful no-ops.
259    pub async fn release(&self) -> Result<(), RuntimeFailure> {
260        ManagedResourceReleaseOperation {
261            entry: self.entry.clone(),
262        }
263        .await
264    }
265}
266
267pub(super) struct ManagedResourceReleaseOperation {
268    pub(super) entry: Rc<ManagedResourceEntry>,
269}
270
271impl Future for ManagedResourceReleaseOperation {
272    type Output = Result<(), RuntimeFailure>;
273
274    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
275        let mut release = self.entry.release.borrow_mut();
276        if matches!(*release, ManagedResourceRelease::Pending) {
277            *release = ManagedResourceRelease::Running(self.entry.resource.release());
278        }
279        match &mut *release {
280            ManagedResourceRelease::Running(future) => match future.as_mut().poll(context) {
281                Poll::Ready(result) => {
282                    *release = ManagedResourceRelease::Complete(result.clone());
283                    Poll::Ready(result)
284                }
285                Poll::Pending => Poll::Pending,
286            },
287            ManagedResourceRelease::Complete(result) => Poll::Ready(result.clone()),
288            ManagedResourceRelease::Pending => unreachable!("pending release was started"),
289        }
290    }
291}
292
293/// A Plugin-generation resource scope backed by Driver-polled cleanup futures.
294#[derive(Clone)]
295pub struct ManagedResourceScope {
296    pub(super) state: Rc<ManagedResourceScopeState>,
297}
298
299#[derive(Debug, Default)]
300pub(super) struct ManagedResourceScopeState {
301    pub(super) resources: RefCell<Vec<ManagedResourceHandle>>,
302    pub(super) closed: Cell<bool>,
303}
304
305impl std::fmt::Debug for ManagedResourceScope {
306    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        formatter
308            .debug_struct("ManagedResourceScope")
309            .field("resource_count", &self.resource_count())
310            .finish()
311    }
312}
313
314impl ManagedResourceScope {
315    pub(super) fn new() -> Self {
316        Self {
317            state: Rc::new(ManagedResourceScopeState::default()),
318        }
319    }
320
321    /// Registers a resource owned by this Plugin Instance generation.
322    pub fn register(
323        &self,
324        resource: impl ManagedResource,
325    ) -> Result<ManagedResourceHandle, ResourceRegistrationError> {
326        if self.state.closed.get() {
327            return Err(ResourceRegistrationError::ScopeClosed);
328        }
329        let handle = ManagedResourceHandle {
330            entry: Rc::new(ManagedResourceEntry {
331                resource: Rc::new(resource),
332                release: RefCell::new(ManagedResourceRelease::Pending),
333            }),
334        };
335        self.state.resources.borrow_mut().push(handle.clone());
336        Ok(handle)
337    }
338
339    /// Returns the number of resources that still need cleanup.
340    pub fn resource_count(&self) -> usize {
341        self.state
342            .resources
343            .borrow()
344            .iter()
345            .filter(|resource| !resource.is_released())
346            .count()
347    }
348
349    pub(super) fn close(&self) {
350        self.state.closed.set(true);
351    }
352
353    pub(super) async fn release_all(&self) -> Option<RuntimeFailure> {
354        let resources = std::mem::take(&mut *self.state.resources.borrow_mut());
355        let mut first_error = None;
356        for resource in resources {
357            if let Err(error) = resource.release().await
358                && first_error.is_none()
359            {
360                first_error = Some(error);
361            }
362        }
363        first_error
364    }
365
366    pub(super) async fn release_all_until(
367        &self,
368        driver: &DriverControl,
369        deadline: Duration,
370    ) -> Result<Option<RuntimeFailure>, ()> {
371        let resources = std::mem::take(&mut *self.state.resources.borrow_mut());
372        let mut first_error = None;
373        for (index, resource) in resources.iter().enumerate() {
374            match wait_until(driver, deadline, resource.release()).await {
375                Some(Ok(())) => {}
376                Some(Err(error)) => {
377                    if first_error.is_none() {
378                        first_error = Some(error);
379                    }
380                }
381                None => {
382                    self.state
383                        .resources
384                        .borrow_mut()
385                        .extend(resources.into_iter().skip(index));
386                    return Err(());
387                }
388            }
389        }
390        Ok(first_error)
391    }
392}
393
394/// A Kernel-owned task handle that is cleaned up with its Plugin generation.
395#[derive(Clone, Debug)]
396pub struct ManagedTask {
397    pub(super) task: Rc<RefCell<Option<DriverTask>>>,
398    pub(super) abort: AbortHandle,
399    pub(super) failed: Rc<Cell<bool>>,
400    pub(super) completed: Rc<Cell<bool>>,
401}
402
403impl ManagedTask {
404    pub(super) fn from_driver_task(task: DriverTask) -> Self {
405        Self {
406            abort: task.abort_handle(),
407            task: Rc::new(RefCell::new(Some(task))),
408            failed: Rc::new(Cell::new(false)),
409            completed: Rc::new(Cell::new(false)),
410        }
411    }
412
413    /// Requests cancellation of the underlying task.
414    pub fn cancel(&self) {
415        self.abort.abort();
416    }
417
418    pub(super) async fn join(&self) -> TaskOutcome {
419        let outcome = std::future::poll_fn(|context| {
420            let mut slot = self.task.borrow_mut();
421            let Some(task) = slot.as_mut() else {
422                return Poll::Ready(TaskOutcome::Completed);
423            };
424            match Pin::new(task).poll(context) {
425                Poll::Ready(outcome) => {
426                    slot.take();
427                    Poll::Ready(outcome)
428                }
429                Poll::Pending => Poll::Pending,
430            }
431        })
432        .await;
433        if self.failed.get() {
434            TaskOutcome::Failed
435        } else {
436            outcome
437        }
438    }
439}
440
441/// Error returned when a managed task cannot be admitted to its scope.
442#[derive(Debug)]
443pub enum ManagedTaskError {
444    /// The Plugin generation has begun shutdown or rollback cleanup.
445    ScopeClosed,
446    /// The Runtime Driver rejected the local task.
447    Driver(SpawnError),
448}
449
450impl From<SpawnError> for ManagedTaskError {
451    fn from(error: SpawnError) -> Self {
452        Self::Driver(error)
453    }
454}
455
456/// A Plugin-generation task scope backed by the selected Runtime Driver.
457#[derive(Clone)]
458pub struct ManagedTaskScope {
459    pub(super) spawn: Rc<dyn Fn(LocalTask) -> Result<DriverTask, SpawnError>>,
460    pub(super) state: Rc<ManagedTaskScopeState>,
461}
462
463pub(super) struct ManagedTaskScopeState {
464    pub(super) tasks: RefCell<Vec<ManagedTask>>,
465    pub(super) closed: Cell<bool>,
466    pub(super) cancellation: CancellationToken,
467    pub(super) failure_handler: RefCell<Option<Rc<dyn Fn()>>>,
468    pub(super) unreported_failure: Cell<bool>,
469}
470
471impl Default for ManagedTaskScopeState {
472    fn default() -> Self {
473        Self {
474            tasks: RefCell::new(Vec::new()),
475            closed: Cell::new(false),
476            cancellation: CancellationToken::new(),
477            failure_handler: RefCell::new(None),
478            unreported_failure: Cell::new(false),
479        }
480    }
481}
482
483impl std::fmt::Debug for ManagedTaskScopeState {
484    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485        formatter
486            .debug_struct("ManagedTaskScopeState")
487            .field("task_count", &self.tasks.borrow().len())
488            .field("closed", &self.closed.get())
489            .field("unreported_failure", &self.unreported_failure.get())
490            .finish_non_exhaustive()
491    }
492}
493
494impl std::fmt::Debug for ManagedTaskScope {
495    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496        formatter
497            .debug_struct("ManagedTaskScope")
498            .field("task_count", &self.task_count())
499            .finish()
500    }
501}
502
503impl ManagedTaskScope {
504    pub(super) fn new<D: RuntimeDriver>(driver: &D) -> Self {
505        let spawner = driver.clone();
506        Self {
507            spawn: Rc::new(move |task| spawner.spawn_local(task)),
508            state: Rc::new(ManagedTaskScopeState::default()),
509        }
510    }
511
512    pub(super) fn new_from_driver_control(driver: &DriverControl) -> Self {
513        let spawn = driver.spawn_local.clone();
514        Self {
515            spawn,
516            state: Rc::new(ManagedTaskScopeState::default()),
517        }
518    }
519
520    /// Spawns work owned by this Plugin Instance generation.
521    pub fn spawn_local(&self, task: LocalTask) -> Result<ManagedTask, ManagedTaskError> {
522        if self.state.closed.get() {
523            return Err(ManagedTaskError::ScopeClosed);
524        }
525        let failed = Rc::new(Cell::new(false));
526        let task_failed = failed.clone();
527        let completed = Rc::new(Cell::new(false));
528        let task_completed = completed.clone();
529        let state = self.state.clone();
530        let monitored = Box::pin(async move {
531            let outcome = AssertUnwindSafe(task).catch_unwind().await;
532            task_completed.set(true);
533            if outcome.is_err() {
534                task_failed.set(true);
535                state.report_failure();
536            }
537        });
538        let driver_task = (self.spawn)(monitored)?;
539        let handle = ManagedTask {
540            failed,
541            completed,
542            ..ManagedTask::from_driver_task(driver_task)
543        };
544        self.state
545            .tasks
546            .borrow_mut()
547            .retain(|task| !task.completed.get());
548        self.state.tasks.borrow_mut().push(handle.clone());
549        Ok(handle)
550    }
551
552    /// Returns the number of tasks still tracked by this scope.
553    pub fn task_count(&self) -> usize {
554        self.state
555            .tasks
556            .borrow()
557            .iter()
558            .filter(|task| !task.completed.get())
559            .count()
560    }
561
562    /// Returns the cooperative cancellation token for this generation.
563    pub fn cancellation(&self) -> CancellationToken {
564        self.state.cancellation.clone()
565    }
566
567    pub(super) fn close(&self) {
568        self.state.closed.set(true);
569        self.state.cancellation.cancel();
570    }
571
572    pub(super) fn set_failure_handler(&self, handler: &Rc<dyn Fn()>) {
573        self.state.failure_handler.replace(Some(handler.clone()));
574        if self.state.unreported_failure.replace(false) {
575            handler();
576        }
577    }
578
579    pub(super) fn cancel(&self) {
580        self.state.cancellation.cancel();
581    }
582
583    pub(super) fn abort_all(&self) {
584        for task in self.state.tasks.borrow().iter() {
585            task.cancel();
586        }
587    }
588
589    pub(super) async fn cancel_all(&self) {
590        self.close();
591        let tasks = std::mem::take(&mut *self.state.tasks.borrow_mut());
592        for task in tasks {
593            task.cancel();
594            let _ = task.join().await;
595        }
596    }
597
598    pub(super) async fn drain_until(&self, driver: &DriverControl, deadline: Duration) -> bool {
599        self.cancel();
600        let tasks = std::mem::take(&mut *self.state.tasks.borrow_mut());
601        for (index, task) in tasks.iter().enumerate() {
602            if wait_until(driver, deadline, task.join()).await.is_none() {
603                for pending in tasks.iter().skip(index) {
604                    pending.cancel();
605                }
606                self.state
607                    .tasks
608                    .borrow_mut()
609                    .extend(tasks.into_iter().skip(index));
610                return false;
611            }
612        }
613        true
614    }
615}
616
617impl ManagedTaskScopeState {
618    pub(super) fn report_failure(&self) {
619        let handler = self.failure_handler.borrow().clone();
620        if let Some(handler) = handler {
621            handler();
622        } else {
623            self.unreported_failure.set(true);
624        }
625    }
626}
627
628/// Context supplied while a Plugin reserves reversible resources.
629#[derive(Clone, Debug)]
630pub struct PrepareContext {
631    pub(super) instance_key: String,
632    pub(super) entrypoint: String,
633    pub(super) configuration: String,
634    pub(super) dependencies: PluginDependencies,
635    pub(super) resources: ManagedResourceScope,
636    pub(super) cancellation: CancellationToken,
637    pub(super) admission: AppAdmission,
638}
639
640impl PrepareContext {
641    /// Returns the App-local Plugin Instance key.
642    pub fn instance_key(&self) -> &str {
643        &self.instance_key
644    }
645
646    /// Returns the exact package entrypoint selected by the immutable Plan.
647    pub fn entrypoint(&self) -> &str {
648        &self.entrypoint
649    }
650
651    /// Returns opaque Plugin-owned configuration selected by the immutable Plan.
652    pub fn configuration(&self) -> &str {
653        &self.configuration
654    }
655
656    /// Returns the phase represented by this context.
657    pub const fn phase(&self) -> PluginLifecyclePhase {
658        PluginLifecyclePhase::Prepare
659    }
660
661    /// Returns the explicit dependencies selected for this Instance.
662    pub fn dependencies(&self) -> &PluginDependencies {
663        &self.dependencies
664    }
665
666    /// Returns the generation-owned resource scope.
667    pub fn resources(&self) -> &ManagedResourceScope {
668        &self.resources
669    }
670
671    /// Returns the generation-owned cooperative cancellation token.
672    pub fn cancellation(&self) -> CancellationToken {
673        self.cancellation.clone()
674    }
675
676    /// Returns the App admission state, which remains closed until readiness.
677    pub fn admission(&self) -> AppAdmission {
678        self.admission.clone()
679    }
680}
681
682/// Context supplied while a Plugin initializes against prepared dependencies.
683#[derive(Clone, Debug)]
684pub struct ActivateContext {
685    pub(super) instance_key: String,
686    pub(super) dependencies: PluginDependencies,
687    pub(super) ready_gate: AppReadyGate,
688    pub(super) tasks: ManagedTaskScope,
689    pub(super) resources: ManagedResourceScope,
690    pub(super) cancellation: CancellationToken,
691    pub(super) admission: AppAdmission,
692}
693
694impl ActivateContext {
695    /// Returns the App-local Plugin Instance key.
696    pub fn instance_key(&self) -> &str {
697        &self.instance_key
698    }
699
700    /// Returns the phase represented by this context.
701    pub const fn phase(&self) -> PluginLifecyclePhase {
702        PluginLifecyclePhase::Activate
703    }
704
705    /// Returns the explicit dependencies selected for this Instance.
706    pub fn dependencies(&self) -> &PluginDependencies {
707        &self.dependencies
708    }
709
710    /// Returns the closed-until-fully-active App Ready Gate.
711    pub fn ready_gate(&self) -> AppReadyGate {
712        self.ready_gate.clone()
713    }
714
715    /// Returns the readiness context a Plugin may pass to managed work.
716    pub fn readiness(&self) -> ReadinessContext {
717        ReadinessContext {
718            instance_key: self.instance_key.clone(),
719            dependencies: self.dependencies.clone(),
720            ready_gate: self.ready_gate.clone(),
721            tasks: self.tasks.clone(),
722            resources: self.resources.clone(),
723            cancellation: self.cancellation.clone(),
724            admission: self.admission.clone(),
725        }
726    }
727
728    /// Returns the generation-owned task scope.
729    pub fn tasks(&self) -> &ManagedTaskScope {
730        &self.tasks
731    }
732
733    /// Returns the generation-owned resource scope.
734    pub fn resources(&self) -> &ManagedResourceScope {
735        &self.resources
736    }
737
738    /// Returns the generation-owned cooperative cancellation token.
739    pub fn cancellation(&self) -> CancellationToken {
740        self.cancellation.clone()
741    }
742
743    /// Returns the App admission state, which remains closed until readiness.
744    pub fn admission(&self) -> AppAdmission {
745        self.admission.clone()
746    }
747}
748
749/// Context supplied after the App Ready Gate has opened.
750#[derive(Clone, Debug)]
751pub struct ReadinessContext {
752    pub(super) instance_key: String,
753    pub(super) dependencies: PluginDependencies,
754    pub(super) ready_gate: AppReadyGate,
755    pub(super) tasks: ManagedTaskScope,
756    pub(super) resources: ManagedResourceScope,
757    pub(super) cancellation: CancellationToken,
758    pub(super) admission: AppAdmission,
759}
760
761impl ReadinessContext {
762    /// Returns the App-local Plugin Instance key.
763    pub fn instance_key(&self) -> &str {
764        &self.instance_key
765    }
766
767    /// Returns the phase represented by this context.
768    pub const fn phase(&self) -> PluginLifecyclePhase {
769        PluginLifecyclePhase::Ready
770    }
771
772    /// Returns the explicit dependencies selected for this Instance.
773    pub fn dependencies(&self) -> &PluginDependencies {
774        &self.dependencies
775    }
776
777    /// Returns the opened App Ready Gate.
778    pub fn ready_gate(&self) -> AppReadyGate {
779        self.ready_gate.clone()
780    }
781
782    /// Waits for the App Ready Gate to open.
783    pub fn wait(&self) -> LocalBoxFuture<'static, ()> {
784        self.ready_gate.wait()
785    }
786
787    /// Returns whether the App Ready Gate has opened.
788    pub fn is_open(&self) -> bool {
789        self.ready_gate.is_open()
790    }
791
792    /// Returns the generation-owned task scope.
793    pub fn tasks(&self) -> &ManagedTaskScope {
794        &self.tasks
795    }
796
797    /// Returns the generation-owned resource scope.
798    pub fn resources(&self) -> &ManagedResourceScope {
799        &self.resources
800    }
801
802    /// Returns the generation-owned cooperative cancellation token.
803    pub fn cancellation(&self) -> CancellationToken {
804        self.cancellation.clone()
805    }
806
807    /// Returns whether new externally triggered work may be admitted.
808    pub fn is_accepting(&self) -> bool {
809        self.admission.is_open()
810    }
811
812    /// Returns the App admission state.
813    pub fn admission(&self) -> AppAdmission {
814        self.admission.clone()
815    }
816}
817
818/// The reason a Plugin generation is being deactivated.
819#[derive(Clone, Copy, Debug, Eq, PartialEq)]
820pub enum DeactivationReason {
821    /// Startup failed and prepared work is being rolled back.
822    StartupRollback,
823    /// The embedding App requested a graceful stop.
824    Shutdown,
825    /// Supervision is releasing a failed generation before recreation.
826    SupervisionRestart,
827}
828
829/// Context supplied while a Plugin releases one generation.
830#[derive(Clone, Debug)]
831pub struct DeactivateContext {
832    pub(super) instance_key: String,
833    pub(super) dependencies: PluginDependencies,
834    pub(super) reason: DeactivationReason,
835    pub(super) tasks: ManagedTaskScope,
836    pub(super) resources: ManagedResourceScope,
837    pub(super) cancellation: CancellationToken,
838    pub(super) admission: AppAdmission,
839    pub(super) cleanup: Option<super::cleanup::CleanupBudget>,
840}
841
842impl DeactivateContext {
843    /// Returns the App-local Plugin Instance key.
844    pub fn instance_key(&self) -> &str {
845        &self.instance_key
846    }
847
848    /// Returns the phase represented by this context.
849    pub const fn phase(&self) -> PluginLifecyclePhase {
850        PluginLifecyclePhase::Deactivate
851    }
852
853    /// Returns the explicit dependencies selected for this Instance.
854    pub fn dependencies(&self) -> &PluginDependencies {
855        &self.dependencies
856    }
857
858    /// Returns why this generation is being deactivated.
859    pub const fn reason(&self) -> DeactivationReason {
860        self.reason
861    }
862
863    /// Returns the generation-owned task scope.
864    pub fn tasks(&self) -> &ManagedTaskScope {
865        &self.tasks
866    }
867
868    /// Returns the generation-owned resource scope.
869    pub fn resources(&self) -> &ManagedResourceScope {
870        &self.resources
871    }
872
873    /// Returns the generation-owned cooperative cancellation token.
874    pub fn cancellation(&self) -> CancellationToken {
875        self.cleanup.as_ref().map_or_else(
876            || self.cancellation.clone(),
877            super::cleanup::CleanupBudget::cancellation,
878        )
879    }
880
881    /// Returns the Host budget remaining for this cleanup phase.
882    ///
883    /// Legacy authoring profiles that do not opt into bounded cleanup return
884    /// `None`.
885    pub fn remaining_budget(&self) -> Option<Duration> {
886        self.cleanup
887            .as_ref()
888            .map(super::cleanup::CleanupBudget::remaining)
889    }
890
891    /// Returns the App admission state, which is closed during deactivation.
892    pub fn admission(&self) -> AppAdmission {
893        self.admission.clone()
894    }
895}
896
897/// The result type returned by prepare, activate, and deactivate hooks.
898pub type PluginFuture = LocalBoxFuture<'static, Result<(), RuntimeFailure>>;
899
900/// Adapter-facing lifecycle Interface for one Plugin Instance generation.
901pub trait PluginLifecycle: std::fmt::Debug + 'static {
902    /// Reserves reversible resources without exposing external work.
903    fn prepare(&self, _context: PrepareContext) -> PluginFuture {
904        Box::pin(futures::future::ready(Ok(())))
905    }
906
907    /// Constructs the complete inert Plugin object for authoring version 2.
908    /// SDKs lower `create` into this hook; ordinary Plugin code does not call it.
909    #[doc(hidden)]
910    fn construct(&self, _context: ActivateContext) -> PluginFuture {
911        Box::pin(futures::future::ready(Ok(())))
912    }
913
914    /// Initializes the generation against already prepared dependencies.
915    fn activate(&self, _context: ActivateContext) -> PluginFuture {
916        Box::pin(futures::future::ready(Ok(())))
917    }
918
919    /// Releases resources and work owned by this generation.
920    fn deactivate(&self, _context: DeactivateContext) -> PluginFuture {
921        Box::pin(futures::future::ready(Ok(())))
922    }
923}
924
925/// Default no-op lifecycle used by endpoint-only native fixtures.
926#[derive(Debug, Default)]
927pub struct NoopPluginLifecycle;
928
929impl PluginLifecycle for NoopPluginLifecycle {}