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