Skip to main content

harmos_sdk/plugin/
owner.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14/* === Imports === */
15
16// Standard Library Imports
17use std::future::Future;
18use std::num::NonZeroUsize;
19use std::pin::Pin;
20use std::sync::{Arc, Mutex, Weak};
21// Third Party Imports
22
23// Local Imports
24use super::{Attachment, Context as PluginContext, Error, Plugin};
25use crate::Setup;
26use crate::operations::{Contract, ExecutionContext, ServingError, StartupError, WireTable};
27use harmos_domain::model::operations::lifecycle;
28
29/* === Constants === */
30
31/* === Macros === */
32
33/* === Type Aliases === */
34
35pub type Table = Arc<Mutex<WireTable>>;
36type Transition<P> = Pin<Box<dyn Future<Output = Completed<P>> + Send>>;
37
38/* === Structs === */
39
40pub struct Owner<P: Plugin> {
41    slot: Arc<Mutex<Slot<P>>>,
42    table: Weak<Mutex<WireTable>>,
43    contracts: Arc<[Contract]>,
44    observers: Arc<[crate::inventory::observers::Declaration]>,
45    retirement: Mutex<Retirement<P>>,
46}
47
48struct LifecycleInvocation<P: Plugin> {
49    owner: Arc<Owner<P>>,
50    state: Option<State<P>>,
51    action: lifecycle::Action,
52}
53
54struct Retirement<P: Plugin> {
55    state: Option<State<P>>,
56    active: bool,
57    finished: bool,
58}
59
60struct RetirementLoan<P: Plugin> {
61    owner: Arc<Owner<P>>,
62    state: Option<State<P>>,
63    finished: bool,
64}
65
66struct Slot<P: Plugin> {
67    plugin: Option<P>,
68    context: Option<P::Context>,
69    phase: Phase,
70    attachment: Option<Arc<dyn Attachment<P::Context>>>,
71    generation_context: Option<[u8; 32]>,
72}
73
74struct State<P: Plugin> {
75    slot: Arc<Mutex<Slot<P>>>,
76    table: Weak<Mutex<WireTable>>,
77    plugin: Option<P>,
78    config: Option<P::Config>,
79    configuration: Option<WireTable>,
80    work: Option<Transition<P>>,
81    result: Option<Result<(), Error<P>>>,
82    action: lifecycle::Action,
83    delivered: bool,
84    failure_reported: bool,
85    shutdown_attempted: bool,
86    cleanup_finished: bool,
87    context: Option<P::Context>,
88    assignments: Vec<u8>,
89    attachment: Option<Arc<dyn Attachment<P::Context>>>,
90    generation_context: Option<[u8; 32]>,
91}
92
93struct Completed<P: Plugin> {
94    plugin: Option<P>,
95    context: Option<P::Context>,
96    configuration: Option<WireTable>,
97    attachment: Option<Arc<dyn Attachment<P::Context>>>,
98    result: Result<(), Error<P>>,
99    shutdown_attempted: bool,
100    cleanup_finished: bool,
101}
102
103/* === Enums === */
104
105#[derive(Clone, Copy, PartialEq, Eq)]
106enum Phase {
107    Empty,
108    Initializing,
109    Initialized,
110    Stopping,
111    Closed,
112}
113
114/* === Traits === */
115
116/* === Implementations === */
117
118impl<P: Plugin> Owner<P> {
119    pub fn new(owner: &str, contracts: Vec<Contract>, capacity: NonZeroUsize) -> Result<(Arc<Self>, Table), Error<P>> {
120        Self::new_with_observers(owner, contracts, capacity, Vec::new())
121    }
122
123    pub fn new_with_observers(
124        assigned_owner: &str, contracts: Vec<Contract>, capacity: NonZeroUsize, observers: Vec<crate::inventory::observers::Declaration>,
125    ) -> Result<(Arc<Self>, Table), Error<P>> {
126        let definitions: Vec<_> = contracts.iter().map(|contract| contract.operation().clone()).collect();
127        crate::inventory::operations::validate(&definitions).map_err(|error| Error::Inventory(Arc::new(error)))?;
128        let mut serving = WireTable::new(assigned_owner).map_err(|error| Error::Serving(Arc::new(error)))?;
129        serving.capacity(capacity);
130        serving.provide(std::any::TypeId::of::<P::Context>());
131        serving.provide(std::any::TypeId::of::<crate::operations::Calls>());
132        let table = Arc::new(Mutex::new(serving));
133        let owner = Arc::new(Self {
134            slot: Arc::new(Mutex::new(Slot {
135                plugin: None,
136                context: None,
137                phase: Phase::Empty,
138                attachment: None,
139                generation_context: None,
140            })),
141            table: Arc::downgrade(&table),
142            contracts: contracts.into(),
143            observers: observers.into(),
144            retirement: Mutex::new(Retirement {
145                state: None,
146                active: false,
147                finished: false,
148            }),
149        });
150        Ok((owner, table))
151    }
152
153    pub async fn quiesce(&self) -> Result<(), Error<P>> {
154        let attachment = self.slot.lock().map_err(|_| Error::Poisoned)?.attachment.clone();
155        if let Some(attachment) = attachment {
156            guarded(async { attachment.quiesce().await.map_err(|error| Error::Serving(Arc::new(error))) }).await?;
157        }
158        Ok(())
159    }
160
161    pub async fn retire(self: &Arc<Self>) -> Result<(), Error<P>> {
162        let table = self.table.upgrade().ok_or(Error::Phase)?;
163        {
164            let table = table.lock().map_err(|_| Error::Poisoned)?;
165            if !table.is_settled() {
166                return Err(Error::Phase);
167            }
168        }
169        let mut loan = {
170            let mut retirement = self.retirement.lock().map_err(|_| Error::Poisoned)?;
171            if retirement.finished {
172                return Ok(());
173            }
174            if retirement.active {
175                return Err(Error::Phase);
176            }
177            let state = match retirement.state.take() {
178                Some(state) => state,
179                None => self.prepare_shutdown()?,
180            };
181            retirement.active = true;
182            RetirementLoan {
183                owner: Arc::clone(self),
184                state: Some(state),
185                finished: false,
186            }
187        };
188        loan.state.as_mut().ok_or(Error::Phase)?.finalize().await?;
189        loan.finished = true;
190        Ok(())
191    }
192
193    fn state(&self, plugin: Option<P>, action: lifecycle::Action) -> State<P> {
194        State {
195            slot: Arc::clone(&self.slot),
196            table: self.table.clone(),
197            plugin,
198            config: None,
199            configuration: None,
200            work: None,
201            result: None,
202            action,
203            delivered: false,
204            failure_reported: false,
205            shutdown_attempted: false,
206            cleanup_finished: false,
207            context: None,
208            assignments: Vec::new(),
209            attachment: None,
210            generation_context: None,
211        }
212    }
213}
214
215impl<P: Plugin> Drop for RetirementLoan<P> {
216    fn drop(&mut self) {
217        let mut retirement = self.owner.retirement.lock().unwrap_or_else(|error| error.into_inner());
218        retirement.active = false;
219        retirement.finished = self.finished;
220        if !self.finished {
221            retirement.state = self.state.take();
222        }
223    }
224}
225
226impl<P: Plugin> Owner<P> {
227    fn prepare_initialize(&self, config_json: &[u8], generation_context: &[u8], assignments: &[u8]) -> Result<State<P>, Error<P>> {
228        let generation_context = if generation_context.is_empty() {
229            None
230        } else {
231            let token: [u8; 32] = generation_context.try_into().map_err(|_| Error::GenerationContext)?;
232            if token == [0; 32] {
233                return Err(Error::GenerationContext);
234            }
235            Some(token)
236        };
237        let config = crate::decode_config(config_json).map_err(|error| Error::Configuration(Arc::new(error)))?;
238        let mut slot = self.slot.lock().map_err(|_| Error::Poisoned)?;
239        if slot.phase != Phase::Empty {
240            return Err(Error::Phase);
241        }
242        slot.phase = Phase::Initializing;
243        let mut state = self.state(None, lifecycle::Action::Initialize);
244        state.config = Some(config);
245        state.generation_context = generation_context;
246        state.assignments = assignments.to_vec();
247        Ok(state)
248    }
249
250    async fn initialize(&self, state: &mut State<P>, context: &P::Context) -> Result<(), Error<P>> {
251        if state.work.is_none() && state.result.is_none() {
252            context.assign_operations(&state.assignments).map_err(|error| Error::Serving(Arc::new(error)))?;
253            let config = state.config.take().ok_or(Error::Phase)?;
254            let configuration = {
255                let table = self.table.upgrade().ok_or(Error::Phase)?;
256                let table = table.lock().map_err(|_| Error::Poisoned)?;
257                table.configuration().map_err(|error| Error::Serving(Arc::new(error)))?
258            };
259            let context = state.generation_context.map_or_else(|| context.clone(), |token| context.with_generation(token));
260            state.work = Some(Box::pin(prepare::<P>(
261                config,
262                configuration,
263                context,
264                self.contracts.clone(),
265                self.observers.clone(),
266                state.assignments.clone(),
267            )));
268        }
269        state.finish().await?;
270        state.result.as_ref().ok_or(Error::Phase)?.clone()?;
271        if state.delivered {
272            return Ok(());
273        }
274        let attachment = state.attachment.clone();
275        let table = self.table.upgrade().ok_or(Error::Phase)?;
276        let binding = {
277            let mut table = table.lock().map_err(|_| Error::Poisoned)?;
278            let configuration = state.configuration.take().ok_or(Error::Phase)?;
279            table.install_configuration(configuration).map_err(|error| Error::Serving(Arc::new(error)))
280        };
281        if let Err(error) = binding {
282            state.result = Some(Err(error.clone()));
283            return Err(error);
284        }
285        let mut slot = self.slot.lock().map_err(|_| Error::Poisoned)?;
286        slot.plugin = state.plugin.take();
287        slot.context = state.context.take();
288        slot.attachment = attachment;
289        slot.generation_context = state.generation_context;
290        slot.phase = Phase::Initialized;
291        state.delivered = true;
292        Ok(())
293    }
294
295    fn prepare_shutdown(&self) -> Result<State<P>, Error<P>> {
296        let mut slot = self.slot.lock().map_err(|_| Error::Poisoned)?;
297        if matches!(slot.phase, Phase::Empty | Phase::Closed) {
298            let mut state = self.state(None, lifecycle::Action::Shutdown);
299            state.result = Some(Ok(()));
300            return Ok(state);
301        }
302        if slot.phase != Phase::Initialized {
303            return Err(Error::Phase);
304        }
305        let plugin = slot.plugin.take().ok_or(Error::Phase)?;
306        slot.phase = Phase::Stopping;
307        let mut state = self.state(Some(plugin), lifecycle::Action::Shutdown);
308        state.attachment = slot.attachment.take();
309        state.context = slot.context.take();
310        state.generation_context = slot.generation_context;
311        Ok(state)
312    }
313}
314
315impl<P: Plugin> crate::operations::lifecycle::PluginLifecycle for Owner<P> {
316    fn begin(self: Arc<Self>, action: lifecycle::Action) -> Box<dyn crate::operations::lifecycle::LifecycleInstance> {
317        Box::new(LifecycleInvocation { owner: self, state: None, action })
318    }
319}
320impl<P: Plugin> crate::operations::lifecycle::LifecycleInstance for LifecycleInvocation<P> {
321    fn run<'a>(
322        &'a mut self, context: ExecutionContext, config: &'a [u8], generation: &'a [u8], assignments: &'a [u8],
323    ) -> crate::operations::lifecycle::Transition<'a> {
324        Box::pin(async move {
325            let result: Result<(), Error<P>> = async {
326                match self.action {
327                    lifecycle::Action::Initialize => {
328                        if self.state.is_none() {
329                            self.state = Some(self.owner.prepare_initialize(config, generation, assignments)?);
330                        }
331                        let carrier = context.resource::<P::Context>().map_err(|error| Error::Resource(Arc::new(error)))?;
332                        self.owner.initialize(self.state.as_mut().ok_or(Error::Phase)?, &carrier).await
333                    }
334                    lifecycle::Action::Shutdown => {
335                        if self.state.is_none() {
336                            self.state = Some(self.owner.prepare_shutdown()?);
337                        }
338                        let state = self.state.as_mut().ok_or(Error::Phase)?;
339                        if state.work.is_none() && state.result.is_none() {
340                            let carrier = context.resource::<P::Context>().map_err(|error| Error::Resource(Arc::new(error)))?;
341                            state.context =
342                                Some(state.generation_context.map_or_else(|| (*carrier).clone(), |token| carrier.with_generation(token)));
343                            state.start_shutdown()?;
344                        }
345                        state.finish().await?;
346                        state.result.as_ref().ok_or(Error::Phase)?.clone()
347                    }
348                }
349            }
350            .await;
351            result.map_err(Into::into)
352        })
353    }
354    fn cleanup(&mut self) -> crate::operations::lifecycle::Transition<'_> {
355        Box::pin(async {
356            match self.state.as_mut() {
357                Some(state) => state.finalize().await.map_err(Into::into),
358                None => Ok(()),
359            }
360        })
361    }
362}
363
364impl<P: Plugin> State<P> {
365    async fn finish(&mut self) -> Result<(), Error<P>> {
366        if let Some(work) = &mut self.work {
367            let completed = work.await;
368            self.shutdown_attempted = completed.shutdown_attempted;
369            self.cleanup_finished = completed.cleanup_finished;
370            self.plugin = completed.plugin;
371            if completed.context.is_some() {
372                self.context = completed.context;
373            }
374            self.result = Some(completed.result);
375            if completed.configuration.is_some() {
376                self.configuration = completed.configuration;
377            }
378            if completed.attachment.is_some() {
379                self.attachment = completed.attachment;
380            }
381            self.work = None;
382        }
383        Ok(())
384    }
385
386    fn start_shutdown(&mut self) -> Result<(), Error<P>> {
387        let attachment = self.attachment.clone().or_else(|| self.plugin.as_ref().and_then(Plugin::attachment));
388        let mut plugin = self.plugin.take().ok_or(Error::Phase)?;
389        let table = self.table.clone();
390        let context = self.context.clone();
391        let mut shutdown_attempted = self.shutdown_attempted;
392        let mut cleanup_finished = self.cleanup_finished;
393        self.result = None;
394        self.work = Some(Box::pin(async move {
395            let result = guarded(async {
396                if let Some(attachment) = attachment {
397                    attachment.quiesce().await.map_err(|error| Error::Serving(Arc::new(error)))?;
398                }
399                let table = table.upgrade().ok_or(Error::Phase)?;
400                let settlement = {
401                    let mut table = table.lock().map_err(|_| Error::Poisoned)?;
402                    table.close_ordinary()
403                };
404                settlement.await.map_err(|error| Error::Serving(Arc::new(error)))?;
405                let retired = {
406                    let mut table = table.lock().map_err(|_| Error::Poisoned)?;
407                    table.release_ordinary().map_err(|error| Error::Serving(Arc::new(error)))?
408                };
409                drop(retired);
410                let mut errors = Vec::new();
411                if !shutdown_attempted && let Some(context) = &context {
412                    // Domain Effects Are Never Replayed By Physical Cleanup Retries.
413                    shutdown_attempted = true;
414                    if let Err(error) = guarded(async { plugin.stop(context).await.map_err(|error| Error::Plugin(Arc::new(error))) }).await
415                    {
416                        errors.push(error);
417                    }
418                }
419                if !cleanup_finished {
420                    match guarded(async { plugin.cleanup().await.map_err(|error| Error::Plugin(Arc::new(error))) }).await {
421                        Ok(()) => cleanup_finished = true,
422                        Err(error) => errors.push(error),
423                    }
424                }
425                combine(errors)
426            })
427            .await;
428            Completed {
429                plugin: Some(plugin),
430                context: None,
431                configuration: None,
432                attachment: None,
433                result,
434                shutdown_attempted,
435                cleanup_finished,
436            }
437        }));
438        Ok(())
439    }
440
441    async fn finalize(&mut self) -> Result<(), Error<P>> {
442        if self.delivered {
443            return Ok(());
444        }
445        if self.action == lifecycle::Action::Initialize {
446            self.finish().await?;
447            self.action = lifecycle::Action::Shutdown;
448            self.result = None;
449        }
450        if self.plugin.is_none() {
451            self.configuration.take();
452            self.slot.lock().map_err(|_| Error::Poisoned)?.phase = Phase::Closed;
453            return Ok(());
454        }
455        if let Some(Err(error)) = &self.result {
456            if !self.failure_reported {
457                self.failure_reported = true;
458                return Err(error.clone());
459            }
460            self.failure_reported = false;
461            self.start_shutdown()?;
462        } else if self.work.is_none() && self.result.is_none() {
463            self.start_shutdown()?;
464        }
465        self.finish().await?;
466        if let Some(Err(error)) = &self.result {
467            self.failure_reported = true;
468            return Err(error.clone());
469        }
470        self.plugin.take();
471        self.slot.lock().map_err(|_| Error::Poisoned)?.phase = Phase::Closed;
472        Ok(())
473    }
474}
475
476/* === Public Functions === */
477
478/* === Private Functions === */
479
480async fn prepare<P: Plugin>(
481    config: P::Config, configuration: WireTable, context: P::Context, contracts: Arc<[Contract]>,
482    observers: Arc<[crate::inventory::observers::Declaration]>, assignments: Vec<u8>,
483) -> Completed<P> {
484    let mut setup = Setup::<P>::new(configuration);
485    for catalog in P::CATALOGS {
486        setup.settings().catalog(catalog);
487    }
488    let prepared = guarded::<P, _>(async { P::prepare(config, &mut setup).await.map_err(|error| Error::Plugin(Arc::new(error))) }).await;
489    let mut errors = Vec::new();
490    let mut plugin = match prepared {
491        Ok(plugin) => Some(plugin),
492        Err(source) => {
493            errors.push(StartupError::Runtime { source: Box::new(source) });
494            None
495        }
496    };
497
498    let mut attachment = None;
499    let binding = guarded::<P, _>(async {
500        if let Some(plugin) = &plugin {
501            attachment = plugin.attachment();
502            let actual = attachment.as_ref().map_or_else(Vec::new, |attachment| attachment.observers());
503            if actual.as_slice() != observers.as_ref() {
504                return Err(Error::Serving(Arc::new(ServingError::Catalog)));
505            }
506            if let Some(attachment) = &attachment {
507                attachment.clone().bind(setup.settings()).map_err(|error| Error::Serving(Arc::new(error)))?;
508            }
509        }
510        Ok(())
511    })
512    .await;
513    if let Err(source) = binding {
514        errors.push(StartupError::Runtime { source: Box::new(source) });
515    }
516    if let Err(error) = setup.validate() {
517        match error {
518            StartupError::Multiple { errors: configuration } => errors.extend(configuration),
519            error => errors.push(error),
520        }
521    }
522    let configuration = setup.into_configuration();
523    if errors.is_empty() {
524        if let Err(source) = configuration.validate_contracts(&contracts) {
525            errors.push(StartupError::Binding { source: Box::new(source) });
526        }
527        if let Err(source) = configuration.validate_callees(&assignments) {
528            errors.push(StartupError::Binding { source: Box::new(source) });
529        }
530    }
531    let result = match StartupError::aggregate(errors) {
532        Some(error) => Err(Error::Serving(Arc::new(ServingError::Startup(Box::new(error))))),
533        None => {
534            let plugin = plugin.as_mut().expect("successful preparation retains the plugin owner");
535            guarded(async { plugin.initialize(&context).await.map_err(|error| Error::Plugin(Arc::new(error))) }).await
536        }
537    };
538    let context = result.is_ok().then_some(context);
539    Completed {
540        plugin,
541        context,
542        configuration: Some(configuration),
543        attachment,
544        result,
545        shutdown_attempted: false,
546        cleanup_finished: false,
547    }
548}
549
550fn combine<P: Plugin>(mut errors: Vec<Error<P>>) -> Result<(), Error<P>> {
551    if errors.len() == 1 {
552        return Err(errors.pop().expect("one lifecycle error"));
553    }
554    let errors = errors.into_iter().map(|source| StartupError::Runtime { source: Box::new(source) }).collect();
555    match StartupError::aggregate(errors) {
556        None => Ok(()),
557        Some(error) => Err(Error::Serving(Arc::new(ServingError::Startup(Box::new(error))))),
558    }
559}
560
561// Guard Only The Borrowed Machinery Future; The Outer Future Retains P
562async fn guarded<P: Plugin, T>(future: impl Future<Output = Result<T, Error<P>>>) -> Result<T, Error<P>> {
563    let mut future = std::pin::pin!(future);
564    std::future::poll_fn(|context| match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| future.as_mut().poll(context))) {
565        Ok(result) => result,
566        Err(_) => std::task::Poll::Ready(Err(Error::Panicked)),
567    })
568    .await
569}
570
571/* === Tests === */
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::Message;
577    use crate::operations::{OperationCatalog, RegistrationBatch, WireDeclaration};
578
579    struct Events(Arc<Mutex<Vec<&'static str>>>);
580    struct ServedCatalog;
581
582    static SERVED: [crate::catalog::Node<WireDeclaration>; 1] = [crate::catalog::Node::Leaf(WireDeclaration::of::<Serve>())];
583
584    impl OperationCatalog for ServedCatalog {
585        const CATALOG: crate::operations::Catalog = crate::operations::Catalog::of::<Self>("ServedCatalog", &SERVED, &[]);
586
587        fn batch() -> RegistrationBatch {
588            RegistrationBatch::new().operation::<Serve>()
589        }
590    }
591
592    #[crate::operation(harmos_path = crate, resources = [Events, u32], id = "serve", response = u32,
593        policy = <TestPlugin as lifecycle::Policy>::INITIALIZATION_POLICY)]
594    struct Serve;
595
596    impl crate::Operation for Serve {
597        type Error = std::convert::Infallible;
598
599        async fn run(self, context: crate::Context<'_, Self>) -> Result<crate::Response<u32>, Self::Error> {
600            context.resource::<Events>().0.lock().unwrap().push("serve");
601            Ok(crate::Response::from_owner(context.resource::<u32>()))
602        }
603    }
604
605    fn contracts() -> Vec<Contract> {
606        vec![WireDeclaration::of::<Serve>().contract().unwrap()]
607    }
608
609    #[derive(Default, serde::Deserialize)]
610    #[serde(default)]
611    struct Config {
612        #[serde(skip)]
613        events: Arc<Mutex<Vec<&'static str>>>,
614
615        value: u32,
616        duplicate: bool,
617        fail_prepare: bool,
618        fail_stop: bool,
619        fail_cleanup: bool,
620    }
621
622    struct TestPlugin(Config);
623
624    impl lifecycle::Policy for TestPlugin {
625        const INITIALIZATION_POLICY: crate::OperationPolicy = crate::OperationPolicy {
626            timeout: std::time::Duration::from_secs(1),
627            attempt_timeout: std::time::Duration::from_secs(1),
628            retries: crate::RetryPolicy::Never,
629        };
630        const SHUTDOWN_POLICY: crate::OperationPolicy = Self::INITIALIZATION_POLICY;
631    }
632
633    impl crate::Lifecycle for TestPlugin {
634        type Config = Config;
635        type Error = std::io::Error;
636        type Context = ();
637
638        async fn prepare(config: Config, setup: &mut Setup<Self>) -> Result<Self, Self::Error> {
639            config.events.lock().unwrap().push("prepare");
640            setup.resource(Events(config.events.clone()));
641            setup.resource(config.value);
642            if config.duplicate {
643                setup.resource(config.value + 1);
644            }
645            if config.fail_prepare {
646                return Err(std::io::Error::other("prepare failure"));
647            }
648            Ok(Self(config))
649        }
650
651        async fn initialize(&mut self, _: &()) -> Result<(), Self::Error> {
652            self.0.events.lock().unwrap().push("initialize");
653            Ok(())
654        }
655
656        async fn stop(&mut self, _: &()) -> Result<(), Self::Error> {
657            self.0.events.lock().unwrap().push("stop");
658            if self.0.fail_stop {
659                return Err(std::io::Error::other("stop failure"));
660            }
661            Ok(())
662        }
663
664        async fn cleanup(&mut self) -> Result<(), Self::Error> {
665            self.0.events.lock().unwrap().push("cleanup");
666            if self.0.fail_cleanup {
667                return Err(std::io::Error::other("cleanup failure"));
668            }
669            Ok(())
670        }
671    }
672
673    impl Plugin for TestPlugin {
674        const CATALOGS: &'static [crate::operations::Catalog] = &[ServedCatalog::CATALOG];
675    }
676
677    fn ready<T>(future: impl Future<Output = T>) -> T {
678        let mut future = std::pin::pin!(future);
679        let mut context = std::task::Context::from_waker(std::task::Waker::noop());
680        match future.as_mut().poll(&mut context) {
681            std::task::Poll::Ready(value) => value,
682            std::task::Poll::Pending => panic!("the lifecycle fixture completes synchronously"),
683        }
684    }
685
686    fn prepared(config: Config) -> Completed<TestPlugin> {
687        ready(prepare::<TestPlugin>(config, WireTable::new("fixture").unwrap(), (), contracts().into(), Arc::from([]), Vec::new()))
688    }
689
690    #[test]
691    fn validates_before_initialize() {
692        let events = Arc::default();
693        let valid = prepared(Config {
694            events: Arc::clone(&events),
695            ..Default::default()
696        });
697        assert!(valid.result.is_ok());
698        assert_eq!(*events.lock().unwrap(), ["prepare", "initialize"]);
699
700        events.lock().unwrap().clear();
701        let invalid = prepared(Config {
702            events: Arc::clone(&events),
703            duplicate: true,
704            ..Default::default()
705        });
706        assert!(invalid.result.is_err());
707        assert!(invalid.plugin.is_some());
708        assert_eq!(*events.lock().unwrap(), ["prepare"]);
709    }
710
711    #[test]
712    fn retained_owner_lifecycle() {
713        let (owner, table) = Owner::<TestPlugin>::new("fixture", contracts(), NonZeroUsize::MIN).unwrap();
714        let mut state = owner.prepare_initialize(b"{}", &[], &[]).unwrap();
715        ready(owner.initialize(&mut state, &())).unwrap();
716        let events = {
717            let slot = owner.slot.lock().unwrap();
718            assert!(slot.context.is_some());
719            slot.plugin.as_ref().unwrap().0.events.clone()
720        };
721        assert_eq!(*events.lock().unwrap(), ["prepare", "initialize"]);
722
723        {
724            let mut table = table.lock().unwrap();
725            let context = table.context();
726            table.prepare(1, "fixture.serve", Serve::definition().fingerprint.0, 1, Serve.encode()).unwrap();
727            ready(table.bind(1, &context)).unwrap();
728            assert_eq!(ready(table.attempt(1, &context)).unwrap(), 0u32.encode());
729            ready(table.finalize(1)).unwrap();
730            ready(table.settle()).unwrap();
731        }
732        ready(owner.retire()).unwrap();
733        assert_eq!(*events.lock().unwrap(), ["prepare", "initialize", "serve", "stop", "cleanup"]);
734    }
735
736    #[test]
737    fn failed_prepare_aggregates() {
738        let completed = prepared(Config {
739            fail_prepare: true,
740            duplicate: true,
741            ..Default::default()
742        });
743        let Err(Error::Serving(error)) = completed.result else {
744            panic!("preparation and setup failures must be reported");
745        };
746        let ServingError::Startup(error) = error.as_ref() else {
747            panic!("setup failures retain their typed source");
748        };
749        let StartupError::Multiple { errors } = error.as_ref() else {
750            panic!("both independent failures must be retained");
751        };
752        assert_eq!(errors.len(), 2);
753        assert!(errors.iter().any(|error| matches!(error, StartupError::DuplicateResource { .. })));
754        assert!(completed.plugin.is_none());
755    }
756
757    #[test]
758    fn setup_replaces_resource() {
759        let mut setup = Setup::<TestPlugin>::new(WireTable::new("fixture").unwrap());
760        let _plugin = ready(<TestPlugin as crate::Lifecycle>::prepare(Config { value: 1, ..Default::default() }, &mut setup)).unwrap();
761        let replacement = Arc::new(2u32);
762        setup.overrides(|setup| {
763            setup.resource::<u32>(replacement.clone());
764        });
765        setup.validate().unwrap();
766        let actual = setup.settings().context().resource::<u32>().unwrap();
767        assert!(Arc::ptr_eq(&replacement, &actual));
768    }
769
770    #[test]
771    fn stop_failure_cleanup() {
772        let events = Arc::default();
773        let plugin = TestPlugin(Config {
774            events: Arc::clone(&events),
775            fail_stop: true,
776            ..Default::default()
777        });
778        let (owner, _table) = Owner::new("fixture", Vec::new(), NonZeroUsize::MIN).unwrap();
779        let mut state = owner.state(Some(plugin), lifecycle::Action::Shutdown);
780        state.context = Some(());
781        state.start_shutdown().unwrap();
782        ready(state.finish()).unwrap();
783        assert!(state.result.as_ref().unwrap().is_err());
784        assert_eq!(*events.lock().unwrap(), ["stop", "cleanup"]);
785
786        assert!(ready(state.finalize()).is_err());
787        ready(state.finalize()).unwrap();
788        assert_eq!(*events.lock().unwrap(), ["stop", "cleanup"]);
789    }
790
791    #[test]
792    fn retirement_reports_failures() {
793        let plugin = TestPlugin(Config {
794            fail_stop: true,
795            fail_cleanup: true,
796            ..Default::default()
797        });
798        let (owner, _table) = Owner::new("fixture", Vec::new(), NonZeroUsize::MIN).unwrap();
799        let mut state = owner.state(Some(plugin), lifecycle::Action::Shutdown);
800        state.context = Some(());
801        state.start_shutdown().unwrap();
802        ready(state.finish()).unwrap();
803
804        let Err(Error::Serving(error)) = state.result.take().unwrap() else {
805            panic!("both lifecycle failures must remain available");
806        };
807        let ServingError::Startup(error) = error.as_ref() else {
808            panic!("lifecycle errors retain an aggregate source");
809        };
810        let StartupError::Multiple { errors } = error.as_ref() else {
811            panic!("both lifecycle failures must remain available");
812        };
813        assert_eq!(errors.len(), 2);
814    }
815}