Skip to main content

trunk_recorder_plugin/
queue.rs

1//! A queue of calls worked off the event thread — what an uploader needs:
2//! retries with backoff, a warning status while calls are waiting, results
3//! and [`Metrics`] (queue depth, upload time, how the service is answering)
4//! reported for you, and calls still waiting at shutdown saved to the data
5//! folder and picked up at the next start.
6//!
7//! ```ignore
8//! let queue = CallQueue::start(host.clone(), QueueOptions::saved_in(&setup.data_dir), move |call| {
9//!     match upload(call) {
10//!         Ok(url) => Attempt::Done { url },
11//!         Err(e) if e.is_temporary() => Attempt::Retry(e.to_string()),
12//!         Err(e) => Attempt::Fail(e.to_string()),
13//!     }
14//! });
15//! // in call_concluded:   queue.push(call);
16//! // in shutdown:         self.queue.shutdown(grace);
17//! ```
18//!
19//! A plugin that sends calls to more than one place (a system's settings can
20//! name a server of its own) starts it with [`CallQueue::start_by`] and says
21//! which place each call goes to: each place is a lane of its own, with its
22//! own workers, retries, capacity and endpoint status, so one that's down
23//! doesn't hold up the others.
24
25use 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/// What became of one try at a call.
36#[derive(Clone, Debug)]
37pub enum Attempt {
38    /// Done; `url`: where it can be found now (or empty).
39    Done { url: String },
40    /// Not for this plugin (reported as skipped).
41    Skip(String),
42    /// Failed for now (the service is down, the network's out): try again later.
43    Retry(String),
44    /// Failed for good (it was refused): report it and move on.
45    Fail(String),
46}
47
48#[derive(Clone, Debug)]
49pub struct QueueOptions {
50    /// How long to wait before each retry; after the last, the call fails.
51    pub retry_after: Vec<Duration>,
52    /// Calls worked on at once (in each lane).
53    pub threads: usize,
54    /// Where to save calls still waiting at shutdown (None: they fail).
55    pub save_to: Option<PathBuf>,
56    /// Calls waiting beyond this (in a lane) are refused (reported as failed).
57    pub capacity: usize,
58    /// A name for the status line, e.g. "upload" → "3 uploads waiting to retry".
59    pub noun: &'static str,
60    /// The service the work goes to ("Broadcastify Calls"), for the
61    /// dashboard's endpoint status ("Broadcastify Calls at <lane>" for a
62    /// lane). None: the plugin's own name is shown (or the lane's).
63    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    /// The defaults, saving waiting calls to `queue.jsonl` in `data_dir`.
74    pub fn saved_in(data_dir: &Path) -> Self {
75        QueueOptions { save_to: Some(data_dir.join("queue.jsonl")), ..Default::default() }
76    }
77
78    /// The endpoint status's name for `lane`.
79    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/// The calls for one place, and how it's answering.
97#[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    /// The waiting count last reported in a status.
111    reported: usize,
112    /// The last error of any lane, for the status line.
113    last_error: String,
114    /// Something changed since metrics last went out.
115    changed: bool,
116    sent: Option<Instant>,
117    /// Each lane's state when metrics last went out.
118    sent_states: Vec<EndpointState>,
119}
120
121/// What a lane has done, for [`Metrics`].
122#[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    /// Failed tries in a row (0 after a success).
130    in_a_row: u32,
131    tried: bool,
132    /// Unix s of the first try (the clock for "silent" before any success).
133    first_try: Option<f64>,
134}
135
136/// Metrics go out at most this often, and at least this often while something changes.
137const METRICS_MIN: Duration = Duration::from_secs(5);
138const METRICS_MAX: Duration = Duration::from_secs(30);
139/// The service counts as down after this many failed tries in a row, or
140/// this long without a success while calls wait.
141const 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    /// The lanes' figures together, and each lane's endpoint.
176    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            // The slowest place is the one to know about.
193            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    /// Send metrics when something changed and it's been a while (or a
209    /// service's state changed: at once).
210    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    /// Start `opts.threads` workers running `work` on each call pushed — and
242    /// on calls saved by the last shutdown, first.
243    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    /// [`start`](Self::start), with each call put in the lane `lane` names
251    /// (the server it goes to, say): each lane has `opts.threads` workers of
252    /// its own, and its own retries and endpoint status. Calls saved by the
253    /// last shutdown are put in the lane `lane` names for them now.
254    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    /// Lane `key`, started (its workers) if it's new.
278    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    /// Calls queued or being worked on.
312    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    /// Work what's ready until `grace` is nearly up; save the rest (or fail it).
322    /// Call it from [`crate::Plugin::shutdown`]; calls pushed after it are refused.
323    pub fn shutdown(&mut self, grace: Duration) {
324        // Leave a little of the grace period for saving.
325        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
355/// Work lane `key`'s calls.
356fn 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        // Retries that are due go to the front: they've waited longest.
363        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            // Stopping: work what's ready (retries not yet due are saved) until the deadline.
374            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_ms = started.elapsed().as_secs_f64() * 1000.0;
390        g = sh.inner.lock().unwrap();
391        g.changed = true;
392        let lane = g.lanes.get_mut(key).expect("lane");
393        lane.busy -= 1;
394        {
395            let t = &mut lane.stats;
396            t.first_try.get_or_insert_with(unix_now);
397            match &r {
398                Attempt::Done { .. } => {
399                    t.tried = true;
400                    t.in_a_row = 0;
401                    t.last_ok = Some(unix_now());
402                    t.latency_ms = Some(t.latency_ms.map_or(took_ms, |l| l + 0.2 * (took_ms - l)));
403                    let f = &it.call.files;
404                    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());
405                }
406                Attempt::Retry(_) | Attempt::Fail(_) => {
407                    t.tried = true;
408                    t.in_a_row += 1;
409                    t.last_error = Some(unix_now());
410                    t.retries += matches!(r, Attempt::Retry(_)) as u64;
411                }
412                Attempt::Skip(_) => {}
413            }
414        }
415        // What went wrong, and where (when there's more than one place).
416        let said = |m: &str| if key.is_empty() { m.to_string() } else { format!("{key}: {m}") };
417        let mut error = None;
418        match r {
419            Attempt::Done { url } => host.call_result(&it.call.path, Outcome::Ok, "", url),
420            Attempt::Skip(m) => host.call_result(&it.call.path, Outcome::Skipped, m, ""),
421            Attempt::Fail(m) => {
422                lane.last_error = m.clone();
423                error = Some(said(&m));
424                host.call_result(&it.call.path, Outcome::Failed, m, "");
425            }
426            Attempt::Retry(m) => match o.retry_after.get(it.tries) {
427                Some(d) => {
428                    it.tries += 1;
429                    it.due = Instant::now() + *d;
430                    it.last_error = m.clone();
431                    lane.last_error = m.clone();
432                    error = Some(said(&m));
433                    lane.waiting.push(it);
434                }
435                None => host.call_result(&it.call.path, Outcome::Failed, format!("gave up after {} tries: {m}", it.tries + 1), ""),
436            },
437        }
438        if let Some(e) = error {
439            g.last_error = e;
440        }
441        // Say when calls start or stop waiting on retries (not on every change).
442        let n = g.waiting();
443        if (n == 0) != (g.reported == 0) || n >= g.reported * 2 && n >= 10 {
444            g.reported = n;
445            if n == 0 {
446                host.status(State::Ok, "");
447            } else {
448                let noun = if n == 1 { o.noun.to_string() } else { format!("{}s", o.noun) };
449                host.status(State::Warning, format!("{n} {noun} waiting to retry: {}", g.last_error));
450            }
451        }
452        g.report(host, o);
453    }
454}
455
456fn load(path: &Path) -> Vec<ConcludedCall> {
457    let Ok(f) = std::fs::File::open(path) else {
458        return Vec::new();
459    };
460    BufReader::new(f)
461        .lines()
462        .map_while(Result::ok)
463        .filter_map(|l| serde_json::from_str::<ConcludedCall>(&l).ok())
464        // (The user may have deleted the call meanwhile.)
465        .filter(|c| c.files.wav.exists() || c.files.m4a.as_ref().is_some_and(|m| m.exists()))
466        .collect()
467}
468
469fn save(path: &Path, items: &[Item]) -> std::io::Result<()> {
470    if let Some(d) = path.parent() {
471        std::fs::create_dir_all(d)?;
472    }
473    let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
474    for it in items {
475        writeln!(f, "{}", serde_json::to_string(&it.call).unwrap_or_default())?;
476    }
477    f.flush()
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use crate::testing;
484    use std::sync::atomic::{AtomicUsize, Ordering};
485
486    fn quick(dir: &Path) -> QueueOptions {
487        QueueOptions { retry_after: vec![Duration::from_millis(20), Duration::from_millis(20)], threads: 1, ..QueueOptions::saved_in(dir) }
488    }
489
490    #[test]
491    fn retries_then_succeeds_and_reports() {
492        let dir = testing::temp_dir("queue");
493        let (host, out) = testing::capture();
494        let tries = Arc::new(AtomicUsize::new(0));
495        let t = tries.clone();
496        let mut q = CallQueue::start(host, quick(&dir), move |_| {
497            if t.fetch_add(1, Ordering::SeqCst) == 0 {
498                Attempt::Retry("down".into())
499            } else {
500                Attempt::Done { url: "u".into() }
501            }
502        });
503        let call = testing::call(&dir, "sys1", 5);
504        q.push(call.clone());
505        let t0 = Instant::now();
506        while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
507            std::thread::sleep(Duration::from_millis(5));
508        }
509        q.shutdown(Duration::from_secs(1));
510        let out = out.output();
511        assert_eq!(tries.load(Ordering::SeqCst), 2);
512        assert_eq!(out.results(), vec![(call.path, Outcome::Ok, String::new(), "u".into())]);
513        assert!(matches!(out.status(), Some((State::Ok, _))));
514    }
515
516    #[test]
517    fn metrics_follow_the_service() {
518        let dir = testing::temp_dir("queue");
519        let (host, out) = testing::capture();
520        let tries = Arc::new(AtomicUsize::new(0));
521        let t = tries.clone();
522        let opts = QueueOptions { endpoint: Some("Example Calls".into()), ..quick(&dir) };
523        let mut q = CallQueue::start(host, opts, move |_| {
524            if t.fetch_add(1, Ordering::SeqCst) == 0 {
525                Attempt::Retry("503".into())
526            } else {
527                Attempt::Done { url: String::new() }
528            }
529        });
530        q.push(testing::call(&dir, "sys1", 5));
531        let t0 = Instant::now();
532        while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
533            std::thread::sleep(Duration::from_millis(5));
534        }
535        q.shutdown(Duration::from_secs(1));
536        let m = out.output().metrics();
537        // The service went degraded on the retry (reported at once), then up.
538        let states: Vec<EndpointState> = m.iter().filter_map(|x| x.endpoints.first().map(|e| e.state)).collect();
539        assert!(states.contains(&EndpointState::Degraded), "{states:?}");
540        assert_eq!(states.last(), Some(&EndpointState::Up), "{states:?}");
541        let last = m.last().unwrap();
542        assert_eq!(last.retries, Some(1));
543        assert_eq!(last.queued, Some(0));
544        assert!(last.latency_ms.is_some() && last.last_ok.is_some());
545        assert_eq!(last.endpoints[0].name, "Example Calls");
546    }
547
548    #[test]
549    fn gives_up_after_the_last_retry() {
550        let dir = testing::temp_dir("queue");
551        let (host, out) = testing::capture();
552        let mut q = CallQueue::start(host, quick(&dir), |_| Attempt::Retry("down".into()));
553        q.push(testing::call(&dir, "sys1", 5));
554        let t0 = Instant::now();
555        while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
556            std::thread::sleep(Duration::from_millis(5));
557        }
558        q.shutdown(Duration::from_secs(1));
559        let r = out.output().results();
560        assert_eq!(r[0].1, Outcome::Failed);
561        assert!(r[0].2.contains("3 tries"), "{}", r[0].2);
562    }
563
564    #[test]
565    fn saves_what_waits_at_shutdown_and_resumes() {
566        let dir = testing::temp_dir("queue");
567        let (host, _) = testing::capture();
568        let opts = QueueOptions { retry_after: vec![Duration::from_secs(3600)], threads: 1, ..QueueOptions::saved_in(&dir) };
569        let mut q = CallQueue::start(host, opts.clone(), |_| Attempt::Retry("down".into()));
570        q.push(testing::call(&dir, "sys1", 5));
571        std::thread::sleep(Duration::from_millis(50));
572        q.shutdown(Duration::from_secs(1));
573        assert!(dir.join("queue.jsonl").exists());
574        // Next start: it's worked first.
575        let (host, out) = testing::capture();
576        let mut q = CallQueue::start(host, opts, |_| Attempt::Done { url: String::new() });
577        let t0 = Instant::now();
578        while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
579            std::thread::sleep(Duration::from_millis(5));
580        }
581        q.shutdown(Duration::from_secs(1));
582        assert_eq!(out.output().results()[0].1, Outcome::Ok);
583        assert!(!dir.join("queue.jsonl").exists());
584    }
585
586    /// Two places: one that hangs and is down doesn't hold up the other,
587    /// and each has its own endpoint status.
588    #[test]
589    fn a_lane_that_is_down_holds_up_no_other() {
590        let dir = testing::temp_dir("queue");
591        let (host, out) = testing::capture();
592        let opts = QueueOptions { endpoint: Some("Example".into()), retry_after: vec![Duration::from_secs(3600)], ..quick(&dir) };
593        let mut q = CallQueue::start_by(
594            host,
595            opts,
596            |c| if c.call.short_name == "down" { "down.example".into() } else { "up.example".into() },
597            |c| {
598                if c.call.short_name == "down" {
599                    std::thread::sleep(Duration::from_millis(300));
600                    Attempt::Retry("timed out".into())
601                } else {
602                    Attempt::Done { url: String::new() }
603                }
604            },
605        );
606        q.push(testing::call(&dir, "down", 1));
607        std::thread::sleep(Duration::from_millis(20));
608        let up = testing::call(&dir, "up", 2);
609        q.push(up.clone());
610        let t0 = Instant::now();
611        while out.output().results().is_empty() && t0.elapsed() < Duration::from_secs(5) {
612            std::thread::sleep(Duration::from_millis(5));
613        }
614        assert!(t0.elapsed() < Duration::from_millis(250), "the up lane waited on the down one");
615        assert_eq!(out.output().results(), vec![(up.path, Outcome::Ok, String::new(), String::new())]);
616        // The down lane's try ends; it waits to retry, and says where.
617        while q.len() > 1 || out.output().status().is_none() {
618            std::thread::sleep(Duration::from_millis(5));
619        }
620        assert_eq!(out.output().status(), Some((State::Warning, "1 call waiting to retry: down.example: timed out".into())));
621        let m = q.shared.inner.lock().unwrap().metrics(&q.opts);
622        let ends: Vec<(&str, EndpointState)> = m.endpoints.iter().map(|e| (e.name.as_str(), e.state)).collect();
623        assert_eq!(ends, [("Example at down.example", EndpointState::Degraded), ("Example at up.example", EndpointState::Up)]);
624        assert_eq!((m.queued, m.retrying), (Some(1), Some(1)));
625        q.shutdown(Duration::from_secs(1));
626        // Saved, and put back in its lane next time.
627        let (host, out) = testing::capture();
628        let mut q = CallQueue::start_by(host, quick(&dir), |c| c.call.short_name.clone(), |_| Attempt::Done { url: String::new() });
629        assert_eq!(q.shared.inner.lock().unwrap().lanes.keys().collect::<Vec<_>>(), ["down"]);
630        let t0 = Instant::now();
631        while !q.is_empty() && t0.elapsed() < Duration::from_secs(5) {
632            std::thread::sleep(Duration::from_millis(5));
633        }
634        q.shutdown(Duration::from_secs(1));
635        assert_eq!(out.output().results()[0].1, Outcome::Ok);
636    }
637}