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