Skip to main content

moonpool_transport/rpc/
failure_monitor.rs

1//! `FailureMonitor`: Reactive failure tracking for addresses and endpoints.
2//!
3//! Tracks two levels of failure:
4//! - **Address-level**: Is a remote machine reachable? (Missing = Failed)
5//! - **Endpoint-level**: Is a specific endpoint permanently dead?
6//!
7//! Producers (`connection_task`) call [`FailureMonitor::set_status`] and
8//! [`FailureMonitor::notify_disconnect`]. Consumers (delivery mode functions)
9//! poll [`FailureMonitor::on_disconnect_or_failure`] to race replies against
10//! disconnect signals.
11//!
12//! # FDB Reference
13//! `SimpleFailureMonitor` from `FailureMonitor.h:146`, `FailureMonitor.actor.cpp`
14
15use std::collections::{BTreeMap, BTreeSet};
16use std::future::Future;
17use std::pin::Pin;
18use std::sync::Arc;
19use std::sync::RwLock;
20use std::task::{Poll, Waker};
21use std::time::Duration;
22
23use moonpool_core::TimeProvider;
24
25use crate::Endpoint;
26
27/// Type-erased sleep closure capturing a concrete [`TimeProvider`].
28///
29/// Keeps [`FailureMonitor`] non-generic while still able to call into the
30/// active time provider (real or simulated).
31type SleepFn = Box<dyn Fn(Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
32
33/// Maximum number of permanently failed endpoints before clearing the map.
34///
35/// Matches FDB: `failedEndpoints.size() > 100000` triggers clear.
36const MAX_FAILED_ENDPOINTS: usize = 100_000;
37
38/// Status of a network address or endpoint.
39///
40/// # FDB Reference
41/// `FailureStatus` from `FailureMonitor.h:34-60`
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum FailureStatus {
44    /// Address is reachable.
45    Available,
46    /// Address is unreachable (default for unknown addresses).
47    Failed,
48}
49
50/// Reactive failure monitor for address and endpoint tracking.
51///
52/// Thread-safe (`Send + Sync`) via `Arc<RwLock<…>>` for interior mutability.
53/// Consumers register wakers via `on_disconnect_or_failure` and similar methods;
54/// producers wake them via `set_status`, `notify_disconnect`, `endpoint_not_found`.
55///
56/// Use [`on_failed_for`](Self::on_failed_for) to wait for a sustained failure
57/// duration before giving up — the canonical building block behind
58/// [`get_reply_unless_failed_for`](crate::rpc::delivery::get_reply_unless_failed_for).
59///
60/// # FDB Reference
61/// `SimpleFailureMonitor` from `FailureMonitor.h:146`, `FailureMonitor.actor.cpp`
62pub struct FailureMonitor {
63    inner: RwLock<FailureMonitorInner>,
64    sleep_fn: SleepFn,
65}
66
67struct FailureMonitorInner {
68    /// Address-level status. Missing entry = Failed (FDB default).
69    address_status: BTreeMap<String, FailureStatus>,
70    /// Permanently failed endpoints (e.g., endpoint not found on remote).
71    failed_endpoints: BTreeSet<Endpoint>,
72    /// Wakers waiting for endpoint state changes, keyed by address.
73    /// Woken on: `set_status` change, `notify_disconnect`, `endpoint_not_found`.
74    endpoint_watchers: BTreeMap<String, Vec<Waker>>,
75    /// Wakers waiting for disconnect events, keyed by address.
76    /// Woken only on: `notify_disconnect`.
77    disconnect_watchers: BTreeMap<String, Vec<Waker>>,
78}
79
80impl FailureMonitor {
81    /// Create a new failure monitor with empty state.
82    ///
83    /// All unknown addresses default to [`FailureStatus::Failed`] until
84    /// a connection succeeds and calls [`set_status`](Self::set_status).
85    ///
86    /// The `time` provider is captured behind a type-erased sleep closure
87    /// so [`FailureMonitor`] itself stays non-generic.
88    pub fn new<T: TimeProvider + 'static>(time: T) -> Self {
89        let sleep_fn: SleepFn = Box::new(move |duration| {
90            let time = time.clone();
91            Box::pin(async move {
92                let _ = time.sleep(duration).await;
93            })
94        });
95        Self {
96            inner: RwLock::new(FailureMonitorInner {
97                address_status: BTreeMap::new(),
98                failed_endpoints: BTreeSet::new(),
99                endpoint_watchers: BTreeMap::new(),
100                disconnect_watchers: BTreeMap::new(),
101            }),
102            sleep_fn,
103        }
104    }
105
106    // =========================================================================
107    // Producer methods (called by connection_task)
108    // =========================================================================
109
110    /// Update the status of an address.
111    ///
112    /// Called by `connection_task` on successful connect (`Available`) or
113    /// connection failure (`Failed`).
114    ///
115    /// Wakes all endpoint watchers for this address on status change.
116    ///
117    /// # FDB Reference
118    /// `SimpleFailureMonitor::setStatus` (FailureMonitor.actor.cpp:83-115)
119    ///
120    /// # Panics
121    ///
122    /// Panics if the internal `RwLock` is poisoned (only possible if a prior
123    /// task panicked while holding the lock).
124    pub fn set_status(&self, address: &str, status: FailureStatus) {
125        let mut inner = self
126            .inner
127            .write()
128            .expect("RwLock poisoned: prior task panicked");
129
130        let changed = match status {
131            FailureStatus::Available => {
132                let prev = inner.address_status.insert(address.to_string(), status);
133                prev != Some(FailureStatus::Available)
134            }
135            FailureStatus::Failed => {
136                // Missing = Failed, so remove the entry
137                inner.address_status.remove(address).is_some()
138            }
139        };
140
141        if changed {
142            wake_all(&mut inner.endpoint_watchers, address);
143        }
144    }
145
146    /// Signal that a connection to the given address has been lost.
147    ///
148    /// Wakes both endpoint watchers and disconnect watchers for this address.
149    /// Called by `connection_task` after `disconnect_notify.notify_waiters()`.
150    ///
151    /// # FDB Reference
152    /// `SimpleFailureMonitor::notifyDisconnect` (FailureMonitor.actor.cpp:150-154)
153    ///
154    /// # Panics
155    ///
156    /// Panics if the internal `RwLock` is poisoned (only possible if a prior
157    /// task panicked while holding the lock).
158    pub fn notify_disconnect(&self, address: &str) {
159        let mut inner = self
160            .inner
161            .write()
162            .expect("RwLock poisoned: prior task panicked");
163        wake_all(&mut inner.endpoint_watchers, address);
164        wake_all(&mut inner.disconnect_watchers, address);
165    }
166
167    /// Mark an endpoint as permanently failed (e.g., not found on remote).
168    ///
169    /// Permanently failed endpoints are never automatically recovered.
170    /// Wakes endpoint watchers for the endpoint's address.
171    ///
172    /// Skips well-known tokens (system endpoints that always exist).
173    ///
174    /// # FDB Reference
175    /// `SimpleFailureMonitor::endpointNotFound` (FailureMonitor.actor.cpp:117-139)
176    ///
177    /// # Panics
178    ///
179    /// Panics if the internal `RwLock` is poisoned (only possible if a prior
180    /// task panicked while holding the lock).
181    pub fn endpoint_not_found(&self, endpoint: &Endpoint) {
182        // Skip well-known tokens (FDB: `if token.first() == -1 return`)
183        if endpoint.token.is_well_known() {
184            return;
185        }
186
187        let mut inner = self
188            .inner
189            .write()
190            .expect("RwLock poisoned: prior task panicked");
191
192        // Cap is informational: every entry here is a permanent NotFound, so
193        // there is nothing to evict. Drop the new addition with a warning when
194        // we hit the limit rather than thrashing the set.
195        if inner.failed_endpoints.len() >= MAX_FAILED_ENDPOINTS
196            && !inner.failed_endpoints.contains(endpoint)
197        {
198            tracing::warn!(
199                cap = MAX_FAILED_ENDPOINTS,
200                ?endpoint,
201                "FailureMonitor: failed-endpoint set at cap, dropping new NotFound marker"
202            );
203            return;
204        }
205
206        inner.failed_endpoints.insert(endpoint.clone());
207
208        let address = endpoint.address.to_string();
209        wake_all(&mut inner.endpoint_watchers, &address);
210    }
211
212    // =========================================================================
213    // Consumer methods (used by delivery mode functions)
214    // =========================================================================
215
216    /// Get the current failure status of an endpoint.
217    ///
218    /// Returns [`FailureStatus::Failed`] if:
219    /// - The endpoint is permanently failed, OR
220    /// - The endpoint's address is unknown or failed
221    ///
222    /// # FDB Reference
223    /// `SimpleFailureMonitor::getState(Endpoint)` (FailureMonitor.actor.cpp:196-206)
224    ///
225    /// # Panics
226    ///
227    /// Panics if the internal `RwLock` is poisoned (only possible if a prior
228    /// task panicked while holding the lock).
229    pub fn state(&self, endpoint: &Endpoint) -> FailureStatus {
230        let inner = self
231            .inner
232            .read()
233            .expect("RwLock poisoned: prior task panicked");
234
235        if inner.failed_endpoints.contains(endpoint) {
236            return FailureStatus::Failed;
237        }
238
239        let address = endpoint.address.to_string();
240        inner
241            .address_status
242            .get(&address)
243            .copied()
244            .unwrap_or(FailureStatus::Failed) // Missing = Failed
245    }
246
247    /// Check if an endpoint is permanently failed.
248    ///
249    /// # FDB Reference
250    /// `SimpleFailureMonitor::permanentlyFailed` (FailureMonitor.h:226-228)
251    ///
252    /// # Panics
253    ///
254    /// Panics if the internal `RwLock` is poisoned (only possible if a prior
255    /// task panicked while holding the lock).
256    pub fn permanently_failed(&self, endpoint: &Endpoint) -> bool {
257        self.inner
258            .read()
259            .expect("RwLock poisoned: prior task panicked")
260            .failed_endpoints
261            .contains(endpoint)
262    }
263
264    /// Returns a future that resolves when the endpoint's address disconnects
265    /// or the endpoint becomes permanently failed.
266    ///
267    /// **Fast path**: Returns `Ready` immediately if already failed.
268    ///
269    /// Used by `try_get_reply()` to race reply vs disconnect.
270    ///
271    /// # FDB Reference
272    /// `SimpleFailureMonitor::onDisconnectOrFailure` (FailureMonitor.actor.cpp:156-178)
273    ///
274    /// # Panics
275    ///
276    /// The returned future panics if the internal `RwLock` is poisoned (only
277    /// possible if a prior task panicked while holding the lock).
278    pub fn on_disconnect_or_failure(
279        self: &Arc<Self>,
280        endpoint: &Endpoint,
281    ) -> impl Future<Output = ()> + Send {
282        let fm = Arc::clone(self);
283        let address = endpoint.address.to_string();
284        let endpoint = endpoint.clone();
285
286        std::future::poll_fn(move |cx| {
287            let inner = fm
288                .inner
289                .read()
290                .expect("RwLock poisoned: prior task panicked");
291
292            // Fast path: already failed
293            if inner.failed_endpoints.contains(&endpoint) {
294                return Poll::Ready(());
295            }
296            if !inner
297                .address_status
298                .get(&address)
299                .is_some_and(|s| *s == FailureStatus::Available)
300            {
301                // Missing or Failed → already failed
302                return Poll::Ready(());
303            }
304
305            // Slow path: register waker
306            drop(inner);
307            let mut inner = fm
308                .inner
309                .write()
310                .expect("RwLock poisoned: prior task panicked");
311            inner
312                .endpoint_watchers
313                .entry(address.clone())
314                .or_default()
315                .push(cx.waker().clone());
316            Poll::Pending
317        })
318    }
319
320    /// Returns a future that resolves when the given address disconnects.
321    ///
322    /// Only triggered by explicit disconnect events, not status changes.
323    ///
324    /// # FDB Reference
325    /// `SimpleFailureMonitor::onDisconnect` (FailureMonitor.actor.cpp:180-182)
326    ///
327    /// # Panics
328    ///
329    /// The returned future panics if the internal `RwLock` is poisoned (only
330    /// possible if a prior task panicked while holding the lock).
331    pub fn on_disconnect(self: &Arc<Self>, address: &str) -> impl Future<Output = ()> + Send {
332        let fm = Arc::clone(self);
333        let address = address.to_string();
334        let registered = Arc::new(std::sync::atomic::AtomicBool::new(false));
335
336        std::future::poll_fn(move |cx| {
337            // If we already registered and got woken, the disconnect happened
338            if registered.load(std::sync::atomic::Ordering::Acquire) {
339                return Poll::Ready(());
340            }
341
342            // First poll: register waker and mark as registered
343            registered.store(true, std::sync::atomic::Ordering::Release);
344            let mut inner = fm
345                .inner
346                .write()
347                .expect("RwLock poisoned: prior task panicked");
348            inner
349                .disconnect_watchers
350                .entry(address.clone())
351                .or_default()
352                .push(cx.waker().clone());
353            Poll::Pending
354        })
355    }
356
357    /// Waits for failure to be detected, then sleeps `duration` before returning.
358    ///
359    /// Two-phase behaviour, **not** sustained-failure detection:
360    ///
361    /// 1. Block via [`on_disconnect_or_failure`](Self::on_disconnect_or_failure)
362    ///    until a failure is observed at least once.
363    /// 2. Sleep `duration` using the time provider captured at construction,
364    ///    regardless of whether the endpoint recovers in the meantime.
365    ///
366    /// The recovery case is intentionally ignored — callers like
367    /// [`get_reply_unless_failed_for`](crate::rpc::delivery::get_reply_unless_failed_for)
368    /// race this future against the reply future, so a recovered endpoint that
369    /// answers in time wins the race anyway. Treat this as a deadline timer
370    /// armed by the first observed failure, not a continuous health check.
371    pub async fn on_failed_for(self: &Arc<Self>, endpoint: &Endpoint, duration: Duration) {
372        self.on_disconnect_or_failure(endpoint).await;
373        (self.sleep_fn)(duration).await;
374    }
375}
376
377impl std::fmt::Debug for FailureMonitor {
378    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379        let inner = self
380            .inner
381            .read()
382            .expect("RwLock poisoned: prior task panicked");
383        f.debug_struct("FailureMonitor")
384            .field("addresses_available", &inner.address_status.len())
385            .field("endpoints_failed", &inner.failed_endpoints.len())
386            .field("endpoint_watchers", &inner.endpoint_watchers.len())
387            .field("disconnect_watchers", &inner.disconnect_watchers.len())
388            // `sleep_fn` is a captured closure with no useful Debug representation.
389            .finish_non_exhaustive()
390    }
391}
392
393/// Drain and wake all wakers registered for the given address.
394fn wake_all(watchers: &mut BTreeMap<String, Vec<Waker>>, address: &str) {
395    if let Some(wakers) = watchers.remove(address) {
396        for waker in wakers {
397            waker.wake();
398        }
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use std::net::{IpAddr, Ipv4Addr};
405
406    use moonpool_core::TokioTimeProvider;
407
408    use super::*;
409    use crate::{NetworkAddress, UID};
410
411    fn test_addr() -> NetworkAddress {
412        NetworkAddress::new(IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), 4500)
413    }
414
415    fn test_endpoint() -> Endpoint {
416        Endpoint::new(test_addr(), UID::new(42, 1))
417    }
418
419    fn make_fm() -> FailureMonitor {
420        FailureMonitor::new(TokioTimeProvider::new())
421    }
422
423    #[test]
424    fn test_unknown_address_is_failed() {
425        let fm = make_fm();
426        let ep = test_endpoint();
427        assert_eq!(fm.state(&ep), FailureStatus::Failed);
428    }
429
430    #[test]
431    fn test_set_status_available() {
432        let fm = make_fm();
433        let ep = test_endpoint();
434        fm.set_status("10.0.1.1:4500", FailureStatus::Available);
435        assert_eq!(fm.state(&ep), FailureStatus::Available);
436    }
437
438    #[test]
439    fn test_endpoint_not_found_marks_permanent() {
440        let fm = make_fm();
441        let ep = test_endpoint();
442        fm.set_status("10.0.1.1:4500", FailureStatus::Available);
443        fm.endpoint_not_found(&ep);
444        assert!(fm.permanently_failed(&ep));
445        assert_eq!(fm.state(&ep), FailureStatus::Failed);
446    }
447
448    #[test]
449    fn test_endpoint_not_found_skips_well_known() {
450        let fm = make_fm();
451        let ep = Endpoint::well_known(test_addr(), crate::WellKnownToken::Ping);
452        fm.endpoint_not_found(&ep);
453        assert!(!fm.permanently_failed(&ep));
454    }
455
456    #[tokio::test]
457    async fn test_on_disconnect_or_failure_fast_path_unknown() {
458        let fm = Arc::new(make_fm());
459        let ep = test_endpoint();
460        // Unknown address → already failed → should resolve immediately
461        fm.on_disconnect_or_failure(&ep).await;
462    }
463
464    #[tokio::test]
465    async fn test_on_disconnect_or_failure_fast_path_permanent() {
466        let fm = Arc::new(make_fm());
467        let ep = test_endpoint();
468        fm.set_status("10.0.1.1:4500", FailureStatus::Available);
469        fm.endpoint_not_found(&ep);
470        // Permanently failed → should resolve immediately
471        fm.on_disconnect_or_failure(&ep).await;
472    }
473
474    #[test]
475    fn test_on_disconnect_or_failure_wakes_on_status_change() {
476        let rt = tokio::runtime::Builder::new_current_thread()
477            .enable_all()
478            .build()
479            .expect("build runtime");
480        rt.block_on(async {
481            let fm = Arc::new(make_fm());
482            let ep = test_endpoint();
483            fm.set_status("10.0.1.1:4500", FailureStatus::Available);
484
485            let fm2 = Arc::clone(&fm);
486            let handle = tokio::spawn(async move {
487                fm2.on_disconnect_or_failure(&ep).await;
488            });
489
490            // Yield to let the future register its waker
491            tokio::task::yield_now().await;
492
493            // Trigger disconnect → should wake the future
494            fm.set_status("10.0.1.1:4500", FailureStatus::Failed);
495
496            handle.await.expect("task should complete");
497        });
498    }
499
500    #[test]
501    fn test_on_disconnect_wakes_on_notify() {
502        let rt = tokio::runtime::Builder::new_current_thread()
503            .enable_all()
504            .build()
505            .expect("build runtime");
506        rt.block_on(async {
507            let fm = Arc::new(make_fm());
508            fm.set_status("10.0.1.1:4500", FailureStatus::Available);
509
510            let fm2 = Arc::clone(&fm);
511            let handle = tokio::spawn(async move {
512                fm2.on_disconnect("10.0.1.1:4500").await;
513            });
514
515            tokio::task::yield_now().await;
516
517            fm.notify_disconnect("10.0.1.1:4500");
518
519            handle.await.expect("task should complete");
520        });
521    }
522
523    #[test]
524    fn test_debug_impl() {
525        let fm = make_fm();
526        fm.set_status("10.0.1.1:4500", FailureStatus::Available);
527        let debug = format!("{fm:?}");
528        assert!(debug.contains("FailureMonitor"));
529        assert!(debug.contains("addresses_available: 1"));
530    }
531
532    #[tokio::test]
533    async fn test_on_failed_for_resolves_after_sustained_failure() {
534        let fm = Arc::new(make_fm());
535        let addr = NetworkAddress::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 4500);
536        let token = UID::new(0xAAAA, 0xBBBB);
537        let endpoint = Endpoint::new(addr.clone(), token);
538
539        // Mark address failed BEFORE awaiting on_failed_for so the disconnect
540        // detection step resolves immediately and we measure only the sleep.
541        fm.set_status(&addr.to_string(), FailureStatus::Failed);
542
543        let start = tokio::time::Instant::now();
544        fm.on_failed_for(&endpoint, Duration::from_millis(50)).await;
545        let elapsed = start.elapsed();
546
547        assert!(
548            elapsed >= Duration::from_millis(50),
549            "should sleep at least 50ms, got {elapsed:?}"
550        );
551        assert!(
552            elapsed < Duration::from_millis(500),
553            "should not exceed 500ms wall clock, got {elapsed:?}"
554        );
555    }
556}