1#![deny(missing_docs)]
76
77use std::{
78 fmt,
79 future::Future,
80 io,
81 panic::{AssertUnwindSafe, catch_unwind},
82 sync::{Arc, Mutex, OnceLock},
83 time::Duration,
84};
85
86use axum::Router;
87pub use baukit_core::{BuildInfo, DeploymentEnvironment, ProcessKind, ServiceIdentity};
88use thiserror::Error;
89use tokio::{
90 net::TcpListener,
91 task::{JoinError, JoinHandle, JoinSet},
92 time::{Instant, timeout},
93};
94use tokio_util::sync::CancellationToken;
95
96pub const RUST_VERSION: &str = env!("BAUKIT_RUST_VERSION");
100
101#[macro_export]
107macro_rules! build_info {
108 () => {
109 $crate::BuildInfo::new(
110 env!("CARGO_PKG_VERSION"),
111 option_env!("GIT_COMMIT").unwrap_or("unknown"),
112 $crate::RUST_VERSION,
113 )
114 };
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct ServiceInfo {
124 name: String,
125 identity: ServiceIdentity,
126 build: BuildInfo,
127}
128
129impl ServiceInfo {
130 pub fn new(
132 product: impl Into<String>,
133 process: ProcessKind,
134 build: BuildInfo,
135 environment: DeploymentEnvironment,
136 ) -> Self {
137 let product = product.into();
138 let identity = ServiceIdentity::new(
139 product,
140 process,
141 build.version().to_owned(),
142 build.commit().to_owned(),
143 environment,
144 );
145 let name = identity.service_name();
146 Self {
147 name,
148 identity,
149 build,
150 }
151 }
152
153 pub fn name(&self) -> &str {
155 &self.name
156 }
157
158 pub fn product(&self) -> &str {
160 self.identity.product()
161 }
162
163 pub const fn process(&self) -> ProcessKind {
165 self.identity.process()
166 }
167
168 pub const fn build(&self) -> &BuildInfo {
170 &self.build
171 }
172
173 pub const fn telemetry_identity(&self) -> &ServiceIdentity {
175 &self.identity
176 }
177}
178
179struct ShutdownState {
180 root: CancellationToken,
181 started_at: OnceLock<Instant>,
182 drain_hooks: Mutex<Vec<DrainHook>>,
183}
184
185type DrainHook = Box<dyn FnOnce() + Send + 'static>;
186
187impl fmt::Debug for ShutdownState {
188 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189 let hook_count = self
190 .drain_hooks
191 .lock()
192 .unwrap_or_else(std::sync::PoisonError::into_inner)
193 .len();
194 formatter
195 .debug_struct("ShutdownState")
196 .field("root", &self.root)
197 .field("started_at", &self.started_at)
198 .field("drain_hook_count", &hook_count)
199 .finish()
200 }
201}
202
203#[derive(Clone, Debug)]
209pub struct ShutdownToken {
210 observed: CancellationToken,
211 state: Arc<ShutdownState>,
212 drain_timeout: Duration,
213}
214
215impl ShutdownToken {
216 pub fn new(drain_timeout: Duration) -> Self {
218 let root = CancellationToken::new();
219 Self {
220 observed: root.clone(),
221 state: Arc::new(ShutdownState {
222 root,
223 started_at: OnceLock::new(),
224 drain_hooks: Mutex::new(Vec::new()),
225 }),
226 drain_timeout,
227 }
228 }
229
230 pub fn child_token(&self) -> Self {
234 Self {
235 observed: self.observed.child_token(),
236 state: Arc::clone(&self.state),
237 drain_timeout: self.drain_timeout,
238 }
239 }
240
241 pub fn trigger(&self) -> bool {
243 let first = self.state.started_at.set(Instant::now()).is_ok();
244 if first {
245 let hooks = std::mem::take(
246 &mut *self
247 .state
248 .drain_hooks
249 .lock()
250 .unwrap_or_else(std::sync::PoisonError::into_inner),
251 );
252 for hook in hooks {
253 if catch_unwind(AssertUnwindSafe(hook)).is_err() {
254 tracing::error!("a shutdown drain hook panicked");
255 }
256 }
257 self.state.root.cancel();
258 }
259 first
260 }
261
262 pub fn on_drain<F>(&self, hook: F)
286 where
287 F: FnOnce() + Send + 'static,
288 {
289 let mut hook = Some(Box::new(hook) as DrainHook);
290 {
291 let mut hooks = self
292 .state
293 .drain_hooks
294 .lock()
295 .unwrap_or_else(std::sync::PoisonError::into_inner);
296 if self.state.started_at.get().is_none() {
297 hooks.push(hook.take().expect("drain hook is available"));
298 }
299 }
300 if let Some(hook) = hook
301 && catch_unwind(AssertUnwindSafe(hook)).is_err()
302 {
303 tracing::error!("a late shutdown drain hook panicked");
304 }
305 }
306
307 pub fn is_cancelled(&self) -> bool {
309 self.observed.is_cancelled()
310 }
311
312 pub async fn cancelled(&self) {
314 self.observed.cancelled().await;
315 }
316
317 pub const fn drain_timeout(&self) -> Duration {
319 self.drain_timeout
320 }
321
322 pub fn deadline(&self) -> Option<Instant> {
324 self.state
325 .started_at
326 .get()
327 .map(|started_at| *started_at + self.drain_timeout)
328 }
329
330 pub async fn run_during_drain<F>(&self, future: F) -> Result<F::Output, DrainTimeout>
336 where
337 F: Future,
338 {
339 self.cancelled().await;
340 let remaining = self.remaining();
341 timeout(remaining, future)
342 .await
343 .map_err(|_| DrainTimeout::new(self.drain_timeout))
344 }
345
346 pub async fn shutdown_on_signal(&self) -> io::Result<ShutdownSignal> {
348 let signal = wait_for_shutdown_signal().await?;
349 self.trigger();
350 Ok(signal)
351 }
352
353 #[must_use = "retain or join the signal listener so setup failures are observed"]
357 pub fn spawn_signal_listener(&self) -> tokio::task::JoinHandle<io::Result<ShutdownSignal>> {
358 let shutdown = self.clone();
359 tokio::spawn(async move { shutdown.shutdown_on_signal().await })
360 }
361
362 fn remaining(&self) -> Duration {
363 self.deadline()
364 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
365 .unwrap_or(self.drain_timeout)
366 }
367}
368
369#[derive(Clone, Copy, Debug, Eq, PartialEq)]
371pub enum ShutdownSignal {
372 Interrupt,
374 Terminate,
376}
377
378#[cfg(unix)]
383pub async fn wait_for_shutdown_signal() -> io::Result<ShutdownSignal> {
384 use tokio::signal::unix::{SignalKind, signal};
385
386 let mut terminate = signal(SignalKind::terminate())?;
387 tokio::select! {
388 ctrl_c = tokio::signal::ctrl_c() => {
389 ctrl_c?;
390 Ok(ShutdownSignal::Interrupt)
391 }
392 received = terminate.recv() => received.map_or_else(
393 || Err(io::Error::other("SIGTERM signal stream ended")),
394 |_| Ok(ShutdownSignal::Terminate),
395 ),
396 }
397}
398
399#[cfg(not(unix))]
401pub async fn wait_for_shutdown_signal() -> io::Result<ShutdownSignal> {
402 tokio::signal::ctrl_c().await?;
403 Ok(ShutdownSignal::Interrupt)
404}
405
406#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
408#[error("shutdown drain deadline expired after {timeout:?}")]
409pub struct DrainTimeout {
410 timeout: Duration,
411}
412
413impl DrainTimeout {
414 const fn new(timeout: Duration) -> Self {
415 Self { timeout }
416 }
417
418 pub const fn timeout(self) -> Duration {
420 self.timeout
421 }
422}
423
424#[derive(Clone, Copy, Debug, Eq, PartialEq)]
426pub enum RestartPolicy {
427 FailProcess,
429 Restart {
431 max_restarts: usize,
433 },
434}
435
436pub struct TaskSupervisor {
438 shutdown: ShutdownToken,
439 tasks: JoinSet<()>,
440}
441
442impl TaskSupervisor {
443 pub fn new(shutdown: ShutdownToken) -> Self {
445 Self {
446 shutdown,
447 tasks: JoinSet::new(),
448 }
449 }
450
451 pub fn spawn<F, Fut>(&mut self, name: impl Into<String>, policy: RestartPolicy, factory: F)
456 where
457 F: Fn() -> Fut + Send + Sync + 'static,
458 Fut: Future<Output = ()> + Send + 'static,
459 {
460 let shutdown = self.shutdown.clone();
461 let name = name.into();
462 let factory = Arc::new(factory);
463 self.tasks.spawn(async move {
464 supervise_task(name, policy, factory, shutdown).await;
465 });
466 }
467
468 pub async fn join(mut self) -> Result<(), DrainTimeout> {
473 self.shutdown.cancelled().await;
474 let remaining = self.shutdown.remaining();
475 let joined = timeout(remaining, async {
476 while let Some(result) = self.tasks.join_next().await {
477 if let Err(error) = result {
478 tracing::error!(%error, "task supervisor failed while joining a worker");
479 }
480 }
481 })
482 .await;
483
484 if joined.is_err() {
485 self.tasks.abort_all();
486 while self.tasks.join_next().await.is_some() {}
487 return Err(DrainTimeout::new(self.shutdown.drain_timeout));
488 }
489
490 Ok(())
491 }
492
493 pub fn len(&self) -> usize {
495 self.tasks.len()
496 }
497
498 pub fn is_empty(&self) -> bool {
500 self.tasks.is_empty()
501 }
502}
503
504async fn supervise_task<F, Fut>(
505 name: String,
506 policy: RestartPolicy,
507 factory: Arc<F>,
508 shutdown: ShutdownToken,
509) where
510 F: Fn() -> Fut + Send + Sync + 'static,
511 Fut: Future<Output = ()> + Send + 'static,
512{
513 let mut restarts = 0_usize;
514 loop {
515 let mut attempt = AbortOnDropTask(tokio::spawn(factory()));
516 let outcome = tokio::select! {
517 _ = shutdown.cancelled() => {
518 if let Err(error) = (&mut attempt.0).await
519 && error.is_panic()
520 {
521 tracing::error!(task = %name, %error, "supervised task panicked while draining");
522 }
523 return;
524 }
525 result = &mut attempt.0 => result,
526 };
527
528 if shutdown.is_cancelled() {
529 return;
530 }
531
532 match &outcome {
533 Ok(()) => tracing::error!(task = %name, "supervised task exited unexpectedly"),
534 Err(error) if error.is_panic() => {
535 tracing::error!(task = %name, %error, "supervised task panicked")
536 }
537 Err(error) => tracing::error!(task = %name, %error, "supervised task was cancelled"),
538 }
539
540 match policy {
541 RestartPolicy::FailProcess => {
542 shutdown.trigger();
543 return;
544 }
545 RestartPolicy::Restart { max_restarts } if restarts < max_restarts => {
546 restarts += 1;
547 tracing::warn!(task = %name, restarts, max_restarts, "restarting supervised task");
548 }
549 RestartPolicy::Restart { max_restarts } => {
550 tracing::error!(task = %name, restarts, max_restarts, "task restart limit exhausted");
551 shutdown.trigger();
552 return;
553 }
554 }
555 }
556}
557
558struct AbortOnDropTask<T>(JoinHandle<T>);
559
560impl<T> Drop for AbortOnDropTask<T> {
561 fn drop(&mut self) {
562 self.0.abort();
563 }
564}
565
566#[derive(Clone, Copy, Debug, Eq, PartialEq)]
568pub enum ListenerKind {
569 Api,
571 Operations,
573}
574
575impl fmt::Display for ListenerKind {
576 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
577 formatter.write_str(match self {
578 Self::Api => "API",
579 Self::Operations => "operations",
580 })
581 }
582}
583
584#[derive(Debug, Error)]
586pub enum ListenerPairError {
587 #[error("{listener} listener failed: {source}")]
589 Listener {
590 listener: ListenerKind,
592 #[source]
594 source: io::Error,
595 },
596 #[error("listener task failed: {0}")]
598 Task(#[source] JoinError),
599 #[error("{0} listener exited before shutdown")]
601 UnexpectedExit(ListenerKind),
602 #[error(transparent)]
604 Drain(#[from] DrainTimeout),
605}
606
607type ListenerResult = Result<ListenerKind, (ListenerKind, io::Error)>;
608
609#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
611pub enum ShutdownOrder {
612 #[default]
614 Together,
615 OpsOutlivesApi,
617}
618
619pub async fn serve_listener_pair(
627 api_listener: TcpListener,
628 api_router: Router,
629 operations_listener: TcpListener,
630 operations_router: Router,
631 shutdown: ShutdownToken,
632) -> Result<(), ListenerPairError> {
633 serve_listener_pair_with_shutdown_order(
634 api_listener,
635 api_router,
636 operations_listener,
637 operations_router,
638 shutdown,
639 ShutdownOrder::Together,
640 )
641 .await
642}
643
644pub async fn serve_listener_pair_with_shutdown_order(
653 api_listener: TcpListener,
654 api_router: Router,
655 operations_listener: TcpListener,
656 operations_router: Router,
657 shutdown: ShutdownToken,
658 shutdown_order: ShutdownOrder,
659) -> Result<(), ListenerPairError> {
660 let mut listeners = JoinSet::new();
661 let operations_shutdown = match shutdown_order {
662 ShutdownOrder::Together => shutdown.observed.clone(),
663 ShutdownOrder::OpsOutlivesApi => CancellationToken::new(),
664 };
665 spawn_listener(
666 &mut listeners,
667 ListenerKind::Api,
668 api_listener,
669 api_router,
670 shutdown.observed.clone(),
671 );
672 spawn_listener(
673 &mut listeners,
674 ListenerKind::Operations,
675 operations_listener,
676 operations_router,
677 operations_shutdown.clone(),
678 );
679
680 let mut first_error = None;
681 while !listeners.is_empty() && !shutdown.is_cancelled() {
682 tokio::select! {
683 _ = shutdown.cancelled() => {}
684 result = listeners.join_next() => {
685 stop_operations_after_api(
686 result.as_ref(),
687 shutdown_order,
688 &operations_shutdown,
689 );
690 record_listener_result(result, &shutdown, &mut first_error);
691 }
692 }
693 }
694
695 if listeners.is_empty() {
696 return first_error.map_or(Ok(()), Err);
697 }
698
699 let remaining = shutdown.remaining();
700 let drained = timeout(remaining, async {
701 while let Some(result) = listeners.join_next().await {
702 stop_operations_after_api(Some(&result), shutdown_order, &operations_shutdown);
703 record_listener_result(Some(result), &shutdown, &mut first_error);
704 }
705 })
706 .await;
707
708 if drained.is_err() {
709 listeners.abort_all();
710 while listeners.join_next().await.is_some() {}
711 return Err(DrainTimeout::new(shutdown.drain_timeout).into());
712 }
713
714 first_error.map_or(Ok(()), Err)
715}
716
717fn spawn_listener(
718 listeners: &mut JoinSet<ListenerResult>,
719 kind: ListenerKind,
720 listener: TcpListener,
721 router: Router,
722 shutdown: CancellationToken,
723) {
724 listeners.spawn(async move {
725 axum::serve(listener, router)
726 .with_graceful_shutdown(async move { shutdown.cancelled().await })
727 .await
728 .map(|()| kind)
729 .map_err(|error| (kind, error))
730 });
731}
732
733fn stop_operations_after_api(
734 result: Option<&Result<ListenerResult, JoinError>>,
735 shutdown_order: ShutdownOrder,
736 operations_shutdown: &CancellationToken,
737) {
738 if shutdown_order != ShutdownOrder::OpsOutlivesApi {
739 return;
740 }
741
742 let listener = match result {
743 Some(Ok(Ok(kind) | Err((kind, _)))) => Some(*kind),
744 Some(Err(_)) | None => None,
745 };
746 if listener == Some(ListenerKind::Api) {
747 operations_shutdown.cancel();
748 }
749}
750
751fn record_listener_result(
752 result: Option<Result<ListenerResult, JoinError>>,
753 shutdown: &ShutdownToken,
754 first_error: &mut Option<ListenerPairError>,
755) {
756 let error = match result {
757 Some(Ok(Ok(kind))) if !shutdown.is_cancelled() => {
758 Some(ListenerPairError::UnexpectedExit(kind))
759 }
760 Some(Ok(Ok(_))) | None => None,
761 Some(Ok(Err((listener, source)))) => Some(ListenerPairError::Listener { listener, source }),
762 Some(Err(error)) => Some(ListenerPairError::Task(error)),
763 };
764
765 if let Some(error) = error {
766 tracing::error!(%error, "listener pair initiated process shutdown");
767 if first_error.is_none() {
768 *first_error = Some(error);
769 }
770 shutdown.trigger();
771 }
772}
773
774#[cfg(test)]
775mod tests {
776 use std::{
777 future::pending,
778 net::{IpAddr, Ipv4Addr, SocketAddr},
779 sync::{
780 Arc,
781 atomic::{AtomicUsize, Ordering},
782 },
783 };
784
785 use axum::routing::get;
786 use tokio::{
787 io::{AsyncReadExt as _, AsyncWriteExt as _},
788 net::TcpStream,
789 sync::Notify,
790 };
791
792 use super::*;
793
794 async fn http_get(address: SocketAddr, path: &str) -> String {
795 let mut stream = TcpStream::connect(address)
796 .await
797 .expect("connect to test listener");
798 let request =
799 format!("GET {path} HTTP/1.1\r\nHost: {address}\r\nConnection: close\r\n\r\n");
800 stream
801 .write_all(request.as_bytes())
802 .await
803 .expect("write test request");
804 let mut response = Vec::new();
805 stream
806 .read_to_end(&mut response)
807 .await
808 .expect("read test response");
809 String::from_utf8(response).expect("HTTP response is UTF-8")
810 }
811
812 #[tokio::test]
813 async fn token_cancels_children() {
814 let shutdown = ShutdownToken::new(Duration::from_secs(5));
815 let child = shutdown.child_token();
816 assert!(!child.is_cancelled());
817
818 assert!(shutdown.trigger());
819 child.cancelled().await;
820
821 assert!(child.is_cancelled());
822 assert!(!shutdown.trigger());
823 }
824
825 #[tokio::test]
826 async fn drain_hooks_run_before_cancellation_and_late_hooks_run_immediately() {
827 let shutdown = ShutdownToken::new(Duration::from_secs(5));
828 let calls = Arc::new(AtomicUsize::new(0));
829 shutdown.on_drain({
830 let calls = Arc::clone(&calls);
831 let shutdown = shutdown.clone();
832 move || {
833 assert!(!shutdown.is_cancelled());
834 calls.fetch_add(1, Ordering::SeqCst);
835 }
836 });
837
838 shutdown.trigger();
839 assert!(shutdown.is_cancelled());
840 assert_eq!(calls.load(Ordering::SeqCst), 1);
841
842 shutdown.on_drain({
843 let calls = Arc::clone(&calls);
844 move || {
845 calls.fetch_add(1, Ordering::SeqCst);
846 }
847 });
848 assert_eq!(calls.load(Ordering::SeqCst), 2);
849 assert!(!shutdown.trigger());
850 assert_eq!(calls.load(Ordering::SeqCst), 2);
851 }
852
853 #[tokio::test(start_paused = true)]
854 async fn drain_deadline_is_enforced() {
855 let shutdown = ShutdownToken::new(Duration::from_secs(7));
856 shutdown.trigger();
857 let drain = tokio::spawn({
858 let shutdown = shutdown.clone();
859 async move { shutdown.run_during_drain(pending::<()>()).await }
860 });
861
862 tokio::time::advance(Duration::from_secs(7)).await;
863 let error = drain
864 .await
865 .expect("drain task should join")
866 .expect_err("pending cleanup should exceed the deadline");
867 assert_eq!(error.timeout(), Duration::from_secs(7));
868 }
869
870 #[tokio::test]
871 async fn supervisor_restarts_until_a_worker_stays_running() {
872 let shutdown = ShutdownToken::new(Duration::from_secs(2));
873 let worker_shutdown = shutdown.child_token();
874 let attempts = Arc::new(AtomicUsize::new(0));
875 let drained = Arc::new(AtomicUsize::new(0));
876 let mut supervisor = TaskSupervisor::new(shutdown.clone());
877 supervisor.spawn("restartable", RestartPolicy::Restart { max_restarts: 2 }, {
878 let attempts = Arc::clone(&attempts);
879 let drained = Arc::clone(&drained);
880 move || {
881 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
882 let worker_shutdown = worker_shutdown.clone();
883 let drained = Arc::clone(&drained);
884 async move {
885 if attempt < 2 {
886 return;
887 }
888 worker_shutdown.cancelled().await;
889 drained.fetch_add(1, Ordering::SeqCst);
890 }
891 }
892 });
893
894 tokio::time::timeout(Duration::from_secs(1), async {
895 while attempts.load(Ordering::SeqCst) < 3 {
896 tokio::task::yield_now().await;
897 }
898 })
899 .await
900 .expect("worker should be restarted twice");
901
902 shutdown.trigger();
903 supervisor.join().await.expect("worker should drain");
904 assert_eq!(attempts.load(Ordering::SeqCst), 3);
905 assert_eq!(drained.load(Ordering::SeqCst), 1);
906 }
907
908 #[tokio::test]
909 async fn exhausted_restart_policy_triggers_process_shutdown() {
910 let shutdown = ShutdownToken::new(Duration::from_secs(2));
911 let attempts = Arc::new(AtomicUsize::new(0));
912 let mut supervisor = TaskSupervisor::new(shutdown.clone());
913 supervisor.spawn("exhausted", RestartPolicy::Restart { max_restarts: 1 }, {
914 let attempts = Arc::clone(&attempts);
915 move || {
916 attempts.fetch_add(1, Ordering::SeqCst);
917 async {}
918 }
919 });
920
921 tokio::time::timeout(Duration::from_secs(1), shutdown.cancelled())
922 .await
923 .expect("restart exhaustion should trigger shutdown");
924 supervisor.join().await.expect("supervisor should join");
925 assert_eq!(attempts.load(Ordering::SeqCst), 2);
926 }
927
928 #[tokio::test]
929 async fn supervisor_panic_triggers_process_shutdown() {
930 let shutdown = ShutdownToken::new(Duration::from_secs(2));
931 let mut supervisor = TaskSupervisor::new(shutdown.clone());
932 supervisor.spawn("panicking", RestartPolicy::FailProcess, || async {
933 panic!("intentional worker panic");
934 });
935
936 tokio::time::timeout(Duration::from_secs(1), shutdown.cancelled())
937 .await
938 .expect("panic should trigger shutdown");
939 supervisor.join().await.expect("supervisor should join");
940 }
941
942 #[tokio::test]
943 async fn listener_pair_shuts_down_when_token_is_triggered() {
944 let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
945 let api = TcpListener::bind(address).await.expect("bind API listener");
946 let operations = TcpListener::bind(address)
947 .await
948 .expect("bind operations listener");
949 let shutdown = ShutdownToken::new(Duration::from_secs(2));
950 let pair = tokio::spawn(serve_listener_pair(
951 api,
952 Router::new(),
953 operations,
954 Router::new(),
955 shutdown.clone(),
956 ));
957
958 tokio::task::yield_now().await;
959 shutdown.trigger();
960
961 tokio::time::timeout(Duration::from_secs(1), pair)
962 .await
963 .expect("listeners should finish promptly")
964 .expect("listener task should join")
965 .expect("listeners should drain successfully");
966 }
967
968 #[tokio::test]
969 async fn operations_listener_responds_while_api_is_draining() {
970 let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
971 let api = TcpListener::bind(address).await.expect("bind API listener");
972 let api_address = api.local_addr().expect("API address");
973 let operations = TcpListener::bind(address)
974 .await
975 .expect("bind operations listener");
976 let operations_address = operations.local_addr().expect("operations address");
977 let request_started = Arc::new(Notify::new());
978 let release_request = Arc::new(Notify::new());
979 let api_router = Router::new().route(
980 "/drain",
981 get({
982 let request_started = Arc::clone(&request_started);
983 let release_request = Arc::clone(&release_request);
984 move || {
985 let request_started = Arc::clone(&request_started);
986 let release_request = Arc::clone(&release_request);
987 async move {
988 request_started.notify_one();
989 release_request.notified().await;
990 "drained"
991 }
992 }
993 }),
994 );
995 let operations_router = Router::new().route("/readyz", get(|| async { "ready" }));
996 let shutdown = ShutdownToken::new(Duration::from_secs(2));
997 let pair = tokio::spawn(serve_listener_pair_with_shutdown_order(
998 api,
999 api_router,
1000 operations,
1001 operations_router,
1002 shutdown.clone(),
1003 ShutdownOrder::OpsOutlivesApi,
1004 ));
1005 let api_request = tokio::spawn(http_get(api_address, "/drain"));
1006
1007 request_started.notified().await;
1008 shutdown.trigger();
1009 assert!(shutdown.is_cancelled());
1010
1011 let operations_response = http_get(operations_address, "/readyz").await;
1012 assert!(operations_response.starts_with("HTTP/1.1 200 OK"));
1013 assert!(operations_response.ends_with("ready"));
1014
1015 release_request.notify_one();
1016 let api_response = api_request.await.expect("API request task joins");
1017 assert!(api_response.starts_with("HTTP/1.1 200 OK"));
1018 tokio::time::timeout(Duration::from_secs(1), pair)
1019 .await
1020 .expect("listeners should finish after the API drains")
1021 .expect("listener task should join")
1022 .expect("listeners should drain successfully");
1023 }
1024
1025 #[test]
1026 fn service_info_reuses_telemetry_identity() {
1027 let build = BuildInfo::new("1.2.3", "abc123", "1.95.0");
1028 let service = ServiceInfo::new(
1029 "orders",
1030 ProcessKind::Worker,
1031 build,
1032 DeploymentEnvironment::Staging,
1033 );
1034
1035 assert_eq!(service.name(), "orders-worker");
1036 assert_eq!(service.product(), "orders");
1037 assert_eq!(service.build().rust_version(), "1.95.0");
1038 assert_eq!(service.telemetry_identity().version(), "1.2.3");
1039 }
1040}
1041
1042#[doc = include_str!("../README.md")]
1044#[cfg(doctest)]
1045struct ReadmeDoctests;