use crate::record::{write_atomic, write_atomic_with};
use crate::stop::StopToken;
use serde_json::{json, Value};
use std::io;
use std::path::PathBuf;
use std::thread::JoinHandle;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub fn spawn_ticker(
interval: Duration,
stop: StopToken,
mut tick: impl FnMut() + Send + 'static,
) -> io::Result<JoinHandle<()>> {
std::thread::Builder::new()
.name("rk-ticker".into())
.spawn(move || loop {
if stop.wait(interval).is_some() {
break;
}
tick();
})
}
pub fn heartbeat_record(state: Value) -> Value {
let updated_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
json!({"pid": std::process::id(), "updated_ms": updated_ms, "state": state})
}
pub fn spawn_heartbeat(
path: PathBuf,
interval: Duration,
stop: StopToken,
state: impl Fn() -> Value + Send + 'static,
mut on_error: impl FnMut(&io::Error) + Send + 'static,
) -> io::Result<JoinHandle<()>> {
let write = move |path: &PathBuf, state: &dyn Fn() -> Value| {
let body = serde_json::to_vec(&heartbeat_record(state()))
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
write_atomic(path, &body)
};
std::thread::Builder::new()
.name("rk-heartbeat".into())
.spawn(move || loop {
if let Err(e) = write(&path, &state) {
on_error(&e);
}
if stop.wait(interval).is_some() {
break;
}
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HeartbeatOptions {
pub interval: Duration,
pub poll: Duration,
}
impl Default for HeartbeatOptions {
fn default() -> Self {
Self {
interval: Duration::from_secs(5),
poll: Duration::from_secs(1),
}
}
}
pub fn spawn_heartbeat_with(
path: PathBuf,
options: HeartbeatOptions,
stop: StopToken,
state: impl Fn() -> Value + Send + 'static,
mut on_error: impl FnMut(&io::Error) + Send + 'static,
) -> io::Result<JoinHandle<()>> {
let poll = options
.poll
.max(Duration::from_millis(1))
.min(options.interval.max(Duration::from_millis(1)));
let interval = options.interval;
std::thread::Builder::new()
.name("rk-heartbeat".into())
.spawn(move || {
let mut last: Option<(Value, std::time::Instant)> = None;
loop {
let now_state = state();
let changed = last.as_ref().is_none_or(|(v, _)| *v != now_state);
let due = last.as_ref().is_none_or(|(_, at)| at.elapsed() >= interval);
if changed || due {
let written = serde_json::to_vec(&heartbeat_record(now_state.clone()))
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
.and_then(|body| write_atomic_with(&path, &body, changed));
match written {
Ok(()) => last = Some((now_state, std::time::Instant::now())),
Err(e) => on_error(&e),
}
}
if stop.wait(poll).is_some() {
break;
}
}
})
}
pub fn spawn_record_heartbeat(
suite: crate::Suite,
service: String,
options: HeartbeatOptions,
stop: StopToken,
state: impl Fn() -> String + Send + 'static,
mut on_error: impl FnMut(&io::Error) + Send + 'static,
) -> io::Result<JoinHandle<()>> {
let path = crate::record_path(&suite, &service);
let poll = options
.poll
.max(Duration::from_millis(1))
.min(options.interval.max(Duration::from_millis(1)));
let interval = options.interval;
std::thread::Builder::new()
.name("rk-record-heartbeat".into())
.spawn(move || {
let mut last: Option<(String, std::time::Instant)> = None;
loop {
let now_state = state();
let changed = last.as_ref().is_none_or(|(v, _)| *v != now_state);
let due = last.as_ref().is_none_or(|(_, at)| at.elapsed() >= interval);
if changed || due {
let written = (|| -> io::Result<()> {
let mut record =
crate::try_read_record(&suite, &service)?.ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "no service record")
})?;
if record.pid != std::process::id() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"the service record belongs to another process",
));
}
record.heartbeat_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
record.state = now_state.clone();
let body = serde_json::to_vec_pretty(&record)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
write_atomic_with(&path, &body, changed)
})();
match written {
Ok(()) => last = Some((now_state, std::time::Instant::now())),
Err(e) => on_error(&e),
}
}
if stop.wait(poll).is_some() {
break;
}
}
})
}