1use std::ops::Range;
2#[cfg(target_os = "linux")]
3use std::os::fd::AsRawFd;
4use std::path::Path;
5use std::sync::Arc;
6
7use dio_align::DioAlign;
8use futures::{StreamExt, TryStreamExt};
9use polars_buffer::Buffer;
10use polars_config::FileAdvice;
11use polars_core::prelude::PlHashMap;
12use polars_core::runtime::ASYNC;
13use polars_error::{PolarsResult, feature_gated, polars_bail, polars_err};
14use polars_utils::aliases::InitHashMaps;
15use polars_utils::io::_limit_path_len_io_err;
16use polars_utils::mmap::MMapSemaphore;
17use polars_utils::pl_path::PlRefPath;
18use tokio::sync::Semaphore;
19
20use crate::cloud::concurrency_config::{ConcurrencyStrategy, FetchConfig};
21use crate::cloud::options::CloudOptions;
22#[cfg(feature = "cloud")]
23use crate::cloud::{
24 CloudLocation, ObjectStorePath, PolarsObjectStore, build_object_store, object_path_from_str,
25};
26use crate::metrics::{IOMetrics, OptIOMetrics};
27
28pub mod dio_align;
29#[cfg(target_os = "linux")]
30mod direct_io;
31#[allow(async_fn_in_trait)]
32pub trait ByteSource: Send + Sync {
33 async fn get_size(&self) -> PolarsResult<usize>;
34 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>>;
37 async fn get_ranges(
39 &self,
40 ranges: &mut [Range<usize>],
41 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>>;
42}
43
44pub struct BufferByteSource(pub Buffer<u8>);
46
47impl BufferByteSource {
48 async fn try_new_mmap_from_path(
49 path: &Path,
50 _cloud_options: Option<&CloudOptions>,
51 ) -> PolarsResult<Self> {
52 let file = Arc::new(
53 tokio::fs::File::open(path)
54 .await
55 .map_err(|err| _limit_path_len_io_err(path, err))?
56 .into_std()
57 .await,
58 );
59
60 Ok(Self(Buffer::from_owner(MMapSemaphore::new_from_file(
61 &file,
62 )?)))
63 }
64}
65
66impl ByteSource for BufferByteSource {
67 async fn get_size(&self) -> PolarsResult<usize> {
68 Ok(self.0.as_ref().len())
69 }
70
71 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
72 let out = self.0.clone().sliced(range);
73 Ok(out)
74 }
75
76 async fn get_ranges(
77 &self,
78 ranges: &mut [Range<usize>],
79 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
80 Ok(ranges
81 .iter()
82 .map(|x| (x.start, self.0.clone().sliced(x.clone())))
83 .collect())
84 }
85}
86
87pub struct FileByteSource {
89 file: Arc<std::fs::File>,
90 o_direct_align: Option<DioAlign>,
92 concurrency: usize,
94 permits: Arc<Semaphore>,
95 size: u64,
97 io_metrics: OptIOMetrics,
98}
99
100#[derive(Clone)]
101pub struct FileReadContext {
102 pub enable_o_direct: bool,
103 pub concurrency: usize,
104 pub permits: Arc<Semaphore>,
105 pub advice: FileAdvice,
107}
108
109impl std::fmt::Debug for FileReadContext {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("FileReadContext")
112 .field("available_permits", &self.permits.available_permits())
113 .field("enable_odirect", &self.enable_o_direct)
114 .field("concurrency", &self.concurrency)
115 .field("advice", &self.advice)
116 .finish()
117 }
118}
119
120#[cfg(target_os = "linux")]
121fn _fadvise(file: &std::fs::File, advice: FileAdvice) {
122 let advice = match advice {
123 FileAdvice::Normal => return,
125 FileAdvice::Sequential => libc::POSIX_FADV_SEQUENTIAL,
126 FileAdvice::Random => libc::POSIX_FADV_RANDOM,
127 FileAdvice::WillNeed => libc::POSIX_FADV_WILLNEED,
128 };
129
130 unsafe {
131 libc::posix_fadvise(file.as_raw_fd(), 0, 0, advice);
132 }
133}
134
135#[cfg(unix)]
136pub(crate) fn pread_exact(
137 file: &std::fs::File,
138 buf: &mut [u8],
139 offset: u64,
140) -> std::io::Result<()> {
141 use std::os::unix::fs::FileExt;
142 file.read_exact_at(buf, offset)
143}
144
145#[cfg(windows)]
146fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
147 use std::os::windows::fs::FileExt;
148 let mut filled = 0;
149 while filled < buf.len() {
150 match file.seek_read(&mut buf[filled..], offset + filled as u64)? {
151 0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
152 n => filled += n,
153 }
154 }
155 Ok(())
156}
157
158fn read_buffered(file: &std::fs::File, offset: u64, len: usize) -> PolarsResult<Buffer<u8>> {
159 let mut buf = vec![0u8; len];
160 pread_exact(file, &mut buf, offset)?;
161 Ok(Buffer::from(buf))
162}
163
164#[cfg(target_os = "linux")]
167fn read_at(
168 file: &std::fs::File,
169 align: Option<DioAlign>,
170 offset: u64,
171 len: usize,
172 size: u64,
173) -> PolarsResult<Buffer<u8>> {
174 match align {
175 Some(align) => direct_io::read_aligned(file, align, offset, len, size),
176 None => read_buffered(file, offset, len),
177 }
178}
179
180#[cfg(not(target_os = "linux"))]
181fn read_at(
182 file: &std::fs::File,
183 _align: Option<DioAlign>,
184 offset: u64,
185 len: usize,
186 _size: u64,
187) -> PolarsResult<Buffer<u8>> {
188 read_buffered(file, offset, len)
189}
190
191impl FileByteSource {
192 async fn try_new_from_path(
193 path: PlRefPath,
194 read_context: FileReadContext,
195 io_metrics: Option<Arc<IOMetrics>>,
196 ) -> PolarsResult<Self> {
197 ASYNC
199 .spawn_blocking(move || Self::open_blocking(path, read_context, io_metrics))
200 .await
201 .expect("blocking task panicked")
202 }
203
204 fn open_blocking(
205 path: PlRefPath,
206 read_context: FileReadContext,
207 io_metrics: Option<Arc<IOMetrics>>,
208 ) -> PolarsResult<Self> {
209 let path = path.as_std_path();
210 let enable_o_direct = read_context.enable_o_direct;
211
212 let file = {
213 #[cfg(target_os = "linux")]
214 let f = if enable_o_direct {
215 direct_io::open_o_direct(path).or_else(|e| match e.raw_os_error() {
216 Some(libc::EINVAL) => std::fs::File::open(path),
218 _ => Err(e),
219 })
220 } else {
221 std::fs::File::open(path)
222 };
223 #[cfg(not(target_os = "linux"))]
224 let f = std::fs::File::open(path);
225
226 f.map_err(|e| _limit_path_len_io_err(path, e))?
227 };
228
229 let file = Arc::new(file);
230
231 #[cfg(target_os = "linux")]
232 let o_direct_align =
233 direct_io::probe_or_disable(&file).map_err(|e| _limit_path_len_io_err(path, e))?;
234
235 #[cfg(not(target_os = "linux"))]
236 let o_direct_align: Option<DioAlign> = None;
237
238 #[cfg(target_os = "linux")]
239 if o_direct_align.is_none() {
240 _fadvise(&file, read_context.advice);
242 }
243
244 let concurrency = read_context.concurrency;
245 let permits = read_context.permits;
246
247 let size = file
248 .metadata()
249 .map_err(|e| _limit_path_len_io_err(path, e))?
250 .len();
251
252 if polars_config::config().verbose() {
253 let name = if polars_config::config().verbose_sensitive() {
254 path.display().to_string()
255 } else {
256 "<file>".to_string()
257 };
258 match (enable_o_direct, o_direct_align) {
259 (true, Some(a)) => eprintln!(
260 "[FileByteSource]: {name}: direct IO active, \
261 alignment offset: {}, memory: {}",
262 a.offset, a.memory
263 ),
264 (true, None) => eprintln!(
265 "[FileByteSource]: {name}: direct IO requested but not active, \
266 using buffered reads"
267 ),
268 (false, _) => {},
269 }
270 }
271
272 Ok(FileByteSource {
273 file,
274 o_direct_align,
275 concurrency,
276 permits,
277 size,
278 io_metrics: OptIOMetrics(io_metrics),
279 })
280 }
281
282 pub fn try_new_from_std(
283 file: std::fs::File,
284 read_context: FileReadContext,
285 io_metrics: Option<Arc<IOMetrics>>,
286 ) -> PolarsResult<Self> {
287 let size = file.metadata()?.len();
288
289 #[cfg(target_os = "linux")]
290 let o_direct_align = direct_io::probe_or_disable(&file)?;
291
292 #[cfg(not(target_os = "linux"))]
293 let o_direct_align: Option<DioAlign> = None;
294
295 #[cfg(target_os = "linux")]
296 if o_direct_align.is_none() {
297 _fadvise(&file, read_context.advice);
298 }
299
300 let concurrency = read_context.concurrency;
301 let permits = read_context.permits;
302
303 let enable_o_direct = read_context.enable_o_direct;
305
306 if polars_config::config().verbose() {
307 match (enable_o_direct, o_direct_align) {
308 (true, Some(a)) => eprintln!(
309 "[FileByteSource]: direct IO active, alignment offset: {}, memory: {}",
310 a.offset, a.memory
311 ),
312 (true, None) => eprintln!(
313 "[FileByteSource]: direct IO requested but not active, using buffered reads",
314 ),
315 (false, _) => {},
316 }
317 }
318
319 Ok(Self {
320 file: Arc::new(file),
321 o_direct_align,
322 concurrency,
323 permits,
324 size,
325 io_metrics: OptIOMetrics(io_metrics),
326 })
327 }
328
329 pub fn set_io_metrics(&mut self, io_metrics: Option<Arc<IOMetrics>>) -> &mut Self {
330 self.io_metrics = OptIOMetrics(io_metrics);
331 self
332 }
333
334 pub fn io_metrics(&self) -> &OptIOMetrics {
335 &self.io_metrics
336 }
337}
338
339impl ByteSource for FileByteSource {
340 async fn get_size(&self) -> PolarsResult<usize> {
341 usize::try_from(self.size)
342 .map_err(|_| polars_err!(ComputeError: "file size {} does not fit in usize", self.size))
343 }
344
345 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
346 assert!(range.end as u64 <= self.size);
347
348 let file = self.file.clone();
349 let offset = range.start as u64;
350 let len = range.len();
351 let size = self.size;
352 let o_direct = self.o_direct_align;
353
354 let permit = self.permits.clone().acquire_owned().await.unwrap();
355
356 self.io_metrics()
357 .record_io_read(len as u64, async move {
358 ASYNC
359 .spawn_blocking(move || {
360 let _permit = permit;
361 read_at(&file, o_direct, offset, len, size)
362 })
363 .await
364 })
365 .await
366 .expect("blocking task panicked")
367 }
368
369 async fn get_ranges(
370 &self,
371 ranges: &mut [Range<usize>],
372 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
373 if let [range] = ranges {
374 let mut out = PlHashMap::with_capacity(1);
375 out.insert(range.start, self.get_range(range.clone()).await?);
376 return Ok(out);
377 }
378
379 ranges.sort_unstable_by_key(|r| r.start);
380
381 let mut spans: Vec<Range<usize>> = Vec::with_capacity(ranges.len());
382
383 const MAX_SPAN: usize = 8 << 20;
385 const MAX_GAP: usize = 4096;
387
388 for r in ranges.iter() {
389 match spans.last_mut() {
390 Some(last)
391 if r.start.saturating_sub(last.end) <= MAX_GAP
392 && r.end.saturating_sub(last.start) <= MAX_SPAN =>
393 {
394 last.end = last.end.max(r.end)
395 },
396 _ => spans.push(r.clone()),
397 }
398 }
399
400 let mut fetched: Vec<(Range<usize>, Buffer<u8>)> = futures::stream::iter(spans)
401 .map(|span| async move {
402 let buf = self.get_range(span.clone()).await?;
403 PolarsResult::Ok((span, buf))
404 })
405 .buffer_unordered(self.concurrency)
406 .try_collect()
407 .await?;
408
409 fetched.sort_unstable_by_key(|(s, _)| s.start);
411
412 if let Some(w) = ranges.windows(2).find(|w| w[0].start == w[1].start) {
413 polars_bail!(
414 ComputeError:
415 "duplicate range start {} in read request ({:?} and {:?})",
416 w[0].start, w[0], w[1]
417 );
418 }
419
420 let mut out = PlHashMap::with_capacity(ranges.len());
421 for r in ranges.iter() {
422 if r.is_empty() {
424 out.insert(r.start, Buffer::new());
425 continue;
426 }
427
428 let idx = fetched.partition_point(|(s, _)| s.start <= r.start);
431 let (span, buf) = &fetched[idx - 1];
432 debug_assert!(span.start <= r.start && r.end <= span.end);
433
434 let off = r.start - span.start;
435 out.insert(r.start, buf.clone().sliced(off..off + r.len()));
436 }
437
438 debug_assert_eq!(out.len(), ranges.len());
439 Ok(out)
440 }
441}
442
443#[cfg(feature = "cloud")]
444pub struct ObjectStoreByteSource {
445 store: PolarsObjectStore,
446 path: ObjectStorePath,
447 config: FetchConfig,
448}
449
450#[cfg(feature = "cloud")]
451impl ObjectStoreByteSource {
452 async fn try_new_from_path(
453 path: PlRefPath,
454 cloud_options: Option<&CloudOptions>,
455 io_metrics: Option<Arc<IOMetrics>>,
456 config: FetchConfig,
457 ) -> PolarsResult<Self> {
458 let (CloudLocation { prefix, .. }, mut store) =
459 build_object_store(path, cloud_options, false).await?;
460 let path = object_path_from_str(&prefix)?;
461
462 store.set_io_metrics(io_metrics);
463
464 Ok(Self {
465 store,
466 path,
467 config,
468 })
469 }
470
471 #[allow(unused)]
472 fn chunk_size(&self) -> usize {
473 self.config.chunk_size
474 }
475
476 fn concurrency_strategy(&self) -> ConcurrencyStrategy {
477 self.config.strategy
478 }
479}
480
481#[cfg(feature = "cloud")]
482impl ByteSource for ObjectStoreByteSource {
483 async fn get_size(&self) -> PolarsResult<usize> {
484 Ok(self
485 .store
486 .head(&self.path, self.concurrency_strategy())
487 .await?
488 .size as usize)
489 }
490
491 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
492 self.store.get_range(&self.path, range, self.config).await
493 }
494
495 async fn get_ranges(
496 &self,
497 ranges: &mut [Range<usize>],
498 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
499 self.store
500 .get_ranges_sort(&self.path, ranges, self.config)
501 .await
502 }
503}
504
505pub enum DynByteSource {
507 Buffer(BufferByteSource),
508 File(FileByteSource),
509 #[cfg(feature = "cloud")]
510 Cloud(ObjectStoreByteSource),
511}
512
513impl DynByteSource {
514 pub fn variant_name(&self) -> &str {
515 match self {
516 Self::Buffer(_) => "Buffer",
517 Self::File(_) => "File",
518 #[cfg(feature = "cloud")]
519 Self::Cloud(_) => "Cloud",
520 }
521 }
522
523 pub fn is_cloud(&self) -> bool {
524 match self {
525 Self::Buffer(_) => false,
526 Self::File(_) => false,
527 #[cfg(feature = "cloud")]
528 Self::Cloud(_) => true,
529 }
530 }
531
532 pub fn chunk_size(&self) -> Option<usize> {
533 match self {
534 Self::Buffer(_) => None,
535 Self::File(_) => None,
536 #[cfg(feature = "cloud")]
537 Self::Cloud(source) => Some(source.config.chunk_size),
538 }
539 }
540
541 pub fn concurrency_strategy(&self) -> Option<ConcurrencyStrategy> {
542 match self {
543 Self::Buffer(_) => None,
544 Self::File(_) => None,
546 #[cfg(feature = "cloud")]
547 Self::Cloud(source) => Some(source.concurrency_strategy()),
548 }
549 }
550}
551
552impl Default for DynByteSource {
553 fn default() -> Self {
554 Self::Buffer(BufferByteSource(Buffer::new()))
555 }
556}
557
558impl ByteSource for DynByteSource {
559 async fn get_size(&self) -> PolarsResult<usize> {
560 match self {
561 Self::Buffer(v) => v.get_size().await,
562 Self::File(v) => v.get_size().await,
563 #[cfg(feature = "cloud")]
564 Self::Cloud(v) => v.get_size().await,
565 }
566 }
567
568 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
569 match self {
570 Self::Buffer(v) => v.get_range(range).await,
571 Self::File(v) => v.get_range(range).await,
572 #[cfg(feature = "cloud")]
573 Self::Cloud(v) => v.get_range(range).await,
574 }
575 }
576
577 async fn get_ranges(
578 &self,
579 ranges: &mut [Range<usize>],
580 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
581 match self {
582 Self::Buffer(v) => v.get_ranges(ranges).await,
583 Self::File(v) => v.get_ranges(ranges).await,
584 #[cfg(feature = "cloud")]
585 Self::Cloud(v) => v.get_ranges(ranges).await,
586 }
587 }
588}
589
590impl From<BufferByteSource> for DynByteSource {
591 fn from(value: BufferByteSource) -> Self {
592 Self::Buffer(value)
593 }
594}
595
596impl From<FileByteSource> for DynByteSource {
597 fn from(value: FileByteSource) -> Self {
598 Self::File(value)
599 }
600}
601
602#[cfg(feature = "cloud")]
603impl From<ObjectStoreByteSource> for DynByteSource {
604 fn from(value: ObjectStoreByteSource) -> Self {
605 Self::Cloud(value)
606 }
607}
608
609impl From<Buffer<u8>> for DynByteSource {
610 fn from(value: Buffer<u8>) -> Self {
611 Self::Buffer(BufferByteSource(value))
612 }
613}
614
615#[derive(Clone, Debug)]
616pub enum DynByteSourceBuilder {
617 Mmap,
618 FilePread(FileReadContext),
620 ObjectStore(FetchConfig),
622}
623
624impl DynByteSourceBuilder {
625 pub async fn try_build_from_path(
626 &self,
627 path: PlRefPath,
628 cloud_options: Option<&CloudOptions>,
629 io_metrics: Option<Arc<IOMetrics>>,
630 ) -> PolarsResult<DynByteSource> {
631 Ok(match self {
632 Self::Mmap => {
633 BufferByteSource::try_new_mmap_from_path(path.as_std_path(), cloud_options)
634 .await?
635 .into()
636 },
637 Self::FilePread(read_context) => {
638 FileByteSource::try_new_from_path(path, read_context.clone(), io_metrics)
639 .await?
640 .into()
641 },
642 Self::ObjectStore(fetch_config) => feature_gated!("cloud", {
643 ObjectStoreByteSource::try_new_from_path(
644 path,
645 cloud_options,
646 io_metrics,
647 *fetch_config,
648 )
649 .await?
650 .into()
651 }),
652 })
653 }
654
655 pub fn chunk_size(&self) -> Option<usize> {
656 match self {
657 Self::Mmap => None,
658 Self::FilePread(_) => None,
659 Self::ObjectStore(fetch_config) => Some(fetch_config.chunk_size),
660 }
661 }
662
663 pub fn concurrency_strategy(&self) -> Option<&ConcurrencyStrategy> {
664 match self {
665 Self::Mmap => None,
666 Self::FilePread(_) => None,
667 Self::ObjectStore(fetch_config) => Some(&fetch_config.strategy),
668 }
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675
676 const MIB: usize = 1 << 20;
677
678 fn source(dir: &std::path::Path, len: usize) -> (FileByteSource, Vec<u8>) {
680 let path = dir.join("f.bin");
681 let contents: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
682 std::fs::write(&path, &contents).unwrap();
683
684 let read_context = FileReadContext {
685 enable_o_direct: false,
686 concurrency: 4,
687 permits: Arc::new(Semaphore::new(4)),
688 advice: FileAdvice::Normal,
689 };
690 let source = FileByteSource::try_new_from_std(
691 std::fs::File::open(&path).unwrap(),
692 read_context,
693 None,
694 )
695 .unwrap();
696 (source, contents)
697 }
698
699 fn runtime() -> tokio::runtime::Runtime {
700 tokio::runtime::Builder::new_multi_thread()
701 .worker_threads(2)
702 .enable_all()
703 .build()
704 .unwrap()
705 }
706
707 #[test]
708 fn get_ranges_resolves_every_range() {
709 let dir = tempfile::tempdir().unwrap();
710 let (source, contents) = source(dir.path(), 17 * MIB);
711 let rt = runtime();
712
713 let cases: [(&str, &[Range<usize>]); 6] = [
714 ("disjoint", &[0..100, 500..600, 900..1000]),
715 ("unsorted", &[900..1000, 0..100, 500..600]),
716 ("partial overlap", &[0..100, 50..150]),
717 ("contained", &[0..200, 50..100]),
718 ("empty range at a span end", &[0..100, 100..100]),
720 (
723 "overlapping spans",
724 &[
725 0..4096,
726 5_000_000..14_000_000,
727 13_000_000..13_500_000,
728 13_500_000..16_000_000,
729 ],
730 ),
731 ];
732
733 for (name, ranges) in cases {
734 let mut ranges = ranges.to_vec();
735 let expect = ranges.clone();
736 let out = rt.block_on(source.get_ranges(&mut ranges)).unwrap();
737
738 assert_eq!(out.len(), expect.len(), "{name}");
739 for r in &expect {
740 let got = out
741 .get(&r.start)
742 .unwrap_or_else(|| panic!("{name}: {r:?} missing"));
743 assert_eq!(got.as_ref(), &contents[r.clone()], "{name}: {r:?}");
744 }
745 }
746 }
747}