1use std::fmt::Display;
2use std::ops::Range;
3use std::sync::Arc;
4use std::time::Instant;
5
6use futures::{Stream, StreamExt as _, TryStreamExt as _};
7use hashbrown::hash_map::RawEntryMut;
8use object_store::path::Path;
9use object_store::{ObjectMeta, ObjectStore, ObjectStoreExt};
10use polars_buffer::Buffer;
11use polars_core::prelude::{InitHashMaps, PlHashMap};
12use polars_error::{PolarsError, PolarsResult};
13use polars_utils::pl_path::PlRefPath;
14use tokio::io::AsyncWriteExt;
15
16use super::concurrency::IoSample;
17use super::concurrency_config::{ConcurrencyStrategy, FetchConfig, get_download_chunk_size};
18use crate::pl_async::{
19 self, MAX_BUDGET_PER_REQUEST, get_concurrency_limit, tune_with_concurrency_budget,
20 with_concurrency_budget,
21};
22
23#[derive(Debug)]
24pub struct PolarsObjectStoreError {
25 pub base_url: PlRefPath,
26 pub source: object_store::Error,
27}
28
29impl PolarsObjectStoreError {
30 pub fn from_url(base_url: &PlRefPath) -> impl FnOnce(object_store::Error) -> Self {
31 |error| Self {
32 base_url: base_url.clone(),
33 source: error,
34 }
35 }
36}
37
38impl Display for PolarsObjectStoreError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(
41 f,
42 "object-store error: {} (path: {})",
43 self.source, &self.base_url
44 )
45 }
46}
47
48impl std::error::Error for PolarsObjectStoreError {
49 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
50 Some(&self.source)
51 }
52}
53
54impl From<PolarsObjectStoreError> for std::io::Error {
55 fn from(value: PolarsObjectStoreError) -> Self {
56 std::io::Error::other(value)
57 }
58}
59
60impl From<PolarsObjectStoreError> for PolarsError {
61 fn from(value: PolarsObjectStoreError) -> Self {
62 PolarsError::IO {
63 error: Arc::new(value.into()),
64 msg: None,
65 }
66 }
67}
68
69mod inner {
70
71 use std::borrow::Cow;
72 use std::future::Future;
73 use std::sync::Arc;
74
75 use object_store::ObjectStore;
76 use polars_core::config;
77 use polars_error::{PolarsError, PolarsResult};
78 use polars_utils::relaxed_cell::RelaxedCell;
79
80 use crate::cloud::concurrency::{ConcurrencyController, ControllerConfig};
81 use crate::cloud::{ObjectStoreErrorContext, PolarsObjectStoreBuilder};
82 use crate::metrics::{IOMetrics, OptIOMetrics};
83
84 #[derive(Debug)]
85 struct Inner {
86 store: tokio::sync::RwLock<Arc<dyn ObjectStore>>,
87 builder: PolarsObjectStoreBuilder,
88 rebuilt: RelaxedCell<bool>,
89 }
90
91 #[derive(Clone, Debug)]
93 pub struct PolarsObjectStore {
94 inner: Arc<Inner>,
95 initial_store: std::sync::Arc<dyn ObjectStore>,
97 io_metrics: OptIOMetrics,
98 concurrency: Arc<std::sync::OnceLock<Arc<ConcurrencyController>>>,
100 }
101
102 impl PolarsObjectStore {
103 pub(crate) fn new_from_inner(
104 store: Arc<dyn ObjectStore>,
105 builder: PolarsObjectStoreBuilder,
106 ) -> Self {
107 let initial_store = store.clone();
108 Self {
109 inner: Arc::new(Inner {
110 store: tokio::sync::RwLock::new(store),
111 builder,
112 rebuilt: RelaxedCell::from(false),
113 }),
114 initial_store,
115 io_metrics: OptIOMetrics(None),
116 concurrency: Arc::new(std::sync::OnceLock::new()), }
118 }
119
120 pub fn set_io_metrics(&mut self, io_metrics: Option<Arc<IOMetrics>>) -> &mut Self {
121 self.io_metrics = OptIOMetrics(io_metrics);
122 self
123 }
124
125 pub fn io_metrics(&self) -> &OptIOMetrics {
126 &self.io_metrics
127 }
128
129 pub fn get_or_init_concurrency(&self) -> &Arc<ConcurrencyController> {
130 self.concurrency
131 .get_or_init(|| Arc::new(ConcurrencyController::new(ControllerConfig::default())))
132 }
133
134 pub async fn to_dyn_object_store(&self) -> Cow<'_, Arc<dyn ObjectStore>> {
136 if !self.inner.rebuilt.load() {
137 Cow::Borrowed(&self.initial_store)
138 } else {
139 Cow::Owned(self.inner.store.read().await.clone())
140 }
141 }
142
143 pub async fn rebuild_inner(
144 &self,
145 from_version: &Arc<dyn ObjectStore>,
146 ) -> PolarsResult<Arc<dyn ObjectStore>> {
147 let mut current_store = self.inner.store.write().await;
148
149 if Arc::ptr_eq(&*current_store, from_version) {
151 *current_store =
152 self.inner
153 .builder
154 .clone()
155 .build_impl(true)
156 .await
157 .map_err(|e| {
158 e.wrap_msg(|e| format!("attempt to rebuild object store failed: {e}"))
159 })?;
160 }
161
162 self.inner.rebuilt.store(true);
163
164 Ok((*current_store).clone())
165 }
166
167 pub async fn exec_with_rebuild_retry_on_err<'s, 'f, Fn, Fut, O>(
168 &'s self,
169 mut func: Fn,
170 ) -> PolarsResult<O>
171 where
172 Fn: FnMut(Cow<'s, Arc<dyn ObjectStore>>) -> Fut + 'f,
173 Fut: Future<Output = object_store::Result<O>>,
174 {
175 let store = self.to_dyn_object_store().await;
176
177 let out = func(store.clone()).await;
178
179 let orig_err = match out {
180 Ok(v) => return Ok(v),
181 Err(e) => e,
182 };
183
184 if config::verbose() {
185 eprintln!(
186 "[PolarsObjectStore]: got error: {}, will rebuild store and retry",
187 &orig_err
188 );
189 }
190
191 let store = self
192 .rebuild_inner(&store)
193 .await
194 .map_err(|e| e.wrap_msg(|e| format!("{e}; original error: {orig_err}")))?;
195
196 func(Cow::Owned(store)).await.map_err(|e| {
197 let e: PolarsError = self.error_context().attach_err_info(e).into();
198
199 if self.inner.builder.is_azure()
200 && std::env::var("POLARS_AUTO_USE_AZURE_STORAGE_ACCOUNT_KEY").as_deref()
201 != Ok("1")
202 {
203 e.wrap_msg(|e| {
206 format!(
207 "{e}; note: if you are using Python, consider setting \
208POLARS_AUTO_USE_AZURE_STORAGE_ACCOUNT_KEY=1 if you would like polars to try to retrieve \
209and use the storage account keys from Azure CLI to authenticate"
210 )
211 })
212 } else {
213 e
214 }
215 })
216 }
217
218 pub fn error_context(&self) -> ObjectStoreErrorContext {
219 ObjectStoreErrorContext::new(self.inner.builder.path().clone())
220 }
221 }
222}
223
224#[derive(Clone)]
225pub struct ObjectStoreErrorContext {
226 path: PlRefPath,
227}
228
229impl ObjectStoreErrorContext {
230 pub fn new(path: PlRefPath) -> Self {
231 Self { path }
232 }
233
234 pub fn attach_err_info(self, err: object_store::Error) -> PolarsObjectStoreError {
235 let ObjectStoreErrorContext { path } = self;
236
237 PolarsObjectStoreError {
238 base_url: path,
239 source: err,
240 }
241 }
242}
243
244pub use inner::PolarsObjectStore;
245
246pub type ObjectStorePath = object_store::path::Path;
247
248impl PolarsObjectStore {
249 pub fn build_buffered_ranges_stream<'a, T: Iterator<Item = Range<usize>>>(
250 &'a self,
251 path: &'a Path,
252 ranges: T,
253 strategy: ConcurrencyStrategy,
254 ) -> impl Stream<Item = PolarsResult<Buffer<u8>>> + use<'a, T> {
255 let controller = match strategy {
256 ConcurrencyStrategy::BytesBased => Some(self.get_or_init_concurrency().clone()),
257 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::Legacy => None,
258 };
259
260 let n_buffered = match strategy {
261 ConcurrencyStrategy::BytesBased => 4096,
267 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::Legacy => {
268 get_concurrency_limit() as usize
269 },
270 };
271
272 futures::stream::iter(ranges.map(move |range| {
273 let controller = controller.clone();
274 async move {
275 if range.is_empty() {
276 return Ok(Buffer::new());
277 }
278 let bytes_req = range.len() as u64;
279
280 let _permit = match &controller {
282 Some(controller) => Some(controller.acquire(bytes_req).await),
283 None => None,
284 };
285
286 let (out, ttfb) = self
287 .io_metrics()
288 .record_io_read(
289 bytes_req,
290 self.exec_with_rebuild_retry_on_err(|s| async move {
291 let t0 = Instant::now();
292 let response = s
293 .get_opts(
294 path,
295 object_store::GetOptions {
296 range: Some((range.start as u64..range.end as u64).into()),
297 ..Default::default()
298 },
299 )
300 .await?;
301 let ttfb = t0.elapsed();
302 let out = response.bytes().await?;
303
304 Ok((out, ttfb))
305 }),
306 )
307 .await?;
308
309 if let Some(controller) = &controller {
310 controller.record_io(IoSample {
311 n_bytes: out.len() as u64,
312 ttfb,
313 completion_time: Instant::now(),
314 });
315 }
316
317 Ok(Buffer::from_owner(out))
318 }
319 }))
320 .buffered(n_buffered)
321 }
322
323 pub async fn get_range(
324 &self,
325 path: &Path,
326 range: Range<usize>,
327 config: FetchConfig,
328 ) -> PolarsResult<Buffer<u8>> {
329 if range.is_empty() {
330 return Ok(Buffer::new());
331 }
332
333 let parts = split_range(range.clone(), Some(config.chunk_size));
334
335 match config.strategy {
336 ConcurrencyStrategy::Legacy => self.get_range_legacy(path, range).await,
337 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::BytesBased => self
338 .build_buffered_ranges_stream(path, parts, config.strategy)
339 .try_collect::<Vec<_>>()
340 .await
341 .map(|parts| {
342 if parts.len() == 1 {
343 return parts.into_iter().next().unwrap();
344 }
345 let mut combined = Vec::with_capacity(range.len());
346 for part in parts {
347 combined.extend_from_slice(&part);
348 }
349 assert_eq!(combined.len(), range.len());
350 Buffer::from_vec(combined)
351 }),
352 }
353 }
354
355 async fn get_range_legacy(&self, path: &Path, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
356 if range.is_empty() {
357 return Ok(Buffer::new());
358 }
359
360 let parts = split_range(range.clone(), None);
361
362 if parts.len() == 1 {
363 let out = tune_with_concurrency_budget(1, move || async move {
364 let bytes = self
365 .io_metrics()
366 .record_io_read(
367 range.len() as u64,
368 self.exec_with_rebuild_retry_on_err(|s| async move {
369 s.get_range(path, range.start as u64..range.end as u64)
370 .await
371 }),
372 )
373 .await?;
374
375 PolarsResult::Ok(Buffer::from_owner(bytes))
376 })
377 .await?;
378
379 Ok(out)
380 } else {
381 let parts = tune_with_concurrency_budget(
382 parts.len().clamp(0, MAX_BUDGET_PER_REQUEST) as u32,
383 || {
384 self.build_buffered_ranges_stream(path, parts, ConcurrencyStrategy::Legacy)
385 .try_collect::<Vec<Buffer<u8>>>()
386 },
387 )
388 .await?;
389
390 let mut combined = Vec::with_capacity(range.len());
391
392 for part in parts {
393 combined.extend_from_slice(&part)
394 }
395
396 assert_eq!(combined.len(), range.len());
397
398 PolarsResult::Ok(Buffer::from_vec(combined))
399 }
400 }
401
402 pub async fn get_ranges_sort(
403 &self,
404 path: &Path,
405 ranges: &mut [Range<usize>],
406 config: FetchConfig,
407 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
408 if ranges.is_empty() {
409 return Ok(Default::default());
410 }
411
412 ranges.sort_unstable_by_key(|x| x.start);
413
414 let ranges_len = ranges.len();
415 let (merged_ranges, merged_ends): (Vec<_>, Vec<_>) =
416 merge_ranges(ranges, Some(config.chunk_size)).unzip();
417
418 let mut out = PlHashMap::with_capacity(ranges_len);
419
420 let mut stream =
422 self.build_buffered_ranges_stream(path, merged_ranges.iter().cloned(), config.strategy);
423
424 let mut current_offset = 0;
425 let mut ends_iter = merged_ends.iter();
426 let mut splitted_parts: Vec<Buffer<u8>> = vec![];
427
428 while let Some(bytes) = stream.try_next().await? {
429 let end = *ends_iter.next().unwrap();
430
431 if end == 0 {
432 splitted_parts.push(bytes);
433 continue;
434 }
435
436 let full_range = ranges[current_offset..end]
437 .iter()
438 .cloned()
439 .reduce(|l, r| l.start.min(r.start)..l.end.max(r.end))
440 .unwrap();
441
442 let bytes = if splitted_parts.is_empty() {
443 bytes
444 } else {
445 let mut out = Vec::with_capacity(full_range.len());
446 for x in splitted_parts.drain(..) {
447 out.extend_from_slice(&x);
448 }
449 out.extend_from_slice(&bytes);
450 Buffer::from(out)
451 };
452
453 assert_eq!(bytes.len(), full_range.len());
454
455 for range in &ranges[current_offset..end] {
456 let slice = bytes
457 .clone()
458 .sliced(range.start - full_range.start..range.end - full_range.start);
459
460 match out.raw_entry_mut().from_key(&range.start) {
461 RawEntryMut::Vacant(slot) => {
462 slot.insert(range.start, slice);
463 },
464 RawEntryMut::Occupied(mut slot) => {
465 if slot.get_mut().len() < slice.len() {
466 *slot.get_mut() = slice;
467 }
468 },
469 }
470 }
471
472 current_offset = end;
473 }
474
475 assert!(splitted_parts.is_empty());
476
477 Ok(out)
478 }
479
480 pub async fn download(&self, path: &Path, file: &mut tokio::fs::File) -> PolarsResult<()> {
482 let size = self.head(path, ConcurrencyStrategy::Unbounded).await?.size;
483 let parts = split_range(0..size as usize, None);
484
485 tune_with_concurrency_budget(
488 parts.len().clamp(0, MAX_BUDGET_PER_REQUEST) as u32,
489 || async {
490 let mut stream =
491 self.build_buffered_ranges_stream(path, parts, ConcurrencyStrategy::Unbounded);
492 let mut len = 0;
493 while let Some(bytes) = stream.try_next().await? {
494 len += bytes.len();
495 file.write_all(&bytes).await?;
496 }
497
498 assert_eq!(len, size as usize);
499
500 PolarsResult::Ok(pl_async::Size::from(len as u64))
501 },
502 )
503 .await?;
504
505 file.sync_all().await.map_err(PolarsError::from)?;
508
509 Ok(())
510 }
511
512 pub async fn head(
514 &self,
515 path: &Path,
516 strategy: ConcurrencyStrategy,
517 ) -> PolarsResult<ObjectMeta> {
518 with_concurrency_budget(1, || {
523 self.exec_with_rebuild_retry_on_err(|s| {
524 async move {
525 let t0 = Instant::now();
526 let head_result = self.io_metrics().record_io_read(0, s.head(path)).await;
527 if let ConcurrencyStrategy::BytesBased = strategy {
528 self.get_or_init_concurrency().record_io(IoSample {
530 n_bytes: 0,
531 ttfb: t0.elapsed(),
532 completion_time: Instant::now(),
533 });
534 }
535
536 if head_result.is_err() {
537 let t0 = Instant::now();
538 let get_range_0_1_result = self
541 .io_metrics()
542 .record_io_read(
543 0,
544 s.get_opts(
545 path,
546 object_store::GetOptions {
547 range: Some((0..1).into()),
548 ..Default::default()
549 },
550 ),
551 )
552 .await;
553
554 if let ConcurrencyStrategy::BytesBased = strategy {
555 self.get_or_init_concurrency().record_io(IoSample {
556 n_bytes: 0,
557 ttfb: t0.elapsed(),
558 completion_time: Instant::now(),
559 });
560 }
561
562 if let Ok(v) = get_range_0_1_result {
563 return Ok(v.meta);
564 }
565 }
566
567 let out = head_result?;
568
569 Ok(out)
570 }
571 })
572 })
573 .await
574 }
575}
576
577fn split_range(
580 range: Range<usize>,
581 chunk_size: Option<usize>,
582) -> impl ExactSizeIterator<Item = Range<usize>> {
583 let chunk_size = chunk_size.unwrap_or_else(get_download_chunk_size);
584
585 let n_parts = [
587 (range.len().div_ceil(chunk_size)).max(1),
588 (range.len() / chunk_size).max(1),
589 ]
590 .into_iter()
591 .min_by_key(|x| (range.len() / *x).abs_diff(chunk_size))
592 .unwrap();
593
594 let chunk_size = (range.len() / n_parts).max(1);
595
596 assert_eq!(n_parts, (range.len() / chunk_size).max(1));
597 let bytes_rem = range.len() % chunk_size;
598
599 (0..n_parts).map(move |part_no| {
600 let (start, end) = if part_no == 0 {
601 let end = range.start + chunk_size + bytes_rem;
603 let end = if end > range.end { range.end } else { end };
604 (range.start, end)
605 } else {
606 let start = bytes_rem + range.start + part_no * chunk_size;
607 (start, start + chunk_size)
608 };
609
610 start..end
611 })
612}
613
614fn merge_ranges(
625 ranges: &[Range<usize>],
626 chunk_size: Option<usize>,
627) -> impl Iterator<Item = (Range<usize>, usize)> + '_ {
628 let chunk_size = chunk_size.unwrap_or_else(get_download_chunk_size);
629
630 let mut current_merged_range = ranges.first().map_or(0..0, Clone::clone);
631 let mut current_n_bytes = current_merged_range.len();
633
634 (0..ranges.len())
635 .filter_map(move |current_idx| {
636 let current_idx = 1 + current_idx;
637
638 if current_idx == ranges.len() {
639 Some((current_merged_range.clone(), current_idx))
641 } else {
642 let range = ranges[current_idx].clone();
643
644 let new_merged = current_merged_range.start.min(range.start)
645 ..current_merged_range.end.max(range.end);
646
647 let (distance, is_overlapping) = {
658 let l = current_merged_range.end.min(range.end);
659 let r = current_merged_range.start.max(range.start);
660
661 (r.abs_diff(l), r < l)
662 };
663
664 let should_merge = is_overlapping || {
665 let leq_current_len_dist_to_chunk_size = new_merged.len().abs_diff(chunk_size)
666 <= current_merged_range.len().abs_diff(chunk_size);
667 let gap_tolerance =
668 (current_n_bytes.max(range.len()) / 8).clamp(1024 * 1024, 8 * 1024 * 1024);
669
670 leq_current_len_dist_to_chunk_size && distance <= gap_tolerance
671 };
672
673 if should_merge {
674 current_merged_range = new_merged;
676 current_n_bytes += if is_overlapping {
677 range.len() - distance
678 } else {
679 range.len()
680 };
681 None
682 } else {
683 let out = (current_merged_range.clone(), current_idx);
684 current_merged_range = range;
685 current_n_bytes = current_merged_range.len();
686 Some(out)
687 }
688 }
689 })
690 .flat_map(move |x| {
691 let (range, end) = x;
693 let split = split_range(range, Some(chunk_size));
694 let len = split.len();
695
696 split
697 .enumerate()
698 .map(move |(i, range)| (range, if 1 + i == len { end } else { 0 }))
699 })
700}
701
702#[cfg(test)]
703mod tests {
704
705 #[test]
706 fn test_split_range() {
707 use super::{get_download_chunk_size, split_range};
708
709 let chunk_size = get_download_chunk_size();
710
711 assert_eq!(chunk_size, 64 * 1024 * 1024);
712
713 #[allow(clippy::single_range_in_vec_init)]
714 {
715 assert_eq!(split_range(0..0, None).collect::<Vec<_>>(), [0..0]);
717 assert_eq!(split_range(3..3, None).collect::<Vec<_>>(), [3..3]);
718 }
719
720 let n = 4 * chunk_size / 3;
727
728 #[allow(clippy::single_range_in_vec_init)]
729 {
730 assert_eq!(split_range(0..n, None).collect::<Vec<_>>(), [0..89478485]);
731 }
732
733 assert_eq!(
734 split_range(0..n + 1, None).collect::<Vec<_>>(),
735 [0..44739243, 44739243..89478486]
736 );
737
738 let n = 12 * chunk_size / 5;
745
746 assert_eq!(
747 split_range(0..n, None).collect::<Vec<_>>(),
748 [0..80530637, 80530637..161061273]
749 );
750
751 assert_eq!(
752 split_range(0..n + 1, None).collect::<Vec<_>>(),
753 [0..53687092, 53687092..107374183, 107374183..161061274]
754 );
755 }
756
757 #[test]
758 fn test_merge_ranges() {
759 use super::{get_download_chunk_size, merge_ranges};
760
761 let chunk_size = get_download_chunk_size();
762
763 assert_eq!(chunk_size, 64 * 1024 * 1024);
764
765 assert_eq!(merge_ranges(&[], None).collect::<Vec<_>>(), []);
767
768 assert_eq!(
772 merge_ranges(&[0..1, 1..127 * 1024 * 1024], None).collect::<Vec<_>>(),
773 [(0..66584576, 0), (66584576..133169152, 2)]
774 );
775
776 assert_eq!(
778 merge_ranges(&[0..1, 1024 * 1024 + 1..1024 * 1024 + 2], None).collect::<Vec<_>>(),
779 [(0..1048578, 2)]
780 );
781
782 assert_eq!(
784 merge_ranges(&[0..1, 1024 * 1024 + 2..1024 * 1024 + 3], None).collect::<Vec<_>>(),
785 [(0..1, 1), (1048578..1048579, 2)]
786 );
787
788 assert_eq!(
790 merge_ranges(&[0..8, 10..11], None).collect::<Vec<_>>(),
791 [(0..11, 2)]
792 );
793
794 assert_eq!(
796 merge_ranges(&[0..1, 3..11], None).collect::<Vec<_>>(),
797 [(0..11, 2)]
798 );
799
800 assert_eq!(
802 merge_ranges(
803 &[0..80 * 1024 * 1024, 10 * 1024 * 1024..70 * 1024 * 1024],
804 None
805 )
806 .collect::<Vec<_>>(),
807 [(0..80 * 1024 * 1024, 2)]
808 );
809 }
810}