Skip to main content

lenso_kernel/
lifecycle.rs

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