1use std::future::Future;
11use std::marker::PhantomData;
12use std::pin::Pin;
13
14use tokio_util::sync::CancellationToken;
15
16use crate::{
17 RuntimeComponentBundle, RuntimeComponentKind, RuntimeComponentRegistry, log_shutdown_report,
18 spawn_termination_signal_handler,
19};
20
21type RuntimeHookFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
22type RuntimeHook = Box<dyn FnOnce() -> RuntimeHookFuture + Send>;
23type RuntimeComponentRegistration = Box<dyn FnOnce(&mut RuntimeComponentRegistry) + Send>;
24type ShutdownResourceFuture = Pin<Box<dyn Future<Output = Result<(), String>> + Send>>;
25type ShutdownResourceFn<T> = Box<dyn FnOnce(T) -> ShutdownResourceFuture + Send>;
26
27fn empty_runtime_hook() -> RuntimeHook {
28 Box::new(|| Box::pin(async {}))
29}
30
31pub struct ServiceLifecycle<S> {
37 server: S,
38 shutdown_token: CancellationToken,
39}
40
41impl<S> ServiceLifecycle<S> {
42 pub fn new(server: S, shutdown_token: CancellationToken) -> Self {
44 Self {
45 server,
46 shutdown_token,
47 }
48 }
49}
50
51pub struct RuntimeServiceComponent<S> {
58 component_name: &'static str,
59 kind: RuntimeComponentKind,
60 service: S,
61 shutdown_token: CancellationToken,
62 stop_on_signal: RuntimeHook,
63}
64
65impl<S> RuntimeServiceComponent<S> {
66 pub fn new<F, Fut>(
68 component_name: &'static str,
69 kind: RuntimeComponentKind,
70 service: S,
71 shutdown_token: CancellationToken,
72 stop_on_signal: F,
73 ) -> Self
74 where
75 F: FnOnce() -> Fut + Send + 'static,
76 Fut: Future<Output = ()> + Send + 'static,
77 {
78 Self {
79 component_name,
80 kind,
81 service,
82 shutdown_token,
83 stop_on_signal: Box::new(move || Box::pin(stop_on_signal())),
84 }
85 }
86
87 fn into_parts(self) -> RuntimeServiceParts<S> {
88 RuntimeServiceParts {
89 service: self.service,
90 shutdown_token: self.shutdown_token,
91 stop_on_signal: self.stop_on_signal,
92 }
93 }
94}
95
96struct RuntimeServiceParts<S> {
97 service: S,
98 shutdown_token: CancellationToken,
99 stop_on_signal: RuntimeHook,
100}
101
102pub trait AsterRuntimeComponent<S> {
108 type Output;
110
111 fn apply(self, builder: AsterRuntimeBuilder<S>) -> Self::Output;
113}
114
115pub struct RuntimeComponentBundleRegistration<B> {
117 bundle: B,
118}
119
120pub struct RuntimeComponentWithShutdown<C, F> {
122 build: F,
123 _component: PhantomData<fn() -> C>,
124}
125
126pub struct TryRuntimeComponentWithShutdown<C, F, E> {
128 build: F,
129 _component: PhantomData<fn() -> C>,
130 _error: PhantomData<fn() -> E>,
131}
132
133pub const fn runtime_component<B>(bundle: B) -> RuntimeComponentBundleRegistration<B> {
135 RuntimeComponentBundleRegistration { bundle }
136}
137
138pub fn runtime_component_with_shutdown<C, F>(build: F) -> RuntimeComponentWithShutdown<C, F>
144where
145 F: FnOnce(CancellationToken) -> C,
146{
147 RuntimeComponentWithShutdown {
148 build,
149 _component: PhantomData,
150 }
151}
152
153pub fn try_runtime_component_with_shutdown<C, F, E>(
155 build: F,
156) -> TryRuntimeComponentWithShutdown<C, F, E>
157where
158 F: FnOnce(CancellationToken) -> Result<C, E>,
159{
160 TryRuntimeComponentWithShutdown {
161 build,
162 _component: PhantomData,
163 _error: PhantomData,
164 }
165}
166
167pub struct ShutdownResourceComponent<T> {
174 component_name: &'static str,
175 kind: RuntimeComponentKind,
176 phase_name: &'static str,
177 dependencies: &'static [&'static str],
178 resource: T,
179 shutdown: ShutdownResourceFn<T>,
180}
181
182impl<T> ShutdownResourceComponent<T> {
183 pub fn new<F, Fut>(
185 component_name: &'static str,
186 kind: RuntimeComponentKind,
187 phase_name: &'static str,
188 resource: T,
189 shutdown: F,
190 ) -> Self
191 where
192 F: FnOnce(T) -> Fut + Send + 'static,
193 Fut: Future<Output = Result<(), String>> + Send + 'static,
194 {
195 Self {
196 component_name,
197 kind,
198 phase_name,
199 dependencies: &[],
200 resource,
201 shutdown: Box::new(move |resource| Box::pin(shutdown(resource))),
202 }
203 }
204
205 pub const fn depends_on_all(mut self, dependencies: &'static [&'static str]) -> Self {
207 self.dependencies = dependencies;
208 self
209 }
210}
211
212impl<T> RuntimeComponentBundle for ShutdownResourceComponent<T>
213where
214 T: Send + 'static,
215{
216 fn register(self, registry: &mut RuntimeComponentRegistry) {
217 let Self {
218 component_name,
219 kind,
220 phase_name,
221 dependencies,
222 resource,
223 shutdown,
224 } = self;
225 registry
226 .component(component_name)
227 .kind(kind)
228 .depends_on_all(dependencies)
229 .shutdown_once(phase_name, None, resource, shutdown);
230 }
231}
232
233pub fn shutdown_resource_component<T, F, Fut>(
235 component_name: &'static str,
236 kind: RuntimeComponentKind,
237 phase_name: &'static str,
238 resource: T,
239 shutdown: F,
240) -> RuntimeComponentBundleRegistration<ShutdownResourceComponent<T>>
241where
242 T: Send + 'static,
243 F: FnOnce(T) -> Fut + Send + 'static,
244 Fut: Future<Output = Result<(), String>> + Send + 'static,
245{
246 runtime_component(ShutdownResourceComponent::new(
247 component_name,
248 kind,
249 phase_name,
250 resource,
251 shutdown,
252 ))
253}
254
255pub fn shutdown_resource_component_after<T, F, Fut>(
257 component_name: &'static str,
258 kind: RuntimeComponentKind,
259 phase_name: &'static str,
260 dependencies: &'static [&'static str],
261 resource: T,
262 shutdown: F,
263) -> RuntimeComponentBundleRegistration<ShutdownResourceComponent<T>>
264where
265 T: Send + 'static,
266 F: FnOnce(T) -> Fut + Send + 'static,
267 Fut: Future<Output = Result<(), String>> + Send + 'static,
268{
269 runtime_component(
270 ShutdownResourceComponent::new(component_name, kind, phase_name, resource, shutdown)
271 .depends_on_all(dependencies),
272 )
273}
274
275impl<S, Service> AsterRuntimeComponent<S> for RuntimeServiceComponent<Service> {
276 type Output = AsterRuntimeBuilder<Service>;
277
278 fn apply(self, mut builder: AsterRuntimeBuilder<S>) -> Self::Output {
279 let component_name = self.component_name;
280 let kind = self.kind;
281 let assembly_error = if builder.service.is_some() {
282 Some(AsterRuntimeError::DuplicateService)
283 } else {
284 builder.assembly_error
285 };
286 builder.components.push(Box::new(move |registry| {
287 registry.component(component_name).kind(kind);
288 }));
289
290 AsterRuntimeBuilder {
291 service: Some(self.into_parts()),
292 shutdown_token: builder.shutdown_token,
293 before_shutdown: builder.before_shutdown,
294 components: builder.components,
295 assembly_error,
296 }
297 }
298}
299
300impl<S, B> AsterRuntimeComponent<S> for RuntimeComponentBundleRegistration<B>
301where
302 B: RuntimeComponentBundle + Send + 'static,
303{
304 type Output = AsterRuntimeBuilder<S>;
305
306 fn apply(self, mut builder: AsterRuntimeBuilder<S>) -> Self::Output {
307 builder
308 .components
309 .push(Box::new(move |registry| self.bundle.register(registry)));
310 builder
311 }
312}
313
314impl<S, C, F> AsterRuntimeComponent<S> for RuntimeComponentWithShutdown<C, F>
315where
316 C: AsterRuntimeComponent<S>,
317 F: FnOnce(CancellationToken) -> C,
318{
319 type Output = C::Output;
320
321 fn apply(self, builder: AsterRuntimeBuilder<S>) -> Self::Output {
322 let component = (self.build)(builder.shutdown_token.clone());
323 component.apply(builder)
324 }
325}
326
327impl<S, C, F, E> AsterRuntimeComponent<S> for TryRuntimeComponentWithShutdown<C, F, E>
328where
329 C: AsterRuntimeComponent<S>,
330 F: FnOnce(CancellationToken) -> Result<C, E>,
331{
332 type Output = Result<C::Output, E>;
333
334 fn apply(self, builder: AsterRuntimeBuilder<S>) -> Self::Output {
335 let component = (self.build)(builder.shutdown_token.clone())?;
336 Ok(component.apply(builder))
337 }
338}
339
340impl<B> RuntimeComponentBundle for RuntimeComponentBundleRegistration<B>
341where
342 B: RuntimeComponentBundle,
343{
344 fn register(self, registry: &mut RuntimeComponentRegistry) {
345 self.bundle.register(registry);
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
351pub enum AsterRuntimeError {
352 #[error("runtime requires exactly one service component")]
354 MissingService,
355 #[error("runtime only supports one service component")]
357 DuplicateService,
358 #[error("runtime component graph is invalid: {source}")]
360 ComponentGraph {
361 source: crate::RuntimeComponentGraphError,
363 },
364 #[error("runtime startup aborted by a required component phase")]
366 Startup {
367 report: crate::StartupReport,
369 },
370}
371
372pub struct AsterRuntime<S> {
387 service: S,
388 shutdown_token: CancellationToken,
389 stop_on_signal: RuntimeHook,
390 before_shutdown: RuntimeHook,
391 registry: RuntimeComponentRegistry,
392}
393
394impl AsterRuntime<()> {
395 pub fn builder() -> AsterRuntimeBuilder<()> {
397 AsterRuntimeBuilder::new()
398 }
399}
400
401impl<S> AsterRuntime<S>
402where
403 S: Future,
404{
405 pub async fn run(mut self) -> Result<S::Output, AsterRuntimeError> {
412 let startup_report = self.registry.startup().await;
413 if startup_report.aborted() {
414 let shutdown_report = self.registry.shutdown().await;
415 log_shutdown_report(&shutdown_report);
416 return Err(AsterRuntimeError::Startup {
417 report: startup_report,
418 });
419 }
420
421 let _signal_task =
422 spawn_termination_signal_handler(self.shutdown_token, self.stop_on_signal);
423
424 let output = self.service.await;
425 tracing::info!("service stopped");
426
427 (self.before_shutdown)().await;
428
429 let report = self.registry.shutdown().await;
430 log_shutdown_report(&report);
431
432 Ok(output)
433 }
434}
435
436pub struct AsterRuntimeBuilder<S = ()> {
438 service: Option<RuntimeServiceParts<S>>,
439 shutdown_token: CancellationToken,
440 before_shutdown: RuntimeHook,
441 components: Vec<RuntimeComponentRegistration>,
442 assembly_error: Option<AsterRuntimeError>,
443}
444
445impl AsterRuntimeBuilder<()> {
446 fn new() -> Self {
447 Self {
448 service: None,
449 shutdown_token: CancellationToken::new(),
450 before_shutdown: empty_runtime_hook(),
451 components: Vec::new(),
452 assembly_error: None,
453 }
454 }
455}
456
457impl<S> AsterRuntimeBuilder<S> {
458 pub fn shutdown_token(&self) -> &CancellationToken {
464 &self.shutdown_token
465 }
466
467 pub fn component<C>(self, component: C) -> C::Output
469 where
470 C: AsterRuntimeComponent<S>,
471 {
472 component.apply(self)
473 }
474
475 pub fn before_shutdown<F, Fut>(mut self, before_shutdown: F) -> Self
477 where
478 F: FnOnce() -> Fut + Send + 'static,
479 Fut: Future<Output = ()> + Send + 'static,
480 {
481 self.before_shutdown = Box::new(move || Box::pin(before_shutdown()));
482 self
483 }
484
485 pub fn build(self) -> Result<AsterRuntime<S>, AsterRuntimeError> {
487 if let Some(error) = self.assembly_error {
488 return Err(error);
489 }
490
491 let Some(service) = self.service else {
492 return Err(AsterRuntimeError::MissingService);
493 };
494
495 let mut registry = RuntimeComponentRegistry::new();
496 for component in self.components {
497 component(&mut registry);
498 }
499 registry
500 .validate()
501 .map_err(|source| AsterRuntimeError::ComponentGraph { source })?;
502
503 Ok(AsterRuntime {
504 service: service.service,
505 shutdown_token: service.shutdown_token,
506 stop_on_signal: service.stop_on_signal,
507 before_shutdown: self.before_shutdown,
508 registry,
509 })
510 }
511}
512
513impl<S> AsterRuntimeBuilder<S>
514where
515 S: Future,
516{
517 pub async fn run(self) -> Result<S::Output, AsterRuntimeError> {
519 self.build()?.run().await
520 }
521}
522
523impl<S> ServiceLifecycle<S>
524where
525 S: Future,
526{
527 pub async fn run<Stop, StopFut, AfterStop, AfterStopFut>(
529 self,
530 stop_on_signal: Stop,
531 after_stop: AfterStop,
532 ) -> S::Output
533 where
534 Stop: FnOnce() -> StopFut + Send + 'static,
535 StopFut: Future<Output = ()> + Send + 'static,
536 AfterStop: FnOnce() -> AfterStopFut,
537 AfterStopFut: Future<Output = ()>,
538 {
539 let _signal_task = spawn_termination_signal_handler(self.shutdown_token, stop_on_signal);
540
541 let server_result = self.server.await;
542 tracing::info!("server stopped");
543 after_stop().await;
544 server_result
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use std::sync::{
551 Arc,
552 atomic::{AtomicBool, Ordering},
553 };
554
555 use super::{AsterRuntime, AsterRuntimeError, RuntimeServiceComponent, ServiceLifecycle};
556 use crate::{RuntimeComponentBundle, RuntimeComponentKind, RuntimeComponentRegistry};
557
558 #[tokio::test]
559 async fn lifecycle_runs_after_stop_and_returns_server_result() {
560 let after_stop_ran = Arc::new(AtomicBool::new(false));
561 let observed_after_stop = Arc::clone(&after_stop_ran);
562
563 let result = ServiceLifecycle::new(async { Ok::<_, &'static str>(42) }, Default::default())
564 .run(
565 || async {},
566 move || {
567 let observed_after_stop = Arc::clone(&observed_after_stop);
568 async move {
569 observed_after_stop.store(true, Ordering::SeqCst);
570 }
571 },
572 )
573 .await;
574
575 assert_eq!(result, Ok(42));
576 assert!(after_stop_ran.load(Ordering::SeqCst));
577 }
578
579 struct TestShutdownComponent {
580 events: Arc<std::sync::Mutex<Vec<&'static str>>>,
581 }
582
583 impl RuntimeComponentBundle for TestShutdownComponent {
584 fn register(self, registry: &mut RuntimeComponentRegistry) {
585 registry.component_shutdown(
586 "test",
587 RuntimeComponentKind::Product,
588 "test_shutdown",
589 None,
590 move || {
591 let events = Arc::clone(&self.events);
592 async move {
593 events.lock().unwrap().push("component");
594 Ok(())
595 }
596 },
597 );
598 }
599 }
600
601 #[tokio::test]
602 async fn aster_runtime_runs_before_shutdown_and_registered_components() {
603 let events = Arc::new(std::sync::Mutex::new(Vec::new()));
604 let before_events = Arc::clone(&events);
605 let component_events = Arc::clone(&events);
606
607 let result = AsterRuntime::builder()
608 .component(RuntimeServiceComponent::new(
609 "http",
610 RuntimeComponentKind::Core,
611 async { Ok::<_, &'static str>(7) },
612 Default::default(),
613 || async {},
614 ))
615 .before_shutdown(move || {
616 let before_events = Arc::clone(&before_events);
617 async move {
618 before_events.lock().unwrap().push("before");
619 }
620 })
621 .component(crate::runtime_component(TestShutdownComponent {
622 events: component_events,
623 }))
624 .run()
625 .await
626 .expect("runtime should run");
627
628 assert_eq!(result, Ok(7));
629 assert_eq!(events.lock().unwrap().as_slice(), ["before", "component"]);
630 }
631
632 #[tokio::test]
633 async fn aster_runtime_runs_component_shutdown_when_startup_aborts() {
634 let events = Arc::new(std::sync::Mutex::new(Vec::new()));
635 let started_events = Arc::clone(&events);
636 let shutdown_events = Arc::clone(&events);
637
638 let result = AsterRuntime::builder()
639 .component(RuntimeServiceComponent::new(
640 "http",
641 RuntimeComponentKind::Core,
642 std::future::pending::<()>(),
643 Default::default(),
644 || async {},
645 ))
646 .component(crate::runtime_component(
647 move |registry: &mut RuntimeComponentRegistry| {
648 registry.component_startup(
649 "database",
650 RuntimeComponentKind::Core,
651 "database_connect",
652 crate::StartupPhaseFailurePolicy::Required,
653 move || {
654 let started_events = Arc::clone(&started_events);
655 async move {
656 started_events.lock().unwrap().push("database_started");
657 Ok(())
658 }
659 },
660 );
661 registry.component_shutdown(
662 "database",
663 RuntimeComponentKind::Core,
664 "database_close",
665 None,
666 move || {
667 let shutdown_events = Arc::clone(&shutdown_events);
668 async move {
669 shutdown_events.lock().unwrap().push("database_shutdown");
670 Ok(())
671 }
672 },
673 );
674 registry.component_startup(
675 "cache",
676 RuntimeComponentKind::Core,
677 "cache_connect",
678 crate::StartupPhaseFailurePolicy::Required,
679 || async { Err("cache unavailable".to_string()) },
680 );
681 },
682 ))
683 .run()
684 .await;
685
686 assert!(matches!(result, Err(AsterRuntimeError::Startup { .. })));
687 assert_eq!(
690 events.lock().unwrap().as_slice(),
691 ["database_started", "database_shutdown"]
692 );
693 }
694
695 #[tokio::test]
696 async fn aster_runtime_builder_shares_shutdown_token_with_components() {
697 let observed = Arc::new(AtomicBool::new(false));
698 let observed_component = Arc::clone(&observed);
699
700 let result = AsterRuntime::builder()
701 .component(crate::runtime_component_with_shutdown(|shutdown| {
702 let component_token = shutdown.clone();
703 RuntimeServiceComponent::new(
704 "http",
705 RuntimeComponentKind::Core,
706 async move {
707 component_token.cancel();
708 Ok::<_, &'static str>(())
709 },
710 shutdown,
711 || async {},
712 )
713 }))
714 .component(crate::runtime_component_with_shutdown(|shutdown| {
715 crate::runtime_component(move |registry: &mut RuntimeComponentRegistry| {
716 registry.component_shutdown(
717 "observer",
718 RuntimeComponentKind::Product,
719 "observe_shared_shutdown",
720 None,
721 move || {
722 let observed_component = Arc::clone(&observed_component);
723 let shutdown = shutdown.clone();
724 async move {
725 observed_component.store(shutdown.is_cancelled(), Ordering::SeqCst);
726 Ok(())
727 }
728 },
729 );
730 })
731 }))
732 .run()
733 .await
734 .expect("runtime should run");
735
736 assert_eq!(result, Ok(()));
737 assert!(observed.load(Ordering::SeqCst));
738 }
739
740 #[test]
741 fn aster_runtime_requires_service_component() {
742 let result = AsterRuntime::builder().build();
743 assert!(matches!(result, Err(AsterRuntimeError::MissingService)));
744 }
745
746 #[test]
747 fn aster_runtime_rejects_duplicate_service_components() {
748 let result = AsterRuntime::builder()
749 .component(RuntimeServiceComponent::new(
750 "http",
751 RuntimeComponentKind::Core,
752 async {},
753 Default::default(),
754 || async {},
755 ))
756 .component(RuntimeServiceComponent::new(
757 "worker",
758 RuntimeComponentKind::Core,
759 async {},
760 Default::default(),
761 || async {},
762 ))
763 .build();
764
765 assert!(matches!(result, Err(AsterRuntimeError::DuplicateService)));
766 }
767
768 #[test]
769 fn aster_runtime_rejects_invalid_component_graph() {
770 let result = AsterRuntime::builder()
771 .component(RuntimeServiceComponent::new(
772 "http",
773 RuntimeComponentKind::Core,
774 async {},
775 Default::default(),
776 || async {},
777 ))
778 .component(crate::runtime_component(
779 |registry: &mut RuntimeComponentRegistry| {
780 registry.component("database").depends_on("missing");
781 },
782 ))
783 .build();
784
785 assert!(matches!(
786 result,
787 Err(AsterRuntimeError::ComponentGraph {
788 source: crate::RuntimeComponentGraphError::MissingDependency {
789 component: "database",
790 dependency: "missing"
791 }
792 })
793 ));
794 }
795
796 #[test]
797 fn shutdown_resource_component_registers_dependencies_and_shutdown() {
798 let registry = RuntimeComponentRegistry::configured(|registry| {
799 crate::shutdown_resource_component_after(
800 "mail_outbox",
801 RuntimeComponentKind::Mail,
802 "mail_outbox_drain",
803 &["background_tasks"],
804 42_u8,
805 |_| async { Ok(()) },
806 )
807 .register(registry);
808 });
809
810 let descriptor = registry
811 .descriptor("mail_outbox")
812 .expect("shutdown resource component should be registered");
813 assert_eq!(descriptor.kind, RuntimeComponentKind::Mail);
814 assert_eq!(descriptor.dependencies, vec!["background_tasks"]);
815 assert_eq!(
816 descriptor
817 .shutdown
818 .first()
819 .expect("shutdown phase should be registered")
820 .phase_name,
821 "mail_outbox_drain"
822 );
823 }
824}