1use std::process::ExitCode;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::time::Duration;
7
8use tokio::sync::Notify;
9use tracing::{error, info, warn};
10
11use crate::ServerState;
12use crate::error::ServerError;
13use crate::worker::LostWorkerReport;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum ShutdownOutcome {
18 Clean,
20 TimedOut,
22 Forced,
24}
25
26impl ShutdownOutcome {
27 #[must_use]
29 pub fn exit_code(self) -> ExitCode {
30 match self {
31 Self::Clean => ExitCode::SUCCESS,
32 Self::TimedOut => ExitCode::FAILURE,
33 Self::Forced => ExitCode::from(130),
34 }
35 }
36}
37
38#[derive(Clone, Debug, Default)]
41pub struct DrainState {
42 inner: Arc<DrainStateInner>,
43}
44
45#[derive(Debug, Default)]
46struct DrainStateInner {
47 draining: AtomicBool,
48 empty: Notify,
49}
50
51impl DrainState {
52 #[must_use]
54 pub fn is_draining(&self) -> bool {
55 self.inner.draining.load(Ordering::Acquire)
56 }
57
58 #[must_use]
60 pub fn begin(&self) -> bool {
61 !self.inner.draining.swap(true, Ordering::AcqRel)
62 }
63
64 pub fn ensure_accepting(
70 &self,
71 namespace: &str,
72 activity_type: &str,
73 ) -> Result<(), ServerError> {
74 if self.is_draining() {
75 Err(ServerError::worker_dispatch(
76 namespace.to_owned(),
77 activity_type.to_owned(),
78 "server is draining and not accepting new activity tasks",
79 ))
80 } else {
81 Ok(())
82 }
83 }
84
85 pub fn notify_activity_drained(&self) {
87 self.inner.empty.notify_waiters();
88 }
89
90 async fn wait_for_empty(&self, state: &ServerState) -> Result<(), ServerError> {
91 loop {
92 let in_flight = state.heartbeat_tracker().in_flight_count()?;
93 if in_flight == 0 {
94 return Ok(());
95 }
96 let notified = self.inner.empty.notified();
97 if state.heartbeat_tracker().in_flight_count()? == 0 {
98 return Ok(());
99 }
100 notified.await;
101 }
102 }
103}
104
105pub async fn drain_after_first_signal(
114 state: ServerState,
115 second_signal: impl std::future::Future<Output = ()>,
116) -> Result<ShutdownOutcome, ServerError> {
117 let drain = state.drain_state().clone();
118 let first = drain.begin();
119 if first {
120 info!("shutdown signal received; beginning graceful drain");
121 }
122
123 let delivered_workers = state.worker_registry().broadcast_drain()?;
124 info!(delivered_workers, "sent drain request to connected workers");
125
126 let timeout = state.runtime_config().drain_timeout;
127 tokio::pin!(second_signal);
128
129 let outcome = tokio::select! {
130 () = &mut second_signal => {
131 warn!("second shutdown signal received; forcing immediate exit");
132 ShutdownOutcome::Forced
133 }
134 result = wait_for_drain_or_timeout(&state, &drain, timeout) => result?,
135 };
136
137 if matches!(outcome, ShutdownOutcome::Forced) {
138 return Ok(outcome);
139 }
140
141 state.shutdown()?;
142 Ok(outcome)
143}
144
145async fn wait_for_drain_or_timeout(
146 state: &ServerState,
147 drain: &DrainState,
148 timeout: Duration,
149) -> Result<ShutdownOutcome, ServerError> {
150 match tokio::time::timeout(timeout, drain.wait_for_empty(state)).await {
151 Ok(result) => {
152 result?;
153 info!("activity drain completed cleanly");
154 Ok(ShutdownOutcome::Clean)
155 }
156 Err(_elapsed) => {
157 let reports = state
158 .heartbeat_tracker()
159 .fail_all_in_flight_workers(state.worker_registry(), state.pending_activities())?;
160 log_lost_workers(&reports);
161 Ok(ShutdownOutcome::TimedOut)
162 }
163 }
164}
165
166fn log_lost_workers(reports: &[LostWorkerReport]) {
167 let failed_tasks: usize = reports.iter().map(|report| report.tasks.len()).sum();
168 if failed_tasks == 0 {
169 info!("activity drain timed out with no tracked in-flight activity failures");
170 } else {
171 error!(
172 failed_workers = reports.len(),
173 failed_tasks,
174 "activity drain timed out; remaining activities surfaced as retryable lost-worker failures"
175 );
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::DrainState;
182
183 #[test]
184 fn begin_is_idempotent_and_sets_draining() {
185 let drain = DrainState::default();
186
187 assert!(!drain.is_draining());
188 assert!(drain.begin());
189 assert!(drain.is_draining());
190 assert!(!drain.begin());
191 }
192}