Skip to main content

flint_store/
gate.rs

1//! A bytes-in-flight bound for the UPLOAD path — the write side's mirror
2//! of the checkout window's `fetch_inflight_max_bytes`.
3//!
4//! An upload part is read WHOLE into memory before its PUT (`s3.rs`
5//! `compose_one_part`), and a whole-object body likewise (lean's
6//! `upload_one`). Until this existed the only bound on what the upload
7//! path held at once was a COUNT — `upload_fanout` objects, times
8//! `part_parallelism` parts of each — so peak RSS was
9//! `min(large_objects, fanout) x part_parallelism x part_size`, a product
10//! of three numbers nobody sets together: 8-wide parts across 32 large
11//! objects at 64 MiB a part is 16 GiB. That product is why the measured
12//! 3.6x of `part_parallelism = 8` shipped as an opt-in in v1.51.0. This
13//! bounds the BYTES, so the width can default to 8.
14//!
15//! Semantics, chosen so nothing can deadlock:
16//!
17//! - a request larger than the whole budget is CLAMPED to the budget, so
18//!   a single body bigger than the window still proceeds — alone, having
19//!   taken all of it — instead of waiting for permits that can never
20//!   exist (the same rule as the read path's `FETCH_UNIT` clamp);
21//! - permits are granted FIFO (tokio's semaphore): a large request at
22//!   the head of the queue is not starved by small ones behind it;
23//! - a permit is released on drop, so every error path releases.
24//!
25//! The counters are the TEST ORACLE. `high_water()` is the most bytes
26//! ever held at once; a test asserts it never exceeded the budget on a
27//! tree wide enough to blow far past it, while a control with a budget
28//! too large to bind reports the unbounded mark on the same tree. The
29//! counters carry the TRUE bytes of every holder, so a clamped request
30//! shows its real size above the budget — alone, which is the point.
31
32use std::sync::atomic::{AtomicU64, Ordering};
33use std::sync::Arc;
34
35use tokio::sync::{OwnedSemaphorePermit, Semaphore};
36
37/// Permit granularity. Bytes are charged in whole units, rounded UP, so
38/// the gate is conservative by less than one unit per holder and the
39/// budget fits the semaphore's `u32` permit count up to 16 TiB.
40const UNIT: u64 = 4096;
41
42struct Counters {
43    held: AtomicU64,
44    high_water: AtomicU64,
45}
46
47/// The gate. Cheap to clone-by-`Arc`; one per store.
48pub struct ByteGate {
49    sem: Arc<Semaphore>,
50    ctr: Arc<Counters>,
51    max_units: u32,
52    max_bytes: u64,
53}
54
55/// Bytes charged to a [`ByteGate`]; released on drop.
56pub struct BytePermit {
57    _permit: OwnedSemaphorePermit,
58    bytes: u64,
59    ctr: Arc<Counters>,
60}
61
62impl std::fmt::Debug for ByteGate {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("ByteGate")
65            .field("max_bytes", &self.max_bytes)
66            .field("held", &self.held())
67            .field("high_water", &self.high_water())
68            .finish()
69    }
70}
71
72impl ByteGate {
73    /// A gate over `max_bytes` (rounded up to a whole unit; at least one
74    /// unit, so a zero budget is a one-at-a-time gate rather than a
75    /// closed door).
76    pub fn new(max_bytes: u64) -> ByteGate {
77        let max_units = max_bytes.div_ceil(UNIT).clamp(1, u32::MAX as u64) as u32;
78        ByteGate {
79            sem: Arc::new(Semaphore::new(max_units as usize)),
80            ctr: Arc::new(Counters { held: AtomicU64::new(0), high_water: AtomicU64::new(0) }),
81            max_units,
82            max_bytes: max_units as u64 * UNIT,
83        }
84    }
85
86    /// The budget, as enforced (unit-rounded).
87    pub fn max_bytes(&self) -> u64 {
88        self.max_bytes
89    }
90
91    /// Hold `bytes` until the returned permit drops. A request above the
92    /// budget clamps to the whole budget and proceeds once it has it.
93    pub async fn acquire(&self, bytes: u64) -> BytePermit {
94        let want = bytes.div_ceil(UNIT).clamp(1, self.max_units as u64) as u32;
95        let permit = self
96            .sem
97            .clone()
98            .acquire_many_owned(want)
99            .await
100            .expect("the upload byte gate's semaphore is never closed");
101        let now = self.ctr.held.fetch_add(bytes, Ordering::SeqCst) + bytes;
102        self.ctr.high_water.fetch_max(now, Ordering::SeqCst);
103        BytePermit { _permit: permit, bytes, ctr: self.ctr.clone() }
104    }
105
106    /// Bytes held right now, by every live permit.
107    pub fn held(&self) -> u64 {
108        self.ctr.held.load(Ordering::SeqCst)
109    }
110
111    /// The most bytes ever held at once.
112    pub fn high_water(&self) -> u64 {
113        self.ctr.high_water.load(Ordering::SeqCst)
114    }
115}
116
117impl Drop for BytePermit {
118    fn drop(&mut self) {
119        self.ctr.held.fetch_sub(self.bytes, Ordering::SeqCst);
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    const MIB: u64 = 1 << 20;
128
129    /// A request larger than the whole budget must still be granted —
130    /// with the whole budget — not wait forever for permits that cannot
131    /// exist. And a permit released lets the next through.
132    #[tokio::test]
133    async fn a_request_above_the_budget_clamps_and_proceeds() {
134        let g = ByteGate::new(2 * MIB);
135        assert_eq!(g.max_bytes(), 2 * MIB);
136        let big = tokio::time::timeout(std::time::Duration::from_secs(5), g.acquire(10 * MIB))
137            .await
138            .expect("a request above the budget deadlocked");
139        assert_eq!(g.held(), 10 * MIB, "the counter carries the TRUE bytes");
140        // While the clamped holder lives, the budget is fully taken: even
141        // one byte waits.
142        let waiter = tokio::time::timeout(std::time::Duration::from_millis(100), g.acquire(1)).await;
143        assert!(waiter.is_err(), "a clamped holder must own the whole budget");
144        drop(big);
145        assert_eq!(g.held(), 0);
146        let _one = tokio::time::timeout(std::time::Duration::from_secs(5), g.acquire(1))
147            .await
148            .expect("released bytes were not granted on");
149    }
150
151    /// Many concurrent acquirers, each under the budget: the high-water
152    /// mark never exceeds it, and the SAME work through a gate too large
153    /// to bind reports a far higher mark — the bound, proved load-bearing
154    /// by its own control.
155    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
156    async fn high_water_never_exceeds_the_budget_under_contention() {
157        async fn run(gate: Arc<ByteGate>) -> u64 {
158            let mut set = tokio::task::JoinSet::new();
159            for i in 0..64u64 {
160                let g = gate.clone();
161                set.spawn(async move {
162                    let _p = g.acquire(MIB / 2 + (i % 3) * 100_000).await;
163                    tokio::time::sleep(std::time::Duration::from_millis(5)).await;
164                });
165            }
166            while set.join_next().await.is_some() {}
167            assert_eq!(gate.held(), 0, "every permit releases");
168            gate.high_water()
169        }
170        let bounded = Arc::new(ByteGate::new(3 * MIB));
171        let hw = run(bounded.clone()).await;
172        assert!(hw <= 3 * MIB, "high-water {hw} exceeded the 3 MiB budget");
173        assert!(hw > 0);
174        let control = Arc::new(ByteGate::new(1 << 30));
175        let chw = run(control.clone()).await;
176        assert!(
177            chw > 3 * MIB,
178            "the control's high-water {chw} did not exceed the budget: the tree is too small \
179             to prove the bound load-bearing"
180        );
181    }
182
183    /// Dropping a permit — on any path, an error's included — gives the
184    /// bytes back; a permit dropped by a cancelled future too.
185    #[tokio::test]
186    async fn permits_release_on_drop_including_cancellation() {
187        let g = Arc::new(ByteGate::new(MIB));
188        let p = g.acquire(MIB).await;
189        assert_eq!(g.held(), MIB);
190        // A second acquirer is parked behind the full budget; cancelling
191        // it must leave nothing charged.
192        let g2 = g.clone();
193        let parked = tokio::spawn(async move {
194            let _p = g2.acquire(MIB).await;
195            std::future::pending::<()>().await;
196        });
197        tokio::task::yield_now().await;
198        parked.abort();
199        let _ = parked.await;
200        assert_eq!(g.held(), MIB, "a cancelled waiter charged nothing");
201        drop(p);
202        assert_eq!(g.held(), 0);
203        assert_eq!(g.high_water(), MIB);
204        // And a permit handed to a task that is aborted mid-hold releases.
205        let g3 = g.clone();
206        let holder = tokio::spawn(async move {
207            let _p = g3.acquire(MIB).await;
208            std::future::pending::<()>().await;
209        });
210        tokio::task::yield_now().await;
211        assert_eq!(g.held(), MIB);
212        holder.abort();
213        let _ = holder.await;
214        assert_eq!(g.held(), 0, "an aborted holder released its bytes");
215        let _again = tokio::time::timeout(std::time::Duration::from_secs(5), g.acquire(MIB))
216            .await
217            .expect("the budget was not returned");
218    }
219}