Skip to main content

camel_api/
in_flight.rs

1//! Context-global in-flight claim (drainclaim), Exchange-carried
2//! extension (claimfamily).
3//!
4//! [`InFlightClaim`] is one accepted-not-completed unit on the
5//! context-global gauge (`CamelContext::total_in_flight()`). The type
6//! lives in camel-api — not camel-component-api — so [`crate::Exchange`]
7//! can carry a claim directly (rc-hllkk, rc-qbigm): stash sites behind
8//! `dyn` traits and raw-`Exchange` channels (resequencer buffers,
9//! embedded aggregator buckets) receive resident exchanges as bare
10//! `Exchange` values with no envelope, and their residency stays counted
11//! only if the claim rides the exchange itself.
12
13use std::fmt;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicU64, Ordering};
16
17/// Context-global in-flight gauge (drainclaim): the accepted-not-completed
18/// exchange counter plus a zero-transition notification.
19///
20/// Counting is owned by [`InFlightClaim`] RAII (attach increments, drop
21/// decrements) — `inc`/`dec` are crate-private so no cross-crate code can
22/// skew the count outside the claim lifecycle. Observers read the live
23/// count via [`InFlightGauge::total`] and await quiescence via
24/// [`InFlightGauge::idle`]: the last release (1→0) fires
25/// `notify_waiters()`, waking every waiter registered before the release
26/// (register-before-check: pin the `notified()` future and `enable()` it,
27/// then read state). The sync-callable notify is what makes `Drop` a
28/// completion signal for notification-based settle.
29pub struct InFlightGauge {
30    count: AtomicU64,
31    idle: tokio::sync::Notify,
32}
33
34impl InFlightGauge {
35    /// Create a gauge with a zero count.
36    pub fn new() -> Self {
37        Self {
38            count: AtomicU64::new(0),
39            idle: tokio::sync::Notify::new(),
40        }
41    }
42
43    /// Increment the in-flight count. Crate-private: claims are the only
44    /// counting path.
45    pub(crate) fn inc(&self) {
46        self.count.fetch_add(1, Ordering::AcqRel);
47    }
48
49    /// Decrement the in-flight count; the last release (1→0) wakes every
50    /// registered idle waiter. Crate-private: claims are the only counting
51    /// path.
52    pub(crate) fn dec(&self) {
53        let prev = self.count.fetch_sub(1, Ordering::Release);
54        if prev == 1 {
55            self.idle.notify_waiters();
56        }
57    }
58
59    /// Read the live in-flight count (Acquire load; pairs with the
60    /// `Release` decrement so a woken waiter observes the zero).
61    pub fn total(&self) -> u64 {
62        self.count.load(Ordering::Acquire)
63    }
64
65    /// Notification slot resolved on the last release (1→0). Waiters must
66    /// register before checking the count (pin + `enable()`); notifications
67    /// are not stored.
68    pub fn idle(&self) -> &tokio::sync::Notify {
69        &self.idle
70    }
71}
72
73impl Default for InFlightGauge {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79/// One accepted-not-completed unit on the context-global in-flight
80/// gauge.
81///
82/// Attaching a claim increments the gauge; dropping it decrements the
83/// gauge exactly once (RAII), covering every release path — normal
84/// pipeline completion, dispatch push failure, queued-envelope drop,
85/// pipeline task abort, panic, and readiness failure — with no manual
86/// rollback code. Fanout sites mint one sibling claim per subscriber copy
87/// via [`InFlightClaim::split`], so each copy counts and releases
88/// independently (drainclaim). The type is deliberately NOT `Clone`:
89/// duplicating a claim would double-release on drop; fanout copies mint
90/// siblings via [`InFlightClaim::split`] instead.
91///
92/// Claim lifecycle during a pipeline run (claimfamily): the pipeline
93/// drain site holds the envelope's claim in task scope AND splits a
94/// sibling onto the exchange, so residency inside pipeline-embedded
95/// stash sites (resequencer buffer, aggregator bucket) stays counted
96/// after the pipeline task itself completes. Out-of-band stash emissions
97/// escape with their carried claims; the in-band pipeline result has its
98/// sibling taken back before the reply, so release stays at task end for
99/// exchanges that complete inside the pipeline.
100pub struct InFlightClaim(Arc<InFlightGauge>);
101
102impl InFlightClaim {
103    /// Mint a claim against `gauge`, incrementing it by one.
104    pub fn attach(gauge: &Arc<InFlightGauge>) -> Self {
105        gauge.inc();
106        Self(Arc::clone(gauge))
107    }
108
109    /// Mint a sibling claim for a fanout copy, incrementing the same
110    /// gauge by one. The original claim stays live.
111    pub fn split(&self) -> Self {
112        self.0.inc();
113        Self(Arc::clone(&self.0))
114    }
115}
116
117impl Drop for InFlightClaim {
118    fn drop(&mut self) {
119        self.0.dec();
120    }
121}
122
123impl fmt::Debug for InFlightClaim {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        // The live count is observable through the counter itself
126        // (`total_in_flight()`); the Debug shape deliberately avoids
127        // leaking the Arc pointer.
128        f.write_str("InFlightClaim")
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn claim_attach_increments_and_drop_decrements() {
138        let gauge = Arc::new(InFlightGauge::new());
139        let claim = InFlightClaim::attach(&gauge);
140        assert_eq!(gauge.total(), 1);
141        drop(claim);
142        assert_eq!(gauge.total(), 0);
143    }
144
145    #[test]
146    fn claim_split_adds_one_sibling() {
147        let gauge = Arc::new(InFlightGauge::new());
148        let original = InFlightClaim::attach(&gauge);
149        assert_eq!(gauge.total(), 1);
150        let sibling = original.split();
151        assert_eq!(gauge.total(), 2);
152        drop(sibling);
153        assert_eq!(gauge.total(), 1);
154        drop(original);
155        assert_eq!(gauge.total(), 0);
156    }
157
158    #[test]
159    fn claim_debug_does_not_leak_pointer() {
160        let gauge = Arc::new(InFlightGauge::new());
161        let claim = InFlightClaim::attach(&gauge);
162        assert_eq!(format!("{claim:?}"), "InFlightClaim");
163    }
164
165    #[test]
166    fn gauge_counts_claim_lifecycle() {
167        let gauge = Arc::new(InFlightGauge::new());
168        let first = InFlightClaim::attach(&gauge);
169        let second = InFlightClaim::attach(&gauge);
170        assert_eq!(gauge.total(), 2);
171        drop(first);
172        assert_eq!(gauge.total(), 1);
173        drop(second);
174        assert_eq!(gauge.total(), 0);
175    }
176
177    #[tokio::test]
178    async fn gauge_notifies_on_last_release_only() {
179        let gauge = Arc::new(InFlightGauge::new());
180        // Register-before-check (D4): enable() subscribes the waiter
181        // without awaiting it, so no release can slip past unobserved.
182        let mut idle = std::pin::pin!(gauge.idle().notified());
183        idle.as_mut().enable();
184        let first = InFlightClaim::attach(&gauge);
185        let second = InFlightClaim::attach(&gauge);
186
187        drop(first);
188        let notified_early =
189            tokio::time::timeout(std::time::Duration::from_millis(100), &mut idle).await;
190        assert!(
191            notified_early.is_err(),
192            "a non-final release must not notify idle waiters"
193        );
194
195        drop(second);
196        tokio::time::timeout(std::time::Duration::from_secs(1), &mut idle)
197            .await
198            .expect("the last release must resolve the enabled waiter");
199    }
200
201    #[tokio::test]
202    async fn gauge_notify_wakes_all_enabled_waiters() {
203        let gauge = Arc::new(InFlightGauge::new());
204        let mut first = std::pin::pin!(gauge.idle().notified());
205        first.as_mut().enable();
206        let mut second = std::pin::pin!(gauge.idle().notified());
207        second.as_mut().enable();
208
209        let claim = InFlightClaim::attach(&gauge);
210        drop(claim);
211
212        tokio::time::timeout(std::time::Duration::from_secs(1), &mut first)
213            .await
214            .expect("first enabled waiter must resolve on last release");
215        tokio::time::timeout(std::time::Duration::from_secs(1), &mut second)
216            .await
217            .expect("second enabled waiter must resolve on last release");
218    }
219}