use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use saddle_core::{ErrorKind, Result, SaddleError};
use tokio::sync::Notify;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ApplicationPhase {
Starting = 0,
Ready = 1,
Draining = 2,
Stopped = 3,
}
const PHASE_SHIFT: u32 = usize::BITS - 2;
const COUNT_MASK: usize = (1 << PHASE_SHIFT) - 1;
#[derive(Debug)]
struct Shared {
state: AtomicUsize,
drained: Notify,
}
#[derive(Clone, Debug)]
pub struct RequestLifecycle {
shared: Arc<Shared>,
}
impl RequestLifecycle {
pub(crate) fn new() -> Self {
Self {
shared: Arc::new(Shared {
state: AtomicUsize::new(encode(ApplicationPhase::Starting, 0)),
drained: Notify::new(),
}),
}
}
pub fn phase(&self) -> ApplicationPhase {
phase(self.shared.state.load(Ordering::Acquire))
}
pub fn try_accept(&self) -> Result<RequestGuard> {
let mut current = self.shared.state.load(Ordering::Acquire);
loop {
match phase(current) {
ApplicationPhase::Ready => {
if count(current) == COUNT_MASK {
return Err(SaddleError::new(
ErrorKind::Internal,
"runtime.request_count_overflow",
"request accounting capacity exhausted",
));
}
match self.shared.state.compare_exchange_weak(
current,
current + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
return Ok(RequestGuard {
shared: Arc::clone(&self.shared),
});
}
Err(observed) => current = observed,
}
}
ApplicationPhase::Starting => {
return Err(SaddleError::new(
ErrorKind::Unavailable,
"runtime.not_ready",
"application is not ready",
));
}
ApplicationPhase::Draining => {
return Err(SaddleError::new(
ErrorKind::Unavailable,
"runtime.shutting_down",
"application is shutting down",
));
}
ApplicationPhase::Stopped => {
return Err(SaddleError::new(
ErrorKind::Unavailable,
"runtime.stopped",
"application is stopped",
));
}
}
}
}
pub(crate) fn mark_ready(&self) {
let result = self.shared.state.compare_exchange(
encode(ApplicationPhase::Starting, 0),
encode(ApplicationPhase::Ready, 0),
Ordering::AcqRel,
Ordering::Acquire,
);
debug_assert!(result.is_ok());
}
pub(crate) fn begin_draining(&self) {
let mut current = self.shared.state.load(Ordering::Acquire);
while matches!(
phase(current),
ApplicationPhase::Starting | ApplicationPhase::Ready
) {
let draining = encode(ApplicationPhase::Draining, count(current));
match self.shared.state.compare_exchange_weak(
current,
draining,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
current = draining;
break;
}
Err(observed) => current = observed,
}
}
if count(current) == 0 {
self.shared.drained.notify_one();
}
}
pub(crate) async fn wait_until_drained(&self) {
loop {
if count(self.shared.state.load(Ordering::Acquire)) == 0 {
return;
}
self.shared.drained.notified().await;
}
}
pub(crate) fn mark_stopped(&self) {
let result = self.shared.state.compare_exchange(
encode(ApplicationPhase::Draining, 0),
encode(ApplicationPhase::Stopped, 0),
Ordering::AcqRel,
Ordering::Acquire,
);
debug_assert!(result.is_ok());
}
}
const fn encode(phase: ApplicationPhase, count: usize) -> usize {
((phase as usize) << PHASE_SHIFT) | count
}
const fn phase(state: usize) -> ApplicationPhase {
match state >> PHASE_SHIFT {
0 => ApplicationPhase::Starting,
1 => ApplicationPhase::Ready,
2 => ApplicationPhase::Draining,
3 => ApplicationPhase::Stopped,
_ => unreachable!(),
}
}
const fn count(state: usize) -> usize {
state & COUNT_MASK
}
#[derive(Debug)]
pub struct RequestGuard {
shared: Arc<Shared>,
}
impl Drop for RequestGuard {
fn drop(&mut self) {
let previous = self.shared.state.fetch_sub(1, Ordering::AcqRel);
debug_assert!(count(previous) > 0);
if count(previous) == 1 && phase(previous) == ApplicationPhase::Draining {
self.shared.drained.notify_one();
}
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Barrier};
use super::*;
fn test_runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.build()
.expect("test runtime must build")
}
#[test]
fn only_ready_applications_accept_requests() {
let requests = RequestLifecycle::new();
assert_eq!(
requests.try_accept().unwrap_err().code(),
"runtime.not_ready"
);
requests.mark_ready();
let request = requests.try_accept().expect("ready request is admitted");
requests.begin_draining();
assert_eq!(requests.phase(), ApplicationPhase::Draining);
assert_eq!(
requests.try_accept().unwrap_err().code(),
"runtime.shutting_down"
);
drop(request);
test_runtime().block_on(requests.wait_until_drained());
requests.mark_stopped();
assert_eq!(requests.phase(), ApplicationPhase::Stopped);
}
#[test]
fn draining_waits_for_every_admitted_request() {
test_runtime().block_on(async {
let requests = RequestLifecycle::new();
requests.mark_ready();
let first = requests.try_accept().unwrap();
let second = requests.try_accept().unwrap();
requests.begin_draining();
let requests_for_waiter = requests.clone();
let waiter = tokio::spawn(async move {
requests_for_waiter.wait_until_drained().await;
});
tokio::task::yield_now().await;
assert!(!waiter.is_finished());
drop(first);
tokio::task::yield_now().await;
assert!(!waiter.is_finished());
drop(second);
waiter.await.unwrap();
});
}
#[test]
fn admission_racing_with_drain_never_leaks_a_request() {
const WORKERS: usize = 8;
let requests = RequestLifecycle::new();
requests.mark_ready();
let barrier = Arc::new(Barrier::new(WORKERS + 1));
let workers: Vec<_> = (0..WORKERS)
.map(|_| {
let requests = requests.clone();
let barrier = Arc::clone(&barrier);
std::thread::spawn(move || {
let admitted_before_drain = requests.try_accept().unwrap();
barrier.wait();
loop {
match requests.try_accept() {
Ok(request) => drop(request),
Err(error) => {
assert_eq!(error.code(), "runtime.shutting_down");
drop(admitted_before_drain);
break;
}
}
}
})
})
.collect();
barrier.wait();
requests.begin_draining();
for worker in workers {
worker.join().unwrap();
}
test_runtime().block_on(requests.wait_until_drained());
assert_eq!(count(requests.shared.state.load(Ordering::Acquire)), 0);
}
#[test]
fn guard_completion_is_safe_from_another_thread() {
let requests = RequestLifecycle::new();
requests.mark_ready();
let guard = requests.try_accept().unwrap();
requests.begin_draining();
std::thread::spawn(move || drop(guard)).join().unwrap();
test_runtime().block_on(requests.wait_until_drained());
assert_eq!(Arc::strong_count(&requests.shared), 1);
}
}