polars_io/cloud/concurrency/
mod.rs1mod admission;
19mod model;
20mod regime;
21
22use std::num::NonZeroU64;
23use std::sync::Arc;
24use std::sync::atomic::AtomicU64;
25use std::sync::atomic::Ordering::Relaxed;
26use std::time::{Duration, Instant};
27
28pub use admission::{InFlightBudget, InFlightPermit, InFlightStats};
29use crossbeam_queue::ArrayQueue;
30pub use model::Model;
31use polars_core::runtime::ASYNC;
32use polars_utils::relaxed_cell::RelaxedCell;
33pub use regime::{Regime, RegimeState};
34
35const SAMPLE_QUEUE_CAPACITY: usize = 8192;
38
39use crate::cloud::concurrency_config::get_random_access_chunk_size;
40use crate::cloud::http_rate_limit::PacingBudget;
41
42#[derive(Clone, Copy, Debug)]
43pub struct IoSample {
44 pub n_bytes: u64,
45 pub ttfb: Duration,
47 pub completion_time: Instant,
49}
50
51#[derive(Debug, Clone)]
52pub struct ControllerConfig {
53 window: Duration,
56 init_byte_budget: u64,
58 floor_byte_budget: u64,
60 request_budget: u64,
62 floor_request_budget: u64,
64 control_interval: Duration,
66 budget_resize_threshold: f64,
68}
69
70impl Default for ControllerConfig {
71 fn default() -> Self {
72 let target_chunk_size = get_random_access_chunk_size() as u64;
74 Self {
75 window: Duration::from_millis(1000),
76
77 init_byte_budget: get_init_byte_budget(target_chunk_size),
88
89 floor_byte_budget: target_chunk_size,
92
93 request_budget: get_request_budget(),
95 floor_request_budget: get_floor_request_budget(),
96 control_interval: Duration::from_millis(100),
97 budget_resize_threshold: 0.05,
98 }
99 }
100}
101
102fn get_init_byte_budget(target_chunk_size: u64) -> u64 {
104 let init_byte_budget = std::env::var("POLARS_INFLIGHT_INIT_BYTE_BUDGET")
105 .map(|x| {
106 x.parse::<NonZeroU64>()
107 .unwrap_or_else(|_| {
108 panic!("invalid value for POLARS_INFLIGHT_INIT_BYTE_BUDGET: {x}")
109 })
110 .get()
111 })
112 .unwrap_or_else(|_| {
113 let n = polars_config::config().max_threads() as u64;
117 n.div_ceil(8).max(4) * target_chunk_size
118 })
119 .max(1);
120
121 if init_byte_budget < target_chunk_size {
122 panic!("in-flight byte budget init must be larger than the target_chunk_size");
123 }
124
125 init_byte_budget
126}
127
128pub fn get_request_budget() -> u64 {
130 std::env::var("POLARS_INFLIGHT_REQUEST_BUDGET")
135 .map(|x| {
136 x.parse::<NonZeroU64>()
137 .unwrap_or_else(|_| panic!("invalid value for POLARS_INFLIGHT_REQUEST_BUDGET: {x}"))
138 .get()
139 })
140 .unwrap_or(512)
141 .max(1)
142}
143
144pub fn get_floor_request_budget() -> u64 {
146 std::env::var("POLARS_INFLIGHT_FLOOR_REQUEST_BUDGET")
151 .map(|x| {
152 x.parse::<NonZeroU64>()
153 .unwrap_or_else(|_| {
154 panic!("invalid value for POLARS_INFLIGHT_FLOOR_REQUEST_BUDGET: {x}")
155 })
156 .get()
157 })
158 .unwrap_or(polars_config::config().max_threads() as u64)
159 .max(1)
160}
161
162#[derive(Debug)]
165pub struct HeadRttChannel {
166 min_ns: AtomicU64,
167 sum_ns: AtomicU64,
168 count: AtomicU64,
169 total: AtomicU64,
172}
173
174#[derive(Clone, Copy, Debug)]
176pub struct HeadRttWindow {
177 pub min: Option<Duration>,
178 pub avg: Option<Duration>,
179 pub count: u64,
180}
181
182impl HeadRttChannel {
183 fn new() -> Self {
184 Self {
185 min_ns: AtomicU64::new(u64::MAX),
186 sum_ns: AtomicU64::new(0),
187 count: AtomicU64::new(0),
188 total: AtomicU64::new(0),
189 }
190 }
191
192 fn record(&self, rtt: Duration) {
194 let ns = rtt.as_nanos() as u64;
195 self.min_ns.fetch_min(ns, Relaxed);
196 self.sum_ns.fetch_add(ns, Relaxed);
197 self.count.fetch_add(1, Relaxed);
198
199 if self.total.fetch_add(1, Relaxed) == 0 && polars_config::config().verbose() {
200 eprintln!(
201 "[InFlightConcurrency]: observed first RTT sample (metadata): {} ms",
202 rtt.as_millis()
203 );
204 }
205 }
206
207 fn take(&self) -> HeadRttWindow {
209 let min_ns = self.min_ns.swap(u64::MAX, Relaxed);
210 let sum_ns = self.sum_ns.swap(0, Relaxed);
211 let count = self.count.swap(0, Relaxed);
212
213 HeadRttWindow {
214 min: (min_ns != u64::MAX).then(|| Duration::from_nanos(min_ns)),
215 avg: (count > 0).then(|| Duration::from_nanos(sum_ns / count)),
216 count,
217 }
218 }
219}
220
221#[derive(Debug)]
222pub struct ConcurrencyController {
223 config: ControllerConfig,
224 sample_queue: Arc<ArrayQueue<IoSample>>,
225 samples_dropped: Arc<RelaxedCell<u64>>,
226 head_rtt: Arc<HeadRttChannel>,
227 inflight_budget: Arc<InFlightBudget>,
228 _control_task: tokio::task::JoinHandle<()>,
229}
230
231impl ConcurrencyController {
232 pub fn new(config: ControllerConfig, pacing_budget: Option<PacingBudget>) -> Self {
233 let sample_queue = Arc::new(ArrayQueue::new(SAMPLE_QUEUE_CAPACITY));
234 let samples_dropped = Arc::new(RelaxedCell::new_u64(0));
235 let head_rtt = Arc::new(HeadRttChannel::new());
236
237 let inflight_budget = Arc::new(InFlightBudget::new(
238 config.init_byte_budget,
239 config.floor_byte_budget,
240 config.request_budget,
241 config.floor_request_budget,
242 ));
243
244 let control_task = Self::spawn_control_loop(
245 sample_queue.clone(),
246 samples_dropped.clone(),
247 head_rtt.clone(),
248 inflight_budget.clone(),
249 config.clone(),
250 pacing_budget,
251 );
252
253 Self {
254 config,
255 sample_queue,
256 samples_dropped,
257 head_rtt,
258 inflight_budget,
259 _control_task: control_task,
260 }
261 }
262
263 pub fn config(&self) -> &ControllerConfig {
264 &self.config
265 }
266
267 pub fn record_io(&self, sample: IoSample) {
269 if self.sample_queue.push(sample).is_err() {
270 self.samples_dropped.fetch_add(1);
272 }
273 }
274
275 pub fn record_head_rtt(&self, rtt: Duration) {
277 self.head_rtt.record(rtt);
278 }
279
280 pub fn inflight_budget(&self) -> &Arc<InFlightBudget> {
281 &self.inflight_budget
282 }
283
284 pub async fn acquire(&self, bytes: u64) -> InFlightPermit {
285 self.inflight_budget.acquire(bytes).await
286 }
287
288 fn spawn_control_loop(
289 sample_queue: Arc<ArrayQueue<IoSample>>,
290 samples_dropped: Arc<RelaxedCell<u64>>,
291 head_rtt: Arc<HeadRttChannel>,
292 admission: Arc<InFlightBudget>,
293 config: ControllerConfig,
294 pacing_budget: Option<PacingBudget>,
295 ) -> tokio::task::JoinHandle<()> {
296 if polars_config::config().verbose() {
297 eprintln!(
298 "[InFlightConcurrency]: spawn control loop: control_interval: {}ms",
299 config.control_interval.as_millis()
300 );
301 }
302 ASYNC.spawn(async move {
303 let mut model = Model::new(config.window);
304 let mut regime = Regime::new(Instant::now());
305
306 let mut ticker = tokio::time::interval(config.control_interval);
307 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
308
309 loop {
310 ticker.tick().await;
311 let now = Instant::now();
312
313 if let Some(ref pacing_budget) = pacing_budget {
315 let rate = pacing_budget.rate();
316 let horizon_s = pacing_budget.horizon().as_secs_f64();
317 let request_budget = rate * horizon_s;
318 admission.resize_request_budget(request_budget as u64);
319 }
320
321 let (state, signal, dropped, bw_hwm_held) = {
323 for _ in 0..SAMPLE_QUEUE_CAPACITY {
324 let Some(s) = sample_queue.pop() else { break };
325 model.record(s);
326 }
327 let dropped = samples_dropped.swap(0);
328 model.update(now);
329 let signal = model.signal();
330 let state = regime.step(signal, now);
331 let bw_hwm_held = model.bw_hwm_bps();
332 (state, signal, dropped, bw_hwm_held)
333 };
334
335 if !matches!(state, RegimeState::WarmIdle { .. }) {
336 let base_byte_budget = match (state, signal) {
338 (RegimeState::Init, _) | (_, None) => config.init_byte_budget,
339 (_, Some(signal)) => signal.bdp_bytes().max(config.init_byte_budget),
340 };
341
342 let gain = match state {
344 RegimeState::Init => 1.0,
345 RegimeState::RampUp { .. } => 2.0,
346 RegimeState::Stable => 2.0,
348 RegimeState::ProbeUp { .. } => 3.0,
349 RegimeState::WarmIdle { .. } => 1.0,
351 };
352 let target_byte_budget = (base_byte_budget as f64 * gain) as u64;
353
354 let current_byte_budget = admission.current_byte_budget();
356 let threshold = config.budget_resize_threshold;
357 let should_resize = match current_byte_budget {
358 0 => target_byte_budget > 0,
359 current => {
360 let ratio = target_byte_budget as f64 / current as f64;
361 ratio < (1.0 - threshold) || ratio > (1.0 + threshold)
362 },
363 };
364
365 if should_resize {
366 admission.resize_byte_budget(target_byte_budget);
367 }
368 }
369
370 let head_rtt_window = head_rtt.take();
372
373 if std::env::var("POLARS_LOG_CONCURRENCY").is_ok() {
375 let stats = admission.stats();
376 eprintln!(
377 "[InFlightConcurrency {}] regime={}, \
378 bw_hwm={:.1} MB/s, \
379 bw_avg={:.1} MB/s, \
380 rtt_min={:.1} ms, \
381 rtt_avg={:.1} ms, \
382 head_rtt_min={:.1} ms, \
383 head_rtt_avg={:.1} ms, \
384 head_n={}, \
385 bdp_obs={:.1} MB, \
386 bytes_budget={:.1} MB, \
387 bytes_in_use={:.1} MB, \
388 bytes_sat={:.2}, \
389 req_budget={}, \
390 req_in_use={}, \
391 req_sat={:.2}",
392 chrono::Utc::now(),
393 state.label(),
394 signal.map(|s| s.bw_hwm_bps).or(bw_hwm_held).unwrap_or(0.0) / 1e6,
395 signal.map_or(0.0, |s| s.bw_avg_bps) / 1e6,
396 signal.map_or(0, |s| s.ttfb_min.as_millis()),
397 signal.map_or(0, |s| s.ttfb_avg.as_millis()),
398 head_rtt_window.min.map_or(0.0, |d| d.as_secs_f64() * 1e3),
399 head_rtt_window.avg.map_or(0.0, |d| d.as_secs_f64() * 1e3),
400 head_rtt_window.count,
401 signal.map_or(0, |s| s.bdp_bytes()) as f64 / 1e6,
402 stats.bytes_budget as f64 / 1e6,
403 stats.bytes_in_use as f64 / 1e6,
404 stats.bytes_saturation,
405 stats.request_budget,
406 stats.requests_in_use,
407 stats.requests_saturation
408 );
409 if dropped > 0 {
410 eprintln!(
411 "[InFlightConcurrency] WARN: {dropped} samples dropped (queue full)"
412 );
413 }
414 }
415 }
416 })
417 }
418}
419
420impl Drop for ConcurrencyController {
421 fn drop(&mut self) {
422 self._control_task.abort();
423 }
424}