use std::convert::Infallible;
use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use std::task::Poll;
use engenho_controllers::{Curve, Heartbeat, PanicMessage, Streak, TickClass};
use tracing::{error, warn};
use crate::child::Listener;
pub(crate) const REBIND: Curve = Curve::from_millis::<1_000, 60_000>();
pub(crate) async fn serve_rebinding<F, Fut>(
listener: Listener,
beat: Arc<Heartbeat>,
mut serve: F,
) -> Infallible
where
F: FnMut() -> Fut,
Fut: Future<Output = ()>,
{
let mut misses = Streak::new(REBIND);
loop {
beat.begin();
let began = tokio::time::Instant::now();
let ended = match contained(serve()).await {
Ok(()) => TickClass::Halted,
Err(panic) => {
error!(
listener = listener.name(),
%panic,
"listener's serve PANICKED and was contained; it holds nothing across \
attempts, so it binds again after the backoff"
);
TickClass::Panicked
}
};
beat.end(ended);
if began.elapsed() >= REBIND.cap() {
misses.reset();
}
let after = misses.miss();
warn!(
listener = listener.name(),
after_ms = u64::try_from(after.as_millis()).unwrap_or(u64::MAX),
"listener is not serving; binding again after the backoff"
);
tokio::time::sleep(after).await;
}
}
async fn contained<F: Future>(fut: F) -> Result<F::Output, PanicMessage> {
let mut fut = std::pin::pin!(fut);
std::future::poll_fn(move |cx| {
match std::panic::catch_unwind(AssertUnwindSafe(|| fut.as_mut().poll(cx))) {
Ok(poll) => poll.map(Ok),
Err(payload) => Poll::Ready(Err(PanicMessage::of(payload.as_ref()))),
}
})
.await
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use std::time::Duration;
use tokio::time::Instant;
use super::*;
struct Attempts {
began: Mutex<Vec<Instant>>,
serves_for: fn(usize) -> Duration,
}
impl Attempts {
fn new(serves_for: fn(usize) -> Duration) -> Arc<Self> {
Arc::new(Self {
began: Mutex::new(Vec::new()),
serves_for,
})
}
async fn attempt(self: Arc<Self>) {
let index = {
let mut began = self.began.lock().unwrap();
began.push(Instant::now());
began.len() - 1
};
tokio::time::sleep((self.serves_for)(index)).await;
}
fn gaps(&self) -> Vec<Duration> {
let began = self.began.lock().unwrap();
began.windows(2).map(|w| w[1] - w[0]).collect()
}
}
async fn run_for(attempts: &Arc<Attempts>, beat: &Arc<Heartbeat>, run_for: Duration) {
let task = tokio::spawn(serve_rebinding(Listener::KubeletHttp, beat.clone(), {
let attempts = attempts.clone();
move || attempts.clone().attempt()
}));
tokio::time::sleep(run_for).await;
task.abort();
let _ = task.await;
}
#[tokio::test(start_paused = true)]
async fn a_listener_that_cannot_bind_retries_on_a_growing_curve() {
let attempts = Attempts::new(|_| Duration::ZERO);
let beat = Arc::new(Heartbeat::new());
run_for(&attempts, &beat, Duration::from_secs(600)).await;
let gaps = attempts.gaps();
assert_eq!(gaps.first().copied(), Some(REBIND.base()), "{gaps:?}");
assert!(gaps.len() >= 8, "ten minutes held only {gaps:?}");
for pair in gaps.windows(2) {
let (a, b) = (pair[0], pair[1]);
assert!(b <= REBIND.cap(), "{b:?} is past the cap: {gaps:?}");
if a < REBIND.cap() {
assert!(
b == REBIND.cap() || b.as_micros() * 10 >= a.as_micros() * 18,
"{a:?} then {b:?}: the rebind gap did not grow toward the cap: {gaps:?}"
);
} else {
assert_eq!(b, REBIND.cap(), "past the cap the gap holds: {gaps:?}");
}
}
assert!(
gaps.iter().filter(|g| **g == REBIND.cap()).count() >= 3,
"ten minutes of failed binds never reached the cap: {gaps:?}"
);
let snap = beat.snapshot();
assert_eq!(
usize::try_from(snap.ticks_started).unwrap(),
gaps.len() + 1,
"every attempt is a beat: {snap:?}"
);
assert_eq!(snap.last_class, Some(TickClass::Halted), "{snap:?}");
}
#[tokio::test(start_paused = true)]
async fn a_serve_that_stayed_up_starts_the_curve_again() {
let attempts = Attempts::new(|i| if i == 4 { REBIND.cap() } else { Duration::ZERO });
let beat = Arc::new(Heartbeat::new());
run_for(&attempts, &beat, Duration::from_secs(120)).await;
let gaps = attempts.gaps();
assert_eq!(
gaps.get(..4),
Some(&[1, 2, 4, 8].map(Duration::from_secs)[..]),
"{gaps:?}"
);
assert_eq!(
gaps.get(4).copied(),
Some(REBIND.cap() + REBIND.base()),
"after serving for the cap the next bind waits only the base: {gaps:?}"
);
}
#[tokio::test(start_paused = true)]
async fn the_heartbeat_says_serving_or_waiting() {
let attempts = Attempts::new(|i| {
if i == 0 {
Duration::from_secs(10)
} else {
Duration::ZERO
}
});
let beat = Arc::new(Heartbeat::new());
let task = tokio::spawn(serve_rebinding(Listener::EtcdFacade, beat.clone(), {
let attempts = attempts.clone();
move || attempts.clone().attempt()
}));
tokio::time::sleep(Duration::from_secs(5)).await;
let serving = beat.snapshot();
tokio::time::sleep(Duration::from_millis(5_500)).await;
let waiting = beat.snapshot();
task.abort();
let _ = task.await;
assert!(serving.in_flight(), "{serving:?}");
assert_eq!(serving.last_class, None, "{serving:?}");
assert!(!waiting.in_flight(), "{waiting:?}");
assert_eq!(waiting.last_class, Some(TickClass::Halted), "{waiting:?}");
}
#[tokio::test(start_paused = true)]
async fn a_serve_that_panics_is_counted_and_binds_again() {
let beat = Arc::new(Heartbeat::new());
let attempts = Attempts::new(|_| Duration::ZERO);
let task = tokio::spawn(serve_rebinding(Listener::EtcdFacade, beat.clone(), {
let attempts = attempts.clone();
move || {
let attempts = attempts.clone();
async move {
attempts.attempt().await;
panic!("the serve attempt tripped over something");
}
}
}));
tokio::time::sleep(Duration::from_millis(3_500)).await;
let waiting = beat.snapshot();
let ended = task.is_finished();
task.abort();
let _ = task.await;
assert!(!ended, "a panicking attempt ended the listener's task");
assert_eq!(
attempts.gaps(),
[REBIND.base(), REBIND.base() * 2],
"a panicking attempt is retried on the rebind curve"
);
assert_eq!(waiting.panics, 3, "every panic is counted: {waiting:?}");
assert_eq!(waiting.ticks_started, 3, "{waiting:?}");
assert!(!waiting.in_flight(), "{waiting:?}");
assert_eq!(waiting.last_class, Some(TickClass::Panicked), "{waiting:?}");
}
}