use axum::{
Router,
extract::{Request, State},
http::{StatusCode, header},
middleware::{Next, from_fn_with_state},
response::{IntoResponse, Response},
};
use std::collections::BTreeMap;
use std::fmt;
use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tokio::sync::Notify;
mod body;
const PHASE_STARTING: u8 = 0;
const PHASE_READY: u8 = 1;
const PHASE_DRAINING: u8 = 2;
const PHASE_STOPPED: u8 = 3;
const MAX_REQUIRED_COMPONENTS: usize = 32;
const MAX_COMPONENT_NAME_BYTES: usize = 64;
const MAX_DRAIN_WAIT: Duration = Duration::from_secs(600);
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ApplicationPhase {
Starting,
Ready,
Draining,
Stopped,
}
impl ApplicationPhase {
fn from_u8(value: u8) -> Self {
match value {
PHASE_READY => Self::Ready,
PHASE_DRAINING => Self::Draining,
PHASE_STOPPED => Self::Stopped,
_ => Self::Starting,
}
}
}
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ApplicationLifecycleError {
#[error("invalid readiness component label `{0}`")]
InvalidComponentLabel(String),
#[error("duplicate readiness component label `{0}`")]
DuplicateComponent(String),
#[error("unknown readiness component label `{0}`")]
UnknownComponent(String),
#[error("readiness accepts at most {MAX_REQUIRED_COMPONENTS} required components")]
TooManyComponents,
#[error("cannot transition application lifecycle from {from:?} to {to:?}")]
InvalidTransition {
from: ApplicationPhase,
to: ApplicationPhase,
},
#[error("readiness component state is unavailable")]
StateUnavailable,
#[error("drain wait must be greater than zero and at most 600 seconds")]
InvalidDrainWait,
#[error("graceful drain timed out with {in_flight} request(s) still active")]
DrainTimedOut {
in_flight: usize,
},
#[error("in-flight request capacity is exhausted")]
RequestCapacityExhausted,
#[error("application phase {phase:?} is not accepting requests")]
RequestNotAdmitted {
phase: ApplicationPhase,
},
}
struct LifecycleInner {
phase: AtomicU8,
in_flight: AtomicUsize,
components: RwLock<BTreeMap<String, bool>>,
drained: Notify,
}
#[derive(Clone)]
pub struct ApplicationLifecycle {
inner: Arc<LifecycleInner>,
}
impl fmt::Debug for ApplicationLifecycle {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let snapshot = self.snapshot();
formatter
.debug_struct("ApplicationLifecycle")
.field("phase", &snapshot.phase)
.field("required_components", &snapshot.required_components)
.field("unready_components", &snapshot.unready_components)
.field("in_flight_requests", &snapshot.in_flight_requests)
.finish_non_exhaustive()
}
}
impl Default for ApplicationLifecycle {
fn default() -> Self {
Self::new()
}
}
impl ApplicationLifecycle {
pub fn new() -> Self {
Self {
inner: Arc::new(LifecycleInner {
phase: AtomicU8::new(PHASE_STARTING),
in_flight: AtomicUsize::new(0),
components: RwLock::new(BTreeMap::new()),
drained: Notify::new(),
}),
}
}
pub fn with_required_components<I, S>(components: I) -> Result<Self, ApplicationLifecycleError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut required = BTreeMap::new();
for component in components {
if required.len() >= MAX_REQUIRED_COMPONENTS {
return Err(ApplicationLifecycleError::TooManyComponents);
}
let component = component.into();
validate_component_label(&component)?;
if required.insert(component.clone(), false).is_some() {
return Err(ApplicationLifecycleError::DuplicateComponent(component));
}
}
Ok(Self {
inner: Arc::new(LifecycleInner {
phase: AtomicU8::new(PHASE_STARTING),
in_flight: AtomicUsize::new(0),
components: RwLock::new(required),
drained: Notify::new(),
}),
})
}
pub fn phase(&self) -> ApplicationPhase {
ApplicationPhase::from_u8(self.inner.phase.load(Ordering::Acquire))
}
pub fn mark_ready(&self) -> Result<(), ApplicationLifecycleError> {
match self.inner.phase.compare_exchange(
PHASE_STARTING,
PHASE_READY,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) | Err(PHASE_READY) => Ok(()),
Err(value) => Err(ApplicationLifecycleError::InvalidTransition {
from: ApplicationPhase::from_u8(value),
to: ApplicationPhase::Ready,
}),
}
}
pub fn set_component_ready(
&self,
component: impl Into<String>,
ready: bool,
) -> Result<(), ApplicationLifecycleError> {
let component = component.into();
validate_component_label(&component)?;
let mut components = self
.inner
.components
.write()
.map_err(|_| ApplicationLifecycleError::StateUnavailable)?;
let state = components
.get_mut(&component)
.ok_or_else(|| ApplicationLifecycleError::UnknownComponent(component.clone()))?;
*state = ready;
Ok(())
}
pub fn begin_draining(&self) -> Result<(), ApplicationLifecycleError> {
loop {
let current = self.inner.phase.load(Ordering::Acquire);
match current {
PHASE_DRAINING => return Ok(()),
PHASE_STOPPED => {
return Err(ApplicationLifecycleError::InvalidTransition {
from: ApplicationPhase::Stopped,
to: ApplicationPhase::Draining,
});
}
PHASE_STARTING | PHASE_READY => {
if self
.inner
.phase
.compare_exchange(
current,
PHASE_DRAINING,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
{
if self.in_flight_requests() == 0 {
self.inner.drained.notify_waiters();
}
return Ok(());
}
}
_ => continue,
}
}
}
pub fn mark_stopped(&self) {
self.inner.phase.store(PHASE_STOPPED, Ordering::Release);
self.inner.drained.notify_waiters();
}
pub fn snapshot(&self) -> ReadinessSnapshot {
let phase = self.phase();
let in_flight_requests = self.in_flight_requests();
let component_counts = self.inner.components.read().ok().map(|components| {
let unready = components.values().filter(|ready| !**ready).count();
(components.len(), unready)
});
let (required_components, unready_components, state_available) = match component_counts {
Some((required, unready)) => (required, unready, true),
None => (0, 0, false),
};
let ready = phase == ApplicationPhase::Ready && state_available && unready_components == 0;
ReadinessSnapshot {
phase,
ready,
state_available,
required_components,
unready_components,
in_flight_requests,
}
}
pub fn in_flight_requests(&self) -> usize {
self.inner.in_flight.load(Ordering::Acquire)
}
pub async fn wait_for_drain(&self, timeout: Duration) -> Result<(), ApplicationLifecycleError> {
if timeout.is_zero() || timeout > MAX_DRAIN_WAIT {
return Err(ApplicationLifecycleError::InvalidDrainWait);
}
let phase = self.phase();
if !matches!(
phase,
ApplicationPhase::Draining | ApplicationPhase::Stopped
) {
return Err(ApplicationLifecycleError::InvalidTransition {
from: phase,
to: ApplicationPhase::Draining,
});
}
let deadline = tokio::time::Instant::now() + timeout;
loop {
let notified = self.inner.drained.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if self.in_flight_requests() == 0 {
return Ok(());
}
if tokio::time::timeout_at(deadline, notified).await.is_err() {
if self.in_flight_requests() == 0 {
return Ok(());
}
return Err(ApplicationLifecycleError::DrainTimedOut {
in_flight: self.in_flight_requests(),
});
}
}
}
fn try_admit(&self) -> Result<ApplicationRequestGuard, ApplicationLifecycleError> {
loop {
if !self.snapshot().ready {
return Err(ApplicationLifecycleError::RequestNotAdmitted {
phase: self.phase(),
});
}
let active = self.inner.in_flight.load(Ordering::Acquire);
let next = active
.checked_add(1)
.ok_or(ApplicationLifecycleError::RequestCapacityExhausted)?;
if self
.inner
.in_flight
.compare_exchange(active, next, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
continue;
}
if self.snapshot().ready {
return Ok(ApplicationRequestGuard {
inner: Arc::clone(&self.inner),
});
}
release_request(&self.inner);
return Err(ApplicationLifecycleError::RequestNotAdmitted {
phase: self.phase(),
});
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct ReadinessSnapshot {
pub phase: ApplicationPhase,
pub ready: bool,
pub state_available: bool,
pub required_components: usize,
pub unready_components: usize,
pub in_flight_requests: usize,
}
struct ApplicationRequestGuard {
inner: Arc<LifecycleInner>,
}
impl Drop for ApplicationRequestGuard {
fn drop(&mut self) {
release_request(&self.inner);
}
}
fn release_request(inner: &LifecycleInner) {
let previous = inner.in_flight.fetch_sub(1, Ordering::AcqRel);
if previous == 1 && inner.phase.load(Ordering::Acquire) >= PHASE_DRAINING {
inner.drained.notify_waiters();
}
}
fn validate_component_label(component: &str) -> Result<(), ApplicationLifecycleError> {
if component.is_empty()
|| component.len() > MAX_COMPONENT_NAME_BYTES
|| !component
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
{
return Err(ApplicationLifecycleError::InvalidComponentLabel(
component.to_string(),
));
}
Ok(())
}
pub fn apply_lifecycle(router: Router, lifecycle: ApplicationLifecycle) -> Router {
router.layer(from_fn_with_state(lifecycle, lifecycle_middleware))
}
async fn lifecycle_middleware(
State(lifecycle): State<ApplicationLifecycle>,
request: Request,
next: Next,
) -> Response {
let is_probe = matches!(
request.method(),
&axum::http::Method::GET | &axum::http::Method::HEAD
) && matches!(request.uri().path(), "/health" | "/ready");
if is_probe {
return next.run(request).await;
}
match lifecycle.try_admit() {
Ok(guard) => body::track(next.run(request).await, guard),
Err(_) => (
StatusCode::SERVICE_UNAVAILABLE,
[
(header::CACHE_CONTROL, "no-store"),
(header::RETRY_AFTER, "1"),
(header::CONTENT_TYPE, "text/plain; charset=utf-8"),
],
"Service is not ready to accept application requests.",
)
.into_response(),
}
}
#[cfg(test)]
#[path = "lifecycle_tests.rs"]
mod tests;