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
311pub type ObjectStorePath = object_store::path::Path;
312
313impl PolarsObjectStore {
314 pub fn build_buffered_ranges_stream<'a, T: Iterator<Item = Range<usize>>>(
315 &'a self,
316 path: &'a Path,
317 ranges: T,
318 strategy: ConcurrencyStrategy,
319 ) -> impl Stream<Item = PolarsResult<Buffer<u8>>> + use<'a, T> {
320 let controller = match strategy {
321 ConcurrencyStrategy::BytesBased => Some(self.get_or_init_concurrency().clone()),
322 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::Legacy => None,
323 };
324
325 let n_buffered = match strategy {
326 ConcurrencyStrategy::BytesBased => 4096,
332 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::Legacy => {
333 get_concurrency_limit() as usize
334 },
335 };
336
337 futures::stream::iter(ranges.map(move |range| {
338 let controller = controller.clone();
339 async move {
340 if range.is_empty() {
341 return Ok(Buffer::new());
342 }
343 let bytes_req = range.len() as u64;
344
345 let _permit = match &controller {
347 Some(controller) => Some(controller.acquire(bytes_req).await),
348 None => None,
349 };
350
351 let (out, ttfb) = self
352 .io_metrics()
353 .record_io_read(
354 bytes_req,
355 self.exec_with_rebuild_retry_on_err(|s| async move {
356 let t0 = Instant::now();
357 let response = s
358 .get_opts(
359 path,
360 object_store::GetOptions {
361 range: Some((range.start as u64..range.end as u64).into()),
362 ..Default::default()
363 },
364 )
365 .await?;
366 let ttfb = t0.elapsed();
367 let out = response.bytes().await?;
368
369 Ok((out, ttfb))
370 }),
371 )
372 .await?;
373
374 if let Some(controller) = &controller {
375 controller.record_io(IoSample {
376 n_bytes: out.len() as u64,
377 ttfb,
378 completion_time: Instant::now(),
379 });
380 }
381
382 Ok(Buffer::from_owner(out))
383 }
384 }))
385 .buffered(n_buffered)
386 }
387
388 pub async fn get_range(
389 &self,
390 path: &Path,
391 range: Range<usize>,
392 config: FetchConfig,
393 ) -> PolarsResult<Buffer<u8>> {
394 if range.is_empty() {
395 return Ok(Buffer::new());
396 }
397
398 let parts = split_range(range.clone(), Some(config.chunk_size));
399
400 match config.strategy {
401 ConcurrencyStrategy::Legacy => self.get_range_legacy(path, range).await,
402 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::BytesBased => self
403 .build_buffered_ranges_stream(path, parts, config.strategy)
404 .try_collect::<Vec<_>>()
405 .await
406 .map(|parts| {
407 if parts.len() == 1 {
408 return parts.into_iter().next().unwrap();
409 }
410 let mut combined = Vec::with_capacity(range.len());
411 for part in parts {
412 combined.extend_from_slice(&part);
413 }
414 assert_eq!(combined.len(), range.len());
415 Buffer::from_vec(combined)
416 }),
417 }
418 }
419
420 async fn get_range_legacy(&self, path: &Path, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
421 if range.is_empty() {
422 return Ok(Buffer::new());
423 }
424
425 let parts = split_range(range.clone(), None);
426
427 if parts.len() == 1 {
428 let out = tune_with_concurrency_budget(1, move || async move {
429 let bytes = self
430 .io_metrics()
431 .record_io_read(
432 range.len() as u64,
433 self.exec_with_rebuild_retry_on_err(|s| async move {
434 s.get_range(path, range.start as u64..range.end as u64)
435 .await
436 }),
437 )
438 .await?;
439
440 PolarsResult::Ok(Buffer::from_owner(bytes))
441 })
442 .await?;
443
444 Ok(out)
445 } else {
446 let parts = tune_with_concurrency_budget(
447 parts.len().clamp(0, MAX_BUDGET_PER_REQUEST) as u32,
448 || {
449 self.build_buffered_ranges_stream(path, parts, ConcurrencyStrategy::Legacy)
450 .try_collect::<Vec<Buffer<u8>>>()
451 },
452 )
453 .await?;
454
455 let mut combined = Vec::with_capacity(range.len());
456
457 for part in parts {
458 combined.extend_from_slice(&part)
459 }
460
461 assert_eq!(combined.len(), range.len());
462
463 PolarsResult::Ok(Buffer::from_vec(combined))
464 }
465 }
466
467 pub async fn get_ranges_sort(
468 &self,
469 path: &Path,
470 ranges: &mut [Range<usize>],
471 config: FetchConfig,
472 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
473 if ranges.is_empty() {
474 return Ok(Default::default());
475 }
476
477 ranges.sort_unstable_by_key(|x| x.start);
478
479 let ranges_len = ranges.len();
480 let (merged_ranges, merged_ends): (Vec<_>, Vec<_>) =
481 merge_ranges(ranges, Some(config.chunk_size)).unzip();
482
483 let mut out = PlHashMap::with_capacity(ranges_len);
484
485 let mut stream =
487 self.build_buffered_ranges_stream(path, merged_ranges.iter().cloned(), config.strategy);
488
489 let mut current_offset = 0;
490 let mut ends_iter = merged_ends.iter();
491 let mut splitted_parts: Vec<Buffer<u8>> = vec![];
492
493 while let Some(bytes) = stream.try_next().await? {
494 let end = *ends_iter.next().unwrap();
495
496 if end == 0 {
497 splitted_parts.push(bytes);
498 continue;
499 }
500
501 let full_range = ranges[current_offset..end]
502 .iter()
503 .cloned()
504 .reduce(|l, r| l.start.min(r.start)..l.end.max(r.end))
505 .unwrap();
506
507 let bytes = if splitted_parts.is_empty() {
508 bytes
509 } else {
510 let mut out = Vec::with_capacity(full_range.len());
511 for x in splitted_parts.drain(..) {
512 out.extend_from_slice(&x);
513 }
514 out.extend_from_slice(&bytes);
515 Buffer::from(out)
516 };
517
518 assert_eq!(bytes.len(), full_range.len());
519
520 for range in &ranges[current_offset..end] {
521 let slice = bytes
522 .clone()
523 .sliced(range.start - full_range.start..range.end - full_range.start);
524
525 match out.raw_entry_mut().from_key(&range.start) {
526 RawEntryMut::Vacant(slot) => {
527 slot.insert(range.start, slice);
528 },
529 RawEntryMut::Occupied(mut slot) => {
530 if slot.get_mut().len() < slice.len() {
531 *slot.get_mut() = slice;
532 }
533 },
534 }
535 }
536
537 current_offset = end;
538 }
539
540 assert!(splitted_parts.is_empty());
541
542 Ok(out)
543 }
544
545 pub async fn download(&self, path: &Path, file: &mut tokio::fs::File) -> PolarsResult<()> {
547 let size = self.head(path, ConcurrencyStrategy::Unbounded).await?.size;
548 let parts = split_range(0..size as usize, None);
549
550 tune_with_concurrency_budget(
553 parts.len().clamp(0, MAX_BUDGET_PER_REQUEST) as u32,
554 || async {
555 let mut stream =
556 self.build_buffered_ranges_stream(path, parts, ConcurrencyStrategy::Unbounded);
557 let mut len = 0;
558 while let Some(bytes) = stream.try_next().await? {
559 len += bytes.len();
560 file.write_all(&bytes).await?;
561 }
562
563 assert_eq!(len, size as usize);
564
565 PolarsResult::Ok(pl_async::Size::from(len as u64))
566 },
567 )
568 .await?;
569
570 file.sync_all().await.map_err(PolarsError::from)?;
573
574 Ok(())
575 }
576
577 pub async fn get_suffix(
583 &self,
584 path: &Path,
585 n: usize,
586 config: FetchConfig,
587 ) -> PolarsResult<(Buffer<u8>, usize)> {
588 let mut suffix_rejected = false;
589
590 if n > 0 && self.suffix_range_supported() {
591 match self
592 .try_get_with_size(path, SizedRange::Suffix(n), config.strategy)
593 .await
594 {
595 Ok(v) => return Ok(v),
596 Err(e) if is_suffix_range_rejection(&e) => {
597 suffix_rejected = true;
598
599 if config::verbose() {
600 eprintln!(
601 "[PolarsObjectStore]: suffix range request rejected ({e}), \
602 falling back to head + range request"
603 );
604 }
605 },
606 Err(e) => return Err(e),
607 }
608 }
609
610 let size = self.head(path, config.strategy).await?.size as usize;
611
612 if suffix_rejected && size > 0 {
614 self.disable_suffix_range();
615 }
616
617 let bytes = self
618 .get_range(path, size.saturating_sub(n)..size, config)
619 .await?;
620
621 Ok((bytes, size))
622 }
623
624 pub async fn get_prefix(
630 &self,
631 path: &Path,
632 n: usize,
633 config: FetchConfig,
634 ) -> PolarsResult<(Buffer<u8>, usize)> {
635 if n > 0 {
636 match self
637 .try_get_with_size(path, SizedRange::Prefix(n), config.strategy)
638 .await
639 {
640 Err(e) if is_prefix_range_rejection(&e) => {},
641 out => return out,
642 }
643 }
644
645 let size = self.head(path, config.strategy).await?.size as usize;
646 let bytes = self.get_range(path, 0..n.min(size), config).await?;
647
648 Ok((bytes, size))
649 }
650
651 async fn try_get_with_size(
653 &self,
654 path: &Path,
655 range: SizedRange,
656 strategy: ConcurrencyStrategy,
657 ) -> PolarsResult<(Buffer<u8>, usize)> {
658 match strategy {
659 ConcurrencyStrategy::BytesBased => {
660 let controller = self.get_or_init_concurrency();
661 let _permit = controller.acquire(range.len() as u64).await;
662 self.try_get_with_size_inner(path, range, Some(&**controller))
663 .await
664 },
665 ConcurrencyStrategy::Legacy => {
666 with_concurrency_budget(1, || self.try_get_with_size_inner(path, range, None)).await
667 },
668 ConcurrencyStrategy::Unbounded => self.try_get_with_size_inner(path, range, None).await,
669 }
670 }
671
672 async fn try_get_with_size_inner(
673 &self,
674 path: &Path,
675 range: SizedRange,
676 controller: Option<&ConcurrencyController>,
677 ) -> PolarsResult<(Buffer<u8>, usize)> {
678 let metrics = self.io_metrics();
679
680 if let SizedRange::Suffix(n) = range {
683 metrics.add_bytes_requested(n as u64);
684 }
685 let prefix_requested_recorded = &std::sync::atomic::AtomicBool::new(false);
686
687 let io_session = metrics.start_rx_session();
688
689 let out = self
690 .exec_with_rebuild_retry_on_err(|s| async move {
691 let t0 = Instant::now();
692 let response = s
693 .get_opts(
694 path,
695 object_store::GetOptions {
696 range: Some(range.into()),
697 ..Default::default()
698 },
699 )
700 .await?;
701 let ttfb = t0.elapsed();
702 let size = response.meta.size as usize;
704
705 if let SizedRange::Prefix(n) = range
706 && !prefix_requested_recorded.swap(true, std::sync::atomic::Ordering::Relaxed)
707 {
708 metrics.add_bytes_requested(n.min(size) as u64);
709 }
710
711 let bytes = response.bytes().await?;
712
713 if let Some(controller) = controller {
714 controller.record_io(IoSample {
715 n_bytes: bytes.len() as u64,
716 ttfb,
717 completion_time: Instant::now(),
718 });
719 }
720
721 Ok((Buffer::from_owner(bytes), size))
722 })
723 .await;
724
725 drop(io_session);
726
727 let (bytes, size) = out?;
728
729 metrics.add_bytes_received(bytes.len() as u64);
731
732 Ok((bytes, size))
733 }
734
735 pub async fn head(
737 &self,
738 path: &Path,
739 strategy: ConcurrencyStrategy,
740 ) -> PolarsResult<ObjectMeta> {
741 match strategy {
742 ConcurrencyStrategy::BytesBased => {
743 let controller = self.get_or_init_concurrency();
744 let _permit = controller.acquire(0).await;
745 self.head_inner(path, Some(&**controller)).await
746 },
747 ConcurrencyStrategy::Legacy => {
748 with_concurrency_budget(1, || self.head_inner(path, None)).await
749 },
750 ConcurrencyStrategy::Unbounded => self.head_inner(path, None).await,
751 }
752 }
753
754 async fn head_inner(
755 &self,
756 path: &Path,
757 controller: Option<&ConcurrencyController>,
758 ) -> PolarsResult<ObjectMeta> {
759 self.exec_with_rebuild_retry_on_err(|s| {
760 async move {
761 let t0 = Instant::now();
762 let head_result = self.io_metrics().record_io_read(0, s.head(path)).await;
763
764 if head_result.is_err() {
765 let t0 = Instant::now();
766
767 let get_range_0_1_result = self
770 .io_metrics()
771 .record_io_read(
772 0,
773 s.get_opts(
774 path,
775 object_store::GetOptions {
776 range: Some((0..1).into()),
777 ..Default::default()
778 },
779 ),
780 )
781 .await;
782
783 if let Ok(v) = get_range_0_1_result {
784 if let Some(controller) = controller {
785 controller.record_head_rtt(t0.elapsed());
786 }
787 return Ok(v.meta);
788 }
789 }
790
791 let out = head_result?;
792
793 if let Some(controller) = controller {
794 controller.record_head_rtt(t0.elapsed());
795 }
796
797 Ok(out)
798 }
799 })
800 .await
801 }
802}
803
804fn is_suffix_range_rejection(err: &PolarsError) -> bool {
807 let msg = err.to_string();
808
809 msg.contains("does not support suffix range requests")
810 || msg.contains("Received non-partial response")
811 || msg.contains("416 Range Not Satisfiable")
812}
813
814#[derive(Clone, Copy, Debug)]
816enum SizedRange {
817 Prefix(usize),
819 Suffix(usize),
821}
822
823impl SizedRange {
824 fn len(self) -> usize {
826 match self {
827 Self::Prefix(n) | Self::Suffix(n) => n,
828 }
829 }
830}
831
832impl From<SizedRange> for object_store::GetRange {
833 fn from(value: SizedRange) -> Self {
834 match value {
835 SizedRange::Prefix(n) => Self::Bounded(0..n as u64),
836 SizedRange::Suffix(n) => Self::Suffix(n as u64),
837 }
838 }
839}
840
841fn is_prefix_range_rejection(err: &PolarsError) -> bool {
848 std::iter::successors(Some(err as &dyn std::error::Error), |e| e.source()).any(|e| {
849 let msg = e.to_string();
850
851 msg.contains("416 Range Not Satisfiable")
852 || msg.contains("Wanted range starting at")
853 || msg.contains("Received non-partial response")
854 || msg.contains("Range request not supported")
856 })
857}
858
859fn split_range(
862 range: Range<usize>,
863 chunk_size: Option<usize>,
864) -> impl ExactSizeIterator<Item = Range<usize>> {
865 let chunk_size = chunk_size.unwrap_or_else(get_download_chunk_size);
866
867 let n_parts = [
869 (range.len().div_ceil(chunk_size)).max(1),
870 (range.len() / chunk_size).max(1),
871 ]
872 .into_iter()
873 .min_by_key(|x| (range.len() / *x).abs_diff(chunk_size))
874 .unwrap();
875
876 let chunk_size = (range.len() / n_parts).max(1);
877
878 assert_eq!(n_parts, (range.len() / chunk_size).max(1));
879 let bytes_rem = range.len() % chunk_size;
880
881 (0..n_parts).map(move |part_no| {
882 let (start, end) = if part_no == 0 {
883 let end = range.start + chunk_size + bytes_rem;
885 let end = if end > range.end { range.end } else { end };
886 (range.start, end)
887 } else {
888 let start = bytes_rem + range.start + part_no * chunk_size;
889 (start, start + chunk_size)
890 };
891
892 start..end
893 })
894}
895
896fn merge_ranges(
907 ranges: &[Range<usize>],
908 chunk_size: Option<usize>,
909) -> impl Iterator<Item = (Range<usize>, usize)> + '_ {
910 let chunk_size = chunk_size.unwrap_or_else(get_download_chunk_size);
911
912 let mut current_merged_range = ranges.first().map_or(0..0, Clone::clone);
913 let mut current_n_bytes = current_merged_range.len();
915
916 (0..ranges.len())
917 .filter_map(move |current_idx| {
918 let current_idx = 1 + current_idx;
919
920 if current_idx == ranges.len() {
921 Some((current_merged_range.clone(), current_idx))
923 } else {
924 let range = ranges[current_idx].clone();
925
926 let new_merged = current_merged_range.start.min(range.start)
927 ..current_merged_range.end.max(range.end);
928
929 let (distance, is_overlapping) = {
940 let l = current_merged_range.end.min(range.end);
941 let r = current_merged_range.start.max(range.start);
942
943 (r.abs_diff(l), r < l)
944 };
945
946 let should_merge = is_overlapping || {
947 let leq_current_len_dist_to_chunk_size = new_merged.len().abs_diff(chunk_size)
948 <= current_merged_range.len().abs_diff(chunk_size);
949 let gap_tolerance =
950 (current_n_bytes.max(range.len()) / 8).clamp(1024 * 1024, 8 * 1024 * 1024);
951
952 leq_current_len_dist_to_chunk_size && distance <= gap_tolerance
953 };
954
955 if should_merge {
956 current_merged_range = new_merged;
958 current_n_bytes += if is_overlapping {
959 range.len() - distance
960 } else {
961 range.len()
962 };
963 None
964 } else {
965 let out = (current_merged_range.clone(), current_idx);
966 current_merged_range = range;
967 current_n_bytes = current_merged_range.len();
968 Some(out)
969 }
970 }
971 })
972 .flat_map(move |x| {
973 let (range, end) = x;
975 let split = split_range(range, Some(chunk_size));
976 let len = split.len();
977
978 split
979 .enumerate()
980 .map(move |(i, range)| (range, if 1 + i == len { end } else { 0 }))
981 })
982}
983
984#[cfg(test)]
985mod tests {
986
987 #[test]
988 fn test_split_range() {
989 use super::{get_download_chunk_size, split_range};
990
991 let chunk_size = get_download_chunk_size();
992
993 assert_eq!(chunk_size, 64 * 1024 * 1024);
994
995 #[allow(clippy::single_range_in_vec_init)]
996 {
997 assert_eq!(split_range(0..0, None).collect::<Vec<_>>(), [0..0]);
999 assert_eq!(split_range(3..3, None).collect::<Vec<_>>(), [3..3]);
1000 }
1001
1002 let n = 4 * chunk_size / 3;
1009
1010 #[allow(clippy::single_range_in_vec_init)]
1011 {
1012 assert_eq!(split_range(0..n, None).collect::<Vec<_>>(), [0..89478485]);
1013 }
1014
1015 assert_eq!(
1016 split_range(0..n + 1, None).collect::<Vec<_>>(),
1017 [0..44739243, 44739243..89478486]
1018 );
1019
1020 let n = 12 * chunk_size / 5;
1027
1028 assert_eq!(
1029 split_range(0..n, None).collect::<Vec<_>>(),
1030 [0..80530637, 80530637..161061273]
1031 );
1032
1033 assert_eq!(
1034 split_range(0..n + 1, None).collect::<Vec<_>>(),
1035 [0..53687092, 53687092..107374183, 107374183..161061274]
1036 );
1037 }
1038
1039 #[test]
1040 fn test_merge_ranges() {
1041 use super::{get_download_chunk_size, merge_ranges};
1042
1043 let chunk_size = get_download_chunk_size();
1044
1045 assert_eq!(chunk_size, 64 * 1024 * 1024);
1046
1047 assert_eq!(merge_ranges(&[], None).collect::<Vec<_>>(), []);
1049
1050 assert_eq!(
1054 merge_ranges(&[0..1, 1..127 * 1024 * 1024], None).collect::<Vec<_>>(),
1055 [(0..66584576, 0), (66584576..133169152, 2)]
1056 );
1057
1058 assert_eq!(
1060 merge_ranges(&[0..1, 1024 * 1024 + 1..1024 * 1024 + 2], None).collect::<Vec<_>>(),
1061 [(0..1048578, 2)]
1062 );
1063
1064 assert_eq!(
1066 merge_ranges(&[0..1, 1024 * 1024 + 2..1024 * 1024 + 3], None).collect::<Vec<_>>(),
1067 [(0..1, 1), (1048578..1048579, 2)]
1068 );
1069
1070 assert_eq!(
1072 merge_ranges(&[0..8, 10..11], None).collect::<Vec<_>>(),
1073 [(0..11, 2)]
1074 );
1075
1076 assert_eq!(
1078 merge_ranges(&[0..1, 3..11], None).collect::<Vec<_>>(),
1079 [(0..11, 2)]
1080 );
1081
1082 assert_eq!(
1084 merge_ranges(
1085 &[0..80 * 1024 * 1024, 10 * 1024 * 1024..70 * 1024 * 1024],
1086 None
1087 )
1088 .collect::<Vec<_>>(),
1089 [(0..80 * 1024 * 1024, 2)]
1090 );
1091 }
1092}