1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Mutex;
9use std::time::Instant;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ReplicationLagSnapshot {
14 pub slave: String,
16 pub lag_seconds: u64,
18 pub collected_at_secs: u64,
20}
21
22impl ReplicationLagSnapshot {
23 pub fn new(slave: &str, lag_seconds: u64, collected_at_secs: u64) -> Self {
25 Self {
26 slave: slave.to_string(),
27 lag_seconds,
28 collected_at_secs,
29 }
30 }
31
32 pub fn exceeds(&self, threshold_secs: u64) -> bool {
34 self.lag_seconds > threshold_secs
35 }
36
37 pub fn is_acceptable(&self, threshold_secs: u64) -> bool {
39 self.lag_seconds <= threshold_secs
40 }
41}
42
43pub struct ReplicationLagMonitor {
47 threshold_secs: u64,
49 history: Mutex<HashMap<String, Vec<ReplicationLagSnapshot>>>,
51 max_history: usize,
53 started: Instant,
55}
56
57impl ReplicationLagMonitor {
58 pub fn new(threshold_secs: u64) -> Self {
60 Self {
61 threshold_secs,
62 history: Mutex::new(HashMap::new()),
63 max_history: 10,
64 started: Instant::now(),
65 }
66 }
67
68 pub fn with_max_history(mut self, max: usize) -> Self {
70 self.max_history = max.max(1);
71 self
72 }
73
74 pub fn report(&self, slave: &str, lag_seconds: u64) {
76 let now = self.started.elapsed().as_secs();
77 let snap = ReplicationLagSnapshot::new(slave, lag_seconds, now);
78 if let Ok(mut history) = self.history.lock() {
79 let entries = history.entry(slave.to_string()).or_default();
80 entries.push(snap);
81 if entries.len() > self.max_history {
82 entries.remove(0);
83 }
84 }
85 }
86
87 pub fn latest_lag(&self, slave: &str) -> Option<u64> {
89 match self.history.lock() {
90 Ok(history) => history
91 .get(slave)
92 .and_then(|v| v.last())
93 .map(|s| s.lag_seconds),
94 Err(_) => None,
95 }
96 }
97
98 pub fn is_lagging(&self, slave: &str) -> bool {
100 match self.latest_lag(slave) {
101 Some(lag) => lag > self.threshold_secs,
102 None => false,
103 }
104 }
105
106 pub fn is_safe_to_read(&self, slave: &str) -> bool {
108 !self.is_lagging(slave)
109 }
110
111 pub fn lagging_slaves(&self) -> Vec<String> {
113 match self.history.lock() {
114 Ok(history) => history
115 .iter()
116 .filter(|(_, v)| {
117 v.last()
118 .map(|s| s.lag_seconds > self.threshold_secs)
119 .unwrap_or(false)
120 })
121 .map(|(k, _)| k.clone())
122 .collect(),
123 Err(_) => Vec::new(),
124 }
125 }
126
127 pub fn safe_slaves(&self) -> Vec<String> {
129 match self.history.lock() {
130 Ok(history) => history
131 .iter()
132 .filter(|(_, v)| {
133 v.last()
134 .map(|s| s.lag_seconds <= self.threshold_secs)
135 .unwrap_or(true)
136 })
137 .map(|(k, _)| k.clone())
138 .collect(),
139 Err(_) => Vec::new(),
140 }
141 }
142
143 pub fn history(&self, slave: &str) -> Vec<ReplicationLagSnapshot> {
145 match self.history.lock() {
146 Ok(history) => history.get(slave).cloned().unwrap_or_default(),
147 Err(_) => Vec::new(),
148 }
149 }
150
151 pub fn avg_lag(&self, slave: &str) -> Option<u64> {
153 match self.history.lock() {
154 Ok(history) => {
155 let entries = history.get(slave)?;
156 if entries.is_empty() {
157 return None;
158 }
159 let total: u64 = entries.iter().map(|s| s.lag_seconds).sum();
160 Some(total / entries.len() as u64)
161 }
162 Err(_) => None,
163 }
164 }
165
166 pub fn max_lag(&self, slave: &str) -> Option<u64> {
168 match self.history.lock() {
169 Ok(history) => history
170 .get(slave)
171 .and_then(|v| v.iter().map(|s| s.lag_seconds).max()),
172 Err(_) => None,
173 }
174 }
175
176 pub fn threshold(&self) -> u64 {
178 self.threshold_secs
179 }
180
181 pub fn reset(&self, slave: &str) {
183 if let Ok(mut history) = self.history.lock() {
184 history.remove(slave);
185 }
186 }
187
188 pub fn reset_all(&self) {
190 if let Ok(mut history) = self.history.lock() {
191 history.clear();
192 }
193 }
194
195 pub fn select_least_lag<'a>(&self, candidates: &'a [String]) -> Option<&'a str> {
199 let history = self.history.lock().ok()?;
200 let mut best: Option<(&str, u64)> = None;
201 for slave in candidates {
202 if let Some(entries) = history.get(slave) {
203 if let Some(latest) = entries.last() {
204 match best {
205 None => best = Some((slave.as_str(), latest.lag_seconds)),
206 Some((_, b_lag)) if latest.lag_seconds < b_lag => {
207 best = Some((slave.as_str(), latest.lag_seconds))
208 }
209 _ => {}
210 }
211 }
212 }
213 }
214 best.map(|(s, _)| s)
215 }
216
217 pub fn min_lag(&self, slave: &str) -> Option<u64> {
219 match self.history.lock() {
220 Ok(history) => history
221 .get(slave)
222 .and_then(|v| v.iter().map(|s| s.lag_seconds).min()),
223 Err(_) => None,
224 }
225 }
226
227 pub fn all_slaves(&self) -> Vec<String> {
229 match self.history.lock() {
230 Ok(history) => history.keys().cloned().collect(),
231 Err(_) => Vec::new(),
232 }
233 }
234
235 pub fn slave_count(&self) -> usize {
237 match self.history.lock() {
238 Ok(history) => history.len(),
239 Err(_) => 0,
240 }
241 }
242
243 pub fn lag_trend(&self, slave: &str) -> i64 {
247 match self.history.lock() {
248 Ok(history) => {
249 if let Some(entries) = history.get(slave) {
250 if entries.len() < 2 {
251 return 0;
252 }
253 let last = entries.last().unwrap().lag_seconds as i64;
254 let prev = entries[entries.len() - 2].lag_seconds as i64;
255 last - prev
256 } else {
257 0
258 }
259 }
260 Err(_) => 0,
261 }
262 }
263
264 pub fn summary(&self) -> String {
266 let history = match self.history.lock() {
267 Ok(h) => h,
268 Err(_) => return "ReplicationLagMonitor: lock poisoned".to_string(),
269 };
270 let mut out = format!(
271 "ReplicationLagMonitor: {} slave(s), threshold={}s\n",
272 history.len(),
273 self.threshold_secs
274 );
275 for (slave, entries) in history.iter() {
276 let latest = entries.last().map(|s| s.lag_seconds).unwrap_or(0);
277 let status = if latest > self.threshold_secs {
278 "LAGGING"
279 } else {
280 "OK"
281 };
282 out.push_str(&format!(" {} : lag={}s [{}]\n", slave, latest, status));
283 }
284 out
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn test_snapshot_exceeds() {
294 let snap = ReplicationLagSnapshot::new("s1", 10, 0);
295 assert!(snap.exceeds(5));
296 assert!(!snap.exceeds(10));
297 assert!(!snap.exceeds(15));
298 }
299
300 #[test]
301 fn test_snapshot_is_acceptable() {
302 let snap = ReplicationLagSnapshot::new("s1", 5, 0);
303 assert!(snap.is_acceptable(5));
304 assert!(snap.is_acceptable(10));
305 assert!(!snap.is_acceptable(3));
306 }
307
308 #[test]
309 fn test_monitor_new_has_threshold() {
310 let m = ReplicationLagMonitor::new(30);
311 assert_eq!(m.threshold(), 30);
312 }
313
314 #[test]
315 fn test_report_and_latest_lag() {
316 let m = ReplicationLagMonitor::new(10);
317 m.report("s1", 5);
318 assert_eq!(m.latest_lag("s1"), Some(5));
319 m.report("s1", 8);
320 assert_eq!(m.latest_lag("s1"), Some(8));
321 }
322
323 #[test]
324 fn test_latest_lag_unknown_slave() {
325 let m = ReplicationLagMonitor::new(10);
326 assert_eq!(m.latest_lag("ghost"), None);
327 }
328
329 #[test]
330 fn test_is_lagging() {
331 let m = ReplicationLagMonitor::new(10);
332 m.report("s1", 5);
333 assert!(!m.is_lagging("s1"));
334 m.report("s1", 15);
335 assert!(m.is_lagging("s1"));
336 }
337
338 #[test]
339 fn test_is_safe_to_read() {
340 let m = ReplicationLagMonitor::new(10);
341 m.report("s1", 5);
342 assert!(m.is_safe_to_read("s1"));
343 m.report("s1", 20);
344 assert!(!m.is_safe_to_read("s1"));
345 }
346
347 #[test]
348 fn test_is_safe_to_read_no_data() {
349 let m = ReplicationLagMonitor::new(10);
350 assert!(m.is_safe_to_read("unknown"), "no data should be safe");
351 }
352
353 #[test]
354 fn test_lagging_slaves() {
355 let m = ReplicationLagMonitor::new(10);
356 m.report("s1", 5);
357 m.report("s2", 15);
358 m.report("s3", 20);
359 let mut lagging = m.lagging_slaves();
360 lagging.sort();
361 assert_eq!(lagging, vec!["s2".to_string(), "s3".to_string()]);
362 }
363
364 #[test]
365 fn test_safe_slaves() {
366 let m = ReplicationLagMonitor::new(10);
367 m.report("s1", 5);
368 m.report("s2", 15);
369 let safe = m.safe_slaves();
370 assert_eq!(safe, vec!["s1".to_string()]);
371 }
372
373 #[test]
374 fn test_history_retention() {
375 let m = ReplicationLagMonitor::new(10).with_max_history(3);
376 m.report("s1", 1);
377 m.report("s1", 2);
378 m.report("s1", 3);
379 m.report("s1", 4);
380 assert_eq!(m.history("s1").len(), 3);
381 assert_eq!(m.latest_lag("s1"), Some(4));
382 }
383
384 #[test]
385 fn test_avg_lag() {
386 let m = ReplicationLagMonitor::new(100);
387 m.report("s1", 10);
388 m.report("s1", 20);
389 m.report("s1", 30);
390 assert_eq!(m.avg_lag("s1"), Some(20));
391 }
392
393 #[test]
394 fn test_avg_lag_no_data() {
395 let m = ReplicationLagMonitor::new(100);
396 assert_eq!(m.avg_lag("ghost"), None);
397 }
398
399 #[test]
400 fn test_max_lag() {
401 let m = ReplicationLagMonitor::new(100);
402 m.report("s1", 10);
403 m.report("s1", 50);
404 m.report("s1", 20);
405 assert_eq!(m.max_lag("s1"), Some(50));
406 }
407
408 #[test]
409 fn test_reset() {
410 let m = ReplicationLagMonitor::new(10);
411 m.report("s1", 5);
412 m.reset("s1");
413 assert_eq!(m.latest_lag("s1"), None);
414 }
415
416 #[test]
417 fn test_reset_all() {
418 let m = ReplicationLagMonitor::new(10);
419 m.report("s1", 5);
420 m.report("s2", 10);
421 m.reset_all();
422 assert_eq!(m.latest_lag("s1"), None);
423 assert_eq!(m.latest_lag("s2"), None);
424 }
425
426 #[test]
427 fn test_select_least_lag() {
428 let m = ReplicationLagMonitor::new(100);
429 m.report("s1", 20);
430 m.report("s2", 5);
431 m.report("s3", 15);
432 let candidates = vec!["s1".to_string(), "s2".to_string(), "s3".to_string()];
433 let selected = m.select_least_lag(&candidates);
434 assert_eq!(selected, Some("s2"));
435 }
436
437 #[test]
438 fn test_select_least_lag_no_data() {
439 let m = ReplicationLagMonitor::new(100);
440 let candidates = vec!["s1".to_string()];
441 assert_eq!(m.select_least_lag(&candidates), None);
442 }
443
444 #[test]
445 fn test_select_least_lag_empty_candidates() {
446 let m = ReplicationLagMonitor::new(100);
447 let candidates: Vec<String> = vec![];
448 assert_eq!(m.select_least_lag(&candidates), None);
449 }
450
451 #[test]
452 fn test_with_max_history_clamped_to_one() {
453 let m = ReplicationLagMonitor::new(10).with_max_history(0);
454 m.report("s1", 5);
455 assert_eq!(m.history("s1").len(), 1);
456 }
457
458 #[test]
459 fn test_min_lag() {
460 let m = ReplicationLagMonitor::new(100);
461 m.report("s1", 30);
462 m.report("s1", 10);
463 m.report("s1", 50);
464 assert_eq!(m.min_lag("s1"), Some(10));
465 }
466
467 #[test]
468 fn test_min_lag_no_data() {
469 let m = ReplicationLagMonitor::new(100);
470 assert_eq!(m.min_lag("ghost"), None);
471 }
472
473 #[test]
474 fn test_all_slaves() {
475 let m = ReplicationLagMonitor::new(100);
476 m.report("s1", 5);
477 m.report("s2", 10);
478 let mut slaves = m.all_slaves();
479 slaves.sort();
480 assert_eq!(slaves, vec!["s1".to_string(), "s2".to_string()]);
481 }
482
483 #[test]
484 fn test_slave_count() {
485 let m = ReplicationLagMonitor::new(100);
486 m.report("s1", 5);
487 m.report("s2", 10);
488 assert_eq!(m.slave_count(), 2);
489 }
490
491 #[test]
492 fn test_lag_trend_rising() {
493 let m = ReplicationLagMonitor::new(100);
494 m.report("s1", 5);
495 m.report("s1", 10);
496 assert_eq!(m.lag_trend("s1"), 5);
497 }
498
499 #[test]
500 fn test_lag_trend_falling() {
501 let m = ReplicationLagMonitor::new(100);
502 m.report("s1", 20);
503 m.report("s1", 10);
504 assert_eq!(m.lag_trend("s1"), -10);
505 }
506
507 #[test]
508 fn test_lag_trend_stable() {
509 let m = ReplicationLagMonitor::new(100);
510 m.report("s1", 10);
511 m.report("s1", 10);
512 assert_eq!(m.lag_trend("s1"), 0);
513 }
514
515 #[test]
516 fn test_lag_trend_single_sample() {
517 let m = ReplicationLagMonitor::new(100);
518 m.report("s1", 10);
519 assert_eq!(m.lag_trend("s1"), 0);
520 }
521
522 #[test]
523 fn test_summary_contains_slave() {
524 let m = ReplicationLagMonitor::new(10);
525 m.report("s1", 5);
526 m.report("s2", 20);
527 let s = m.summary();
528 assert!(s.contains("s1"));
529 assert!(s.contains("s2"));
530 assert!(s.contains("OK"));
531 assert!(s.contains("LAGGING"));
532 }
533}