1use crate::RedisPool;
2use rand::RngCore;
3use serde::Serialize;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::Arc;
6
7#[derive(Clone)]
8pub struct Counter {
9 count: Arc<AtomicUsize>,
10}
11
12impl Counter {
13 #[must_use]
14 pub fn new(n: usize) -> Self {
15 Self {
16 count: Arc::new(AtomicUsize::new(n)),
17 }
18 }
19
20 #[must_use]
21 pub fn value(&self) -> usize {
22 self.count.load(Ordering::SeqCst)
23 }
24
25 pub fn decrby(&self, n: usize) {
26 self.count.fetch_sub(n, Ordering::SeqCst);
27 }
28
29 pub fn incrby(&self, n: usize) {
30 self.count.fetch_add(n, Ordering::SeqCst);
31 }
32}
33
34struct ProcessStats {
35 rtt_us: String,
36 quiet: String,
37 busy: usize,
38 beat: f64,
39 concurrency: usize,
40 info: ProcessInfo,
41 rss: String,
42}
43
44#[derive(Serialize)]
45struct ProcessInfo {
46 hostname: String,
47 identity: String,
48 started_at: f64,
49 pid: u32,
50 tag: String,
51 concurrency: usize,
52 queues: Vec<String>,
53 labels: Vec<String>,
54 version: String,
55 embedded: bool,
56}
57
58pub struct StatsPublisher {
59 hostname: String,
60 identity: String,
61 queues: Vec<String>,
62 started_at: chrono::DateTime<chrono::Utc>,
63 busy_jobs: Counter,
64 concurrency: usize,
65}
66
67fn generate_identity(hostname: &String) -> String {
68 let pid = std::process::id();
69 let mut bytes = [0u8; 12];
70 rand::rng().fill_bytes(&mut bytes);
71 let nonce = hex::encode(bytes);
72
73 format!("{hostname}:{pid}:{nonce}")
74}
75
76pub(crate) fn generate_tid() -> String {
81 let mut bytes = [0u8; 6];
82 rand::rng().fill_bytes(&mut bytes);
83 hex::encode(bytes)
84}
85
86impl StatsPublisher {
87 #[must_use]
88 pub fn new(
89 hostname: String,
90 queues: Vec<String>,
91 busy_jobs: Counter,
92 concurrency: usize,
93 ) -> Self {
94 let identity = generate_identity(&hostname);
95 let started_at = chrono::Utc::now();
96
97 Self {
98 hostname,
99 identity,
100 queues,
101 started_at,
102 busy_jobs,
103 concurrency,
104 }
105 }
106
107 pub async fn publish_stats(&self, redis: RedisPool) -> Result<(), Box<dyn std::error::Error>> {
119 let stats = self.create_process_stats().await?;
120 let mut conn = redis.get().await?;
121 let _: () = conn
122 .cmd_with_key("HSET", self.identity.clone())
123 .arg("info")
124 .arg(serde_json::to_string(&stats.info)?)
125 .arg("concurrency")
126 .arg(stats.concurrency)
127 .arg("busy")
128 .arg(stats.busy)
129 .arg("beat")
130 .arg(stats.beat)
131 .arg("rtt_us")
132 .arg(stats.rtt_us)
133 .arg("quiet")
134 .arg(stats.quiet)
135 .arg("rss")
136 .arg(stats.rss)
137 .query_async::<()>(conn.unnamespaced_borrow_mut())
138 .await?;
139
140 conn.expire(self.identity.clone(), 60).await?;
141
142 conn.sadd("processes".to_string(), self.identity.clone())
143 .await?;
144
145 conn.expire(format!("{}:work", self.identity), 60).await?;
150
151 Ok(())
152 }
153
154 pub(crate) async fn deregister(&self, redis: RedisPool) -> crate::Result<()> {
165 let mut conn = redis.get().await?;
166 conn.srem_and_unlink(
167 "processes".to_string(),
168 self.identity.clone(),
169 self.identity.clone(),
170 )
171 .await?;
172 conn.unlink(format!("{}:work", self.identity)).await?;
173 Ok(())
174 }
175
176 pub(crate) fn identity(&self) -> &str {
177 &self.identity
178 }
179
180 async fn create_process_stats(&self) -> Result<ProcessStats, Box<dyn std::error::Error>> {
181 let rss_in_kb = format!("{}", get_rss_kb());
182
183 Ok(ProcessStats {
184 rtt_us: "0".into(),
185 busy: self.busy_jobs.value(),
186 quiet: "false".into(),
187 rss: rss_in_kb,
188 concurrency: self.concurrency,
189 beat: chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
190 info: ProcessInfo {
191 concurrency: self.concurrency,
192 hostname: self.hostname.clone(),
193 identity: self.identity.clone(),
194 queues: self.queues.clone(),
195 started_at: self.started_at.timestamp_millis() as f64 / 1000.0,
196 pid: std::process::id(),
197 labels: vec![],
198 tag: String::new(),
199 version: env!("CARGO_PKG_VERSION").to_string(),
200 embedded: false,
201 },
202 })
203 }
204}
205
206#[cfg(target_os = "macos")]
208#[allow(deprecated)] fn get_rss_kb() -> u64 {
210 use std::mem;
211 unsafe {
212 let mut info: libc::mach_task_basic_info_data_t = mem::zeroed();
213 let mut count = (mem::size_of::<libc::mach_task_basic_info_data_t>()
214 / mem::size_of::<libc::natural_t>())
215 as libc::mach_msg_type_number_t;
216 let ret = libc::task_info(
217 libc::mach_task_self(),
218 libc::MACH_TASK_BASIC_INFO,
219 &mut info as *mut _ as libc::task_info_t,
220 &mut count,
221 );
222 if ret == libc::KERN_SUCCESS {
223 info.resident_size as u64 / 1024
224 } else {
225 0
226 }
227 }
228}
229
230#[cfg(target_os = "linux")]
231fn get_rss_kb() -> u64 {
232 std::fs::read_to_string("/proc/self/statm")
233 .ok()
234 .and_then(|s| s.split_whitespace().nth(1)?.parse::<u64>().ok())
235 .map(|pages| pages * 4) .unwrap_or(0)
237}
238
239#[cfg(not(any(target_os = "macos", target_os = "linux")))]
240fn get_rss_kb() -> u64 {
241 0
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use bb8::Pool;
248 use crate::RedisConnectionManager;
249
250 async fn test_pool() -> crate::RedisPool {
251 let manager = RedisConnectionManager::new("redis://127.0.0.1/").unwrap();
252 Pool::builder().build(manager).await.unwrap()
253 }
254
255 async fn sismember(redis: &RedisPool, set: &str, member: &str) -> bool {
256 let mut conn = redis.get().await.unwrap();
257 redis::cmd("SISMEMBER")
258 .arg(set)
259 .arg(member)
260 .query_async::<i64>(conn.unnamespaced_borrow_mut())
261 .await
262 .unwrap_or(0)
263 == 1
264 }
265
266 async fn exists(redis: &RedisPool, key: &str) -> bool {
267 let mut conn = redis.get().await.unwrap();
268 redis::cmd("EXISTS")
269 .arg(key)
270 .query_async::<i64>(conn.unnamespaced_borrow_mut())
271 .await
272 .unwrap_or(0)
273 > 0
274 }
275
276 fn new_publisher() -> StatsPublisher {
277 StatsPublisher::new(
278 "testhost".to_string(),
279 vec!["default".to_string()],
280 Counter::new(0),
281 1,
282 )
283 }
284
285 #[tokio::test]
286 async fn deregister_removes_identity_from_processes_set() {
287 let redis = test_pool().await;
288 let p = new_publisher();
289
290 p.publish_stats(redis.clone()).await.unwrap();
291 assert!(
292 sismember(&redis, "processes", p.identity()).await,
293 "should be in set after publish_stats"
294 );
295
296 p.deregister(redis.clone()).await.unwrap();
297 assert!(
298 !sismember(&redis, "processes", p.identity()).await,
299 "should be removed from set after deregister"
300 );
301 }
302
303 #[tokio::test]
304 async fn deregister_deletes_heartbeat_hash() {
305 let redis = test_pool().await;
306 let p = new_publisher();
307
308 p.publish_stats(redis.clone()).await.unwrap();
309 assert!(exists(&redis, p.identity()).await, "heartbeat hash should exist");
310
311 p.deregister(redis.clone()).await.unwrap();
312 assert!(
313 !exists(&redis, p.identity()).await,
314 "heartbeat hash should be deleted after deregister"
315 );
316 }
317
318 #[tokio::test]
319 async fn deregister_does_not_affect_sibling_process() {
320 let redis = test_pool().await;
321 let p1 = new_publisher();
322 let p2 = new_publisher(); p1.publish_stats(redis.clone()).await.unwrap();
325 p2.publish_stats(redis.clone()).await.unwrap();
326
327 p1.deregister(redis.clone()).await.unwrap();
328
329 assert!(
330 sismember(&redis, "processes", p2.identity()).await,
331 "sibling process must remain registered after p1 deregisters"
332 );
333
334 p2.deregister(redis.clone()).await.unwrap();
335 }
336
337 #[tokio::test]
338 async fn deregister_is_idempotent() {
339 let redis = test_pool().await;
340 let p = new_publisher();
341
342 p.publish_stats(redis.clone()).await.unwrap();
343 p.deregister(redis.clone()).await.unwrap();
344 p.deregister(redis.clone()).await.unwrap();
346 }
347}