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::client::{HttpError, HttpErrorKind};
9use object_store::path::Path;
10use object_store::{ObjectMeta, ObjectStore, ObjectStoreExt};
11use polars_buffer::Buffer;
12use polars_core::config;
13use polars_core::prelude::{InitHashMaps, PlHashMap};
14use polars_error::{PolarsError, PolarsResult};
15use polars_utils::pl_path::PlRefPath;
16use tokio::io::AsyncWriteExt;
17
18use super::concurrency::{ConcurrencyController, IoSample};
19use super::concurrency_config::{ConcurrencyStrategy, FetchConfig, get_download_chunk_size};
20use crate::pl_async::{
21 self, MAX_BUDGET_PER_REQUEST, get_concurrency_limit, tune_with_concurrency_budget,
22 with_concurrency_budget,
23};
24
25#[derive(Debug)]
26pub struct PolarsObjectStoreError {
27 pub base_url: PlRefPath,
28 pub source: object_store::Error,
29}
30
31impl Display for PolarsObjectStoreError {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 write!(
34 f,
35 "object-store error: {} (path: {})",
36 self.source, self.base_url
37 )
38 }
39}
40
41impl std::error::Error for PolarsObjectStoreError {
42 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
43 Some(&self.source)
44 }
45}
46
47pub fn object_store_error_kind(source: &object_store::Error) -> std::io::ErrorKind {
50 use std::io::ErrorKind::*;
51
52 match source {
53 object_store::Error::NotFound { .. } => NotFound,
54 object_store::Error::PermissionDenied { .. }
55 | object_store::Error::Unauthenticated { .. } => PermissionDenied,
56 _ => io_error_kind_from_source(source),
57 }
58}
59
60pub fn io_error_kind_from_source(error: &(dyn std::error::Error + 'static)) -> std::io::ErrorKind {
63 use std::io::ErrorKind::*;
64
65 let mut kind = Other;
66 let mut http_kind = None;
67 let mut cur = Some(error);
68 while let Some(e) = cur {
69 if let Some(io) = e.downcast_ref::<std::io::Error>()
70 && io.kind() != Other
71 {
72 kind = io.kind();
73 } else if let Some(http) = e.downcast_ref::<HttpError>() {
74 http_kind = Some(http.kind());
75 }
76 cur = e.source();
77 }
78 if kind == Other {
79 kind = match http_kind {
82 Some(HttpErrorKind::Timeout) => TimedOut,
83 Some(HttpErrorKind::Connect) => NotConnected,
84 Some(HttpErrorKind::Interrupted) => ConnectionAborted,
85 _ => Other,
86 };
87 }
88 kind
89}
90
91impl From<PolarsObjectStoreError> for std::io::Error {
92 fn from(value: PolarsObjectStoreError) -> Self {
93 let kind = object_store_error_kind(&value.source);
94 std::io::Error::new(kind, value)
95 }
96}
97
98impl From<PolarsObjectStoreError> for PolarsError {
99 fn from(value: PolarsObjectStoreError) -> Self {
100 PolarsError::IO {
101 error: Arc::new(value.into()),
102 msg: None,
103 }
104 }
105}
106
107mod inner {
108
109 use std::borrow::Cow;
110 use std::future::Future;
111 use std::sync::Arc;
112
113 use object_store::ObjectStore;
114 use polars_core::config;
115 use polars_error::{PolarsError, PolarsResult};
116 use polars_utils::relaxed_cell::RelaxedCell;
117
118 use crate::cloud::concurrency::{ConcurrencyController, ControllerConfig};
119 use crate::cloud::{ObjectStoreErrorContext, PolarsObjectStoreBuilder};
120 use crate::metrics::{IOMetrics, OptIOMetrics};
121
122 #[derive(Debug)]
123 struct Inner {
124 store: tokio::sync::RwLock<Arc<dyn ObjectStore>>,
125 builder: PolarsObjectStoreBuilder,
126 rebuilt: RelaxedCell<bool>,
127 suffix_range_supported: RelaxedCell<bool>,
128 }
129
130 #[derive(Clone, Debug)]
132 pub struct PolarsObjectStore {
133 inner: Arc<Inner>,
134 initial_store: std::sync::Arc<dyn ObjectStore>,
136 io_metrics: OptIOMetrics,
137 concurrency: Arc<std::sync::OnceLock<Arc<ConcurrencyController>>>,
139 }
140
141 impl PolarsObjectStore {
142 pub(crate) fn new_from_inner(
143 store: Arc<dyn ObjectStore>,
144 builder: PolarsObjectStoreBuilder,
145 ) -> Self {
146 let initial_store = store.clone();
147 let suffix_range_supported = RelaxedCell::from(!builder.is_azure());
150 Self {
151 inner: Arc::new(Inner {
152 store: tokio::sync::RwLock::new(store),
153 builder,
154 rebuilt: RelaxedCell::from(false),
155 suffix_range_supported,
156 }),
157 initial_store,
158 io_metrics: OptIOMetrics(None),
159 concurrency: Arc::new(std::sync::OnceLock::new()),
160 }
161 }
162
163 pub fn set_io_metrics(&mut self, io_metrics: Option<Arc<IOMetrics>>) -> &mut Self {
164 self.io_metrics = OptIOMetrics(io_metrics);
165 self
166 }
167
168 pub fn io_metrics(&self) -> &OptIOMetrics {
169 &self.io_metrics
170 }
171
172 pub fn get_or_init_concurrency(&self) -> &Arc<ConcurrencyController> {
173 self.concurrency.get_or_init(|| {
174 let signal = self.inner.builder.rate_limit_signal();
176 Arc::new(ConcurrencyController::new(
177 ControllerConfig::default(),
178 signal,
179 ))
180 })
181 }
182
183 pub async fn to_dyn_object_store(&self) -> Cow<'_, Arc<dyn ObjectStore>> {
185 if !self.inner.rebuilt.load() {
186 Cow::Borrowed(&self.initial_store)
187 } else {
188 Cow::Owned(self.inner.store.read().await.clone())
189 }
190 }
191
192 pub async fn rebuild_inner(
193 &self,
194 from_version: &Arc<dyn ObjectStore>,
195 ) -> PolarsResult<Arc<dyn ObjectStore>> {
196 let mut current_store = self.inner.store.write().await;
197
198 if Arc::ptr_eq(&*current_store, from_version) {
200 *current_store =
201 self.inner
202 .builder
203 .clone()
204 .build_impl(true)
205 .await
206 .map_err(|e| {
207 e.wrap_msg(|e| format!("attempt to rebuild object store failed: {e}"))
208 })?;
209 }
210
211 self.inner.rebuilt.store(true);
212
213 Ok((*current_store).clone())
214 }
215
216 pub async fn exec_with_rebuild_retry_on_err<'s, 'f, Fn, Fut, O>(
217 &'s self,
218 mut func: Fn,
219 ) -> PolarsResult<O>
220 where
221 Fn: FnMut(Cow<'s, Arc<dyn ObjectStore>>) -> Fut + 'f,
222 Fut: Future<Output = object_store::Result<O>>,
223 {
224 let store = self.to_dyn_object_store().await;
225
226 let out = func(store.clone()).await;
227
228 let orig_err = match out {
229 Ok(v) => return Ok(v),
230 Err(e) => e,
231 };
232
233 let rebuild = self.inner.builder.rebuild_on_error();
234
235 if config::verbose() {
236 eprintln!(
237 "[PolarsObjectStore]: got error: {}, will {}retry",
238 orig_err,
239 if rebuild { "rebuild store and " } else { "" }
240 );
241 }
242
243 let store = if rebuild {
244 let store = self
245 .rebuild_inner(&store)
246 .await
247 .map_err(|e| e.wrap_msg(|e| format!("{e}; original error: {orig_err}")))?;
248 Cow::Owned(store)
249 } else {
250 store
251 };
252
253 func(store).await.map_err(|e| {
254 let e: PolarsError = self.error_context().attach_err_info(e).into();
255
256 if self.inner.builder.is_azure()
257 && std::env::var("POLARS_AUTO_USE_AZURE_STORAGE_ACCOUNT_KEY").as_deref()
258 != Ok("1")
259 {
260 e.wrap_msg(|e| {
263 format!(
264 "{e}; note: if you are using Python, consider setting \
265POLARS_AUTO_USE_AZURE_STORAGE_ACCOUNT_KEY=1 if you would like polars to try to retrieve \
266and use the storage account keys from Azure CLI to authenticate"
267 )
268 })
269 } else {
270 e
271 }
272 })
273 }
274
275 pub(crate) fn suffix_range_supported(&self) -> bool {
276 self.inner.suffix_range_supported.load()
277 }
278
279 pub(crate) fn disable_suffix_range(&self) {
280 self.inner.suffix_range_supported.store(false);
281 }
282
283 pub fn error_context(&self) -> ObjectStoreErrorContext {
284 ObjectStoreErrorContext::new(self.inner.builder.path().clone())
285 }
286 }
287}
288
289#[derive(Clone)]
290pub struct ObjectStoreErrorContext {
291 path: PlRefPath,
292}
293
294impl ObjectStoreErrorContext {
295 pub fn new(path: PlRefPath) -> Self {
296 Self { path }
297 }
298
299 pub fn attach_err_info(self, err: object_store::Error) -> PolarsObjectStoreError {
300 let ObjectStoreErrorContext { path } = self;
301
302 PolarsObjectStoreError {
303 base_url: path,
304 source: err,
305 }
306 }
307}
308
309pub use inner::PolarsObjectStore;
310
311async fn collect_body(
314 stream: futures::stream::BoxStream<'static, object_store::Result<bytes::Bytes>>,
315 len: u64,
316 controller: &ConcurrencyController,
317) -> object_store::Result<bytes::Bytes> {
318 const ARRIVAL_FLUSH_BYTES: u64 = 64 * 1024;
319
320 let mut pending = 0;
321 let out = object_store::collect_bytes(
322 stream.inspect_ok(|chunk| {
323 pending += chunk.len() as u64;
324 if pending >= ARRIVAL_FLUSH_BYTES {
325 controller.record_arrived(std::mem::take(&mut pending));
326 }
327 }),
328 Some(len),
329 )
330 .await;
331 controller.record_arrived(pending);
332 out
333}
334
335pub type ObjectStorePath = object_store::path::Path;
336
337impl PolarsObjectStore {
338 pub fn build_buffered_ranges_stream<'a, T: Iterator<Item = Range<usize>>>(
339 &'a self,
340 path: &'a Path,
341 ranges: T,
342 strategy: ConcurrencyStrategy,
343 ) -> impl Stream<Item = PolarsResult<Buffer<u8>>> + use<'a, T> {
344 let controller = match strategy {
345 ConcurrencyStrategy::BytesBased => Some(self.get_or_init_concurrency().clone()),
346 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::Legacy => None,
347 };
348
349 let n_buffered = match strategy {
350 ConcurrencyStrategy::BytesBased => 4096,
356 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::Legacy => {
357 get_concurrency_limit() as usize
358 },
359 };
360
361 futures::stream::iter(ranges.map(move |range| {
362 let controller = controller.clone();
363 async move {
364 if range.is_empty() {
365 return Ok(Buffer::new());
366 }
367 let bytes_req = range.len() as u64;
368
369 let _permit = match &controller {
371 Some(controller) => Some(controller.acquire(bytes_req).await),
372 None => None,
373 };
374
375 let controller_ref = controller.as_deref();
376 let (out, ttfb) = self
377 .io_metrics()
378 .record_io_read(
379 bytes_req,
380 self.exec_with_rebuild_retry_on_err(|s| async move {
381 let t0 = Instant::now();
382 let response = s
383 .get_opts(
384 path,
385 object_store::GetOptions {
386 range: Some((range.start as u64..range.end as u64).into()),
387 ..Default::default()
388 },
389 )
390 .await?;
391 let ttfb = t0.elapsed();
392 let out = match controller_ref {
393 Some(controller)
394 if matches!(
395 response.payload,
396 object_store::GetResultPayload::Stream(_)
397 ) =>
398 {
399 let len = response.range.end - response.range.start;
400 collect_body(response.into_stream(), len, controller).await?
401 },
402 _ => {
403 let out = response.bytes().await?;
404 if let Some(controller) = controller_ref {
406 controller.record_arrived(out.len() as u64);
407 }
408 out
409 },
410 };
411
412 Ok((out, ttfb))
413 }),
414 )
415 .await?;
416
417 if let Some(controller) = &controller {
418 controller.record_io(IoSample {
419 n_bytes: out.len() as u64,
420 ttfb,
421 completion_time: Instant::now(),
422 });
423 }
424
425 Ok(Buffer::from_owner(out))
426 }
427 }))
428 .buffered(n_buffered)
429 }
430
431 pub async fn get_range(
432 &self,
433 path: &Path,
434 range: Range<usize>,
435 config: FetchConfig,
436 ) -> PolarsResult<Buffer<u8>> {
437 if range.is_empty() {
438 return Ok(Buffer::new());
439 }
440
441 let parts = split_range(range.clone(), Some(config.chunk_size));
442
443 match config.strategy {
444 ConcurrencyStrategy::Legacy => self.get_range_legacy(path, range).await,
445 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::BytesBased => self
446 .build_buffered_ranges_stream(path, parts, config.strategy)
447 .try_collect::<Vec<_>>()
448 .await
449 .map(|parts| {
450 if parts.len() == 1 {
451 return parts.into_iter().next().unwrap();
452 }
453 let mut combined = Vec::with_capacity(range.len());
454 for part in parts {
455 combined.extend_from_slice(&part);
456 }
457 assert_eq!(combined.len(), range.len());
458 Buffer::from_vec(combined)
459 }),
460 }
461 }
462
463 async fn get_range_legacy(&self, path: &Path, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
464 if range.is_empty() {
465 return Ok(Buffer::new());
466 }
467
468 let parts = split_range(range.clone(), None);
469
470 if parts.len() == 1 {
471 let out = tune_with_concurrency_budget(1, move || async move {
472 let bytes = self
473 .io_metrics()
474 .record_io_read(
475 range.len() as u64,
476 self.exec_with_rebuild_retry_on_err(|s| async move {
477 s.get_range(path, range.start as u64..range.end as u64)
478 .await
479 }),
480 )
481 .await?;
482
483 PolarsResult::Ok(Buffer::from_owner(bytes))
484 })
485 .await?;
486
487 Ok(out)
488 } else {
489 let parts = tune_with_concurrency_budget(
490 parts.len().clamp(0, MAX_BUDGET_PER_REQUEST) as u32,
491 || {
492 self.build_buffered_ranges_stream(path, parts, ConcurrencyStrategy::Legacy)
493 .try_collect::<Vec<Buffer<u8>>>()
494 },
495 )
496 .await?;
497
498 let mut combined = Vec::with_capacity(range.len());
499
500 for part in parts {
501 combined.extend_from_slice(&part)
502 }
503
504 assert_eq!(combined.len(), range.len());
505
506 PolarsResult::Ok(Buffer::from_vec(combined))
507 }
508 }
509
510 pub async fn get_ranges_sort(
511 &self,
512 path: &Path,
513 ranges: &mut [Range<usize>],
514 config: FetchConfig,
515 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
516 if ranges.is_empty() {
517 return Ok(Default::default());
518 }
519
520 ranges.sort_unstable_by_key(|x| x.start);
521
522 let ranges_len = ranges.len();
523 let (merged_ranges, merged_ends): (Vec<_>, Vec<_>) =
524 merge_ranges(ranges, Some(config.chunk_size)).unzip();
525
526 let mut out = PlHashMap::with_capacity(ranges_len);
527
528 let mut stream =
530 self.build_buffered_ranges_stream(path, merged_ranges.iter().cloned(), config.strategy);
531
532 let mut current_offset = 0;
533 let mut ends_iter = merged_ends.iter();
534 let mut splitted_parts: Vec<Buffer<u8>> = vec![];
535
536 while let Some(bytes) = stream.try_next().await? {
537 let end = *ends_iter.next().unwrap();
538
539 if end == 0 {
540 splitted_parts.push(bytes);
541 continue;
542 }
543
544 let full_range = ranges[current_offset..end]
545 .iter()
546 .cloned()
547 .reduce(|l, r| l.start.min(r.start)..l.end.max(r.end))
548 .unwrap();
549
550 let bytes = if splitted_parts.is_empty() {
551 bytes
552 } else {
553 let mut out = Vec::with_capacity(full_range.len());
554 for x in splitted_parts.drain(..) {
555 out.extend_from_slice(&x);
556 }
557 out.extend_from_slice(&bytes);
558 Buffer::from(out)
559 };
560
561 assert_eq!(bytes.len(), full_range.len());
562
563 for range in &ranges[current_offset..end] {
564 let slice = bytes
565 .clone()
566 .sliced(range.start - full_range.start..range.end - full_range.start);
567
568 match out.raw_entry_mut().from_key(&range.start) {
569 RawEntryMut::Vacant(slot) => {
570 slot.insert(range.start, slice);
571 },
572 RawEntryMut::Occupied(mut slot) => {
573 if slot.get_mut().len() < slice.len() {
574 *slot.get_mut() = slice;
575 }
576 },
577 }
578 }
579
580 current_offset = end;
581 }
582
583 assert!(splitted_parts.is_empty());
584
585 Ok(out)
586 }
587
588 pub async fn download(&self, path: &Path, file: &mut tokio::fs::File) -> PolarsResult<()> {
590 let size = self.head(path, ConcurrencyStrategy::Unbounded).await?.size;
591 let parts = split_range(0..size as usize, None);
592
593 tune_with_concurrency_budget(
596 parts.len().clamp(0, MAX_BUDGET_PER_REQUEST) as u32,
597 || async {
598 let mut stream =
599 self.build_buffered_ranges_stream(path, parts, ConcurrencyStrategy::Unbounded);
600 let mut len = 0;
601 while let Some(bytes) = stream.try_next().await? {
602 len += bytes.len();
603 file.write_all(&bytes).await?;
604 }
605
606 assert_eq!(len, size as usize);
607
608 PolarsResult::Ok(pl_async::Size::from(len as u64))
609 },
610 )
611 .await?;
612
613 file.sync_all().await.map_err(PolarsError::from)?;
616
617 Ok(())
618 }
619
620 pub async fn get_suffix(
626 &self,
627 path: &Path,
628 n: usize,
629 config: FetchConfig,
630 ) -> PolarsResult<(Buffer<u8>, usize)> {
631 let mut suffix_rejected = false;
632
633 if n > 0 && self.suffix_range_supported() {
634 match self
635 .try_get_with_size(path, SizedRange::Suffix(n), config.strategy)
636 .await
637 {
638 Ok(v) => return Ok(v),
639 Err(e) if is_suffix_range_rejection(&e) => {
640 suffix_rejected = true;
641
642 if config::verbose() {
643 eprintln!(
644 "[PolarsObjectStore]: suffix range request rejected ({e}), \
645 falling back to head + range request"
646 );
647 }
648 },
649 Err(e) => return Err(e),
650 }
651 }
652
653 let size = self.head(path, config.strategy).await?.size as usize;
654
655 if suffix_rejected && size > 0 {
657 self.disable_suffix_range();
658 }
659
660 let bytes = self
661 .get_range(path, size.saturating_sub(n)..size, config)
662 .await?;
663
664 Ok((bytes, size))
665 }
666
667 pub async fn get_prefix(
673 &self,
674 path: &Path,
675 n: usize,
676 config: FetchConfig,
677 ) -> PolarsResult<(Buffer<u8>, usize)> {
678 if n > 0 {
679 match self
680 .try_get_with_size(path, SizedRange::Prefix(n), config.strategy)
681 .await
682 {
683 Err(e) if is_prefix_range_rejection(&e) => {},
684 out => return out,
685 }
686 }
687
688 let size = self.head(path, config.strategy).await?.size as usize;
689 let bytes = self.get_range(path, 0..n.min(size), config).await?;
690
691 Ok((bytes, size))
692 }
693
694 async fn try_get_with_size(
696 &self,
697 path: &Path,
698 range: SizedRange,
699 strategy: ConcurrencyStrategy,
700 ) -> PolarsResult<(Buffer<u8>, usize)> {
701 match strategy {
702 ConcurrencyStrategy::BytesBased => {
703 let controller = self.get_or_init_concurrency();
704 let _permit = controller.acquire(range.len() as u64).await;
705 self.try_get_with_size_inner(path, range, Some(&**controller))
706 .await
707 },
708 ConcurrencyStrategy::Legacy => {
709 with_concurrency_budget(1, || self.try_get_with_size_inner(path, range, None)).await
710 },
711 ConcurrencyStrategy::Unbounded => self.try_get_with_size_inner(path, range, None).await,
712 }
713 }
714
715 async fn try_get_with_size_inner(
716 &self,
717 path: &Path,
718 range: SizedRange,
719 controller: Option<&ConcurrencyController>,
720 ) -> PolarsResult<(Buffer<u8>, usize)> {
721 let metrics = self.io_metrics();
722
723 if let SizedRange::Suffix(n) = range {
726 metrics.add_bytes_requested(n as u64);
727 }
728 let prefix_requested_recorded = &std::sync::atomic::AtomicBool::new(false);
729
730 let io_session = metrics.start_rx_session();
731
732 let out = self
733 .exec_with_rebuild_retry_on_err(|s| async move {
734 let t0 = Instant::now();
735 let response = s
736 .get_opts(
737 path,
738 object_store::GetOptions {
739 range: Some(range.into()),
740 ..Default::default()
741 },
742 )
743 .await?;
744 let ttfb = t0.elapsed();
745 let size = response.meta.size as usize;
747
748 if let SizedRange::Prefix(n) = range
749 && !prefix_requested_recorded.swap(true, std::sync::atomic::Ordering::Relaxed)
750 {
751 metrics.add_bytes_requested(n.min(size) as u64);
752 }
753
754 let bytes = response.bytes().await?;
755
756 if let Some(controller) = controller {
757 controller.record_arrived(bytes.len() as u64);
758 controller.record_io(IoSample {
759 n_bytes: bytes.len() as u64,
760 ttfb,
761 completion_time: Instant::now(),
762 });
763 }
764
765 Ok((Buffer::from_owner(bytes), size))
766 })
767 .await;
768
769 drop(io_session);
770
771 let (bytes, size) = out?;
772
773 metrics.add_bytes_received(bytes.len() as u64);
775
776 Ok((bytes, size))
777 }
778
779 pub async fn head(
781 &self,
782 path: &Path,
783 strategy: ConcurrencyStrategy,
784 ) -> PolarsResult<ObjectMeta> {
785 match strategy {
786 ConcurrencyStrategy::BytesBased => {
787 let controller = self.get_or_init_concurrency();
788 let _permit = controller.acquire(0).await;
789 self.head_inner(path, Some(&**controller)).await
790 },
791 ConcurrencyStrategy::Legacy => {
792 with_concurrency_budget(1, || self.head_inner(path, None)).await
793 },
794 ConcurrencyStrategy::Unbounded => self.head_inner(path, None).await,
795 }
796 }
797
798 async fn head_inner(
799 &self,
800 path: &Path,
801 controller: Option<&ConcurrencyController>,
802 ) -> PolarsResult<ObjectMeta> {
803 self.exec_with_rebuild_retry_on_err(|s| {
804 async move {
805 let t0 = Instant::now();
806 let head_result = self.io_metrics().record_io_read(0, s.head(path)).await;
807
808 if head_result.is_err() {
809 let t0 = Instant::now();
810
811 let get_range_0_1_result = self
814 .io_metrics()
815 .record_io_read(
816 0,
817 s.get_opts(
818 path,
819 object_store::GetOptions {
820 range: Some((0..1).into()),
821 ..Default::default()
822 },
823 ),
824 )
825 .await;
826
827 if let Ok(v) = get_range_0_1_result {
828 if let Some(controller) = controller {
829 controller.record_head_rtt(t0.elapsed());
830 }
831 return Ok(v.meta);
832 }
833 }
834
835 let out = head_result?;
836
837 if let Some(controller) = controller {
838 controller.record_head_rtt(t0.elapsed());
839 }
840
841 Ok(out)
842 }
843 })
844 .await
845 }
846}
847
848fn is_suffix_range_rejection(err: &PolarsError) -> bool {
851 let msg = err.to_string();
852
853 msg.contains("does not support suffix range requests")
854 || msg.contains("Received non-partial response")
855 || msg.contains("416 Range Not Satisfiable")
856}
857
858#[derive(Clone, Copy, Debug)]
860enum SizedRange {
861 Prefix(usize),
863 Suffix(usize),
865}
866
867impl SizedRange {
868 fn len(self) -> usize {
870 match self {
871 Self::Prefix(n) | Self::Suffix(n) => n,
872 }
873 }
874}
875
876impl From<SizedRange> for object_store::GetRange {
877 fn from(value: SizedRange) -> Self {
878 match value {
879 SizedRange::Prefix(n) => Self::Bounded(0..n as u64),
880 SizedRange::Suffix(n) => Self::Suffix(n as u64),
881 }
882 }
883}
884
885fn is_prefix_range_rejection(err: &PolarsError) -> bool {
892 std::iter::successors(Some(err as &dyn std::error::Error), |e| e.source()).any(|e| {
893 let msg = e.to_string();
894
895 msg.contains("416 Range Not Satisfiable")
896 || msg.contains("Wanted range starting at")
897 || msg.contains("Received non-partial response")
898 || msg.contains("Range request not supported")
900 })
901}
902
903fn split_range(
906 range: Range<usize>,
907 chunk_size: Option<usize>,
908) -> impl ExactSizeIterator<Item = Range<usize>> {
909 let chunk_size = chunk_size.unwrap_or_else(get_download_chunk_size);
910
911 let n_parts = [
913 (range.len().div_ceil(chunk_size)).max(1),
914 (range.len() / chunk_size).max(1),
915 ]
916 .into_iter()
917 .min_by_key(|x| (range.len() / *x).abs_diff(chunk_size))
918 .unwrap();
919
920 let chunk_size = (range.len() / n_parts).max(1);
921
922 assert_eq!(n_parts, (range.len() / chunk_size).max(1));
923 let bytes_rem = range.len() % chunk_size;
924
925 (0..n_parts).map(move |part_no| {
926 let (start, end) = if part_no == 0 {
927 let end = range.start + chunk_size + bytes_rem;
929 let end = if end > range.end { range.end } else { end };
930 (range.start, end)
931 } else {
932 let start = bytes_rem + range.start + part_no * chunk_size;
933 (start, start + chunk_size)
934 };
935
936 start..end
937 })
938}
939
940fn merge_ranges(
951 ranges: &[Range<usize>],
952 chunk_size: Option<usize>,
953) -> impl Iterator<Item = (Range<usize>, usize)> + '_ {
954 let chunk_size = chunk_size.unwrap_or_else(get_download_chunk_size);
955
956 let mut current_merged_range = ranges.first().map_or(0..0, Clone::clone);
957 let mut current_n_bytes = current_merged_range.len();
959
960 (0..ranges.len())
961 .filter_map(move |current_idx| {
962 let current_idx = 1 + current_idx;
963
964 if current_idx == ranges.len() {
965 Some((current_merged_range.clone(), current_idx))
967 } else {
968 let range = ranges[current_idx].clone();
969
970 let new_merged = current_merged_range.start.min(range.start)
971 ..current_merged_range.end.max(range.end);
972
973 let (distance, is_overlapping) = {
984 let l = current_merged_range.end.min(range.end);
985 let r = current_merged_range.start.max(range.start);
986
987 (r.abs_diff(l), r < l)
988 };
989
990 let should_merge = is_overlapping || {
991 let leq_current_len_dist_to_chunk_size = new_merged.len().abs_diff(chunk_size)
992 <= current_merged_range.len().abs_diff(chunk_size);
993 let gap_tolerance =
994 (current_n_bytes.max(range.len()) / 8).clamp(1024 * 1024, 8 * 1024 * 1024);
995
996 leq_current_len_dist_to_chunk_size && distance <= gap_tolerance
997 };
998
999 if should_merge {
1000 current_merged_range = new_merged;
1002 current_n_bytes += if is_overlapping {
1003 range.len() - distance
1004 } else {
1005 range.len()
1006 };
1007 None
1008 } else {
1009 let out = (current_merged_range.clone(), current_idx);
1010 current_merged_range = range;
1011 current_n_bytes = current_merged_range.len();
1012 Some(out)
1013 }
1014 }
1015 })
1016 .flat_map(move |x| {
1017 let (range, end) = x;
1019 let split = split_range(range, Some(chunk_size));
1020 let len = split.len();
1021
1022 split
1023 .enumerate()
1024 .map(move |(i, range)| (range, if 1 + i == len { end } else { 0 }))
1025 })
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030
1031 #[test]
1032 fn test_split_range() {
1033 use super::{get_download_chunk_size, split_range};
1034
1035 let chunk_size = get_download_chunk_size();
1036
1037 assert_eq!(chunk_size, 64 * 1024 * 1024);
1038
1039 #[allow(clippy::single_range_in_vec_init)]
1040 {
1041 assert_eq!(split_range(0..0, None).collect::<Vec<_>>(), [0..0]);
1043 assert_eq!(split_range(3..3, None).collect::<Vec<_>>(), [3..3]);
1044 }
1045
1046 let n = 4 * chunk_size / 3;
1053
1054 #[allow(clippy::single_range_in_vec_init)]
1055 {
1056 assert_eq!(split_range(0..n, None).collect::<Vec<_>>(), [0..89478485]);
1057 }
1058
1059 assert_eq!(
1060 split_range(0..n + 1, None).collect::<Vec<_>>(),
1061 [0..44739243, 44739243..89478486]
1062 );
1063
1064 let n = 12 * chunk_size / 5;
1071
1072 assert_eq!(
1073 split_range(0..n, None).collect::<Vec<_>>(),
1074 [0..80530637, 80530637..161061273]
1075 );
1076
1077 assert_eq!(
1078 split_range(0..n + 1, None).collect::<Vec<_>>(),
1079 [0..53687092, 53687092..107374183, 107374183..161061274]
1080 );
1081 }
1082
1083 #[test]
1084 fn test_merge_ranges() {
1085 use super::{get_download_chunk_size, merge_ranges};
1086
1087 let chunk_size = get_download_chunk_size();
1088
1089 assert_eq!(chunk_size, 64 * 1024 * 1024);
1090
1091 assert_eq!(merge_ranges(&[], None).collect::<Vec<_>>(), []);
1093
1094 assert_eq!(
1098 merge_ranges(&[0..1, 1..127 * 1024 * 1024], None).collect::<Vec<_>>(),
1099 [(0..66584576, 0), (66584576..133169152, 2)]
1100 );
1101
1102 assert_eq!(
1104 merge_ranges(&[0..1, 1024 * 1024 + 1..1024 * 1024 + 2], None).collect::<Vec<_>>(),
1105 [(0..1048578, 2)]
1106 );
1107
1108 assert_eq!(
1110 merge_ranges(&[0..1, 1024 * 1024 + 2..1024 * 1024 + 3], None).collect::<Vec<_>>(),
1111 [(0..1, 1), (1048578..1048579, 2)]
1112 );
1113
1114 assert_eq!(
1116 merge_ranges(&[0..8, 10..11], None).collect::<Vec<_>>(),
1117 [(0..11, 2)]
1118 );
1119
1120 assert_eq!(
1122 merge_ranges(&[0..1, 3..11], None).collect::<Vec<_>>(),
1123 [(0..11, 2)]
1124 );
1125
1126 assert_eq!(
1128 merge_ranges(
1129 &[0..80 * 1024 * 1024, 10 * 1024 * 1024..70 * 1024 * 1024],
1130 None
1131 )
1132 .collect::<Vec<_>>(),
1133 [(0..80 * 1024 * 1024, 2)]
1134 );
1135 }
1136}