use std::convert::Infallible;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Poll};
use axum::http::{HeaderValue, Request, Response, header};
use tower::{Layer, Service};
pub const DEFAULT_RETRY_AFTER: u32 = 60;
#[derive(Clone, Debug)]
pub struct Maintenance {
engaged: Arc<AtomicBool>,
exempt: Arc<Vec<String>>,
retry_after: u32,
}
impl Default for Maintenance {
fn default() -> Self {
Self::new()
}
}
impl Maintenance {
#[must_use]
pub fn new() -> Self {
Maintenance {
engaged: Arc::new(AtomicBool::new(false)),
exempt: Arc::new(Vec::new()),
retry_after: DEFAULT_RETRY_AFTER,
}
}
#[must_use]
pub fn engaged() -> Self {
let maintenance = Self::new();
maintenance.engage();
maintenance
}
#[must_use]
pub fn allow(mut self, prefix: impl Into<String>) -> Self {
let mut prefix = prefix.into();
while prefix.len() > 1 && prefix.ends_with('/') {
prefix.pop();
}
if !prefix.starts_with('/') {
prefix.insert(0, '/');
}
Arc::make_mut(&mut self.exempt).push(prefix);
self
}
#[must_use]
pub fn retry_after(mut self, seconds: u32) -> Self {
self.retry_after = seconds;
self
}
pub fn engage(&self) {
self.engaged.store(true, Ordering::Release);
}
pub fn disengage(&self) {
self.engaged.store(false, Ordering::Release);
}
#[must_use]
pub fn is_engaged(&self) -> bool {
self.engaged.load(Ordering::Acquire)
}
#[must_use]
pub fn is_exempt(&self, path: &str) -> bool {
self.exempt.iter().any(|prefix| covers(prefix, path))
}
#[must_use]
pub fn blocks(&self, path: &str) -> bool {
self.is_engaged() && !self.is_exempt(path)
}
}
fn covers(prefix: &str, path: &str) -> bool {
if prefix == "/" {
return true;
}
path == prefix
|| (path.len() > prefix.len()
&& path.starts_with(prefix)
&& path.as_bytes()[prefix.len()] == b'/')
}
impl<S> Layer<S> for Maintenance {
type Service = MaintenanceService<S>;
fn layer(&self, inner: S) -> Self::Service {
MaintenanceService {
inner,
maintenance: self.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct MaintenanceService<S> {
inner: S,
maintenance: Maintenance,
}
impl<S> Service<Request<axum::body::Body>> for MaintenanceService<S>
where
S: Service<
Request<axum::body::Body>,
Response = Response<axum::body::Body>,
Error = Infallible,
> + Clone
+ Send
+ 'static,
S::Future: Send + 'static,
{
type Response = Response<axum::body::Body>;
type Error = Infallible;
type Future =
Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: Request<axum::body::Body>) -> Self::Future {
if self.maintenance.blocks(request.uri().path()) {
let response = unavailable(self.maintenance.retry_after);
return Box::pin(async move { Ok(response) });
}
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
Box::pin(async move { inner.call(request).await })
}
}
fn unavailable(retry_after: u32) -> Response<axum::body::Body> {
use axum::response::IntoResponse as _;
let mut response = crate::api::Problem::of(crate::api::ProblemKind::Unavailable)
.with_detail("The application is down for maintenance. Please try again shortly.")
.into_response();
if let Ok(value) = HeaderValue::from_str(&retry_after.to_string()) {
response.headers_mut().insert(header::RETRY_AFTER, value);
}
response
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
#[test]
fn a_new_switch_is_off_and_blocks_nothing() {
let maintenance = Maintenance::new();
assert!(!maintenance.is_engaged());
assert!(!maintenance.blocks("/"));
}
#[test]
fn engaging_blocks_everything_not_exempt() {
let maintenance = Maintenance::new().allow("/up");
maintenance.engage();
assert!(maintenance.blocks("/"));
assert!(maintenance.blocks("/users/1"));
assert!(!maintenance.blocks("/up"));
assert!(!maintenance.blocks("/up/ready"));
}
#[test]
fn an_exempt_prefix_matches_on_a_segment_boundary() {
let maintenance = Maintenance::engaged().allow("/up");
assert!(maintenance.blocks("/upload"));
assert!(maintenance.blocks("/updates"));
}
#[test]
fn a_prefix_is_normalised_before_it_is_matched() {
let maintenance = Maintenance::engaged().allow("up/");
assert!(!maintenance.blocks("/up/ready"));
}
#[test]
fn every_clone_shares_one_switch() {
let maintenance = Maintenance::new();
let clone = maintenance.clone();
maintenance.engage();
assert!(clone.is_engaged());
clone.disengage();
assert!(!maintenance.is_engaged());
}
#[test]
fn the_response_is_a_problem_with_retry_after() {
let response = unavailable(30);
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(
response.headers().get(header::RETRY_AFTER).unwrap(),
&HeaderValue::from_static("30")
);
assert_eq!(
response.headers().get(header::CONTENT_TYPE).unwrap(),
&HeaderValue::from_static("application/problem+json")
);
}
}