Skip to main content

polars_io/cloud/concurrency/
admission.rs

1use std::sync::Arc;
2use std::sync::atomic::Ordering::Relaxed;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use tokio::sync::Notify;
6
7/// Byte-granularity budget with dynamic resize.
8///
9/// Uses an atomic counter + Notify rather than tokio::Semaphore because
10/// we need instant shrink semantics (tokio::Semaphore requires "stealing"
11/// permits by acquiring them, which blocks under saturation).
12#[derive(Debug)]
13pub(super) struct ByteBudget {
14    // Current budget, measured in number of bytes, reflecting the maximum
15    // allowed in-flight volume.
16    current_budget: AtomicU64,
17    // Lowest allowed budget for the current_budget.
18    floor_budget: u64,
19    // Volume in use for in-flight traffic, as allowed by the current_budget.
20    inflight_in_use: AtomicU64,
21    waiters: Notify,
22}
23
24impl ByteBudget {
25    fn new(initial: u64, floor_budget: u64) -> Self {
26        Self {
27            current_budget: AtomicU64::new(initial),
28            floor_budget,
29            inflight_in_use: AtomicU64::new(0),
30            waiters: Notify::new(),
31        }
32    }
33
34    /// Acquire a bytes-based permit. The call site is responsible for capping the
35    /// request size to prevent deadlock.
36    async fn acquire_strict(&self, n_bytes: u64) {
37        // Pre-empt deadlock.
38        assert!(n_bytes <= self.floor_budget);
39
40        // NOTE: Large waiters can starve under sustained small-request load.
41        // In practice, this may not be material issue.
42        loop {
43            let cap = self.current_budget.load(Ordering::Acquire);
44            let inflight = self.inflight_in_use.load(Ordering::Acquire);
45
46            if inflight + n_bytes <= cap {
47                if self
48                    .inflight_in_use
49                    .compare_exchange_weak(
50                        inflight,
51                        inflight + n_bytes,
52                        Ordering::AcqRel,
53                        Ordering::Relaxed,
54                    )
55                    .is_ok()
56                {
57                    // Fits. There may be leftover capacity for the next
58                    // waiter (e.g. one big release satisfying several small
59                    // acquires), so keep the wake chain alive — but only
60                    // because progress occurred.
61                    self.waiters.notify_one();
62                    return;
63                }
64                continue;
65            }
66
67            // Doesn't fit: register, re-check, park.
68            let notified = self.waiters.notified();
69            let cap = self.current_budget.load(Ordering::Acquire);
70            let inflight = self.inflight_in_use.load(Ordering::Acquire);
71            if inflight + n_bytes <= cap {
72                continue;
73            }
74            notified.await;
75        }
76    }
77
78    fn release(&self, bytes: u64) {
79        self.inflight_in_use.fetch_sub(bytes, Ordering::AcqRel);
80        self.waiters.notify_one();
81    }
82
83    fn resize(&self, new: u64) {
84        let new = new.max(self.floor_budget);
85        let old = self.current_budget.swap(new, Ordering::AcqRel);
86        if new > old {
87            // Grow: maybe someone can now proceed.
88            self.waiters.notify_waiters();
89        }
90    }
91
92    fn current_budget(&self) -> u64 {
93        self.current_budget.load(Ordering::Relaxed)
94    }
95
96    fn floor_byte_budget(&self) -> u64 {
97        self.floor_budget
98    }
99
100    fn inflight_in_use(&self) -> u64 {
101        self.inflight_in_use.load(Ordering::Relaxed)
102    }
103}
104
105#[derive(Debug)]
106pub(super) struct RequestBudget {
107    // Current budget, measured in count, reflecting the maximum allowed in-flight.
108    current_budget: AtomicU64,
109    // Lowest allowed budget for the current_budget.
110    floor_budget: u64,
111    // Count in use for in-flight traffic, as allowed by the current_budget.
112    inflight_in_use: AtomicU64,
113    waiters: Notify,
114}
115
116impl RequestBudget {
117    fn new(initial: u64, floor_budget: u64) -> Self {
118        Self {
119            current_budget: AtomicU64::new(initial),
120            floor_budget,
121            inflight_in_use: AtomicU64::new(0),
122            waiters: Notify::new(),
123        }
124    }
125
126    /// Acquire a count-based permit. The call site is responsible for capping the
127    /// request size to prevent deadlock.
128    async fn acquire_one(&self) {
129        let n = 1;
130
131        loop {
132            let cap = self.current_budget.load(Ordering::Acquire);
133            let inflight = self.inflight_in_use.load(Ordering::Acquire);
134
135            if inflight + n <= cap {
136                if self
137                    .inflight_in_use
138                    .compare_exchange_weak(
139                        inflight,
140                        inflight + n,
141                        Ordering::AcqRel,
142                        Ordering::Relaxed,
143                    )
144                    .is_ok()
145                {
146                    // Fits.
147                    self.waiters.notify_one();
148                    return;
149                }
150                continue;
151            }
152
153            // Doesn't fit: register, re-check, park.
154            let notified = self.waiters.notified();
155            let cap = self.current_budget.load(Ordering::Acquire);
156            let inflight = self.inflight_in_use.load(Ordering::Acquire);
157            if inflight + n <= cap {
158                continue;
159            }
160            notified.await;
161        }
162    }
163
164    fn release_one(&self) {
165        let n = 1;
166
167        self.inflight_in_use.fetch_sub(n, Ordering::AcqRel);
168        self.waiters.notify_one();
169    }
170
171    fn resize(&self, new: u64) {
172        let new = new.max(self.floor_budget);
173        let old = self.current_budget.swap(new, Ordering::AcqRel);
174        if new > old {
175            // Grow: maybe someone can now proceed.
176            self.waiters.notify_waiters();
177        }
178    }
179
180    fn inflight_in_use(&self) -> u64 {
181        self.inflight_in_use.load(Ordering::Relaxed)
182    }
183}
184
185#[derive(Clone, Copy, Debug)]
186pub struct InFlightStats {
187    pub bytes_budget: u64,
188    pub bytes_in_use: u64,
189    // May exceed 1.0 transiently after a budget shrink, while
190    // previously-admitted traffic drains. Expected, not a bug.
191    pub bytes_saturation: f64,
192    pub request_budget: u64,
193    pub requests_in_use: u64,
194    pub requests_saturation: f64,
195}
196
197#[derive(Debug)]
198pub struct InFlightBudget {
199    byte_budget: Arc<ByteBudget>,
200    request_budget: Arc<RequestBudget>,
201}
202
203impl InFlightBudget {
204    pub fn new(
205        initial_byte_budget: u64,
206        floor_byte_budget: u64,
207        initial_request_budget: u64,
208        floor_request_budget: u64,
209    ) -> Self {
210        let inflight_budget = Self {
211            byte_budget: Arc::new(ByteBudget::new(initial_byte_budget, floor_byte_budget)),
212            request_budget: Arc::new(RequestBudget::new(
213                initial_request_budget,
214                floor_request_budget,
215            )),
216        };
217
218        if polars_config::config().verbose() {
219            eprintln!(
220                "[InFlightConcurrency]: \
221                initial_byte_budget: {}, \
222                floor_byte_budget: {}, \
223                request_budget: {}, \
224                floor_request_budget: {}",
225                initial_byte_budget,
226                floor_byte_budget,
227                initial_request_budget,
228                floor_request_budget
229            );
230        }
231
232        inflight_budget
233    }
234
235    pub async fn acquire(self: &Arc<Self>, n_bytes: u64) -> InFlightPermit {
236        // NOTE: since chunk_size is a target, merge_ranges and split_ranges may overshoot
237        // the floor. Cap'ing prevents deadlock at the expense of memory management precision.
238        let n_bytes = n_bytes.min(self.byte_budget.floor_byte_budget());
239
240        // Byte budget (may wait). Cancel-safe internally.
241        self.byte_budget.acquire_strict(n_bytes).await;
242
243        // Guard immediately — synchronous, so there's no cancellation
244        // window between reservation and guard.
245        let bytes = BytesReservation {
246            budget: self.byte_budget.clone(),
247            n_bytes,
248        };
249
250        self.request_budget.acquire_one().await;
251
252        let request = RequestReservation {
253            budget: self.request_budget.clone(),
254        };
255
256        InFlightPermit {
257            _bytes_reservation: bytes,
258            _req_permit: request,
259        }
260    }
261
262    pub fn current_byte_budget(&self) -> u64 {
263        self.byte_budget.current_budget()
264    }
265
266    pub fn floor_byte_budget(&self) -> u64 {
267        self.byte_budget.floor_byte_budget()
268    }
269
270    pub fn resize_byte_budget(&self, new: u64) {
271        self.byte_budget.resize(new);
272    }
273
274    pub fn resize_request_budget(&self, new: u64) {
275        self.request_budget.resize(new);
276    }
277
278    pub fn stats(&self) -> InFlightStats {
279        let bytes_budget = self.byte_budget.current_budget();
280        let bytes_in_use = self.byte_budget.inflight_in_use();
281        let bytes_saturation = if bytes_budget > 0 {
282            bytes_in_use as f64 / bytes_budget as f64
283        } else {
284            0.0
285        };
286
287        let request_budget = self.request_budget.current_budget.load(Relaxed);
288        let requests_in_use = self.request_budget.inflight_in_use();
289        let requests_saturation = if request_budget > 0 {
290            requests_in_use as f64 / request_budget as f64
291        } else {
292            0.0
293        };
294
295        InFlightStats {
296            bytes_budget,
297            bytes_in_use,
298            bytes_saturation,
299            request_budget,
300            requests_in_use,
301            requests_saturation,
302        }
303    }
304}
305
306struct BytesReservation {
307    budget: Arc<ByteBudget>,
308    n_bytes: u64,
309}
310
311impl Drop for BytesReservation {
312    fn drop(&mut self) {
313        self.budget.release(self.n_bytes);
314    }
315}
316
317struct RequestReservation {
318    budget: Arc<RequestBudget>,
319}
320
321impl Drop for RequestReservation {
322    fn drop(&mut self) {
323        self.budget.release_one();
324    }
325}
326
327pub struct InFlightPermit {
328    _bytes_reservation: BytesReservation,
329    _req_permit: RequestReservation,
330}