Skip to main content

polars_io/
pl_async.rs

1use std::error::Error;
2use std::future::Future;
3
4use polars_buffer::Buffer;
5use polars_core::config::verbose;
6use polars_core::runtime::RAYON;
7use polars_utils::relaxed_cell::RelaxedCell;
8use tokio::sync::Semaphore;
9
10static CONCURRENCY_BUDGET: std::sync::OnceLock<(Semaphore, u32)> = std::sync::OnceLock::new();
11pub(super) const MAX_BUDGET_PER_REQUEST: usize = 10;
12
13pub trait GetSize {
14    fn size(&self) -> u64;
15}
16
17impl GetSize for Buffer<u8> {
18    fn size(&self) -> u64 {
19        self.len() as u64
20    }
21}
22
23impl<T: GetSize> GetSize for Vec<T> {
24    fn size(&self) -> u64 {
25        self.iter().map(|v| v.size()).sum()
26    }
27}
28
29impl<T: GetSize, E: Error> GetSize for Result<T, E> {
30    fn size(&self) -> u64 {
31        match self {
32            Ok(v) => v.size(),
33            Err(_) => 0,
34        }
35    }
36}
37
38#[cfg(feature = "cloud")]
39pub(crate) struct Size(u64);
40
41#[cfg(feature = "cloud")]
42impl GetSize for Size {
43    fn size(&self) -> u64 {
44        self.0
45    }
46}
47#[cfg(feature = "cloud")]
48impl From<u64> for Size {
49    fn from(value: u64) -> Self {
50        Self(value)
51    }
52}
53
54enum Optimization {
55    Step,
56    Accept,
57    Finished,
58}
59
60struct SemaphoreTuner {
61    previous_download_speed: u64,
62    last_tune: std::time::Instant,
63    downloaded: RelaxedCell<u64>,
64    download_time: RelaxedCell<u64>,
65    opt_state: Optimization,
66    increments: u32,
67}
68
69impl SemaphoreTuner {
70    fn new() -> Self {
71        Self {
72            previous_download_speed: 0,
73            last_tune: std::time::Instant::now(),
74            downloaded: RelaxedCell::from(0),
75            download_time: RelaxedCell::from(0),
76            opt_state: Optimization::Step,
77            increments: 0,
78        }
79    }
80    fn should_tune(&self) -> bool {
81        match self.opt_state {
82            Optimization::Finished => false,
83            _ => self.last_tune.elapsed().as_millis() > 350,
84        }
85    }
86
87    fn add_stats(&self, downloaded_bytes: u64, download_time: u64) {
88        self.downloaded.fetch_add(downloaded_bytes);
89        self.download_time.fetch_add(download_time);
90    }
91
92    fn increment(&mut self, semaphore: &Semaphore) {
93        semaphore.add_permits(1);
94        self.increments += 1;
95    }
96
97    fn tune(&mut self, semaphore: &'static Semaphore) -> bool {
98        let bytes_downloaded = self.downloaded.load();
99        let time_elapsed = self.download_time.load();
100        let download_speed = bytes_downloaded
101            .checked_div(time_elapsed)
102            .unwrap_or_default();
103
104        let increased = download_speed > self.previous_download_speed;
105        self.previous_download_speed = download_speed;
106        match self.opt_state {
107            Optimization::Step => {
108                self.increment(semaphore);
109                self.opt_state = Optimization::Accept
110            },
111            Optimization::Accept => {
112                // Accept the step
113                if increased {
114                    // Set new step
115                    self.increment(semaphore);
116                    // Keep accept state to check next iteration
117                }
118                // Decline the step
119                else {
120                    self.opt_state = Optimization::Finished;
121                    FINISHED_TUNING.store(true);
122                    if verbose() {
123                        eprintln!(
124                            "concurrency tuner finished after adding {} steps",
125                            self.increments
126                        )
127                    }
128                    // Finished.
129                    return true;
130                }
131            },
132            Optimization::Finished => {},
133        }
134        self.last_tune = std::time::Instant::now();
135        // Not finished.
136        false
137    }
138}
139static INCR: RelaxedCell<u64> = RelaxedCell::new_u64(0);
140static FINISHED_TUNING: RelaxedCell<bool> = RelaxedCell::new_bool(false);
141static PERMIT_STORE: std::sync::OnceLock<tokio::sync::RwLock<SemaphoreTuner>> =
142    std::sync::OnceLock::new();
143
144fn get_semaphore() -> &'static (Semaphore, u32) {
145    CONCURRENCY_BUDGET.get_or_init(|| {
146        let permits = std::env::var("POLARS_CONCURRENCY_BUDGET")
147            .map(|s| {
148                let budget = s.parse::<usize>().expect("integer");
149                FINISHED_TUNING.store(true);
150                budget
151            })
152            .unwrap_or_else(|_| std::cmp::max(RAYON.current_num_threads(), MAX_BUDGET_PER_REQUEST));
153        (Semaphore::new(permits), permits as u32)
154    })
155}
156
157pub fn get_concurrency_limit() -> u32 {
158    get_semaphore().1
159}
160
161pub async fn tune_with_concurrency_budget<F, Fut>(requested_budget: u32, callable: F) -> Fut::Output
162where
163    F: FnOnce() -> Fut,
164    Fut: Future,
165    Fut::Output: GetSize,
166{
167    let (semaphore, initial_budget) = get_semaphore();
168
169    // This would never finish otherwise.
170    assert!(requested_budget <= *initial_budget);
171
172    // Keep permit around.
173    // On drop it is returned to the semaphore.
174    let _permit_acq = semaphore.acquire_many(requested_budget).await.unwrap();
175
176    let now = std::time::Instant::now();
177    let res = callable().await;
178
179    if FINISHED_TUNING.load() || res.size() == 0 {
180        return res;
181    }
182
183    let duration = now.elapsed().as_millis() as u64;
184    let permit_store = PERMIT_STORE.get_or_init(|| tokio::sync::RwLock::new(SemaphoreTuner::new()));
185
186    let Ok(tuner) = permit_store.try_read() else {
187        return res;
188    };
189    // Keep track of download speed
190    tuner.add_stats(res.size(), duration);
191
192    // We only tune every n ms
193    if !tuner.should_tune() {
194        return res;
195    }
196    // Drop the read tuner before trying to acquire a writer
197    drop(tuner);
198
199    // Reduce locking by letting only 1 in 5 tasks lock the tuner
200    if !INCR.fetch_add(1).is_multiple_of(5) {
201        return res;
202    }
203    // Never lock as we will deadlock. This can run under rayon
204    let Ok(mut tuner) = permit_store.try_write() else {
205        return res;
206    };
207    let finished = tuner.tune(semaphore);
208    if finished {
209        drop(_permit_acq);
210        // Undo the last step
211        let undo = semaphore.acquire().await.unwrap();
212        std::mem::forget(undo)
213    }
214    res
215}
216
217pub async fn with_concurrency_budget<F, Fut>(requested_budget: u32, callable: F) -> Fut::Output
218where
219    F: FnOnce() -> Fut,
220    Fut: Future,
221{
222    let (semaphore, initial_budget) = get_semaphore();
223
224    // This would never finish otherwise.
225    assert!(requested_budget <= *initial_budget);
226
227    // Keep permit around.
228    // On drop it is returned to the semaphore.
229    let _permit_acq = semaphore.acquire_many(requested_budget).await.unwrap();
230
231    callable().await
232}