Skip to main content

r402_http/server/
tracker.rs

1//! In-flight counter for background settlement tasks.
2
3use std::sync::Arc;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::time::Duration;
6
7use tokio::sync::Notify;
8
9/// Shared in-flight counter for background settlement tasks.
10///
11/// Created by the operator at startup, attached to a
12/// [`Paygate`](super::paygate::Paygate) via
13/// [`PaygateBuilder::with_settlement_tracker`](super::paygate::PaygateBuilder::with_settlement_tracker),
14/// and drained at shutdown via
15/// [`Paygate::settlement_tracker`](super::paygate::Paygate::settlement_tracker)
16/// and [`Self::wait_for_drain`]. The implementation is lock-free in the
17/// steady state: a single [`AtomicUsize`] for the counter and a
18/// [`tokio::sync::Notify`] for the drain wake-up.
19///
20/// Cloning the tracker is cheap and shares state, so it can be passed to
21/// multiple paygates serving the same shutdown channel (for example,
22/// when one process hosts several routes behind different price tags).
23#[derive(Clone, Debug)]
24pub struct BackgroundSettlementTracker {
25    inner: Arc<TrackerInner>,
26}
27
28#[derive(Debug)]
29struct TrackerInner {
30    in_flight: AtomicUsize,
31    drained: Notify,
32}
33
34impl Default for BackgroundSettlementTracker {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl BackgroundSettlementTracker {
41    /// Constructs a tracker with zero in-flight tasks.
42    #[must_use]
43    pub fn new() -> Self {
44        Self {
45            inner: Arc::new(TrackerInner {
46                in_flight: AtomicUsize::new(0),
47                drained: Notify::new(),
48            }),
49        }
50    }
51
52    /// Returns the current approximate number of in-flight settlement
53    /// tasks. Useful for `/healthz` style readiness probes.
54    #[must_use]
55    pub fn in_flight(&self) -> usize {
56        self.inner.in_flight.load(Ordering::SeqCst)
57    }
58
59    /// Increments the in-flight counter and returns a guard that
60    /// decrements it on drop. Internal: the paygate's
61    /// `handle_request_background` is the only intended caller.
62    pub(crate) fn start(&self) -> SettlementInFlightGuard {
63        let _previous = self.inner.in_flight.fetch_add(1, Ordering::SeqCst);
64        SettlementInFlightGuard {
65            inner: Arc::clone(&self.inner),
66        }
67    }
68
69    /// Awaits the in-flight count to reach zero, bounded by `timeout`.
70    /// Returns `Ok(())` once drained, or `Err(remaining)` after the
71    /// deadline with the count of still-running tasks.
72    ///
73    /// # Errors
74    ///
75    /// Returns the count of in-flight tasks when the timeout elapses
76    /// before the drain completes. Callers may then choose to abort the
77    /// runtime, log, or extend the deadline.
78    pub async fn wait_for_drain(&self, timeout: Duration) -> Result<(), usize> {
79        if self.in_flight() == 0 {
80            return Ok(());
81        }
82        let deadline = tokio::time::Instant::now() + timeout;
83        loop {
84            let notified = self.inner.drained.notified();
85            tokio::pin!(notified);
86            tokio::select! {
87                () = &mut notified => {}
88                () = tokio::time::sleep_until(deadline) => {
89                    let remaining = self.in_flight();
90                    return if remaining == 0 { Ok(()) } else { Err(remaining) };
91                }
92            }
93            if self.in_flight() == 0 {
94                return Ok(());
95            }
96        }
97    }
98}
99
100/// Drop-guard returned by [`BackgroundSettlementTracker::start`].
101///
102/// On drop, decrements the in-flight counter and notifies any awaiter
103/// blocked in [`BackgroundSettlementTracker::wait_for_drain`]. The guard
104/// is `Send + Sync` so it can be carried across `await` points by the
105/// background settlement supervisor.
106#[derive(Debug)]
107pub(crate) struct SettlementInFlightGuard {
108    inner: Arc<TrackerInner>,
109}
110
111impl Drop for SettlementInFlightGuard {
112    fn drop(&mut self) {
113        let previous = self.inner.in_flight.fetch_sub(1, Ordering::SeqCst);
114        if previous == 1 {
115            self.inner.drained.notify_waiters();
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[tokio::test]
125    async fn empty_tracker_drains_immediately() {
126        let tracker = BackgroundSettlementTracker::new();
127        assert_eq!(tracker.in_flight(), 0);
128        tracker.wait_for_drain(Duration::ZERO).await.unwrap();
129    }
130
131    #[tokio::test]
132    async fn drain_waits_for_guard_drop() {
133        let tracker = BackgroundSettlementTracker::new();
134        let guard = tracker.start();
135        assert_eq!(tracker.in_flight(), 1);
136
137        let tracker_clone = tracker.clone();
138        let drop_task = tokio::spawn(async move {
139            tokio::time::sleep(Duration::from_millis(10)).await;
140            drop(guard);
141            assert_eq!(tracker_clone.in_flight(), 0);
142        });
143
144        tracker
145            .wait_for_drain(Duration::from_secs(1))
146            .await
147            .expect("drain should complete after the guard drops");
148        drop_task.await.unwrap();
149    }
150
151    #[tokio::test]
152    async fn drain_times_out_when_guards_outlive_deadline() {
153        let tracker = BackgroundSettlementTracker::new();
154        let _guard = tracker.start();
155
156        let result = tracker.wait_for_drain(Duration::from_millis(20)).await;
157        assert_eq!(result, Err(1), "deadline elapses with the guard alive");
158    }
159
160    #[tokio::test]
161    async fn nested_guards_decrement_in_order() {
162        let tracker = BackgroundSettlementTracker::new();
163        let g1 = tracker.start();
164        let g2 = tracker.start();
165        let g3 = tracker.start();
166        assert_eq!(tracker.in_flight(), 3);
167        drop(g2);
168        assert_eq!(tracker.in_flight(), 2);
169        drop(g1);
170        assert_eq!(tracker.in_flight(), 1);
171        drop(g3);
172        assert_eq!(tracker.in_flight(), 0);
173    }
174}