Skip to main content

zenkey_fleet/bus/
teardown.rs

1//! The teardown shape the crate shares: **drain everything, then report**.
2//!
3//! [`crate::Monitor::shutdown`] states the rule — "every watch is drained even
4//! if one fails to undeclare … and the failures are reported together" —
5//! because a set half torn down is worse than one torn down noisily. The other
6//! teardowns wrote the loop themselves and bailed on the first failure with a
7//! `?`, leaving everything after it declared (#327, #346).
8//!
9//! This is that loop, once, for anything with an `undeclare`.
10
11use std::future::Future;
12
13use crate::{Error, Result};
14
15/// Undeclare every item, then report what failed — all of it, in one error
16/// naming each item by the label it was drained under.
17///
18/// The label is what a reader needs to find the thing that would not go away:
19/// a key, a selector, a querier's role. Nothing is skipped for an earlier
20/// failure.
21pub(crate) async fn drain_undeclare<T, F, Fut>(items: Vec<(String, T)>, undeclare: F) -> Result<()>
22where
23    F: Fn(T) -> Fut,
24    Fut: Future<Output = Result<()>>,
25{
26    let mut failed = Vec::new();
27    for (label, item) in items {
28        if let Err(e) = undeclare(item).await {
29            // The whole chain: `Display` alone names the operation, and the
30            // per-handle reason is what a teardown report is for (#348).
31            failed.push(format!("{label}: {}", crate::one_line(&e)));
32        }
33    }
34    if failed.is_empty() {
35        Ok(())
36    } else {
37        Err(Error::bus(
38            "undeclare",
39            failed.join("; "),
40            "one or more handles refused",
41        ))
42    }
43}
44
45/// How long a declaration may take before this crate calls it a failure.
46///
47/// A `declare_subscriber`/`declare_queryable`/`declare_token` that never
48/// returns hangs the tool with nothing to report, which is exactly the shape
49/// #341 fixed for `zenoh::open` — and [`OPEN_TIMEOUT`](crate::OPEN_TIMEOUT) is
50/// the precedent this follows. Shorter than the open, because a declaration
51/// happens against a session that is already up (#346).
52pub const DECLARE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
53
54/// Await one declaration under [`DECLARE_TIMEOUT`], naming what stalled.
55///
56/// A stall is an [`Error::Bus`] like any other declare failure: the caller's
57/// input was fine, the fabric did not complete the operation. RFC 13 §3 — a
58/// tool that cannot obtain an observation says so rather than waiting forever
59/// in silence.
60pub(crate) async fn declared<T, E>(
61    op: &'static str,
62    target: impl std::fmt::Display,
63    // `IntoFuture`, not `Future`: zenoh's declaration builders are builders
64    // until awaited, which is what lets a caller pass one straight in.
65    builder: impl std::future::IntoFuture<Output = std::result::Result<T, E>>,
66) -> Result<T>
67where
68    E: std::fmt::Display,
69{
70    match tokio::time::timeout(DECLARE_TIMEOUT, builder.into_future()).await {
71        Ok(Ok(v)) => Ok(v),
72        Ok(Err(e)) => Err(Error::bus(op, target.to_string(), e.to_string())),
73        Err(_) => Err(Error::bus(
74            op,
75            target.to_string(),
76            format!("did not complete within {DECLARE_TIMEOUT:?}"),
77        )),
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::sync::atomic::{AtomicUsize, Ordering};
85
86    /// The property the `?`-in-a-loop shape did not have: a failure in the
87    /// middle stops nothing, and the report names every one of them.
88    #[tokio::test]
89    async fn a_failure_stops_nothing_and_every_failure_is_reported() {
90        let visited = AtomicUsize::new(0);
91        let err = drain_undeclare(
92            vec![
93                ("first".to_string(), Ok(())),
94                ("second".to_string(), Err("busy")),
95                ("third".to_string(), Ok(())),
96                ("fourth".to_string(), Err("gone")),
97            ],
98            |outcome: std::result::Result<(), &str>| {
99                visited.fetch_add(1, Ordering::Relaxed);
100                async move { outcome.map_err(|e| Error::bus("undeclare", "handle", e)) }
101            },
102        )
103        .await
104        .expect_err("two of the four would not undeclare")
105        .to_string();
106
107        assert_eq!(visited.load(Ordering::Relaxed), 4, "every item was drained");
108        // Each failure is named *and* carries its reason. The exact join is
109        // not the property; both halves being present is (#348 moved the
110        // reason from `Display` into `source`, so this reads the chain).
111        for (label, reason) in [("second", "busy"), ("fourth", "gone")] {
112            assert!(err.contains(label), "{label} missing from: {err}");
113            assert!(err.contains(reason), "{reason} missing from: {err}");
114        }
115    }
116
117    /// A clean teardown says nothing.
118    #[tokio::test]
119    async fn everything_undeclared_is_silent() {
120        let all_fine = drain_undeclare(vec![("only".to_string(), ())], |()| async { Ok(()) }).await;
121        assert!(all_fine.is_ok());
122    }
123}