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 if config::verbose() {
234 eprintln!(
235 "[PolarsObjectStore]: got error: {}, will rebuild store and retry",
236 orig_err
237 );
238 }
239
240 let store = self
241 .rebuild_inner(&store)
242 .await
243 .map_err(|e| e.wrap_msg(|e| format!("{e}; original error: {orig_err}")))?;
244
245 func(Cow::Owned(store)).await.map_err(|e| {
246 let e: PolarsError = self.error_context().attach_err_info(e).into();
247
248 if self.inner.builder.is_azure()
249 && std::env::var("POLARS_AUTO_USE_AZURE_STORAGE_ACCOUNT_KEY").as_deref()
250 != Ok("1")
251 {
252 e.wrap_msg(|e| {
255 format!(
256 "{e}; note: if you are using Python, consider setting \
257POLARS_AUTO_USE_AZURE_STORAGE_ACCOUNT_KEY=1 if you would like polars to try to retrieve \
258and use the storage account keys from Azure CLI to authenticate"
259 )
260 })
261 } else {
262 e
263 }
264 })
265 }
266
267 pub(crate) fn suffix_range_supported(&self) -> bool {
268 self.inner.suffix_range_supported.load()
269 }
270
271 pub(crate) fn disable_suffix_range(&self) {
272 self.inner.suffix_range_supported.store(false);
273 }
274
275 pub fn error_context(&self) -> ObjectStoreErrorContext {
276 ObjectStoreErrorContext::new(self.inner.builder.path().clone())
277 }
278 }
279}
280
281#[derive(Clone)]
282pub struct ObjectStoreErrorContext {
283 path: PlRefPath,
284}
285
286impl ObjectStoreErrorContext {
287 pub fn new(path: PlRefPath) -> Self {
288 Self { path }
289 }
290
291 pub fn attach_err_info(self, err: object_store::Error) -> PolarsObjectStoreError {
292 let ObjectStoreErrorContext { path } = self;
293
294 PolarsObjectStoreError {
295 base_url: path,
296 source: err,
297 }
298 }
299}
300
301pub use inner::PolarsObjectStore;
302
303pub type ObjectStorePath = object_store::path::Path;
304
305impl PolarsObjectStore {
306 pub fn build_buffered_ranges_stream<'a, T: Iterator<Item = Range<usize>>>(
307 &'a self,
308 path: &'a Path,
309 ranges: T,
310 strategy: ConcurrencyStrategy,
311 ) -> impl Stream<Item = PolarsResult<Buffer<u8>>> + use<'a, T> {
312 let controller = match strategy {
313 ConcurrencyStrategy::BytesBased => Some(self.get_or_init_concurrency().clone()),
314 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::Legacy => None,
315 };
316
317 let n_buffered = match strategy {
318 ConcurrencyStrategy::BytesBased => 4096,
324 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::Legacy => {
325 get_concurrency_limit() as usize
326 },
327 };
328
329 futures::stream::iter(ranges.map(move |range| {
330 let controller = controller.clone();
331 async move {
332 if range.is_empty() {
333 return Ok(Buffer::new());
334 }
335 let bytes_req = range.len() as u64;
336
337 let _permit = match &controller {
339 Some(controller) => Some(controller.acquire(bytes_req).await),
340 None => None,
341 };
342
343 let (out, ttfb) = self
344 .io_metrics()
345 .record_io_read(
346 bytes_req,
347 self.exec_with_rebuild_retry_on_err(|s| async move {
348 let t0 = Instant::now();
349 let response = s
350 .get_opts(
351 path,
352 object_store::GetOptions {
353 range: Some((range.start as u64..range.end as u64).into()),
354 ..Default::default()
355 },
356 )
357 .await?;
358 let ttfb = t0.elapsed();
359 let out = response.bytes().await?;
360
361 Ok((out, ttfb))
362 }),
363 )
364 .await?;
365
366 if let Some(controller) = &controller {
367 controller.record_io(IoSample {
368 n_bytes: out.len() as u64,
369 ttfb,
370 completion_time: Instant::now(),
371 });
372 }
373
374 Ok(Buffer::from_owner(out))
375 }
376 }))
377 .buffered(n_buffered)
378 }
379
380 pub async fn get_range(
381 &self,
382 path: &Path,
383 range: Range<usize>,
384 config: FetchConfig,
385 ) -> PolarsResult<Buffer<u8>> {
386 if range.is_empty() {
387 return Ok(Buffer::new());
388 }
389
390 let parts = split_range(range.clone(), Some(config.chunk_size));
391
392 match config.strategy {
393 ConcurrencyStrategy::Legacy => self.get_range_legacy(path, range).await,
394 ConcurrencyStrategy::Unbounded | ConcurrencyStrategy::BytesBased => self
395 .build_buffered_ranges_stream(path, parts, config.strategy)
396 .try_collect::<Vec<_>>()
397 .await
398 .map(|parts| {
399 if parts.len() == 1 {
400 return parts.into_iter().next().unwrap();
401 }
402 let mut combined = Vec::with_capacity(range.len());
403 for part in parts {
404 combined.extend_from_slice(&part);
405 }
406 assert_eq!(combined.len(), range.len());
407 Buffer::from_vec(combined)
408 }),
409 }
410 }
411
412 async fn get_range_legacy(&self, path: &Path, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
413 if range.is_empty() {
414 return Ok(Buffer::new());
415 }
416
417 let parts = split_range(range.clone(), None);
418
419 if parts.len() == 1 {
420 let out = tune_with_concurrency_budget(1, move || async move {
421 let bytes = self
422 .io_metrics()
423 .record_io_read(
424 range.len() as u64,
425 self.exec_with_rebuild_retry_on_err(|s| async move {
426 s.get_range(path, range.start as u64..range.end as u64)
427 .await
428 }),
429 )
430 .await?;
431
432 PolarsResult::Ok(Buffer::from_owner(bytes))
433 })
434 .await?;
435
436 Ok(out)
437 } else {
438 let parts = tune_with_concurrency_budget(
439 parts.len().clamp(0, MAX_BUDGET_PER_REQUEST) as u32,
440 || {
441 self.build_buffered_ranges_stream(path, parts, ConcurrencyStrategy::Legacy)
442 .try_collect::<Vec<Buffer<u8>>>()
443 },
444 )
445 .await?;
446
447 let mut combined = Vec::with_capacity(range.len());
448
449 for part in parts {
450 combined.extend_from_slice(&part)
451 }
452
453 assert_eq!(combined.len(), range.len());
454
455 PolarsResult::Ok(Buffer::from_vec(combined))
456 }
457 }
458
459 pub async fn get_ranges_sort(
460 &self,
461 path: &Path,
462 ranges: &mut [Range<usize>],
463 config: FetchConfig,
464 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
465 if ranges.is_empty() {
466 return Ok(Default::default());
467 }
468
469 ranges.sort_unstable_by_key(|x| x.start);
470
471 let ranges_len = ranges.len();
472 let (merged_ranges, merged_ends): (Vec<_>, Vec<_>) =
473 merge_ranges(ranges, Some(config.chunk_size)).unzip();
474
475 let mut out = PlHashMap::with_capacity(ranges_len);
476
477 let mut stream =
479 self.build_buffered_ranges_stream(path, merged_ranges.iter().cloned(), config.strategy);
480
481 let mut current_offset = 0;
482 let mut ends_iter = merged_ends.iter();
483 let mut splitted_parts: Vec<Buffer<u8>> = vec![];
484
485 while let Some(bytes) = stream.try_next().await? {
486 let end = *ends_iter.next().unwrap();
487
488 if end == 0 {
489 splitted_parts.push(bytes);
490 continue;
491 }
492
493 let full_range = ranges[current_offset..end]
494 .iter()
495 .cloned()
496 .reduce(|l, r| l.start.min(r.start)..l.end.max(r.end))
497 .unwrap();
498
499 let bytes = if splitted_parts.is_empty() {
500 bytes
501 } else {
502 let mut out = Vec::with_capacity(full_range.len());
503 for x in splitted_parts.drain(..) {
504 out.extend_from_slice(&x);
505 }
506 out.extend_from_slice(&bytes);
507 Buffer::from(out)
508 };
509
510 assert_eq!(bytes.len(), full_range.len());
511
512 for range in &ranges[current_offset..end] {
513 let slice = bytes
514 .clone()
515 .sliced(range.start - full_range.start..range.end - full_range.start);
516
517 match out.raw_entry_mut().from_key(&range.start) {
518 RawEntryMut::Vacant(slot) => {
519 slot.insert(range.start, slice);
520 },
521 RawEntryMut::Occupied(mut slot) => {
522 if slot.get_mut().len() < slice.len() {
523 *slot.get_mut() = slice;
524 }
525 },
526 }
527 }
528
529 current_offset = end;
530 }
531
532 assert!(splitted_parts.is_empty());
533
534 Ok(out)
535 }
536
537 pub async fn download(&self, path: &Path, file: &mut tokio::fs::File) -> PolarsResult<()> {
539 let size = self.head(path, ConcurrencyStrategy::Unbounded).await?.size;
540 let parts = split_range(0..size as usize, None);
541
542 tune_with_concurrency_budget(
545 parts.len().clamp(0, MAX_BUDGET_PER_REQUEST) as u32,
546 || async {
547 let mut stream =
548 self.build_buffered_ranges_stream(path, parts, ConcurrencyStrategy::Unbounded);
549 let mut len = 0;
550 while let Some(bytes) = stream.try_next().await? {
551 len += bytes.len();
552 file.write_all(&bytes).await?;
553 }
554
555 assert_eq!(len, size as usize);
556
557 PolarsResult::Ok(pl_async::Size::from(len as u64))
558 },
559 )
560 .await?;
561
562 file.sync_all().await.map_err(PolarsError::from)?;
565
566 Ok(())
567 }
568
569 pub async fn get_suffix(
575 &self,
576 path: &Path,
577 n: usize,
578 config: FetchConfig,
579 ) -> PolarsResult<(Buffer<u8>, usize)> {
580 let mut suffix_rejected = false;
581
582 if n > 0 && self.suffix_range_supported() {
583 match self.try_get_suffix(path, n, config.strategy).await {
584 Ok(v) => return Ok(v),
585 Err(e) if is_suffix_range_rejection(&e) => {
586 suffix_rejected = true;
587
588 if config::verbose() {
589 eprintln!(
590 "[PolarsObjectStore]: suffix range request rejected ({e}), \
591 falling back to head + range request"
592 );
593 }
594 },
595 Err(e) => return Err(e),
596 }
597 }
598
599 let size = self.head(path, config.strategy).await?.size as usize;
600
601 if suffix_rejected && size > 0 {
603 self.disable_suffix_range();
604 }
605
606 let bytes = self
607 .get_range(path, size.saturating_sub(n)..size, config)
608 .await?;
609
610 Ok((bytes, size))
611 }
612
613 async fn try_get_suffix(
614 &self,
615 path: &Path,
616 n: usize,
617 strategy: ConcurrencyStrategy,
618 ) -> PolarsResult<(Buffer<u8>, usize)> {
619 match strategy {
620 ConcurrencyStrategy::BytesBased => {
621 let controller = self.get_or_init_concurrency();
622 let _permit = controller.acquire(n as u64).await;
623 self.try_get_suffix_inner(path, n, Some(&**controller))
624 .await
625 },
626 ConcurrencyStrategy::Legacy => {
627 with_concurrency_budget(1, || self.try_get_suffix_inner(path, n, None)).await
628 },
629 ConcurrencyStrategy::Unbounded => self.try_get_suffix_inner(path, n, None).await,
630 }
631 }
632
633 async fn try_get_suffix_inner(
634 &self,
635 path: &Path,
636 n: usize,
637 controller: Option<&ConcurrencyController>,
638 ) -> PolarsResult<(Buffer<u8>, usize)> {
639 let metrics = self.io_metrics();
640
641 metrics.add_bytes_requested(n as u64);
642
643 let io_session = metrics.start_io_session();
644
645 let out = self
646 .exec_with_rebuild_retry_on_err(|s| async move {
647 let t0 = Instant::now();
648 let response = s
649 .get_opts(
650 path,
651 object_store::GetOptions {
652 range: Some(object_store::GetRange::Suffix(n as u64)),
653 ..Default::default()
654 },
655 )
656 .await?;
657 let ttfb = t0.elapsed();
658 let size = response.meta.size as usize;
660 let bytes = response.bytes().await?;
661
662 if let Some(controller) = controller {
663 controller.record_io(IoSample {
664 n_bytes: bytes.len() as u64,
665 ttfb,
666 completion_time: Instant::now(),
667 });
668 }
669
670 Ok((Buffer::from_owner(bytes), size))
671 })
672 .await;
673
674 drop(io_session);
675
676 let (bytes, size) = out?;
677
678 metrics.add_bytes_received(bytes.len() as u64);
680
681 Ok((bytes, size))
682 }
683
684 pub async fn head(
686 &self,
687 path: &Path,
688 strategy: ConcurrencyStrategy,
689 ) -> PolarsResult<ObjectMeta> {
690 match strategy {
691 ConcurrencyStrategy::BytesBased => {
692 let controller = self.get_or_init_concurrency();
693 let _permit = controller.acquire(0).await;
694 self.head_inner(path, Some(&**controller)).await
695 },
696 ConcurrencyStrategy::Legacy => {
697 with_concurrency_budget(1, || self.head_inner(path, None)).await
698 },
699 ConcurrencyStrategy::Unbounded => self.head_inner(path, None).await,
700 }
701 }
702
703 async fn head_inner(
704 &self,
705 path: &Path,
706 controller: Option<&ConcurrencyController>,
707 ) -> PolarsResult<ObjectMeta> {
708 self.exec_with_rebuild_retry_on_err(|s| {
709 async move {
710 let t0 = Instant::now();
711 let head_result = self.io_metrics().record_io_read(0, s.head(path)).await;
712
713 if head_result.is_err() {
714 let t0 = Instant::now();
715
716 let get_range_0_1_result = self
719 .io_metrics()
720 .record_io_read(
721 0,
722 s.get_opts(
723 path,
724 object_store::GetOptions {
725 range: Some((0..1).into()),
726 ..Default::default()
727 },
728 ),
729 )
730 .await;
731
732 if let Ok(v) = get_range_0_1_result {
733 if let Some(controller) = controller {
734 controller.record_head_rtt(t0.elapsed());
735 }
736 return Ok(v.meta);
737 }
738 }
739
740 let out = head_result?;
741
742 if let Some(controller) = controller {
743 controller.record_head_rtt(t0.elapsed());
744 }
745
746 Ok(out)
747 }
748 })
749 .await
750 }
751}
752
753fn is_suffix_range_rejection(err: &PolarsError) -> bool {
756 let msg = err.to_string();
757
758 msg.contains("does not support suffix range requests")
759 || msg.contains("Received non-partial response")
760 || msg.contains("416 Range Not Satisfiable")
761}
762
763fn split_range(
766 range: Range<usize>,
767 chunk_size: Option<usize>,
768) -> impl ExactSizeIterator<Item = Range<usize>> {
769 let chunk_size = chunk_size.unwrap_or_else(get_download_chunk_size);
770
771 let n_parts = [
773 (range.len().div_ceil(chunk_size)).max(1),
774 (range.len() / chunk_size).max(1),
775 ]
776 .into_iter()
777 .min_by_key(|x| (range.len() / *x).abs_diff(chunk_size))
778 .unwrap();
779
780 let chunk_size = (range.len() / n_parts).max(1);
781
782 assert_eq!(n_parts, (range.len() / chunk_size).max(1));
783 let bytes_rem = range.len() % chunk_size;
784
785 (0..n_parts).map(move |part_no| {
786 let (start, end) = if part_no == 0 {
787 let end = range.start + chunk_size + bytes_rem;
789 let end = if end > range.end { range.end } else { end };
790 (range.start, end)
791 } else {
792 let start = bytes_rem + range.start + part_no * chunk_size;
793 (start, start + chunk_size)
794 };
795
796 start..end
797 })
798}
799
800fn merge_ranges(
811 ranges: &[Range<usize>],
812 chunk_size: Option<usize>,
813) -> impl Iterator<Item = (Range<usize>, usize)> + '_ {
814 let chunk_size = chunk_size.unwrap_or_else(get_download_chunk_size);
815
816 let mut current_merged_range = ranges.first().map_or(0..0, Clone::clone);
817 let mut current_n_bytes = current_merged_range.len();
819
820 (0..ranges.len())
821 .filter_map(move |current_idx| {
822 let current_idx = 1 + current_idx;
823
824 if current_idx == ranges.len() {
825 Some((current_merged_range.clone(), current_idx))
827 } else {
828 let range = ranges[current_idx].clone();
829
830 let new_merged = current_merged_range.start.min(range.start)
831 ..current_merged_range.end.max(range.end);
832
833 let (distance, is_overlapping) = {
844 let l = current_merged_range.end.min(range.end);
845 let r = current_merged_range.start.max(range.start);
846
847 (r.abs_diff(l), r < l)
848 };
849
850 let should_merge = is_overlapping || {
851 let leq_current_len_dist_to_chunk_size = new_merged.len().abs_diff(chunk_size)
852 <= current_merged_range.len().abs_diff(chunk_size);
853 let gap_tolerance =
854 (current_n_bytes.max(range.len()) / 8).clamp(1024 * 1024, 8 * 1024 * 1024);
855
856 leq_current_len_dist_to_chunk_size && distance <= gap_tolerance
857 };
858
859 if should_merge {
860 current_merged_range = new_merged;
862 current_n_bytes += if is_overlapping {
863 range.len() - distance
864 } else {
865 range.len()
866 };
867 None
868 } else {
869 let out = (current_merged_range.clone(), current_idx);
870 current_merged_range = range;
871 current_n_bytes = current_merged_range.len();
872 Some(out)
873 }
874 }
875 })
876 .flat_map(move |x| {
877 let (range, end) = x;
879 let split = split_range(range, Some(chunk_size));
880 let len = split.len();
881
882 split
883 .enumerate()
884 .map(move |(i, range)| (range, if 1 + i == len { end } else { 0 }))
885 })
886}
887
888#[cfg(test)]
889mod tests {
890
891 #[test]
892 fn test_split_range() {
893 use super::{get_download_chunk_size, split_range};
894
895 let chunk_size = get_download_chunk_size();
896
897 assert_eq!(chunk_size, 64 * 1024 * 1024);
898
899 #[allow(clippy::single_range_in_vec_init)]
900 {
901 assert_eq!(split_range(0..0, None).collect::<Vec<_>>(), [0..0]);
903 assert_eq!(split_range(3..3, None).collect::<Vec<_>>(), [3..3]);
904 }
905
906 let n = 4 * chunk_size / 3;
913
914 #[allow(clippy::single_range_in_vec_init)]
915 {
916 assert_eq!(split_range(0..n, None).collect::<Vec<_>>(), [0..89478485]);
917 }
918
919 assert_eq!(
920 split_range(0..n + 1, None).collect::<Vec<_>>(),
921 [0..44739243, 44739243..89478486]
922 );
923
924 let n = 12 * chunk_size / 5;
931
932 assert_eq!(
933 split_range(0..n, None).collect::<Vec<_>>(),
934 [0..80530637, 80530637..161061273]
935 );
936
937 assert_eq!(
938 split_range(0..n + 1, None).collect::<Vec<_>>(),
939 [0..53687092, 53687092..107374183, 107374183..161061274]
940 );
941 }
942
943 #[test]
944 fn test_merge_ranges() {
945 use super::{get_download_chunk_size, merge_ranges};
946
947 let chunk_size = get_download_chunk_size();
948
949 assert_eq!(chunk_size, 64 * 1024 * 1024);
950
951 assert_eq!(merge_ranges(&[], None).collect::<Vec<_>>(), []);
953
954 assert_eq!(
958 merge_ranges(&[0..1, 1..127 * 1024 * 1024], None).collect::<Vec<_>>(),
959 [(0..66584576, 0), (66584576..133169152, 2)]
960 );
961
962 assert_eq!(
964 merge_ranges(&[0..1, 1024 * 1024 + 1..1024 * 1024 + 2], None).collect::<Vec<_>>(),
965 [(0..1048578, 2)]
966 );
967
968 assert_eq!(
970 merge_ranges(&[0..1, 1024 * 1024 + 2..1024 * 1024 + 3], None).collect::<Vec<_>>(),
971 [(0..1, 1), (1048578..1048579, 2)]
972 );
973
974 assert_eq!(
976 merge_ranges(&[0..8, 10..11], None).collect::<Vec<_>>(),
977 [(0..11, 2)]
978 );
979
980 assert_eq!(
982 merge_ranges(&[0..1, 3..11], None).collect::<Vec<_>>(),
983 [(0..11, 2)]
984 );
985
986 assert_eq!(
988 merge_ranges(
989 &[0..80 * 1024 * 1024, 10 * 1024 * 1024..70 * 1024 * 1024],
990 None
991 )
992 .collect::<Vec<_>>(),
993 [(0..80 * 1024 * 1024, 2)]
994 );
995 }
996}