use std::net::{IpAddr, SocketAddr};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, TryRecvError};
use std::time::Duration;
use std::os::fd::RawFd;
use crate::clock::monotonic_ns;
use crate::dns::{DnsConfig, Lookup, Resolver};
use crate::endpoint::ConnId;
use crate::endpoint::sealed::DialIo;
use crate::error::{Error, Result};
use crate::route::{self, QuickAckCheck, RouteCheck};
pub(super) trait BgDial<E>: Send {
fn host(&self) -> &str;
fn port(&self) -> u16;
fn addr(&self) -> Option<SocketAddr>;
fn start(&mut self, addr: SocketAddr) -> Result<()>;
fn step(&mut self) -> Result<Option<E>>;
fn awaiting_route(&self) -> Option<RawFd>;
fn route_checked(&mut self, check: RouteCheck);
}
impl<E, D: DialIo<Conn = E> + Send> BgDial<E> for D {
fn host(&self) -> &str {
DialIo::host(self)
}
fn port(&self) -> u16 {
DialIo::port(self)
}
fn addr(&self) -> Option<SocketAddr> {
DialIo::addr(self)
}
fn start(&mut self, addr: SocketAddr) -> Result<()> {
DialIo::start(self, addr)
}
fn step(&mut self) -> Result<Option<E>> {
DialIo::step(self)
}
fn awaiting_route(&self) -> Option<RawFd> {
DialIo::awaiting_route(self)
}
fn route_checked(&mut self, check: RouteCheck) {
DialIo::route_checked(self, check);
}
}
pub(super) type BoxDial<E> = Box<dyn BgDial<E>>;
pub(super) type Prepare<E> = Box<dyn Fn(&mut E, ConnId) -> Result<()> + Send>;
struct Job<E> {
slot: usize,
generation: u32,
dial: BoxDial<E>,
prepare: Prepare<E>,
deadline_ns: i64,
started: bool,
}
enum Msg<E> {
Dial(Job<E>),
Free(E),
}
pub(super) struct Done<E> {
pub(super) slot: usize,
pub(super) generation: u32,
pub(super) result: Result<E>,
}
pub(super) struct Background<E> {
msgs: Sender<Msg<E>>,
done: Receiver<Done<E>>,
outstanding: Vec<(usize, u32)>,
}
impl<E: Send + 'static> Background<E> {
pub(super) fn start(cpus: &[usize], dns: DnsConfig, step: Duration) -> Result<Self> {
let (msgs, msg_rx) = mpsc::channel::<Msg<E>>();
let (done_tx, done) = mpsc::channel::<Done<E>>();
crate::sys::spawn_pinned("ergon-io-dial", cpus, move || {
run(&msg_rx, &done_tx, Resolver::new(dns), step);
})?;
Ok(Self {
msgs,
done,
outstanding: Vec::new(),
})
}
pub(super) fn submit(
&mut self,
slot: usize,
generation: u32,
dial: BoxDial<E>,
prepare: Prepare<E>,
timeout: Duration,
) -> Result<()> {
self.msgs
.send(Msg::Dial(Job {
slot,
generation,
dial,
prepare,
deadline_ns: monotonic_ns()
.saturating_add(i64::try_from(timeout.as_nanos()).unwrap_or(i64::MAX)),
started: false,
}))
.map_err(|_| Error::Config("background connect thread stopped".into()))?;
self.outstanding.push((slot, generation));
Ok(())
}
}
impl<E> Background<E> {
pub(super) fn free(&self, conn: E) {
let _ = self.msgs.send(Msg::Free(conn));
}
pub(super) const fn has_outstanding(&self) -> bool {
!self.outstanding.is_empty()
}
pub(super) fn try_take(&mut self) -> Option<Done<E>> {
match self.done.try_recv() {
Ok(d) => {
self.outstanding.retain(|&o| o != (d.slot, d.generation));
Some(d)
}
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => {
self.outstanding.pop().map(|(slot, generation)| Done {
slot,
generation,
result: Err(Error::Config("background connect thread stopped".into())),
})
}
}
}
}
fn run<E>(msgs: &Receiver<Msg<E>>, done: &Sender<Done<E>>, mut resolver: Resolver, step: Duration) {
let mut active: Vec<Job<E>> = Vec::new();
let mut alive = true;
let take = |m: Msg<E>, active: &mut Vec<Job<E>>| match m {
Msg::Dial(j) => active.push(j),
Msg::Free(c) => drop(c),
};
loop {
if active.is_empty() {
match msgs.recv() {
Ok(m) => take(m, &mut active),
Err(_) => return,
}
}
loop {
match msgs.try_recv() {
Ok(m) => take(m, &mut active),
Err(TryRecvError::Disconnected) if active.is_empty() => return,
Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
}
}
if active.is_empty() {
continue;
}
let now = monotonic_ns();
resolver.poll(now);
let mut i = 0;
while i < active.len() {
match advance(&mut active[i], &mut resolver, now) {
Ok(None) => i += 1,
result => {
let job = active.swap_remove(i);
let id =
ConnId::new(u32::try_from(job.slot).unwrap_or(u32::MAX), job.generation);
let result = result
.and_then(|c| c.ok_or(Error::Closed))
.and_then(|mut c| (job.prepare)(&mut c, id).map(|()| c));
let _ = done.send(Done {
slot: job.slot,
generation: job.generation,
result,
});
}
}
}
if !active.is_empty() {
if alive {
match msgs.recv_timeout(step) {
Ok(m) => take(m, &mut active),
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => alive = false,
}
} else {
std::thread::sleep(step);
}
}
}
}
fn advance<E>(job: &mut Job<E>, resolver: &mut Resolver, now: i64) -> Result<Option<E>> {
if now > job.deadline_ns {
return Err(Error::Timeout("connect"));
}
if !job.started {
let addr = match job.dial.addr() {
Some(a) => a,
None => match resolver.lookup(job.dial.host(), now)? {
Lookup::Ready(ip) => SocketAddr::new(IpAddr::V4(ip), job.dial.port()),
Lookup::Pending => return Ok(None),
},
};
job.dial.start(addr)?;
job.started = true;
}
let r = job.dial.step();
if let (Ok(None), Some(fd)) = (&r, job.dial.awaiting_route()) {
let found = route::check_socket(QuickAckCheck::Require, fd)
.map_err(|e| Error::QuickAck(Box::new(e)))?;
job.dial.route_checked(found);
}
r
}
#[cfg(test)]
pub fn dial_helper_test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[cfg(test)]
mod tests {
use std::time::Instant;
use super::*;
struct Boom;
impl BgDial<u8> for Boom {
fn host(&self) -> &'static str {
"boom.test"
}
fn port(&self) -> u16 {
1
}
fn addr(&self) -> Option<SocketAddr> {
Some(SocketAddr::from(([127, 0, 0, 1], 1)))
}
fn start(&mut self, _: SocketAddr) -> Result<()> {
Ok(())
}
#[allow(clippy::panic)]
fn step(&mut self) -> Result<Option<u8>> {
panic!("dial step panicked");
}
fn awaiting_route(&self) -> Option<RawFd> {
None
}
fn route_checked(&mut self, _: RouteCheck) {}
}
#[test]
fn a_stopped_helper_fails_every_outstanding_connect() -> Result<()> {
let _helpers = super::dial_helper_test_lock();
let mut bg = Background::<u8>::start(
&[],
DnsConfig::system(),
crate::placement::DIAL_STEP_OWN_CPU,
)?;
bg.submit(
3,
7,
Box::new(Boom),
Box::new(|_, _| Ok(())),
Duration::from_secs(5),
)?;
bg.submit(
4,
9,
Box::new(Boom),
Box::new(|_, _| Ok(())),
Duration::from_secs(5),
)?;
let end = Instant::now() + Duration::from_secs(5);
let mut failed = Vec::new();
while bg.has_outstanding() {
if let Some(d) = bg.try_take() {
assert!(d.result.is_err());
failed.push((d.slot, d.generation));
}
assert!(Instant::now() < end, "outstanding connects never reported");
}
failed.sort_unstable();
assert_eq!(failed, vec![(3, 7), (4, 9)]);
Ok(())
}
type R<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
fn dial_threads() -> R<Vec<String>> {
let mut out = Vec::new();
for e in std::fs::read_dir("/proc/self/task")? {
let p = e?.path();
if std::fs::read_to_string(p.join("comm")).is_ok_and(|c| c.trim() == "ergon-io-dial") {
out.extend(p.file_name().and_then(|n| n.to_str()).map(str::to_owned));
}
}
Ok(out)
}
fn started_helper(before: &[String]) -> R<String> {
let end = Instant::now() + Duration::from_secs(2);
loop {
if let Some(t) = dial_threads()?.into_iter().find(|t| !before.contains(t)) {
return Ok(t);
}
if Instant::now() > end {
return Err("the dial helper never started".into());
}
std::thread::sleep(Duration::from_millis(1));
}
}
struct Hang;
impl BgDial<u8> for Hang {
fn host(&self) -> &'static str {
"hang.test"
}
fn port(&self) -> u16 {
1
}
fn addr(&self) -> Option<SocketAddr> {
Some(SocketAddr::from(([127, 0, 0, 1], 1)))
}
fn start(&mut self, _: SocketAddr) -> Result<()> {
Ok(())
}
fn step(&mut self) -> Result<Option<u8>> {
Ok(None)
}
fn awaiting_route(&self) -> Option<RawFd> {
None
}
fn route_checked(&mut self, _: RouteCheck) {}
}
fn voluntary_switches(tid: &str) -> R<u64> {
let status = std::fs::read_to_string(format!("/proc/self/task/{tid}/status"))?;
Ok(status
.lines()
.find_map(|l| l.strip_prefix("voluntary_ctxt_switches:"))
.ok_or("no voluntary_ctxt_switches")?
.trim()
.parse()?)
}
#[test]
fn an_idle_helper_never_wakes_and_stops_when_dropped() -> R {
let _helpers = super::dial_helper_test_lock();
let before = dial_threads()?;
let bg = Background::<u8>::start(
&[],
DnsConfig::system(),
crate::placement::DIAL_STEP_OWN_CPU,
)?;
let tid = started_helper(&before)?;
std::thread::sleep(Duration::from_millis(50));
let first = voluntary_switches(&tid)?;
std::thread::sleep(Duration::from_millis(300));
let woke = voluntary_switches(&tid)?.saturating_sub(first);
assert!(woke <= 2, "the idle helper woke {woke} times in 300 ms");
drop(bg);
let end = Instant::now() + Duration::from_secs(2);
while std::path::Path::new(&format!("/proc/self/task/{tid}")).exists() {
assert!(Instant::now() < end, "the helper outlived its driver");
std::thread::sleep(Duration::from_millis(1));
}
Ok(())
}
#[test]
fn a_parked_helper_wakes_for_work() -> R {
let _helpers = super::dial_helper_test_lock();
let mut bg = Background::<u8>::start(
&[],
DnsConfig::system(),
crate::placement::DIAL_STEP_OWN_CPU,
)?;
std::thread::sleep(Duration::from_millis(50));
bg.submit(
1,
1,
Box::new(Boom),
Box::new(|_, _| Ok(())),
Duration::from_secs(5),
)?;
let end = Instant::now() + Duration::from_secs(5);
while bg.has_outstanding() {
if let Some(d) = bg.try_take() {
assert_eq!((d.slot, d.generation), (1, 1));
assert!(d.result.is_err());
}
assert!(
Instant::now() < end,
"the parked helper never took the dial"
);
}
Ok(())
}
#[test]
fn a_dial_in_flight_steps_at_the_given_interval() -> R {
let _helpers = super::dial_helper_test_lock();
let before = dial_threads()?;
let mut bg = Background::<u8>::start(&[], DnsConfig::system(), Duration::from_millis(1))?;
let tid = started_helper(&before)?;
bg.submit(
1,
1,
Box::new(Hang),
Box::new(|_, _| Ok(())),
Duration::from_secs(5),
)?;
std::thread::sleep(Duration::from_millis(50));
let first = voluntary_switches(&tid)?;
std::thread::sleep(Duration::from_millis(300));
let woke = voluntary_switches(&tid)?.saturating_sub(first);
assert!(
woke <= 500,
"the helper woke {woke} times in 300 ms while a dial was in flight"
);
Ok(())
}
#[test]
fn work_arrives_at_once_while_a_dial_is_in_flight() -> R {
let _helpers = super::dial_helper_test_lock();
let mut bg = Background::<u8>::start(&[], DnsConfig::system(), Duration::from_millis(200))?;
bg.submit(
1,
1,
Box::new(Hang),
Box::new(|_, _| Ok(())),
Duration::from_secs(5),
)?;
std::thread::sleep(Duration::from_millis(20));
let t0 = Instant::now();
bg.submit(
2,
2,
Box::new(Boom),
Box::new(|_, _| Ok(())),
Duration::from_secs(5),
)?;
let end = Instant::now() + Duration::from_millis(50);
let mut got = false;
while !got {
if let Some(d) = bg.try_take() {
assert!(d.result.is_err());
got = (d.slot, d.generation) == (2, 2);
}
assert!(
Instant::now() < end,
"the in-flight dial delayed new work past {t0:?}"
);
}
Ok(())
}
}