use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RedialBackoff {
initial: Duration,
max: Duration,
current: Duration,
}
impl RedialBackoff {
#[must_use]
pub fn new(initial: Duration, max: Duration) -> Self {
let max = max.max(initial);
Self {
initial,
max,
current: initial,
}
}
#[must_use]
pub const fn current(&self) -> Duration {
self.current
}
pub fn increase(&mut self) {
let doubled = self.current.saturating_mul(2);
self.current = doubled.min(self.max);
}
pub fn reset(&mut self) {
self.current = self.initial;
}
}
#[derive(Clone, Debug)]
pub struct CandidateCursor {
candidates: Vec<String>,
index: usize,
}
impl CandidateCursor {
pub fn new(candidates: Vec<String>) -> Result<Self, RedialError> {
if candidates.is_empty() {
return Err(RedialError::NoCandidates);
}
Ok(Self {
candidates,
index: 0,
})
}
#[must_use]
pub fn current(&self) -> &str {
self.candidates
.get(self.index)
.or_else(|| self.candidates.first())
.map_or("", String::as_str)
}
pub fn advance(&mut self) {
self.index = (self.index + 1) % self.candidates.len();
}
}
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum RedialError {
#[error("liminal worker redial requires at least one candidate address")]
NoCandidates,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ServeResult {
Stopped,
Dropped {
served_work: bool,
},
}
pub fn run_redial_loop<Conn, Connect, Serve, Sleep, Stop, IsRetryable, Err>(
cursor: &mut CandidateCursor,
backoff: &mut RedialBackoff,
mut connect: Connect,
mut serve: Serve,
mut sleep: Sleep,
mut stop: Stop,
is_retryable: IsRetryable,
) -> Result<(), Err>
where
Connect: FnMut(&str) -> Result<Conn, Err>,
Serve: FnMut(Conn) -> ServeResult,
Sleep: FnMut(Duration),
Stop: FnMut() -> bool,
IsRetryable: Fn(&Err) -> bool,
{
loop {
if stop() {
return Ok(());
}
match connect(cursor.current()) {
Ok(connection) => match serve(connection) {
ServeResult::Stopped => return Ok(()),
ServeResult::Dropped { served_work } => {
if served_work {
backoff.reset();
}
cursor.advance();
}
},
Err(error) => {
if !is_retryable(&error) {
return Err(error);
}
cursor.advance();
sleep(backoff.current());
backoff.increase();
}
}
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::time::Duration;
use super::{CandidateCursor, RedialBackoff, RedialError, ServeResult, run_redial_loop};
fn cursor(addresses: &[&str]) -> Result<CandidateCursor, RedialError> {
CandidateCursor::new(
addresses
.iter()
.map(|address| (*address).to_owned())
.collect(),
)
}
#[test]
fn empty_candidate_list_is_rejected() {
let error = CandidateCursor::new(Vec::new()).err();
assert_eq!(error, Some(RedialError::NoCandidates));
}
#[test]
fn cursor_advances_and_wraps() -> Result<(), RedialError> {
let mut ring = cursor(&["a", "b", "c"])?;
assert_eq!(ring.current(), "a");
ring.advance();
assert_eq!(ring.current(), "b");
ring.advance();
assert_eq!(ring.current(), "c");
ring.advance();
assert_eq!(
ring.current(),
"a",
"advancing past the last wraps to the first"
);
Ok(())
}
#[test]
fn backoff_grows_then_resets() {
let mut backoff = RedialBackoff::new(Duration::from_millis(10), Duration::from_millis(40));
assert_eq!(backoff.current(), Duration::from_millis(10));
backoff.increase();
assert_eq!(backoff.current(), Duration::from_millis(20));
backoff.increase();
assert_eq!(backoff.current(), Duration::from_millis(40));
backoff.increase();
assert_eq!(
backoff.current(),
Duration::from_millis(40),
"saturates at max"
);
backoff.reset();
assert_eq!(
backoff.current(),
Duration::from_millis(10),
"served work resets the schedule"
);
}
#[test]
fn backoff_max_is_clamped_up_to_initial() {
let backoff = RedialBackoff::new(Duration::from_millis(50), Duration::from_millis(10));
assert_eq!(backoff.current(), Duration::from_millis(50));
}
#[test]
fn redial_migrates_to_survivor_on_owner_drop() -> Result<(), RedialError> {
let owner = "owner:1";
let survivor = "survivor:2";
let mut ring = cursor(&[owner, survivor])?;
let mut backoff = RedialBackoff::new(Duration::from_millis(1), Duration::from_millis(4));
let dialed: RefCell<Vec<String>> = RefCell::new(Vec::new());
let connect = |address: &str| -> Result<String, &'static str> {
dialed.borrow_mut().push(address.to_owned());
Ok(address.to_owned())
};
let serve = |connection: String| {
if connection == owner {
ServeResult::Dropped { served_work: true }
} else {
ServeResult::Stopped
}
};
let dial_cap = 8usize;
let stop = || dialed.borrow().len() >= dial_cap;
let result = run_redial_loop(
&mut ring,
&mut backoff,
connect,
serve,
|_| {},
stop,
|_err| true,
);
assert_eq!(result, Ok(()));
let sequence = dialed.borrow();
assert_eq!(
sequence.as_slice(),
&[owner.to_owned(), survivor.to_owned()],
"on owner drop the worker must migrate to (re-register in) the survivor"
);
Ok(())
}
#[test]
fn retryable_connect_failure_backs_off_then_succeeds() -> Result<(), RedialError> {
let mut ring = cursor(&["down", "up"])?;
let mut backoff = RedialBackoff::new(Duration::from_millis(1), Duration::from_millis(4));
let sleeps: RefCell<Vec<Duration>> = RefCell::new(Vec::new());
let attempts = RefCell::new(0u32);
let connect = |address: &str| -> Result<String, &'static str> {
*attempts.borrow_mut() += 1;
if address == "down" {
Err("listener not up yet")
} else {
Ok(address.to_owned())
}
};
let serve = |_connection: String| ServeResult::Stopped;
let result = run_redial_loop(
&mut ring,
&mut backoff,
connect,
serve,
|duration| sleeps.borrow_mut().push(duration),
|| false,
|_err| true,
);
assert_eq!(result, Ok(()));
assert!(
*attempts.borrow() >= 2,
"the down candidate failed before the up one served"
);
assert_eq!(
sleeps.borrow().first().copied(),
Some(Duration::from_millis(1)),
"the first connect failure backs off by the initial pause"
);
Ok(())
}
#[test]
fn non_retryable_connect_failure_is_surfaced() -> Result<(), RedialError> {
let mut ring = cursor(&["only"])?;
let mut backoff = RedialBackoff::new(Duration::from_millis(1), Duration::from_millis(4));
let connect = |_address: &str| -> Result<String, &'static str> { Err("rejected") };
let serve = |_connection: String| ServeResult::Stopped;
let result = run_redial_loop(
&mut ring,
&mut backoff,
connect,
serve,
|_| {},
|| false,
|_err| false,
);
assert_eq!(result, Err("rejected"));
Ok(())
}
#[test]
fn stop_before_first_dial_serves_nothing() -> Result<(), RedialError> {
let mut ring = cursor(&["a"])?;
let mut backoff = RedialBackoff::new(Duration::from_millis(1), Duration::from_millis(4));
let dialed = RefCell::new(false);
let connect = |_address: &str| -> Result<(), &'static str> {
*dialed.borrow_mut() = true;
Ok(())
};
let serve = |()| ServeResult::Stopped;
let result = run_redial_loop(
&mut ring,
&mut backoff,
connect,
serve,
|_| {},
|| true,
|_err| true,
);
assert_eq!(result, Ok(()));
assert!(
!*dialed.borrow(),
"a stop before the first dial opens no connection"
);
Ok(())
}
}