1use std::collections::{BTreeMap, VecDeque};
26use std::io::{BufRead, BufReader, Write};
27use std::path::{Path, PathBuf};
28use std::sync::{Arc, Condvar, Mutex};
29use std::thread::JoinHandle;
30use std::time::{Duration, Instant};
31
32use crate::protocol::{ConcludedCall, Endpoint, EndpointState, Metrics, Outcome, State};
33use crate::sdk::Host;
34
35#[derive(Clone, Debug)]
37pub enum Attempt {
38 Done { url: String },
40 Skip(String),
42 Retry(String),
44 Fail(String),
46}
47
48#[derive(Clone, Debug)]
49pub struct QueueOptions {
50 pub retry_after: Vec<Duration>,
52 pub threads: usize,
54 pub save_to: Option<PathBuf>,
56 pub capacity: usize,
58 pub noun: &'static str,
60 pub endpoint: Option<String>,
64}
65
66impl Default for QueueOptions {
67 fn default() -> Self {
68 QueueOptions { retry_after: [10, 60, 300, 900].map(Duration::from_secs).to_vec(), threads: 2, save_to: None, capacity: 10_000, noun: "call", endpoint: None }
69 }
70}
71
72impl QueueOptions {
73 pub fn saved_in(data_dir: &Path) -> Self {
75 QueueOptions { save_to: Some(data_dir.join("queue.jsonl")), ..Default::default() }
76 }
77
78 fn endpoint_name(&self, lane: &str) -> Option<String> {
80 match (&self.endpoint, lane) {
81 (Some(e), "") => Some(e.clone()),
82 (Some(e), l) => Some(format!("{e} at {l}")),
83 (None, "") => None,
84 (None, l) => Some(l.to_string()),
85 }
86 }
87}
88
89struct Item {
90 call: ConcludedCall,
91 tries: usize,
92 due: Instant,
93 last_error: String,
94}
95
96#[derive(Default)]
98struct Lane {
99 ready: VecDeque<Item>,
100 waiting: Vec<Item>,
101 busy: usize,
102 last_error: String,
103 stats: Tally,
104}
105
106#[derive(Default)]
107struct Inner {
108 lanes: BTreeMap<String, Lane>,
109 closing: Option<Instant>,
110 reported: usize,
112 last_error: String,
114 changed: bool,
116 sent: Option<Instant>,
117 sent_states: Vec<EndpointState>,
119}
120
121#[derive(Default)]
123struct Tally {
124 retries: u64,
125 bytes: u64,
126 latency_ms: Option<f64>,
127 last_ok: Option<f64>,
128 last_error: Option<f64>,
129 in_a_row: u32,
131 tried: bool,
132 first_try: Option<f64>,
134}
135
136const METRICS_MIN: Duration = Duration::from_secs(5);
138const METRICS_MAX: Duration = Duration::from_secs(30);
139const DOWN_AFTER: u32 = 3;
142const DOWN_SILENT: Duration = Duration::from_secs(300);
143
144fn unix_now() -> f64 {
145 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0.0, |d| d.as_secs_f64())
146}
147
148impl Lane {
149 fn endpoint_state(&self) -> EndpointState {
150 let t = &self.stats;
151 if !t.tried {
152 return EndpointState::Unknown;
153 }
154 let since = t.last_ok.or(t.first_try).unwrap_or_else(unix_now);
155 let silent = !self.waiting.is_empty() && unix_now() - since > DOWN_SILENT.as_secs_f64();
156 if t.in_a_row >= DOWN_AFTER || silent {
157 EndpointState::Down
158 } else if t.in_a_row > 0 || !self.waiting.is_empty() {
159 EndpointState::Degraded
160 } else {
161 EndpointState::Up
162 }
163 }
164}
165
166impl Inner {
167 fn states(&self) -> Vec<EndpointState> {
168 self.lanes.values().map(Lane::endpoint_state).collect()
169 }
170
171 fn waiting(&self) -> usize {
172 self.lanes.values().map(|l| l.waiting.len()).sum()
173 }
174
175 fn metrics(&self, o: &QueueOptions) -> Metrics {
177 let mut m = Metrics { queued: Some(0), retrying: Some(0), in_flight: Some(0), retries: Some(0), bytes_sent: Some(0), ..Default::default() };
178 let add = |a: &mut Option<u64>, b: usize| *a = Some(a.unwrap_or(0) + b as u64);
179 let later = |a: &mut Option<f64>, b: Option<f64>| {
180 if b > *a {
181 *a = b;
182 }
183 };
184 for (name, l) in &self.lanes {
185 let t = &l.stats;
186 add(&mut m.queued, l.ready.len() + l.waiting.len());
187 add(&mut m.retrying, l.waiting.len());
188 add(&mut m.in_flight, l.busy);
189 add(&mut m.retries, t.retries as usize);
190 add(&mut m.bytes_sent, t.bytes as usize);
191 let latency = t.latency_ms.map(|l| (l * 10.0).round() / 10.0);
192 later(&mut m.latency_ms, latency);
194 later(&mut m.last_ok, t.last_ok);
195 if t.last_error > m.last_error {
196 m.last_error = t.last_error;
197 m.last_error_text = l.last_error.clone();
198 }
199 if let Some(n) = o.endpoint_name(name) {
200 let state = l.endpoint_state();
201 let last_error = if state == EndpointState::Up { String::new() } else { l.last_error.clone() };
202 m.endpoints.push(Endpoint { name: n, state, latency_ms: t.latency_ms, last_ok: t.last_ok, last_error });
203 }
204 }
205 m
206 }
207
208 fn report(&mut self, host: &Host, o: &QueueOptions) {
211 let states = self.states();
212 let since = self.sent.map_or(Duration::MAX, |t| t.elapsed());
213 let due = (self.changed && since >= METRICS_MIN) || since >= METRICS_MAX || states != self.sent_states;
214 if due {
215 host.metrics(&self.metrics(o));
216 self.sent = Some(Instant::now());
217 self.sent_states = states;
218 self.changed = false;
219 }
220 }
221}
222
223struct Shared {
224 inner: Mutex<Inner>,
225 wake: Condvar,
226}
227
228type Work = dyn Fn(&ConcludedCall) -> Attempt + Send + Sync;
229type LaneOf = dyn Fn(&ConcludedCall) -> String + Send + Sync;
230
231pub struct CallQueue {
232 shared: Arc<Shared>,
233 workers: Mutex<Vec<JoinHandle<()>>>,
234 host: Host,
235 opts: QueueOptions,
236 work: Arc<Work>,
237 lane_of: Box<LaneOf>,
238}
239
240impl CallQueue {
241 pub fn start<F>(host: Host, opts: QueueOptions, work: F) -> CallQueue
244 where
245 F: Fn(&ConcludedCall) -> Attempt + Send + Sync + 'static,
246 {
247 Self::start_by(host, opts, |_| String::new(), work)
248 }
249
250 pub fn start_by<L, F>(host: Host, opts: QueueOptions, lane: L, work: F) -> CallQueue
255 where
256 L: Fn(&ConcludedCall) -> String + Send + Sync + 'static,
257 F: Fn(&ConcludedCall) -> Attempt + Send + Sync + 'static,
258 {
259 let shared = Arc::new(Shared { inner: Mutex::new(Inner::default()), wake: Condvar::new() });
260 let q = CallQueue { shared, workers: Mutex::new(Vec::new()), host, opts, work: Arc::new(work), lane_of: Box::new(lane) };
261 if let Some(path) = &q.opts.save_to {
262 let saved = load(path);
263 if !saved.is_empty() {
264 q.host.info(format!("{} {}(s) left from last time", saved.len(), q.opts.noun));
265 let now = Instant::now();
266 let mut g = q.shared.inner.lock().unwrap();
267 for call in saved {
268 let key = (q.lane_of)(&call);
269 q.lane(&mut g, &key).ready.push_back(Item { call, tries: 0, due: now, last_error: String::new() });
270 }
271 }
272 let _ = std::fs::remove_file(path);
273 }
274 q
275 }
276
277 fn lane<'a>(&self, g: &'a mut Inner, key: &str) -> &'a mut Lane {
279 if !g.lanes.contains_key(key) {
280 g.lanes.insert(key.to_string(), Lane::default());
281 let mut workers = self.workers.lock().unwrap();
282 for i in 0..self.opts.threads.max(1) {
283 let (sh, h, w, o, k) = (self.shared.clone(), self.host.clone(), self.work.clone(), self.opts.clone(), key.to_string());
284 let name = if key.is_empty() { format!("queue-{i}") } else { format!("queue-{key}-{i}") };
285 workers.push(std::thread::Builder::new().name(name).spawn(move || worker(&sh, &k, &h, &*w, &o)).expect("thread"));
286 }
287 }
288 g.lanes.get_mut(key).unwrap()
289 }
290
291 pub fn push(&self, call: ConcludedCall) {
292 let key = (self.lane_of)(&call);
293 let mut g = self.shared.inner.lock().unwrap();
294 if g.closing.is_some() {
295 drop(g);
296 self.host.call_result(&call.path, Outcome::Failed, "recorder stopping", "");
297 return;
298 }
299 let lane = self.lane(&mut g, &key);
300 if lane.ready.len() + lane.waiting.len() >= self.opts.capacity {
301 drop(g);
302 self.host.call_result(&call.path, Outcome::Failed, format!("too many {}s waiting", self.opts.noun), "");
303 return;
304 }
305 lane.ready.push_back(Item { call, tries: 0, due: Instant::now(), last_error: String::new() });
306 g.changed = true;
307 drop(g);
308 self.shared.wake.notify_all();
309 }
310
311 pub fn len(&self) -> usize {
313 let g = self.shared.inner.lock().unwrap();
314 g.lanes.values().map(|l| l.ready.len() + l.waiting.len() + l.busy).sum()
315 }
316
317 pub fn is_empty(&self) -> bool {
318 self.len() == 0
319 }
320
321 pub fn shutdown(&mut self, grace: Duration) {
324 let deadline = Instant::now() + grace.mul_f64(0.8);
326 {
327 let mut g = self.shared.inner.lock().unwrap();
328 if g.closing.is_some() {
329 return;
330 }
331 g.closing = Some(deadline);
332 }
333 self.shared.wake.notify_all();
334 for w in std::mem::take(&mut *self.workers.lock().unwrap()) {
335 let _ = w.join();
336 }
337 let mut g = self.shared.inner.lock().unwrap();
338 let left: Vec<Item> = g.lanes.values_mut().flat_map(|l| l.ready.drain(..).chain(l.waiting.drain(..)).collect::<Vec<_>>()).collect();
339 if left.is_empty() {
340 return;
341 }
342 match &self.opts.save_to {
343 Some(path) => match save(path, &left) {
344 Ok(()) => self.host.info(format!("{} {}(s) saved for next time", left.len(), self.opts.noun)),
345 Err(e) => {
346 self.host.error(format!("couldn't save {} waiting {}(s): {e}", left.len(), self.opts.noun));
347 left.iter().for_each(|i| self.host.call_result(&i.call.path, Outcome::Failed, "recorder stopped", ""));
348 }
349 },
350 None => left.iter().for_each(|i| self.host.call_result(&i.call.path, Outcome::Failed, "recorder stopped", "")),
351 }
352 }
353}
354
355fn worker(sh: &Shared, key: &str, host: &Host, work: &Work, o: &QueueOptions) {
357 let mut g = sh.inner.lock().unwrap();
358 loop {
359 let now = Instant::now();
360 let closing = g.closing;
361 let lane = g.lanes.get_mut(key).expect("lane");
362 let mut i = 0;
364 while i < lane.waiting.len() {
365 if lane.waiting[i].due <= now {
366 let it = lane.waiting.swap_remove(i);
367 lane.ready.push_front(it);
368 } else {
369 i += 1;
370 }
371 }
372 if let Some(deadline) = closing {
373 if now >= deadline || lane.ready.is_empty() {
375 return;
376 }
377 }
378 let Some(mut it) = lane.ready.pop_front() else {
379 let next = lane.waiting.iter().map(|w| w.due).min();
380 g.report(host, o);
381 let wait = next.map_or(Duration::from_secs(1), |t| t.saturating_duration_since(now)).min(Duration::from_secs(1));
382 g = sh.wake.wait_timeout(g, wait).unwrap().0;
383 continue;
384 };
385 lane.busy += 1;
386 drop(g);
387 let started = Instant::now();
388 let r = work(&it.call);
389 let took = started.elapsed();
390 let took_ms = took.as_secs_f64() * 1000.0;
391 g = sh.inner.lock().unwrap();
392 g.changed = true;
393 let lane = g.lanes.get_mut(key).expect("lane");
394 lane.busy -= 1;
395 {
396 let t = &mut lane.stats;
397 t.first_try.get_or_insert_with(unix_now);
398 match &r {
399 Attempt::Done { .. } => {
400 t.tried = true;
401 t.in_a_row = 0;
402 t.last_ok = Some(unix_now());
403 t.latency_ms = Some(t.latency_ms.map_or(took_ms, |l| l + 0.2 * (took_ms - l)));
404 let f = &it.call.files;
405 t.bytes += f.m4a.as_ref().and_then(|m| std::fs::metadata(m).ok()).or_else(|| std::fs::metadata(&f.wav).ok()).map_or(0, |m| m.len());
406 }
407 Attempt::Retry(_) | Attempt::Fail(_) => {
408 t.tried = true;
409 t.in_a_row += 1;
410 t.last_error = Some(unix_now());
411 t.retries += matches!(r, Attempt::Retry(_)) as u64;
412 }
413 Attempt::Skip(_) => {}
414 }
415 }
416 let said = |m: &str| if key.is_empty() { m.to_string() } else { format!("{key}: {m}") };
418 let mut error = None;
419 match r {
420 Attempt::Done { url } => host.call_result_took(&it.call.path, Outcome::Ok, "", url, took),
421 Attempt::Skip(m) => host.call_result(&it.call.path, Outcome::Skipped, m, ""),
422 Attempt::Fail(m) => {
423 lane.last_error = m.clone();
424 error = Some(said(&m));
425 host.call_result(&it.call.path, Outcome::Failed, m, "");
426 }
427 Attempt::Retry(m) => match o.retry_after.get(it.tries) {
428 Some(d) => {
429 it.tries += 1;
430 it.due = Instant::now() + *d;
431 it.last_error = m.clone();
432 lane.last_error = m.clone();
433 error = Some(said(&m));
434 lane.waiting.push(it);
435 }
436 None => host.call_result(&it.call.path, Outcome::Failed, format!("gave up after {} tries: {m}", it.tries + 1), ""),
437 },
438 }
439 if let Some(e) = error {
440 g.last_error = e;
441 }
442 let n = g.waiting();
444 if (n == 0) != (g.reported == 0) || n >= g.reported * 2 && n >= 10 {
445 g.reported = n;
446 if n == 0 {
447 host.status(State::Ok, "");
448 } else {
449 let noun = if n == 1 { o.noun.to_string() } else { format!("{}s", o.noun) };
450 host.status(State::Warning, format!("{n} {noun} waiting to retry: {}", g.last_error));
451 }
452 }
453 g.report(host, o);
454 }
455}
456
457fn load(path: &Path) -> Vec<ConcludedCall> {
458 let Ok(f) = std::fs::File::open(path) else {
459 return Vec::new();
460 };
461 BufReader::new(f)
462 .lines()
463 .map_while(Result::ok)
464 .filter_map(|l| serde_json::from_str::<ConcludedCall>(&l).ok())
465 .filter(|c| c.files.wav.exists() || c.files.m4a.as_ref().is_some_and(|m| m.exists()))
467 .collect()
468}
469
470fn save(path: &Path, items: &[Item]) -> std::io::Result<()> {
471 if let Some(d) = path.parent() {
472 std::fs::create_dir_all(d)?;
473 }
474 let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
475 for it in items {
476 writeln!(f, "{}", serde_json::to_string(&it.call).unwrap_or_default())?;
477 }
478 f.flush()
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use crate::testing;
485 use std::sync::atomic::{AtomicUsize, Ordering};
486
487 fn quick(dir: &Path) -> QueueOptions {
488 QueueOptions { retry_after: vec![Duration::from_millis(20), Duration::from_millis(20)], threads: 1, ..QueueOptions::saved_in(dir) }
489 }
490
491 #[test]
492 fn retries_then_succeeds_and_reports() {
493 let dir = testing::temp_dir("queue");
494 let (host, out) = testing::capture();
495 let tries = Arc::new(AtomicUsize::new(0));
496 let t = tries.clone();
497 let mut q = CallQueue::start(host, quick(&dir), move |_| {
498 if t.fetch_add(1, Ordering::SeqCst) == 0 {
499 Attempt::Retry("down".into())
500 } else {
501 Attempt::Done { url: "u".into() }
502 }
503 });
504 let call = testing::call(&dir, "sys1", 5);
505 q.push(call.clone());
506 let t0 = Instant::now();
507 while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
508 std::thread::sleep(Duration::from_millis(5));
509 }
510 q.shutdown(Duration::from_secs(1));
511 let out = out.output();
512 assert_eq!(tries.load(Ordering::SeqCst), 2);
513 assert_eq!(out.results(), vec![(call.path, Outcome::Ok, String::new(), "u".into())]);
514 assert!(matches!(out.status(), Some((State::Ok, _))));
515 }
516
517 #[test]
518 fn metrics_follow_the_service() {
519 let dir = testing::temp_dir("queue");
520 let (host, out) = testing::capture();
521 let tries = Arc::new(AtomicUsize::new(0));
522 let t = tries.clone();
523 let opts = QueueOptions { endpoint: Some("Example Calls".into()), ..quick(&dir) };
524 let mut q = CallQueue::start(host, opts, move |_| {
525 if t.fetch_add(1, Ordering::SeqCst) == 0 {
526 Attempt::Retry("503".into())
527 } else {
528 Attempt::Done { url: String::new() }
529 }
530 });
531 q.push(testing::call(&dir, "sys1", 5));
532 let t0 = Instant::now();
533 while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
534 std::thread::sleep(Duration::from_millis(5));
535 }
536 q.shutdown(Duration::from_secs(1));
537 let m = out.output().metrics();
538 let states: Vec<EndpointState> = m.iter().filter_map(|x| x.endpoints.first().map(|e| e.state)).collect();
540 assert!(states.contains(&EndpointState::Degraded), "{states:?}");
541 assert_eq!(states.last(), Some(&EndpointState::Up), "{states:?}");
542 let last = m.last().unwrap();
543 assert_eq!(last.retries, Some(1));
544 assert_eq!(last.queued, Some(0));
545 assert!(last.latency_ms.is_some() && last.last_ok.is_some());
546 assert_eq!(last.endpoints[0].name, "Example Calls");
547 }
548
549 #[test]
550 fn gives_up_after_the_last_retry() {
551 let dir = testing::temp_dir("queue");
552 let (host, out) = testing::capture();
553 let mut q = CallQueue::start(host, quick(&dir), |_| Attempt::Retry("down".into()));
554 q.push(testing::call(&dir, "sys1", 5));
555 let t0 = Instant::now();
556 while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
557 std::thread::sleep(Duration::from_millis(5));
558 }
559 q.shutdown(Duration::from_secs(1));
560 let r = out.output().results();
561 assert_eq!(r[0].1, Outcome::Failed);
562 assert!(r[0].2.contains("3 tries"), "{}", r[0].2);
563 }
564
565 #[test]
566 fn saves_what_waits_at_shutdown_and_resumes() {
567 let dir = testing::temp_dir("queue");
568 let (host, _) = testing::capture();
569 let opts = QueueOptions { retry_after: vec![Duration::from_secs(3600)], threads: 1, ..QueueOptions::saved_in(&dir) };
570 let mut q = CallQueue::start(host, opts.clone(), |_| Attempt::Retry("down".into()));
571 q.push(testing::call(&dir, "sys1", 5));
572 std::thread::sleep(Duration::from_millis(50));
573 q.shutdown(Duration::from_secs(1));
574 assert!(dir.join("queue.jsonl").exists());
575 let (host, out) = testing::capture();
577 let mut q = CallQueue::start(host, opts, |_| Attempt::Done { url: String::new() });
578 let t0 = Instant::now();
579 while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
580 std::thread::sleep(Duration::from_millis(5));
581 }
582 q.shutdown(Duration::from_secs(1));
583 assert_eq!(out.output().results()[0].1, Outcome::Ok);
584 assert!(!dir.join("queue.jsonl").exists());
585 }
586
587 #[test]
590 fn a_lane_that_is_down_holds_up_no_other() {
591 let dir = testing::temp_dir("queue");
592 let (host, out) = testing::capture();
593 let opts = QueueOptions { endpoint: Some("Example".into()), retry_after: vec![Duration::from_secs(3600)], ..quick(&dir) };
594 let mut q = CallQueue::start_by(
595 host,
596 opts,
597 |c| if c.call.short_name == "down" { "down.example".into() } else { "up.example".into() },
598 |c| {
599 if c.call.short_name == "down" {
600 std::thread::sleep(Duration::from_millis(300));
601 Attempt::Retry("timed out".into())
602 } else {
603 Attempt::Done { url: String::new() }
604 }
605 },
606 );
607 q.push(testing::call(&dir, "down", 1));
608 std::thread::sleep(Duration::from_millis(20));
609 let up = testing::call(&dir, "up", 2);
610 q.push(up.clone());
611 let t0 = Instant::now();
612 while out.output().results().is_empty() && t0.elapsed() < Duration::from_secs(5) {
613 std::thread::sleep(Duration::from_millis(5));
614 }
615 assert!(t0.elapsed() < Duration::from_millis(250), "the up lane waited on the down one");
616 assert_eq!(out.output().results(), vec![(up.path, Outcome::Ok, String::new(), String::new())]);
617 while q.len() > 1 || out.output().status().is_none() {
619 std::thread::sleep(Duration::from_millis(5));
620 }
621 assert_eq!(out.output().status(), Some((State::Warning, "1 call waiting to retry: down.example: timed out".into())));
622 let m = q.shared.inner.lock().unwrap().metrics(&q.opts);
623 let ends: Vec<(&str, EndpointState)> = m.endpoints.iter().map(|e| (e.name.as_str(), e.state)).collect();
624 assert_eq!(ends, [("Example at down.example", EndpointState::Degraded), ("Example at up.example", EndpointState::Up)]);
625 assert_eq!((m.queued, m.retrying), (Some(1), Some(1)));
626 q.shutdown(Duration::from_secs(1));
627 let (host, out) = testing::capture();
629 let mut q = CallQueue::start_by(host, quick(&dir), |c| c.call.short_name.clone(), |_| Attempt::Done { url: String::new() });
630 assert_eq!(q.shared.inner.lock().unwrap().lanes.keys().collect::<Vec<_>>(), ["down"]);
631 let t0 = Instant::now();
632 while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
633 std::thread::sleep(Duration::from_millis(5));
634 }
635 q.shutdown(Duration::from_secs(1));
636 assert_eq!(out.output().results()[0].1, Outcome::Ok);
637 }
638}