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