use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SendError, Sender};
use std::sync::{Mutex, OnceLock, PoisonError};
use std::time::{Duration, Instant};
use ergon_tune::{Host, HostCheckPolicy};
use crate::checks::{self, DevChecks};
use crate::clock::monotonic_ns;
use crate::error::Error;
const EVERY_NS: i64 = 10_000_000_000;
static LAST_NS: AtomicI64 = AtomicI64::new(i64::MIN);
static HELD_BACK: AtomicU64 = AtomicU64::new(0);
static WRITER: OnceLock<Option<Sender<Msg>>> = OnceLock::new();
const RECHECK: Duration = Duration::from_secs(1);
pub type Job = Box<dyn FnOnce(&mut State) + Send>;
enum Msg {
Line(String),
Once(String),
Run(Job),
}
pub struct State {
host: Host,
dev: DevChecks,
seen: HashSet<String>,
last_recheck: Instant,
policy: HostCheckPolicy,
}
impl State {
fn new() -> Self {
let mut st = Self::at(Host::system());
st.policy = HostCheckPolicy::from_env().unwrap_or_default();
st
}
pub(crate) fn at(host: Host) -> Self {
Self {
host,
dev: DevChecks::new(),
seen: HashSet::new(),
last_recheck: Instant::now(),
policy: HostCheckPolicy::On,
}
}
#[allow(clippy::unused_self)] pub fn print(&self, line: String) {
eprintln!("{line}");
keep(line);
}
pub fn print_once(&mut self, line: String) {
if self.seen.insert(line.clone()) {
self.print(line);
}
}
pub fn first(&mut self, key: String) -> bool {
self.seen.insert(key)
}
pub fn check_device(&mut self, dev: &str, ctx: &checks::DevContext) {
let found = self.dev.check(&self.host, dev, ctx);
for l in self.policy.lines("ergon-io", &found) {
self.print_once(l);
}
}
fn handle(&mut self, m: Msg) {
match m {
Msg::Line(l) => self.print(l),
Msg::Once(l) => self.print_once(l),
Msg::Run(job) => job(self),
}
}
fn recheck_if_due(&mut self) {
if !self.dev.watching() || self.last_recheck.elapsed() < RECHECK {
return;
}
self.last_recheck = Instant::now();
self.recheck_now();
}
pub(crate) fn recheck_now(&mut self) {
let found = self.dev.recheck(&self.host);
for f in &found {
if self.policy == HostCheckPolicy::Off && f.is_warning() {
continue;
}
if let Some(l) = checks::line(f) {
if f.is_unknown() {
self.print_once(l);
} else {
self.print(l);
}
}
}
}
}
#[cfg(feature = "curl")]
pub fn prepare() {
WRITER.get_or_init(|| {
let host = Host::system();
let affinity = ergon_tune::thread_affinity(&host, ergon_tune::Thread::Current)
.map(|s| list(&s))
.unwrap_or_default();
let polling = affinity.first().copied().filter(|_| affinity.len() == 1);
let env = std::env::var(crate::placement::HELPER_CPUS_ENV).ok();
let (cpus, allowed) = match crate::placement::of(
&host,
&[],
env.as_deref(),
polling.and_then(|c| u32::try_from(c).ok()),
) {
Ok(p) => (list(&p.cpus), list(&p.allowed)),
Err(_) => (Vec::new(), list(&crate::placement::process_cpus(&host).0)),
};
start(&writer_masks(&cpus, &allowed, &affinity))
});
}
pub fn prepare_on(p: &crate::placement::Placement, cpus: &[usize]) {
WRITER.get_or_init(|| {
let avoid: Vec<usize> = p
.polling
.and_then(|c| usize::try_from(c).ok())
.into_iter()
.collect();
start(&writer_masks(cpus, &list(&p.allowed), &avoid))
});
}
fn list(set: &ergon_tune::CpuSet) -> Vec<usize> {
set.iter().filter_map(|c| usize::try_from(c).ok()).collect()
}
fn start(masks: &[Vec<usize>]) -> Option<Sender<Msg>> {
let (tx, which) = start_with(masks, &mut |cpus, serve| {
crate::sys::spawn_pinned("ergon-io-warn", cpus, serve)
})?;
if which > 0 {
let list = |m: &[usize]| {
m.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
};
let now = masks.get(which).map_or_else(String::new, |m| list(m));
let _ = tx.send(Msg::Once(format!(
"ergon-io info: ergon-io's warning thread could not be started on CPUs {} (the \
kernel refused that mask). It runs on {}; it is parked until there is work, and \
wakes once a second while it watches an egress device.",
masks.first().map_or_else(String::new, |m| list(m)),
if now.is_empty() {
"the CPUs of the thread that created the first driver or client".to_owned()
} else {
format!("those of CPUs {now} that the cpuset allows")
}
)));
}
Some(tx)
}
fn writer_masks(preferred: &[usize], allowed: &[usize], avoid: &[usize]) -> Vec<Vec<usize>> {
let others: Vec<usize> = allowed
.iter()
.copied()
.filter(|c| !avoid.contains(c))
.collect();
let mut out: Vec<Vec<usize>> = Vec::new();
for m in [preferred.to_vec(), others] {
if !m.is_empty() && !out.contains(&m) {
out.push(m);
}
}
out.push(Vec::new());
out
}
type Spawn<'a> = dyn FnMut(&[usize], Box<dyn FnOnce() + Send>) -> std::io::Result<()> + 'a;
fn start_with(masks: &[Vec<usize>], spawn: &mut Spawn<'_>) -> Option<(Sender<Msg>, usize)> {
masks.iter().enumerate().find_map(|(i, cpus)| {
let (tx, rx) = mpsc::channel::<Msg>();
spawn(cpus, Box::new(move || serve(&rx)))
.ok()
.map(|()| (tx, i))
})
}
fn serve(rx: &Receiver<Msg>) {
let mut st = State::new();
loop {
let msg = if st.dev.watching() {
match rx.recv_timeout(RECHECK) {
Ok(m) => Some(m),
Err(RecvTimeoutError::Timeout) => None,
Err(RecvTimeoutError::Disconnected) => return,
}
} else {
match rx.recv() {
Ok(m) => Some(m),
Err(_) => return,
}
};
if let Some(m) = msg {
st.handle(m);
}
st.recheck_if_due();
}
}
fn send(m: Msg) {
deliver(WRITER.get().and_then(Option::as_ref), m);
}
static SKIPPED: AtomicBool = AtomicBool::new(false);
fn deliver(writer: Option<&Sender<Msg>>, m: Msg) {
let m = match writer {
Some(tx) => match tx.send(m) {
Ok(()) => return,
Err(SendError(m)) => m,
},
None => m,
};
let line = match m {
Msg::Line(l) | Msg::Once(l) => l,
Msg::Run(_) => {
if SKIPPED.swap(true, Ordering::Relaxed) {
return;
}
"ergon-io warning: ergon-io's warning thread is not running, so the work queued to \
it is skipped, never done on the polling thread: the route lookups of \
quickack_check=warn (those connections' RouteCheck stays Pending), the NAPI-id check \
and the egress-device checks."
.to_owned()
}
};
eprintln!("{line}");
keep(line);
}
pub fn emit(line: String) {
send(Msg::Line(line));
}
pub fn once(line: String) {
send(Msg::Once(line));
}
pub fn run(job: impl FnOnce(&mut State) + Send + 'static) {
send(Msg::Run(Box::new(job)));
}
pub fn connect_failure(what: &str, error: &Error) {
let now = monotonic_ns();
let last = LAST_NS.load(Ordering::Relaxed);
let due = last == i64::MIN || now - last >= EVERY_NS;
if !due
|| LAST_NS
.compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
.is_err()
{
HELD_BACK.fetch_add(1, Ordering::Relaxed);
return;
}
let held = HELD_BACK.swap(0, Ordering::Relaxed);
emit(format!(
"ergon-io warning: {what}: {error}{}; retrying (implement \
ClientHandler::on_connect_error to handle this yourself)",
if held > 0 {
format!(" ({held} more since the last warning)")
} else {
String::new()
}
));
}
#[must_use]
pub fn flush(timeout: Duration) -> bool {
let (tx, rx) = mpsc::channel::<()>();
run(move |_| {
let _ = tx.send(());
});
rx.recv_timeout(timeout).is_ok()
}
static KEEP: AtomicBool = AtomicBool::new(false);
static KEPT: Mutex<Vec<String>> = Mutex::new(Vec::new());
fn keep(line: String) {
if KEEP.load(Ordering::Relaxed) {
KEPT.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(line);
}
}
#[doc(hidden)]
pub fn keep_lines() {
KEEP.store(true, Ordering::Relaxed);
}
#[doc(hidden)]
#[must_use]
pub fn take_lines() -> Vec<String> {
std::mem::take(&mut *KEPT.lock().unwrap_or_else(PoisonError::into_inner))
}
#[doc(hidden)]
pub fn forget_seen() {
run(|st| {
st.seen.clear();
});
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use super::{Msg, deliver, start_with, writer_masks};
type R = Result<(), Box<dyn std::error::Error>>;
fn einval() -> std::io::Error {
std::io::Error::from_raw_os_error(libc::EINVAL)
}
#[test]
fn the_writer_starts_when_the_housekeeping_cpus_are_outside_the_cpuset() -> R {
let allowed = [2_usize, 3];
let masks = writer_masks(&[0, 1], &[0, 1, 2, 3, 4, 5, 6, 7], &[2]);
let mut tried = Vec::new();
let started = start_with(&masks, &mut |cpus, serve| {
tried.push(cpus.to_vec());
if !cpus.is_empty() && !cpus.iter().any(|c| allowed.contains(c)) {
return Err(einval());
}
std::thread::spawn(serve);
Ok(())
});
let (tx, which) = started.ok_or("the writer did not start")?;
assert_eq!(tried[0], vec![0, 1], "housekeeping first");
assert!(which > 0, "{tried:?}");
assert!(!tried[which].contains(&2), "{tried:?}");
let (done, got) = std::sync::mpsc::channel();
tx.send(Msg::Run(Box::new(move |_| {
let _ = done.send(std::thread::current().id());
})))?;
let ran_on = got.recv_timeout(Duration::from_secs(5))?;
assert_ne!(ran_on, std::thread::current().id());
Ok(())
}
#[test]
fn the_writer_avoids_every_cpu_an_unpinned_creator_may_run_on() {
assert_eq!(
writer_masks(&[], &[0, 1, 2, 3, 4], &[0, 1, 2]),
vec![vec![3, 4], vec![]]
);
}
#[test]
fn a_single_cpu_cpuset_falls_back_to_the_creating_threads_mask() -> R {
let masks = writer_masks(&[0, 1, 2, 3, 4, 6, 7], &[0, 1, 2, 3, 4, 5, 6, 7], &[5]);
let mut tried = Vec::new();
let started = start_with(&masks, &mut |cpus, serve| {
tried.push(cpus.to_vec());
if !cpus.is_empty() && !cpus.contains(&5) {
return Err(einval());
}
std::thread::spawn(serve);
Ok(())
});
let (_tx, which) = started.ok_or("the writer did not start")?;
assert_eq!(tried.last(), Some(&Vec::new()), "{tried:?}");
assert_eq!(tried.len(), which + 1);
let mut unique = tried.clone();
unique.dedup();
assert_eq!(unique, tried);
Ok(())
}
#[test]
fn without_a_writer_work_is_not_done_on_the_calling_thread() {
let ran = Arc::new(AtomicBool::new(false));
let r = Arc::clone(&ran);
deliver(
None,
Msg::Run(Box::new(move |_| r.store(true, Ordering::Relaxed))),
);
assert!(!ran.load(Ordering::Relaxed));
}
}