1use axum::{
4 Router,
5 extract::{Request, State},
6 http::{StatusCode, header},
7 middleware::{Next, from_fn_with_state},
8 response::{IntoResponse, Response},
9};
10use std::collections::BTreeMap;
11use std::fmt;
12use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
13use std::sync::{Arc, RwLock};
14use std::time::Duration;
15use tokio::sync::Notify;
16mod body;
17
18const PHASE_STARTING: u8 = 0;
19const PHASE_READY: u8 = 1;
20const PHASE_DRAINING: u8 = 2;
21const PHASE_STOPPED: u8 = 3;
22const MAX_REQUIRED_COMPONENTS: usize = 32;
23const MAX_COMPONENT_NAME_BYTES: usize = 64;
24const MAX_DRAIN_WAIT: Duration = Duration::from_secs(600);
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
28#[serde(rename_all = "snake_case")]
29#[non_exhaustive]
30pub enum ApplicationPhase {
31 Starting,
33 Ready,
35 Draining,
37 Stopped,
39}
40
41impl ApplicationPhase {
42 fn from_u8(value: u8) -> Self {
43 match value {
44 PHASE_READY => Self::Ready,
45 PHASE_DRAINING => Self::Draining,
46 PHASE_STOPPED => Self::Stopped,
47 _ => Self::Starting,
48 }
49 }
50}
51
52#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
54#[non_exhaustive]
55pub enum ApplicationLifecycleError {
56 #[error("invalid readiness component label `{0}`")]
58 InvalidComponentLabel(String),
59 #[error("duplicate readiness component label `{0}`")]
61 DuplicateComponent(String),
62 #[error("unknown readiness component label `{0}`")]
64 UnknownComponent(String),
65 #[error("readiness accepts at most {MAX_REQUIRED_COMPONENTS} required components")]
67 TooManyComponents,
68 #[error("cannot transition application lifecycle from {from:?} to {to:?}")]
70 InvalidTransition {
71 from: ApplicationPhase,
73 to: ApplicationPhase,
75 },
76 #[error("readiness component state is unavailable")]
78 StateUnavailable,
79 #[error("drain wait must be greater than zero and at most 600 seconds")]
81 InvalidDrainWait,
82 #[error("graceful drain timed out with {in_flight} request(s) still active")]
84 DrainTimedOut {
85 in_flight: usize,
87 },
88 #[error("in-flight request capacity is exhausted")]
90 RequestCapacityExhausted,
91 #[error("application phase {phase:?} is not accepting requests")]
93 RequestNotAdmitted {
94 phase: ApplicationPhase,
96 },
97}
98
99struct LifecycleInner {
100 phase: AtomicU8,
101 in_flight: AtomicUsize,
102 components: RwLock<BTreeMap<String, bool>>,
103 drained: Notify,
104}
105
106#[derive(Clone)]
113pub struct ApplicationLifecycle {
114 inner: Arc<LifecycleInner>,
115}
116
117impl fmt::Debug for ApplicationLifecycle {
118 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119 let snapshot = self.snapshot();
120 formatter
121 .debug_struct("ApplicationLifecycle")
122 .field("phase", &snapshot.phase)
123 .field("required_components", &snapshot.required_components)
124 .field("unready_components", &snapshot.unready_components)
125 .field("in_flight_requests", &snapshot.in_flight_requests)
126 .finish_non_exhaustive()
127 }
128}
129
130impl Default for ApplicationLifecycle {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl ApplicationLifecycle {
137 pub fn new() -> Self {
139 Self {
140 inner: Arc::new(LifecycleInner {
141 phase: AtomicU8::new(PHASE_STARTING),
142 in_flight: AtomicUsize::new(0),
143 components: RwLock::new(BTreeMap::new()),
144 drained: Notify::new(),
145 }),
146 }
147 }
148
149 pub fn with_required_components<I, S>(components: I) -> Result<Self, ApplicationLifecycleError>
151 where
152 I: IntoIterator<Item = S>,
153 S: Into<String>,
154 {
155 let mut required = BTreeMap::new();
156 for component in components {
157 if required.len() >= MAX_REQUIRED_COMPONENTS {
158 return Err(ApplicationLifecycleError::TooManyComponents);
159 }
160 let component = component.into();
161 validate_component_label(&component)?;
162 if required.insert(component.clone(), false).is_some() {
163 return Err(ApplicationLifecycleError::DuplicateComponent(component));
164 }
165 }
166
167 Ok(Self {
168 inner: Arc::new(LifecycleInner {
169 phase: AtomicU8::new(PHASE_STARTING),
170 in_flight: AtomicUsize::new(0),
171 components: RwLock::new(required),
172 drained: Notify::new(),
173 }),
174 })
175 }
176
177 pub fn phase(&self) -> ApplicationPhase {
179 ApplicationPhase::from_u8(self.inner.phase.load(Ordering::Acquire))
180 }
181
182 pub fn mark_ready(&self) -> Result<(), ApplicationLifecycleError> {
184 match self.inner.phase.compare_exchange(
185 PHASE_STARTING,
186 PHASE_READY,
187 Ordering::AcqRel,
188 Ordering::Acquire,
189 ) {
190 Ok(_) | Err(PHASE_READY) => Ok(()),
191 Err(value) => Err(ApplicationLifecycleError::InvalidTransition {
192 from: ApplicationPhase::from_u8(value),
193 to: ApplicationPhase::Ready,
194 }),
195 }
196 }
197
198 pub fn set_component_ready(
200 &self,
201 component: impl Into<String>,
202 ready: bool,
203 ) -> Result<(), ApplicationLifecycleError> {
204 let component = component.into();
205 validate_component_label(&component)?;
206 let mut components = self
207 .inner
208 .components
209 .write()
210 .map_err(|_| ApplicationLifecycleError::StateUnavailable)?;
211 let state = components
212 .get_mut(&component)
213 .ok_or_else(|| ApplicationLifecycleError::UnknownComponent(component.clone()))?;
214 *state = ready;
215 Ok(())
216 }
217
218 pub fn begin_draining(&self) -> Result<(), ApplicationLifecycleError> {
220 loop {
221 let current = self.inner.phase.load(Ordering::Acquire);
222 match current {
223 PHASE_DRAINING => return Ok(()),
224 PHASE_STOPPED => {
225 return Err(ApplicationLifecycleError::InvalidTransition {
226 from: ApplicationPhase::Stopped,
227 to: ApplicationPhase::Draining,
228 });
229 }
230 PHASE_STARTING | PHASE_READY => {
231 if self
232 .inner
233 .phase
234 .compare_exchange(
235 current,
236 PHASE_DRAINING,
237 Ordering::AcqRel,
238 Ordering::Acquire,
239 )
240 .is_ok()
241 {
242 if self.in_flight_requests() == 0 {
243 self.inner.drained.notify_waiters();
244 }
245 return Ok(());
246 }
247 }
248 _ => continue,
249 }
250 }
251 }
252
253 pub fn mark_stopped(&self) {
255 self.inner.phase.store(PHASE_STOPPED, Ordering::Release);
256 self.inner.drained.notify_waiters();
257 }
258
259 pub fn snapshot(&self) -> ReadinessSnapshot {
261 let phase = self.phase();
262 let in_flight_requests = self.in_flight_requests();
263 let component_counts = self.inner.components.read().ok().map(|components| {
264 let unready = components.values().filter(|ready| !**ready).count();
265 (components.len(), unready)
266 });
267
268 let (required_components, unready_components, state_available) = match component_counts {
269 Some((required, unready)) => (required, unready, true),
270 None => (0, 0, false),
271 };
272 let ready = phase == ApplicationPhase::Ready && state_available && unready_components == 0;
273
274 ReadinessSnapshot {
275 phase,
276 ready,
277 state_available,
278 required_components,
279 unready_components,
280 in_flight_requests,
281 }
282 }
283
284 pub fn in_flight_requests(&self) -> usize {
287 self.inner.in_flight.load(Ordering::Acquire)
288 }
289
290 pub async fn wait_for_drain(&self, timeout: Duration) -> Result<(), ApplicationLifecycleError> {
293 if timeout.is_zero() || timeout > MAX_DRAIN_WAIT {
294 return Err(ApplicationLifecycleError::InvalidDrainWait);
295 }
296 let phase = self.phase();
297 if !matches!(
298 phase,
299 ApplicationPhase::Draining | ApplicationPhase::Stopped
300 ) {
301 return Err(ApplicationLifecycleError::InvalidTransition {
302 from: phase,
303 to: ApplicationPhase::Draining,
304 });
305 }
306
307 let deadline = tokio::time::Instant::now() + timeout;
308 loop {
309 let notified = self.inner.drained.notified();
310 tokio::pin!(notified);
311 notified.as_mut().enable();
314 if self.in_flight_requests() == 0 {
315 return Ok(());
316 }
317 if tokio::time::timeout_at(deadline, notified).await.is_err() {
318 if self.in_flight_requests() == 0 {
321 return Ok(());
322 }
323 return Err(ApplicationLifecycleError::DrainTimedOut {
324 in_flight: self.in_flight_requests(),
325 });
326 }
327 }
328 }
329
330 fn try_admit(&self) -> Result<ApplicationRequestGuard, ApplicationLifecycleError> {
331 loop {
332 if !self.snapshot().ready {
333 return Err(ApplicationLifecycleError::RequestNotAdmitted {
334 phase: self.phase(),
335 });
336 }
337 let active = self.inner.in_flight.load(Ordering::Acquire);
338 let next = active
339 .checked_add(1)
340 .ok_or(ApplicationLifecycleError::RequestCapacityExhausted)?;
341 if self
342 .inner
343 .in_flight
344 .compare_exchange(active, next, Ordering::AcqRel, Ordering::Acquire)
345 .is_err()
346 {
347 continue;
348 }
349 if self.snapshot().ready {
350 return Ok(ApplicationRequestGuard {
351 inner: Arc::clone(&self.inner),
352 });
353 }
354 release_request(&self.inner);
355 return Err(ApplicationLifecycleError::RequestNotAdmitted {
356 phase: self.phase(),
357 });
358 }
359 }
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
364#[non_exhaustive]
365pub struct ReadinessSnapshot {
366 pub phase: ApplicationPhase,
368 pub ready: bool,
370 pub state_available: bool,
372 pub required_components: usize,
374 pub unready_components: usize,
376 pub in_flight_requests: usize,
378}
379
380struct ApplicationRequestGuard {
381 inner: Arc<LifecycleInner>,
382}
383
384impl Drop for ApplicationRequestGuard {
385 fn drop(&mut self) {
386 release_request(&self.inner);
387 }
388}
389
390fn release_request(inner: &LifecycleInner) {
391 let previous = inner.in_flight.fetch_sub(1, Ordering::AcqRel);
392 if previous == 1 && inner.phase.load(Ordering::Acquire) >= PHASE_DRAINING {
393 inner.drained.notify_waiters();
394 }
395}
396
397fn validate_component_label(component: &str) -> Result<(), ApplicationLifecycleError> {
398 if component.is_empty()
399 || component.len() > MAX_COMPONENT_NAME_BYTES
400 || !component
401 .bytes()
402 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
403 {
404 return Err(ApplicationLifecycleError::InvalidComponentLabel(
405 component.to_string(),
406 ));
407 }
408 Ok(())
409}
410
411pub fn apply_lifecycle(router: Router, lifecycle: ApplicationLifecycle) -> Router {
417 router.layer(from_fn_with_state(lifecycle, lifecycle_middleware))
418}
419
420pub(crate) fn is_health_probe<B>(request: &axum::http::Request<B>) -> bool {
425 matches!(
426 request.method(),
427 &axum::http::Method::GET | &axum::http::Method::HEAD
428 ) && matches!(request.uri().path(), "/health" | "/ready")
429}
430
431async fn lifecycle_middleware(
432 State(lifecycle): State<ApplicationLifecycle>,
433 request: Request,
434 next: Next,
435) -> Response {
436 if is_health_probe(&request) {
437 return next.run(request).await;
438 }
439
440 match lifecycle.try_admit() {
441 Ok(guard) => body::track(next.run(request).await, guard),
442 Err(_) => (
443 StatusCode::SERVICE_UNAVAILABLE,
444 [
445 (header::CACHE_CONTROL, "no-store"),
446 (header::RETRY_AFTER, "1"),
447 (header::CONTENT_TYPE, "text/plain; charset=utf-8"),
448 ],
449 "Service is not ready to accept application requests.",
450 )
451 .into_response(),
452 }
453}
454
455#[cfg(test)]
456#[path = "lifecycle_tests.rs"]
457pub(crate) mod tests;