use crate::clock::{monotonic_ns, rdtsc, ticks_per_us};
use crate::endpoint::ConnId;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Retry {
pub initial_ms: u32,
pub max_ms: u32,
pub multiplier: f64,
pub jitter: f64,
pub max_attempts: u32,
pub stable_ms: u32,
pub seed: u64,
pub background: bool,
}
impl Default for Retry {
fn default() -> Self {
Self {
initial_ms: 250,
max_ms: 30_000,
multiplier: 2.0,
jitter: 0.2,
max_attempts: 0,
stable_ms: 60_000,
seed: 0,
background: true,
}
}
}
impl Retry {
fn check(&self) -> Result<()> {
if self.initial_ms == 0 || self.max_ms < self.initial_ms {
return Err(Error::Config(format!(
"retry: initial_ms {} must be at least 1 and at most max_ms {}",
self.initial_ms, self.max_ms
)));
}
if !(self.multiplier >= 1.0 && self.multiplier.is_finite()) {
return Err(Error::Config(format!(
"retry: multiplier {} must be at least 1",
self.multiplier
)));
}
if !(0.0..=1.0).contains(&self.jitter) {
return Err(Error::Config(format!(
"retry: jitter {} must be 0 to 1",
self.jitter
)));
}
Ok(())
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn delay_ms(&self, attempt: u32, r: f64) -> u64 {
let exp = i32::try_from(attempt.saturating_sub(1)).unwrap_or(i32::MAX);
let base =
(f64::from(self.initial_ms) * self.multiplier.powi(exp)).min(f64::from(self.max_ms));
(base * self.jitter.mul_add(-r, 1.0)).round().max(1.0) as u64
}
}
pub trait Connector<D> {
fn start(&mut self, dial: D, background: bool) -> Result<ConnId>;
}
#[cfg(target_os = "linux")]
impl<E, D> Connector<D> for crate::driver::Driver<E>
where
E: crate::endpoint::Endpoint + Send + 'static,
D: crate::endpoint::Dial<Conn = E> + Send + 'static,
{
fn start(&mut self, dial: D, background: bool) -> Result<ConnId> {
if background {
self.connect_background(dial)
} else {
self.connect(dial)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LinkId(usize);
impl LinkId {
#[must_use]
pub const fn index(self) -> usize {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkState {
Waiting {
at_ns: i64,
attempt: u32,
},
Connecting(ConnId),
Open(ConnId),
Stopped,
GaveUp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Opened {
pub link: LinkId,
pub reconnect: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Closed {
Retry {
link: LinkId,
attempt: u32,
delay_ms: u64,
},
GaveUp {
link: LinkId,
attempts: u32,
},
Stopped {
link: LinkId,
},
}
type Factory<D> = Box<dyn FnMut() -> Result<D>>;
struct Link<D> {
make: Factory<D>,
state: LinkState,
attempts: u32,
opened_ns: i64,
was_open: bool,
stop: bool,
not_before: i64,
}
pub struct Reconnect<D> {
retry: Retry,
links: Vec<Link<D>>,
rng: u64,
waiting: usize,
next_check: u64,
check_ticks: u64,
cursor: usize,
}
const CHECK_US: f64 = 250.0;
impl<D> Reconnect<D> {
pub fn new(retry: Retry) -> Result<Self> {
retry.check()?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let check_ticks = (CHECK_US * ticks_per_us()) as u64;
Ok(Self {
retry,
links: Vec::new(),
rng: rng_state(retry.seed)?,
waiting: 0,
next_check: 0,
check_ticks,
cursor: 0,
})
}
pub fn add(&mut self, make: impl FnMut() -> Result<D> + 'static) -> LinkId {
self.links.push(Link {
make: Box::new(make),
state: LinkState::Waiting {
at_ns: i64::MIN,
attempt: 0,
},
attempts: 0,
opened_ns: 0,
was_open: false,
stop: false,
not_before: i64::MIN,
});
self.waiting += 1;
LinkId(self.links.len() - 1)
}
#[must_use]
pub fn state(&self, link: LinkId) -> Option<LinkState> {
self.links.get(link.0).map(|l| l.state)
}
#[must_use]
pub fn link_of(&self, id: ConnId) -> Option<LinkId> {
self.links
.iter()
.position(
|l| matches!(l.state, LinkState::Connecting(c) | LinkState::Open(c) if c == id),
)
.map(LinkId)
}
pub fn on_open(&mut self, id: ConnId) -> Option<Opened> {
self.on_open_at(id, monotonic_ns())
}
pub fn on_open_at(&mut self, id: ConnId, now_ns: i64) -> Option<Opened> {
let link = self.link_of(id)?;
let l = &mut self.links[link.0];
l.state = LinkState::Open(id);
l.opened_ns = now_ns;
let reconnect = l.was_open;
l.was_open = true;
Some(Opened { link, reconnect })
}
pub fn on_closed(&mut self, id: ConnId) -> Option<Closed> {
self.on_closed_at(id, monotonic_ns())
}
pub fn on_closed_at(&mut self, id: ConnId, now_ns: i64) -> Option<Closed> {
let link = self.link_of(id)?;
let stable_ns = i64::from(self.retry.stable_ms) * 1_000_000;
let l = &mut self.links[link.0];
if matches!(l.state, LinkState::Open(_)) && now_ns - l.opened_ns >= stable_ns {
l.attempts = 0;
}
if l.stop {
l.state = LinkState::Stopped;
return Some(Closed::Stopped { link });
}
Some(self.schedule(link, now_ns))
}
pub fn stop(&mut self, link: LinkId) {
let Some(l) = self.links.get_mut(link.0) else {
return;
};
l.stop = true;
if matches!(l.state, LinkState::Waiting { .. }) {
l.state = LinkState::Stopped;
self.waiting -= 1;
}
}
pub fn resume(&mut self, link: LinkId) {
let Some(l) = self.links.get_mut(link.0) else {
return;
};
l.stop = false;
if matches!(l.state, LinkState::Stopped | LinkState::GaveUp) {
l.attempts = 0;
l.state = LinkState::Waiting {
at_ns: l.not_before,
attempt: 0,
};
self.waiting += 1;
self.next_check = 0;
}
}
#[inline]
pub fn poll(&mut self, c: &mut impl Connector<D>) -> usize {
if self.waiting == 0 {
return 0;
}
let t = rdtsc();
if t < self.next_check {
return 0;
}
self.next_check = t.wrapping_add(self.check_ticks);
self.poll_at(c, monotonic_ns())
}
#[cold]
pub fn poll_at(&mut self, c: &mut impl Connector<D>, now_ns: i64) -> usize {
self.poll_at_gated(c, now_ns, || Ok(()))
}
#[inline]
pub fn poll_gated(
&mut self,
c: &mut impl Connector<D>,
gate: impl FnMut() -> std::result::Result<(), u64>,
) -> usize {
if self.waiting == 0 {
return 0;
}
let t = rdtsc();
if t < self.next_check {
return 0;
}
self.next_check = t.wrapping_add(self.check_ticks);
self.poll_at_gated(c, monotonic_ns(), gate)
}
#[cold]
pub fn poll_at_gated(
&mut self,
c: &mut impl Connector<D>,
now_ns: i64,
mut gate: impl FnMut() -> std::result::Result<(), u64>,
) -> usize {
let links = self.links.len();
let first = if self.cursor < links { self.cursor } else { 0 };
let longest = ms_to_ns(u64::from(self.retry.max_ms));
let bg = self.retry.background;
let mut started = 0;
for step in 0..links {
let i = (first + step) % links;
let LinkState::Waiting { at_ns, attempt } = self.links[i].state else {
continue;
};
if at_ns > now_ns {
continue;
}
if let Err(wait_ns) = gate() {
let wait = i64::try_from(wait_ns).unwrap_or(i64::MAX).min(longest);
self.links[i].state = LinkState::Waiting {
at_ns: now_ns.saturating_add(wait),
attempt,
};
continue;
}
self.cursor = (i + 1) % links;
self.waiting -= 1;
let l = &mut self.links[i];
match (l.make)().and_then(|d| c.start(d, bg)) {
Ok(id) => {
l.state = LinkState::Connecting(id);
started += 1;
}
Err(Error::Cpu(e)) => {
l.state = LinkState::GaveUp;
#[cfg(target_os = "linux")]
crate::warn::once(format!(
"ergon-io warning: Reconnect gave up link {i}: cpu: {e}"
));
#[cfg(not(target_os = "linux"))]
let _ = e;
}
Err(_) => {
let _ = self.schedule(LinkId(i), now_ns);
}
}
}
started
}
pub fn defer(&mut self, link: LinkId, wait_ns: u64) {
self.defer_at(link, wait_ns, monotonic_ns());
}
pub fn defer_at(&mut self, link: LinkId, wait_ns: u64, now_ns: i64) {
let Some(l) = self.links.get_mut(link.0) else {
return;
};
let until = now_ns.saturating_add(i64::try_from(wait_ns).unwrap_or(i64::MAX));
l.not_before = l.not_before.max(until);
if let LinkState::Waiting { at_ns, attempt } = l.state {
l.state = LinkState::Waiting {
at_ns: at_ns.max(l.not_before),
attempt,
};
}
}
fn schedule(&mut self, link: LinkId, now_ns: i64) -> Closed {
let r = self.random();
let l = &mut self.links[link.0];
let attempt = l.attempts + 1;
if self.retry.max_attempts > 0 && attempt > self.retry.max_attempts {
l.state = LinkState::GaveUp;
return Closed::GaveUp {
link,
attempts: l.attempts,
};
}
l.attempts = attempt;
let backoff_ms = self.retry.delay_ms(attempt, r);
let backoff_at = now_ns.saturating_add(ms_to_ns(backoff_ms));
let at_ns = backoff_at.max(l.not_before);
let delay_ms = if at_ns == backoff_at {
backoff_ms
} else {
u64::try_from(at_ns.saturating_sub(now_ns))
.unwrap_or(0)
.div_ceil(1_000_000)
};
l.state = LinkState::Waiting { at_ns, attempt };
self.waiting += 1;
Closed::Retry {
link,
attempt,
delay_ms,
}
}
const fn random(&mut self) -> f64 {
unit(&mut self.rng)
}
}
fn ms_to_ns(ms: u64) -> i64 {
i64::try_from(ms)
.unwrap_or(i64::MAX)
.saturating_mul(1_000_000)
}
pub(crate) fn rng_state(seed: u64) -> Result<u64> {
let seed = if seed == 0 {
let mut b = [0u8; 8];
ring::rand::SecureRandom::fill(&ring::rand::SystemRandom::new(), &mut b)
.map_err(|_| Error::Config("no system randomness".into()))?;
u64::from_le_bytes(b)
} else {
seed
};
let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
Ok(z.max(1))
}
#[allow(clippy::cast_precision_loss)]
pub(crate) const fn unit(rng: &mut u64) -> f64 {
let mut x = *rng;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*rng = x;
(x >> 11) as f64 / (1u64 << 53) as f64
}