rightkit_daemon/
ticker.rs1use crate::record::{write_atomic, write_atomic_with};
10use crate::stop::StopToken;
11use serde_json::{json, Value};
12use std::io;
13use std::path::PathBuf;
14use std::thread::JoinHandle;
15use std::time::{Duration, SystemTime, UNIX_EPOCH};
16
17pub fn spawn_ticker(
20 interval: Duration,
21 stop: StopToken,
22 mut tick: impl FnMut() + Send + 'static,
23) -> io::Result<JoinHandle<()>> {
24 std::thread::Builder::new()
25 .name("rk-ticker".into())
26 .spawn(move || loop {
27 if stop.wait(interval).is_some() {
28 break;
29 }
30 tick();
31 })
32}
33
34pub fn heartbeat_record(state: Value) -> Value {
36 let updated_ms = SystemTime::now()
37 .duration_since(UNIX_EPOCH)
38 .map(|d| d.as_millis() as u64)
39 .unwrap_or(0);
40 json!({"pid": std::process::id(), "updated_ms": updated_ms, "state": state})
41}
42
43pub fn spawn_heartbeat(
47 path: PathBuf,
48 interval: Duration,
49 stop: StopToken,
50 state: impl Fn() -> Value + Send + 'static,
51 mut on_error: impl FnMut(&io::Error) + Send + 'static,
52) -> io::Result<JoinHandle<()>> {
53 let write = move |path: &PathBuf, state: &dyn Fn() -> Value| {
54 let body = serde_json::to_vec(&heartbeat_record(state()))
55 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
56 write_atomic(path, &body)
57 };
58 std::thread::Builder::new()
59 .name("rk-heartbeat".into())
60 .spawn(move || loop {
61 if let Err(e) = write(&path, &state) {
62 on_error(&e);
63 }
64 if stop.wait(interval).is_some() {
65 break;
66 }
67 })
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct HeartbeatOptions {
73 pub interval: Duration,
75 pub poll: Duration,
79}
80
81impl Default for HeartbeatOptions {
82 fn default() -> Self {
84 Self {
85 interval: Duration::from_secs(5),
86 poll: Duration::from_secs(1),
87 }
88 }
89}
90
91pub fn spawn_heartbeat_with(
103 path: PathBuf,
104 options: HeartbeatOptions,
105 stop: StopToken,
106 state: impl Fn() -> Value + Send + 'static,
107 mut on_error: impl FnMut(&io::Error) + Send + 'static,
108) -> io::Result<JoinHandle<()>> {
109 let poll = options
110 .poll
111 .max(Duration::from_millis(1))
112 .min(options.interval.max(Duration::from_millis(1)));
113 let interval = options.interval;
114 std::thread::Builder::new()
115 .name("rk-heartbeat".into())
116 .spawn(move || {
117 let mut last: Option<(Value, std::time::Instant)> = None;
118 loop {
119 let now_state = state();
120 let changed = last.as_ref().is_none_or(|(v, _)| *v != now_state);
121 let due = last.as_ref().is_none_or(|(_, at)| at.elapsed() >= interval);
122 if changed || due {
123 let written = serde_json::to_vec(&heartbeat_record(now_state.clone()))
124 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
125 .and_then(|body| write_atomic_with(&path, &body, changed));
126 match written {
127 Ok(()) => last = Some((now_state, std::time::Instant::now())),
128 Err(e) => on_error(&e),
129 }
130 }
131 if stop.wait(poll).is_some() {
132 break;
133 }
134 }
135 })
136}
137
138pub fn spawn_record_heartbeat(
146 suite: crate::Suite,
147 service: String,
148 options: HeartbeatOptions,
149 stop: StopToken,
150 state: impl Fn() -> String + Send + 'static,
151 mut on_error: impl FnMut(&io::Error) + Send + 'static,
152) -> io::Result<JoinHandle<()>> {
153 let path = crate::record_path(&suite, &service);
154 let poll = options
155 .poll
156 .max(Duration::from_millis(1))
157 .min(options.interval.max(Duration::from_millis(1)));
158 let interval = options.interval;
159 std::thread::Builder::new()
160 .name("rk-record-heartbeat".into())
161 .spawn(move || {
162 let mut last: Option<(String, std::time::Instant)> = None;
163 loop {
164 let now_state = state();
165 let changed = last.as_ref().is_none_or(|(v, _)| *v != now_state);
166 let due = last.as_ref().is_none_or(|(_, at)| at.elapsed() >= interval);
167 if changed || due {
168 let written = (|| -> io::Result<()> {
169 let mut record =
170 crate::try_read_record(&suite, &service)?.ok_or_else(|| {
171 io::Error::new(io::ErrorKind::NotFound, "no service record")
172 })?;
173 if record.pid != std::process::id() {
174 return Err(io::Error::new(
175 io::ErrorKind::PermissionDenied,
176 "the service record belongs to another process",
177 ));
178 }
179 record.heartbeat_ms = SystemTime::now()
180 .duration_since(UNIX_EPOCH)
181 .map(|d| d.as_millis() as u64)
182 .unwrap_or(0);
183 record.state = now_state.clone();
184 let body = serde_json::to_vec_pretty(&record)
185 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
186 write_atomic_with(&path, &body, changed)
187 })();
188 match written {
189 Ok(()) => last = Some((now_state, std::time::Instant::now())),
190 Err(e) => on_error(&e),
191 }
192 }
193 if stop.wait(poll).is_some() {
194 break;
195 }
196 }
197 })
198}