1use async_trait::async_trait;
2use parking_lot::Mutex;
3use std::sync::{
4 Arc,
5 atomic::{AtomicBool, AtomicU8, Ordering},
6};
7use std::time::Duration;
8use tokio::sync::{Notify, oneshot};
9use tokio::task::JoinHandle;
10use tokio_util::sync::CancellationToken;
11
12type LcResult<T = ()> = std::result::Result<T, LifecycleError>;
16
17type TaskResult<T = ()> = anyhow::Result<T>;
19
20type ReadyFn<T> = fn(
22 Arc<T>,
23 CancellationToken,
24 ReadySignal,
25)
26 -> std::pin::Pin<Box<dyn std::future::Future<Output = TaskResult<()>> + Send>>;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[repr(u8)]
33pub enum Status {
34 Stopped,
35 Starting,
36 Running,
37 Stopping,
38}
39
40impl Status {
41 #[inline]
42 #[must_use]
43 pub const fn as_u8(self) -> u8 {
44 match self {
45 Status::Stopped => 0,
46 Status::Starting => 1,
47 Status::Running => 2,
48 Status::Stopping => 3,
49 }
50 }
51 #[inline]
52 #[must_use]
53 pub const fn from_u8(x: u8) -> Self {
54 match x {
55 1 => Status::Starting,
56 2 => Status::Running,
57 3 => Status::Stopping,
58 _ => Status::Stopped,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum StopReason {
66 Finished,
67 Cancelled,
68 Timeout,
69}
70
71pub struct ReadySignal(oneshot::Sender<()>);
75
76impl ReadySignal {
77 #[inline]
78 pub fn notify(self) {
79 if self.0.send(()).is_err() {
80 tracing::debug!("ReadySignal::notify: receiver already dropped");
81 }
82 }
83 #[inline]
85 #[must_use]
86 pub fn from_sender(sender: tokio::sync::oneshot::Sender<()>) -> Self {
87 ReadySignal(sender)
88 }
89}
90
91#[async_trait]
96pub trait Runnable: Send + Sync + 'static {
97 async fn run(self: Arc<Self>, cancel: CancellationToken) -> TaskResult<()>;
99}
100
101#[derive(Debug, thiserror::Error)]
105pub enum LifecycleError {
106 #[error("already started")]
107 AlreadyStarted,
108}
109
110pub struct Lifecycle {
119 name: &'static str,
120 status: Arc<AtomicU8>,
121 handle: Mutex<Option<JoinHandle<()>>>,
122 cancel: Mutex<Option<CancellationToken>>,
123 finished: Arc<AtomicBool>,
125 was_cancelled: Arc<AtomicBool>,
127 finished_notify: Arc<Notify>,
129}
130
131impl Lifecycle {
132 #[must_use]
133 pub fn new_named(name: &'static str) -> Self {
134 Self {
135 name,
136 status: Arc::new(AtomicU8::new(Status::Stopped.as_u8())),
137 handle: Mutex::new(None),
138 cancel: Mutex::new(None),
139 finished: Arc::new(AtomicBool::new(false)),
140 was_cancelled: Arc::new(AtomicBool::new(false)),
141 finished_notify: Arc::new(Notify::new()),
142 }
143 }
144
145 #[must_use]
146 pub fn new() -> Self {
147 Self::new_named("lifecycle")
148 }
149
150 #[inline]
151 pub fn name(&self) -> &'static str {
152 self.name
153 }
154
155 #[inline]
158 fn load_status(&self) -> Status {
159 Status::from_u8(self.status.load(Ordering::Acquire))
160 }
161
162 #[inline]
163 fn store_status(&self, s: Status) {
164 self.status.store(s.as_u8(), Ordering::Release);
165 }
166
167 #[tracing::instrument(skip(self, make), level = "debug")]
177 pub fn start<F, Fut>(&self, make: F) -> LcResult
178 where
179 F: FnOnce(CancellationToken) -> Fut + Send + 'static,
180 Fut: std::future::Future<Output = TaskResult<()>> + Send + 'static,
181 {
182 self.start_core(CancellationToken::new(), move |tok, _| make(tok), false)
183 }
184
185 #[tracing::instrument(skip(self, make, token), level = "debug")]
190 pub fn start_with_token<F, Fut>(&self, token: CancellationToken, make: F) -> LcResult
191 where
192 F: FnOnce(CancellationToken) -> Fut + Send + 'static,
193 Fut: std::future::Future<Output = TaskResult<()>> + Send + 'static,
194 {
195 self.start_core(token, move |tok, _| make(tok), false)
196 }
197
198 #[tracing::instrument(skip(self, make), level = "debug")]
203 pub fn start_with_ready<F, Fut>(&self, make: F) -> LcResult
204 where
205 F: FnOnce(CancellationToken, ReadySignal) -> Fut + Send + 'static,
206 Fut: std::future::Future<Output = TaskResult<()>> + Send + 'static,
207 {
208 self.start_core(
209 CancellationToken::new(),
210 move |tok, rdy| async move {
211 let Some(rdy) = rdy else {
212 return Err(anyhow::anyhow!("ReadySignal must be present"));
213 };
214 make(tok, rdy).await
215 },
216 true,
217 )
218 }
219
220 #[tracing::instrument(skip(self, make, token), level = "debug")]
225 pub fn start_with_ready_and_token<F, Fut>(&self, token: CancellationToken, make: F) -> LcResult
226 where
227 F: FnOnce(CancellationToken, ReadySignal) -> Fut + Send + 'static,
228 Fut: std::future::Future<Output = TaskResult<()>> + Send + 'static,
229 {
230 self.start_core(
231 token,
232 move |tok, rdy| async move {
233 let Some(rdy) = rdy else {
234 return Err(anyhow::anyhow!("ReadySignal must be present"));
235 };
236 make(tok, rdy).await
237 },
238 true,
239 )
240 }
241
242 fn start_core<F, Fut>(&self, token: CancellationToken, make: F, ready_mode: bool) -> LcResult
247 where
248 F: Send + 'static + FnOnce(CancellationToken, Option<ReadySignal>) -> Fut,
249 Fut: std::future::Future<Output = TaskResult<()>> + Send + 'static,
250 {
251 let cas_ok = self
253 .status
254 .compare_exchange(
255 Status::Stopped.as_u8(),
256 Status::Starting.as_u8(),
257 Ordering::AcqRel,
258 Ordering::Acquire,
259 )
260 .is_ok();
261 if !cas_ok {
262 return Err(LifecycleError::AlreadyStarted);
263 }
264
265 self.finished.store(false, Ordering::Release);
266 self.was_cancelled.store(false, Ordering::Release);
267
268 {
270 let mut c = self.cancel.lock();
271 *c = Some(token.clone());
272 }
273
274 let (ready_tx, ready_rx) = oneshot::channel::<()>();
277 if ready_mode {
278 let status_on_ready = self.status.clone();
279 tokio::spawn(async move {
280 if ready_rx.await.is_ok() {
281 _ = status_on_ready.compare_exchange(
282 Status::Starting.as_u8(),
283 Status::Running.as_u8(),
284 Ordering::AcqRel,
285 Ordering::Acquire,
286 );
287 tracing::debug!("lifecycle status -> running (ready)");
288 } else {
289 tracing::debug!("ready signal dropped; staying in Starting until finish");
292 }
293 });
294 } else {
295 self.store_status(Status::Running);
296 tracing::debug!("lifecycle status -> running");
297 }
298
299 let finished_flag = self.finished.clone();
300 let finished_notify = self.finished_notify.clone();
301 let status_on_finish = self.status.clone();
302
303 let gear_name = self.name;
305 let task_id = format!("{gear_name}-{self:p}");
306 let handle = tokio::spawn(async move {
307 tracing::debug!(task_id = %task_id, gear = %gear_name, "lifecycle task starting");
308 let res = make(token, ready_mode.then(|| ReadySignal(ready_tx))).await;
309 if let Err(e) = res {
310 tracing::error!(error=%e, task_id=%task_id, gear = %gear_name, "lifecycle task error");
311 }
312 finished_flag.store(true, Ordering::Release);
313 finished_notify.notify_waiters();
314 status_on_finish.store(Status::Stopped.as_u8(), Ordering::Release);
315 tracing::debug!(task_id=%task_id, gear = %gear_name, "lifecycle task finished");
316 });
317
318 {
320 let mut h = self.handle.lock();
321 *h = Some(handle);
322 }
323
324 Ok(())
325 }
326
327 #[tracing::instrument(skip(self, timeout), level = "debug")]
332 pub async fn stop(&self, timeout: Duration) -> LcResult<StopReason> {
333 let gear_name = self.name;
334 let task_id = format!("{gear_name}-{self:p}");
335 let st = self.load_status();
336 if !matches!(st, Status::Starting | Status::Running | Status::Stopping) {
337 return Ok(StopReason::Finished);
339 }
340
341 self.store_status(Status::Stopping);
342
343 if let Some(tok) = { self.cancel.lock().take() } {
345 self.was_cancelled.store(true, Ordering::Release);
346 tok.cancel();
347 }
348
349 let finished_flag = self.finished.clone();
351 let notify = self.finished_notify.clone();
352 let finished_wait = async move {
353 if finished_flag.load(Ordering::Acquire) {
354 return;
355 }
356 notify.notified().await;
357 };
358
359 let reason = tokio::select! {
360 () = finished_wait => {
361 if self.was_cancelled.load(Ordering::Acquire) {
362 StopReason::Cancelled
363 } else {
364 StopReason::Finished
365 }
366 }
367 () = tokio::time::sleep(timeout) => StopReason::Timeout,
368 };
369
370 let handle_opt = { self.handle.lock().take() };
372 if let Some(handle) = handle_opt {
373 if matches!(reason, StopReason::Timeout) && !handle.is_finished() {
374 tracing::warn!("lifecycle stop timed out; aborting task");
375 handle.abort();
376 }
377
378 match handle.await {
379 Ok(()) => {
380 tracing::debug!(task_id = %task_id, gear = %gear_name, "lifecycle task completed successfully");
381 }
382 Err(e) if e.is_cancelled() => {
383 tracing::debug!(task_id = %task_id, gear = %gear_name, "lifecycle task was cancelled/aborted");
384 }
385 Err(e) if e.is_panic() => {
386 match e.try_into_panic() {
388 Ok(panic_payload) => {
389 let panic_msg = panic_payload
390 .downcast_ref::<&str>()
391 .copied()
392 .map(str::to_owned)
393 .or_else(|| panic_payload.downcast_ref::<String>().cloned())
394 .unwrap_or_else(|| "unknown panic".to_owned());
395
396 tracing::error!(
397 task_id = %task_id,
398 gear = %gear_name,
399 panic_message = %panic_msg,
400 "lifecycle task panicked - this indicates a serious bug"
401 );
402 }
403 _ => {
404 tracing::error!(
405 task_id = %task_id,
406 gear = %gear_name,
407 "lifecycle task panicked (could not extract panic message)"
408 );
409 }
410 }
411 }
412 Err(e) => {
413 tracing::warn!(task_id = %task_id, gear = %gear_name, error = %e, "lifecycle task join error");
414 }
415 }
416
417 self.finished.store(true, Ordering::Release);
418 self.finished_notify.notify_waiters();
419 }
420
421 self.store_status(Status::Stopped);
422 tracing::info!(?reason, "lifecycle stopped");
423 Ok(reason)
424 }
425
426 #[inline]
428 #[must_use]
429 pub fn status(&self) -> Status {
430 self.load_status()
431 }
432
433 #[inline]
435 pub fn is_running(&self) -> bool {
436 matches!(self.status(), Status::Starting | Status::Running)
437 }
438
439 #[inline]
441 #[must_use]
442 pub fn try_start<F, Fut>(&self, make: F) -> bool
443 where
444 F: FnOnce(CancellationToken) -> Fut + Send + 'static,
445 Fut: std::future::Future<Output = TaskResult<()>> + Send + 'static,
446 {
447 self.start(make).is_ok()
448 }
449
450 pub async fn wait_stopped(&self) {
452 if self.finished.load(Ordering::Acquire) {
453 return;
454 }
455 self.finished_notify.notified().await;
456 }
457}
458
459impl Default for Lifecycle {
460 fn default() -> Self {
461 Self::new()
462 }
463}
464
465impl Drop for Lifecycle {
466 fn drop(&mut self) {
468 if let Some(tok) = self.cancel.get_mut().take() {
469 tok.cancel();
470 }
471 if let Some(handle) = self.handle.get_mut().take() {
472 handle.abort();
473 }
474 }
475}
476
477#[must_use]
481pub struct WithLifecycle<T: Runnable> {
482 inner: Arc<T>,
483 lc: Arc<Lifecycle>,
484 pub(crate) stop_timeout: Duration,
486 stop_timeout_fn: Option<fn(&T) -> Duration>,
487 await_ready: bool,
489 has_ready_handler: bool,
490 run_ready_fn: Option<ReadyFn<T>>,
491}
492
493impl<T: Runnable> WithLifecycle<T> {
494 pub fn new(inner: T) -> Self {
495 Self {
496 inner: Arc::new(inner),
497 lc: Arc::new(Lifecycle::new_named(std::any::type_name::<T>())),
498 stop_timeout: Duration::from_secs(30),
499 stop_timeout_fn: None,
500 await_ready: false,
501 has_ready_handler: false,
502 run_ready_fn: None,
503 }
504 }
505
506 pub fn from_arc(inner: Arc<T>) -> Self {
507 Self {
508 inner,
509 lc: Arc::new(Lifecycle::new_named(std::any::type_name::<T>())),
510 stop_timeout: Duration::from_secs(30),
511 stop_timeout_fn: None,
512 await_ready: false,
513 has_ready_handler: false,
514 run_ready_fn: None,
515 }
516 }
517
518 pub fn new_with_name(inner: T, name: &'static str) -> Self {
519 Self {
520 inner: Arc::new(inner),
521 lc: Arc::new(Lifecycle::new_named(name)),
522 stop_timeout: Duration::from_secs(30),
523 stop_timeout_fn: None,
524 await_ready: false,
525 has_ready_handler: false,
526 run_ready_fn: None,
527 }
528 }
529
530 pub fn from_arc_with_name(inner: Arc<T>, name: &'static str) -> Self {
531 Self {
532 inner,
533 lc: Arc::new(Lifecycle::new_named(name)),
534 stop_timeout: Duration::from_secs(30),
535 stop_timeout_fn: None,
536 await_ready: false,
537 has_ready_handler: false,
538 run_ready_fn: None,
539 }
540 }
541
542 pub fn with_stop_timeout(mut self, d: Duration) -> Self {
560 self.stop_timeout = d;
561 self.stop_timeout_fn = None;
562 self
563 }
564
565 pub fn with_stop_timeout_fn(mut self, provider: fn(&T) -> Duration) -> Self {
568 self.stop_timeout_fn = Some(provider);
569 self
570 }
571
572 #[inline]
573 #[must_use]
574 pub fn status(&self) -> Status {
575 self.lc.status()
576 }
577
578 #[inline]
579 #[must_use]
580 pub fn inner(&self) -> &T {
581 self.inner.as_ref()
582 }
583
584 #[inline]
586 #[must_use]
587 pub fn inner_arc(&self) -> Arc<T> {
588 self.inner.clone()
589 }
590
591 pub fn with_ready_mode(
593 mut self,
594 await_ready: bool,
595 has_ready_handler: bool,
596 run_ready_fn: Option<ReadyFn<T>>,
597 ) -> Self {
598 self.await_ready = await_ready;
599 self.has_ready_handler = has_ready_handler;
600 self.run_ready_fn = run_ready_fn;
601 self
602 }
603}
604
605impl<T: Runnable + Default> Default for WithLifecycle<T> {
606 fn default() -> Self {
607 Self::new(T::default())
608 }
609}
610
611#[async_trait]
612impl<T: Runnable> crate::contracts::RunnableCapability for WithLifecycle<T> {
613 #[tracing::instrument(skip(self, external_cancel), level = "debug")]
614 async fn start(&self, external_cancel: CancellationToken) -> TaskResult<()> {
615 let inner = self.inner.clone();
616 let composed = external_cancel.child_token();
617
618 if !self.await_ready {
619 self.lc
620 .start_with_token(composed, move |cancel| inner.run(cancel))
621 .map_err(anyhow::Error::from)
622 } else if self.has_ready_handler {
623 let f = self.run_ready_fn.ok_or_else(|| {
624 anyhow::anyhow!("run_ready_fn must be set when has_ready_handler")
625 })?;
626 self.lc
627 .start_with_ready_and_token(composed, move |cancel, ready| f(inner, cancel, ready))
628 .map_err(anyhow::Error::from)
629 } else {
630 self.lc
631 .start_with_ready_and_token(composed, move |cancel, ready| async move {
632 ready.notify();
634 inner.run(cancel).await
635 })
636 .map_err(anyhow::Error::from)
637 }
638 }
639
640 #[tracing::instrument(skip(self, deadline_token), level = "debug")]
650 async fn stop(&self, deadline_token: CancellationToken) -> TaskResult<()> {
651 let timeout = self
652 .stop_timeout_fn
653 .map_or(self.stop_timeout, |provider| provider(&self.inner));
654 tokio::select! {
655 res = self.lc.stop(timeout) => {
656 _ = res.map_err(anyhow::Error::from)?;
657 Ok(())
658 }
659 () = deadline_token.cancelled() => {
660 tracing::debug!("Hard-stop deadline reached, aborting lifecycle");
662 _ = self.lc.stop(Duration::from_millis(0)).await?;
663 Ok(())
664 }
665 }
666 }
667}
668
669impl<T: Runnable> Drop for WithLifecycle<T> {
670 fn drop(&mut self) {
672 if Arc::strong_count(&self.lc) == 1 {
673 if let Some(tok) = self.lc.cancel.lock().as_ref() {
674 tok.cancel();
675 }
676 if let Some(handle) = self.lc.handle.lock().as_ref() {
677 handle.abort();
678 }
679 }
680 }
681}
682
683#[cfg(test)]
686#[cfg_attr(coverage_nightly, coverage(off))]
687mod tests {
688 use super::*;
689 use std::sync::atomic::{AtomicU32, Ordering as AOrd};
690 use tokio::time::{Duration, sleep};
691
692 struct TestRunnable {
693 counter: AtomicU32,
694 }
695
696 impl TestRunnable {
697 fn new() -> Self {
698 Self {
699 counter: AtomicU32::new(0),
700 }
701 }
702 fn count(&self) -> u32 {
703 self.counter.load(AOrd::Relaxed)
704 }
705 }
706
707 #[async_trait::async_trait]
708 impl Runnable for TestRunnable {
709 async fn run(self: Arc<Self>, cancel: CancellationToken) -> TaskResult<()> {
710 let mut interval = tokio::time::interval(Duration::from_millis(10));
711 loop {
712 tokio::select! {
713 _ = interval.tick() => { self.counter.fetch_add(1, AOrd::Relaxed); }
714 () = cancel.cancelled() => break,
715 }
716 }
717 Ok(())
718 }
719 }
720
721 #[tokio::test]
722 async fn lifecycle_basic() {
723 let lc = Arc::new(Lifecycle::new());
724 assert_eq!(lc.status(), Status::Stopped);
725
726 let result = lc.start(|cancel| async move {
727 cancel.cancelled().await;
728 Ok(())
729 });
730 assert!(result.is_ok());
731
732 let stop_result = lc.stop(Duration::from_millis(100)).await;
733 assert!(stop_result.is_ok());
734 assert_eq!(lc.status(), Status::Stopped);
735 }
736
737 #[tokio::test]
738 async fn with_lifecycle_wrapper_basics() {
739 let runnable = TestRunnable::new();
740 let wrapper = WithLifecycle::new(runnable);
741
742 assert_eq!(wrapper.status(), Status::Stopped);
743 assert_eq!(wrapper.inner().count(), 0);
744
745 let wrapper = wrapper.with_stop_timeout(Duration::from_mins(1));
746 assert_eq!(wrapper.stop_timeout.as_secs(), 60);
747 }
748
749 #[tokio::test]
750 async fn start_sets_running_immediately() {
751 let lc = Lifecycle::new();
752 lc.start(|cancel| async move {
753 cancel.cancelled().await;
754 Ok(())
755 })
756 .unwrap();
757
758 let s = lc.status();
759 assert!(matches!(s, Status::Running | Status::Starting));
760
761 let _ = lc.stop(Duration::from_millis(50)).await.unwrap();
762 assert_eq!(lc.status(), Status::Stopped);
763 }
764
765 #[tokio::test]
766 async fn start_with_ready_transitions_and_stop() {
767 let lc = Lifecycle::new();
768
769 let (ready_tx, ready_rx) = oneshot::channel::<()>();
770 lc.start_with_ready(move |cancel, ready| async move {
771 _ = ready_rx.await;
772 ready.notify();
773 cancel.cancelled().await;
774 Ok(())
775 })
776 .unwrap();
777
778 assert_eq!(lc.status(), Status::Starting);
779
780 _ = ready_tx.send(());
781 sleep(Duration::from_millis(10)).await;
782 assert_eq!(lc.status(), Status::Running);
783
784 let reason = lc.stop(Duration::from_millis(100)).await.unwrap();
785 assert!(matches!(
786 reason,
787 StopReason::Cancelled | StopReason::Finished
788 ));
789 assert_eq!(lc.status(), Status::Stopped);
790 }
791
792 #[tokio::test]
793 async fn stop_while_starting_before_ready() {
794 let lc = Lifecycle::new();
795
796 lc.start_with_ready(move |cancel, _ready| async move {
797 cancel.cancelled().await;
798 Ok(())
799 })
800 .unwrap();
801
802 assert_eq!(lc.status(), Status::Starting);
803
804 let reason = lc.stop(Duration::from_millis(100)).await.unwrap();
805 assert!(matches!(
806 reason,
807 StopReason::Cancelled | StopReason::Finished
808 ));
809 assert_eq!(lc.status(), Status::Stopped);
810 }
811
812 #[tokio::test]
813 async fn timeout_path_aborts_and_notifies() {
814 let lc = Lifecycle::new();
815
816 lc.start(|_cancel| async move {
817 loop {
818 sleep(Duration::from_secs(1000)).await;
819 }
820 #[allow(unreachable_code)]
821 Ok::<_, anyhow::Error>(())
822 })
823 .unwrap();
824
825 let reason = lc.stop(Duration::from_millis(30)).await.unwrap();
826 assert_eq!(reason, StopReason::Timeout);
827 assert_eq!(lc.status(), Status::Stopped);
828 }
829
830 #[tokio::test]
831 async fn try_start_and_second_start_fails() {
832 let lc = Lifecycle::new();
833
834 assert!(lc.try_start(|cancel| async move {
835 cancel.cancelled().await;
836 Ok(())
837 }));
838
839 let err = lc.start(|_c| async { Ok(()) }).unwrap_err();
840 match err {
841 LifecycleError::AlreadyStarted => {}
842 }
843
844 let _ = lc.stop(Duration::from_millis(80)).await.unwrap();
845 assert_eq!(lc.status(), Status::Stopped);
846 }
847
848 #[tokio::test]
849 async fn stop_is_idempotent_and_safe_concurrent() {
850 let lc = Arc::new(Lifecycle::new());
851
852 lc.start(|cancel| async move {
853 cancel.cancelled().await;
854 Ok(())
855 })
856 .unwrap();
857
858 let a = lc.clone();
859 let b = lc.clone();
860 let (r1, r2) = tokio::join!(
861 async move { a.stop(Duration::from_millis(80)).await },
862 async move { b.stop(Duration::from_millis(80)).await },
863 );
864
865 let r1 = r1.unwrap();
866 let r2 = r2.unwrap();
867 assert!(matches!(
868 r1,
869 StopReason::Finished | StopReason::Cancelled | StopReason::Timeout
870 ));
871 assert!(matches!(
872 r2,
873 StopReason::Finished | StopReason::Cancelled | StopReason::Timeout
874 ));
875 assert_eq!(lc.status(), Status::Stopped);
876 }
877
878 #[tokio::test]
879 async fn stateful_wrapper_start_stop_roundtrip() {
880 use crate::contracts::RunnableCapability;
881
882 let wrapper = WithLifecycle::new(TestRunnable::new());
883 assert_eq!(wrapper.status(), Status::Stopped);
884
885 wrapper.start(CancellationToken::new()).await.unwrap();
886 assert!(wrapper.lc.is_running());
887
888 wrapper.stop(CancellationToken::new()).await.unwrap();
889 assert_eq!(wrapper.status(), Status::Stopped);
890 }
891
892 #[tokio::test]
893 async fn with_lifecycle_double_start_fails() {
894 use crate::contracts::RunnableCapability;
895
896 let wrapper = WithLifecycle::new(TestRunnable::new());
897 let cancel = CancellationToken::new();
898 wrapper.start(cancel.clone()).await.unwrap();
899 let err = wrapper.start(cancel).await;
900 assert!(err.is_err());
901 wrapper.stop(CancellationToken::new()).await.unwrap();
902 }
903
904 #[tokio::test]
905 async fn with_lifecycle_concurrent_stop_calls() {
906 use crate::contracts::RunnableCapability;
907 let wrapper = Arc::new(WithLifecycle::new(TestRunnable::new()));
908 wrapper.start(CancellationToken::new()).await.unwrap();
909 let a = wrapper.clone();
910 let b = wrapper.clone();
911 let (r1, r2) = tokio::join!(
912 async move { a.stop(CancellationToken::new()).await },
913 async move { b.stop(CancellationToken::new()).await },
914 );
915 assert!(r1.is_ok());
916 assert!(r2.is_ok());
917 assert_eq!(wrapper.status(), Status::Stopped);
918 }
919
920 #[tokio::test]
921 async fn lifecycle_handles_panics_properly() {
922 let lc = Lifecycle::new();
923
924 lc.start(|_cancel| async {
926 panic!("test panic message");
927 })
928 .unwrap();
929
930 tokio::time::sleep(Duration::from_millis(50)).await;
932
933 let reason = lc.stop(Duration::from_secs(1)).await.unwrap();
935
936 assert!(matches!(
939 reason,
940 StopReason::Finished | StopReason::Cancelled | StopReason::Timeout
941 ));
942 assert_eq!(lc.status(), Status::Stopped);
943 }
944
945 #[tokio::test]
946 async fn lifecycle_task_naming_and_logging() {
947 let lc = Lifecycle::new();
948
949 lc.start(|cancel| async move {
951 cancel.cancelled().await;
952 Ok(())
953 })
954 .unwrap();
955
956 assert!(lc.is_running());
958
959 let reason = lc.stop(Duration::from_millis(100)).await.unwrap();
961 assert!(matches!(
962 reason,
963 StopReason::Cancelled | StopReason::Finished
964 ));
965 assert_eq!(lc.status(), Status::Stopped);
966 }
967
968 #[tokio::test]
969 async fn lifecycle_join_handles_all_tasks() {
970 let lc = Arc::new(Lifecycle::new());
971
972 lc.start(|cancel| async move {
974 tokio::time::sleep(Duration::from_millis(10)).await;
975 cancel.cancelled().await;
976 Ok(())
977 })
978 .unwrap();
979
980 let start = std::time::Instant::now();
982 let reason = lc.stop(Duration::from_millis(200)).await.unwrap();
983 let elapsed = start.elapsed();
984
985 assert!(elapsed >= Duration::from_millis(10));
987 assert!(matches!(
988 reason,
989 StopReason::Cancelled | StopReason::Finished
990 ));
991 assert_eq!(lc.status(), Status::Stopped);
992 }
993}