use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use smb2::client::{Connection, Session, Tree};
use smb2::msg::echo::EchoRequest;
use smb2::types::Command;
fn env_or(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
fn env_num(key: &str, default: u64) -> u64 {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
#[derive(Debug, Clone, Copy)]
struct Probe {
seq: u64,
sent_ms: u128,
replied_ms: Option<u128>,
}
struct Config {
addr: String,
user: String,
pass: String,
share: String,
dir: String,
secs: u64,
probe_ms: u64,
probe_cap: Duration,
label: String,
}
fn config() -> Config {
Config {
addr: env_or("SMB2_HOST", "127.0.0.1:10445"),
user: env_or("SMB2_USER", ""),
pass: env_or("SMB2_PASS", ""),
share: env_or("SMB2_SHARE", "public"),
dir: env_or("LP_DIR", "liveness-probe"),
secs: env_num("LP_SECS", 120),
probe_ms: env_num("LP_PROBE_MS", 5000),
probe_cap: Duration::from_secs(env_num("LP_PROBE_CAP_SECS", 120)),
label: env_or("LP_LABEL", "run"),
}
}
async fn session_to(cfg: &Config) -> smb2::Result<(Connection, Tree)> {
let mut conn = Connection::connect(&cfg.addr, Duration::from_secs(10)).await?;
conn.negotiate().await?;
Session::setup(&mut conn, &cfg.user, &cfg.pass, "").await?;
let tree = Tree::connect(&mut conn, &cfg.share).await?;
Ok((conn, tree))
}
#[tokio::main]
async fn main() {
let _ = env_logger::try_init();
let cfg = config();
let t0 = Instant::now();
let cap = Duration::from_secs(cfg.secs) + cfg.probe_cap + Duration::from_secs(60);
tokio::spawn(async move {
tokio::time::sleep(cap).await;
eprintln!("liveness_probe: hard cap of {cap:?} reached; aborting");
std::process::exit(2);
});
println!(
"# liveness_probe label={} host={} share={} dir={} secs={} probe_ms={}",
cfg.label, cfg.addr, cfg.share, cfg.dir, cfg.secs, cfg.probe_ms
);
let (mut m_conn, m_tree) = session_to(&cfg).await.expect("measured session setup");
m_conn.set_keepalive(None);
m_conn.set_response_timeout(None);
let _ = m_tree.create_directory(&mut m_conn, &cfg.dir).await;
let mut m_watcher = m_tree
.watch(&mut m_conn, &cfg.dir, true)
.await
.expect("measured watch");
let m_events = Arc::new(AtomicU64::new(0));
let m_events_task = Arc::clone(&m_events);
tokio::spawn(async move {
loop {
match m_watcher.next_events().await {
Ok(evs) => {
m_events_task.fetch_add(evs.len() as u64, Ordering::Relaxed);
}
Err(e) => {
println!("MEASURED_WATCHER_ERR,{e}");
return;
}
}
}
});
let (mut s_conn, s_tree) = session_to(&cfg).await.expect("shipping session setup");
let s_dir = format!("{}-shipping", cfg.dir);
let _ = s_tree.create_directory(&mut s_conn, &s_dir).await;
let mut s_watcher = s_tree
.watch(&mut s_conn, &s_dir, true)
.await
.expect("shipping watch");
let s_fault: Arc<Mutex<Option<(u128, String)>>> = Arc::new(Mutex::new(None));
let s_fault_task = Arc::clone(&s_fault);
tokio::spawn(async move {
loop {
if let Err(e) = s_watcher.next_events().await {
*s_fault_task.lock().unwrap() = Some((t0.elapsed().as_millis(), format!("{e}")));
return;
}
}
});
let s_diag = s_conn.clone();
tokio::spawn(async move {
let mut last = (0u64, 0u64, 0u64);
loop {
tokio::time::sleep(Duration::from_millis(500)).await;
let d = s_diag.diagnostics();
let now = (
d.metrics.keepalive_probes_sent,
d.metrics.keepalive_failures,
d.metrics.keepalive_probes_skipped,
);
if now != last {
println!(
"SHIPPING_KEEPALIVE,{},{},{},{}",
t0.elapsed().as_millis(),
now.0,
now.1,
now.2
);
last = now;
}
}
});
let probes: Arc<Mutex<Vec<Probe>>> = Arc::new(Mutex::new(Vec::new()));
let deadline = t0 + Duration::from_secs(cfg.secs);
let mut seq = 0u64;
while Instant::now() < deadline {
let conn = m_conn.clone();
let probes = Arc::clone(&probes);
let cap = cfg.probe_cap;
let sent_ms = t0.elapsed().as_millis();
let my_seq = seq;
seq += 1;
tokio::spawn(async move {
let outcome =
tokio::time::timeout(cap, conn.execute(Command::Echo, &EchoRequest, None)).await;
let replied_ms = match outcome {
Ok(_) => Some(t0.elapsed().as_millis()),
Err(_) => None,
};
probes.lock().unwrap().push(Probe {
seq: my_seq,
sent_ms,
replied_ms,
});
});
tokio::time::sleep(Duration::from_millis(cfg.probe_ms)).await;
}
tokio::time::sleep(cfg.probe_cap.min(Duration::from_secs(15))).await;
let mut probes = probes.lock().unwrap().clone();
probes.sort_by_key(|p| p.seq);
for p in &probes {
match p.replied_ms {
Some(r) => println!(
"PROBE,{},{},{},{},answered",
cfg.label,
p.seq,
p.sent_ms,
r - p.sent_ms
),
None => println!("PROBE,{},{},{},,unanswered", cfg.label, p.seq, p.sent_ms),
}
}
let mut arrivals: Vec<u128> = probes.iter().filter_map(|p| p.replied_ms).collect();
arrivals.sort_unstable();
let mut prev = 0u128;
let mut max_gap = 0u128;
let mut gaps: Vec<u128> = Vec::new();
for a in &arrivals {
let gap = a - prev;
gaps.push(gap);
max_gap = max_gap.max(gap);
prev = *a;
}
let budget = cfg.probe_ms as u128;
let mut run = 0u32;
let mut worst_run = 0u32;
for p in &probes {
let missed = match p.replied_ms {
Some(r) => r - p.sent_ms > budget,
None => true,
};
if missed {
run += 1;
worst_run = worst_run.max(run);
} else {
run = 0;
}
}
let answered = probes.iter().filter(|p| p.replied_ms.is_some()).count();
println!(
"SUMMARY,{},probes={},answered={},within_{}ms={},longest_consecutive_miss={},max_silence_gap_ms={}",
cfg.label,
probes.len(),
answered,
budget,
probes
.iter()
.filter(|p| p.replied_ms.map(|r| r - p.sent_ms <= budget).unwrap_or(false))
.count(),
worst_run,
max_gap
);
let mut sorted = gaps.clone();
sorted.sort_unstable();
if !sorted.is_empty() {
println!(
"GAPS,{},p50={},p90={},p99={},max={}",
cfg.label,
sorted[sorted.len() / 2],
sorted[sorted.len() * 9 / 10],
sorted[sorted.len() * 99 / 100],
sorted[sorted.len() - 1]
);
}
match &*s_fault.lock().unwrap() {
Some((at, e)) => println!("SHIPPING_WATCHER,{},died_at_ms={at},{e}", cfg.label),
None => println!("SHIPPING_WATCHER,{},survived", cfg.label),
}
println!(
"MEASURED_WATCHER_EVENTS,{},{}",
cfg.label,
m_events.load(Ordering::Relaxed)
);
let _ = m_tree.delete_directory(&mut m_conn, &s_dir).await;
let _ = m_tree.delete_directory(&mut m_conn, &cfg.dir).await;
drop((s_conn, s_tree));
}