Skip to main content

vortex_file/
open.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::sync::Arc;
5
6use futures::executor::block_on;
7use parking_lot::RwLock;
8use vortex_array::dtype::DType;
9use vortex_array::memory::MemorySessionExt;
10use vortex_array::session::ArraySessionExt;
11use vortex_buffer::Alignment;
12use vortex_buffer::ByteBuffer;
13use vortex_error::VortexError;
14use vortex_error::VortexExpect;
15use vortex_error::VortexResult;
16use vortex_io::VortexReadAt;
17use vortex_io::session::RuntimeSessionExt;
18use vortex_layout::segments::InstrumentedSegmentCache;
19use vortex_layout::segments::NoOpSegmentCache;
20use vortex_layout::segments::SegmentCache;
21use vortex_layout::segments::SegmentCacheSourceAdapter;
22use vortex_layout::segments::SegmentId;
23use vortex_layout::segments::SharedSegmentSource;
24use vortex_layout::session::LayoutSessionExt;
25use vortex_metrics::DefaultMetricsRegistry;
26use vortex_metrics::Label;
27use vortex_metrics::MetricsRegistry;
28use vortex_session::VortexSession;
29use vortex_utils::aliases::hash_map::HashMap;
30
31use crate::DeserializeStep;
32use crate::EOF_SIZE;
33use crate::MAX_POSTSCRIPT_SIZE;
34use crate::VortexFile;
35use crate::footer::Footer;
36use crate::segments::BufferSegmentSource;
37use crate::segments::FileSegmentSource;
38use crate::segments::InitialReadSegmentCache;
39use crate::segments::RequestMetrics;
40
41const INITIAL_READ_SIZE: usize = MAX_POSTSCRIPT_SIZE as usize + EOF_SIZE;
42
43/// Open options for a Vortex file reader.
44pub struct VortexOpenOptions {
45    /// The session to use for opening the file.
46    session: VortexSession,
47    /// Cache to use for file segments.
48    segment_cache: Option<Arc<dyn SegmentCache>>,
49    /// The number of bytes to read when parsing the footer.
50    initial_read_size: usize,
51    /// An optional, externally provided, file size.
52    file_size: Option<u64>,
53    /// An optional, externally provided, DType.
54    dtype: Option<DType>,
55    /// An optional, externally provided, file layout.
56    footer: Option<Footer>,
57    /// The segments read during the initial read.
58    initial_read_segments: RwLock<HashMap<SegmentId, ByteBuffer>>,
59    /// A metrics registry for the file.
60    metrics_registry: Option<Arc<dyn MetricsRegistry>>,
61    /// Default labels applied to all the file's metrics
62    labels: Vec<Label>,
63    /// Whether to cache file's LayoutReader between scans
64    cache_layout_reader: bool,
65}
66
67pub trait OpenOptionsSessionExt:
68    ArraySessionExt + LayoutSessionExt + RuntimeSessionExt + MemorySessionExt
69{
70    /// Create a new [`VortexOpenOptions`] using the provided session to open a file.
71    fn open_options(&self) -> VortexOpenOptions {
72        VortexOpenOptions {
73            session: self.session(),
74            segment_cache: None,
75            initial_read_size: INITIAL_READ_SIZE,
76            file_size: None,
77            dtype: None,
78            footer: None,
79            initial_read_segments: Default::default(),
80            metrics_registry: None,
81            labels: Vec::default(),
82            cache_layout_reader: false,
83        }
84    }
85}
86impl<S: ArraySessionExt + LayoutSessionExt + RuntimeSessionExt + MemorySessionExt>
87    OpenOptionsSessionExt for S
88{
89}
90
91impl VortexOpenOptions {
92    /// Configure the initial read size for the Vortex file.
93    pub fn with_initial_read_size(mut self, initial_read_size: usize) -> Self {
94        self.initial_read_size = initial_read_size;
95        self
96    }
97
98    /// Cache file's LayoutReader between scans
99    pub fn with_layout_reader_cache(mut self) -> Self {
100        self.cache_layout_reader = true;
101        self
102    }
103
104    /// Configure a custom [`SegmentCache`].
105    pub fn with_segment_cache(mut self, segment_cache: Arc<dyn SegmentCache>) -> Self {
106        self.segment_cache = Some(segment_cache);
107        self
108    }
109
110    /// Configure a known file size.
111    ///
112    /// This helps to prevent an I/O request to discover the size of the file.
113    /// Of course, all bets are off if you pass an incorrect value.
114    pub fn with_file_size(mut self, file_size: u64) -> Self {
115        self.file_size = Some(file_size);
116        self
117    }
118
119    /// Configure a known file size.
120    ///
121    /// This helps to prevent an I/O request to discover the size of the file.
122    /// Of course, all bets are off if you pass an incorrect value.
123    pub fn with_some_file_size(mut self, file_size: Option<u64>) -> Self {
124        self.file_size = file_size;
125        self
126    }
127
128    /// Configure a known DType.
129    ///
130    /// If this is provided, then the Vortex file may be opened with fewer I/O requests.
131    ///
132    /// For Vortex files that do not contain a `DType`, this is required.
133    pub fn with_dtype(mut self, dtype: DType) -> Self {
134        self.dtype = Some(dtype);
135        self
136    }
137
138    /// Configure a known file layout.
139    ///
140    /// If this is provided, then the Vortex file can be opened without performing any I/O.
141    /// Once open, the [`Footer`] can be accessed via [`crate::VortexFile::footer`].
142    pub fn with_footer(mut self, footer: Footer) -> Self {
143        self.dtype = Some(footer.layout().dtype().clone());
144        self.footer = Some(footer);
145        self
146    }
147
148    /// Configure a custom [`MetricsRegistry`] implementation.
149    pub fn with_metrics_registry(mut self, metrics: Arc<dyn MetricsRegistry>) -> Self {
150        self.metrics_registry = Some(metrics);
151        self
152    }
153
154    /// Adds labels to all the file's metrics.
155    pub fn with_labels(mut self, labels: Vec<Label>) -> Self {
156        self.labels.extend(labels);
157        self
158    }
159
160    /// Open a Vortex file using the provided I/O source.
161    ///
162    /// This is the most common way to open a [`VortexFile`] and tends to provide the best
163    /// out-of-the-box performance. The underlying I/O system will continue to be optimised for
164    /// different file systems and object stores so we encourage users to use this method
165    /// whenever possible and file issues if they encounter problems.
166    pub async fn open(self, source: Arc<dyn VortexReadAt>) -> VortexResult<VortexFile> {
167        self.open_read(source).await
168    }
169
170    /// Open a Vortex file from a filesystem path.
171    #[cfg(not(target_arch = "wasm32"))]
172    pub async fn open_path(self, path: impl AsRef<std::path::Path>) -> VortexResult<VortexFile> {
173        use vortex_io::std_file::FileReadAt;
174        let handle = self.session.handle();
175        let allocator = self.session.allocator();
176        let source = Arc::new(FileReadAt::open_with_allocator(path, handle, allocator)?);
177        self.open(source).await
178    }
179
180    /// Open a Vortex file from an in-memory buffer.
181    ///
182    /// This uses a `BufferSegmentSource` that resolves segments synchronously
183    /// by slicing the buffer directly, bypassing the async I/O pipeline.
184    pub fn open_buffer<B: Into<ByteBuffer>>(self, buffer: B) -> VortexResult<VortexFile> {
185        let buffer: ByteBuffer = buffer.into();
186
187        if self.segment_cache.is_some() {
188            tracing::warn!("segment cache is ignored for in-memory `open_buffer`");
189        }
190        if self.metrics_registry.is_some() {
191            tracing::warn!("metrics registry is ignored for in-memory `open_buffer`");
192        }
193
194        let cache_layout_reader = self.cache_layout_reader;
195        let mut opts = self.with_initial_read_size(0);
196
197        let footer = match opts.footer.take() {
198            Some(footer) => footer,
199            None => block_on(opts.read_footer(&buffer))?,
200        };
201
202        let segment_source = Arc::new(BufferSegmentSource::new(
203            buffer,
204            Arc::clone(footer.segment_map()),
205        ));
206        let file = VortexFile::new(footer, segment_source, opts.session);
207        Ok(if cache_layout_reader {
208            file.with_caching()
209        } else {
210            file
211        })
212    }
213
214    /// An API for opening a [`VortexFile`] using any [`VortexReadAt`] implementation.
215    pub async fn open_read<R: VortexReadAt + Clone>(self, reader: R) -> VortexResult<VortexFile> {
216        let segment_cache = self
217            .segment_cache
218            .clone()
219            .unwrap_or_else(|| Arc::new(NoOpSegmentCache));
220
221        let metrics_registry = self
222            .metrics_registry
223            .clone()
224            .unwrap_or_else(|| Arc::new(DefaultMetricsRegistry::default()));
225
226        let footer = if let Some(footer) = self.footer {
227            footer
228        } else {
229            self.read_footer(&reader).await?
230        };
231
232        let segment_cache = Arc::new(InstrumentedSegmentCache::new(
233            InitialReadSegmentCache {
234                initial: self.initial_read_segments,
235                fallback: segment_cache,
236            },
237            metrics_registry.as_ref(),
238            self.labels.clone(),
239        ));
240
241        let metrics = RequestMetrics::new(metrics_registry.as_ref(), self.labels);
242
243        // Create a segment source backed by the VortexRead implementation.
244        let segment_source = Arc::new(SharedSegmentSource::new(FileSegmentSource::open(
245            Arc::clone(footer.segment_map()),
246            reader,
247            self.session.handle(),
248            metrics,
249        )));
250
251        // Wrap up the segment source to first resolve segments from the initial read cache.
252        let segment_source = Arc::new(SegmentCacheSourceAdapter::new(
253            segment_cache,
254            segment_source,
255        ));
256
257        let file = VortexFile::new(footer, segment_source, self.session.clone());
258        Ok(if self.cache_layout_reader {
259            file.with_caching()
260        } else {
261            file
262        })
263    }
264
265    async fn read_footer(&self, read: &dyn VortexReadAt) -> VortexResult<Footer> {
266        // Fetch the file size and perform the initial read.
267        let file_size = match self.file_size {
268            None => read.size().await?,
269            Some(file_size) => file_size,
270        };
271        let mut initial_read_size = self
272            .initial_read_size
273            // Make sure we read enough to cover the postscript
274            .max(MAX_POSTSCRIPT_SIZE as usize + EOF_SIZE);
275        if let Ok(file_size) = usize::try_from(file_size) {
276            initial_read_size = initial_read_size.min(file_size);
277        }
278
279        let initial_offset = file_size - initial_read_size as u64;
280        let initial_read: ByteBuffer = read
281            .read_at(initial_offset, initial_read_size, Alignment::none())
282            .await?
283            .try_into_host()?
284            .await?;
285
286        let mut deserializer = Footer::deserializer(initial_read, self.session.clone())
287            .with_size(file_size)
288            .with_some_dtype(self.dtype.clone());
289
290        let footer = loop {
291            match deserializer.deserialize()? {
292                DeserializeStep::NeedMoreData { offset, len } => {
293                    let more_data = read
294                        .read_at(offset, len, Alignment::none())
295                        .await?
296                        .try_into_host()?
297                        .await?;
298                    deserializer.prefix_data(more_data);
299                }
300                DeserializeStep::NeedFileSize => unreachable!("We passed file_size above"),
301                DeserializeStep::Done(footer) => break Ok::<_, VortexError>(footer),
302            }
303        }?;
304
305        // If the initial read happened to cover any segments, then we can populate the
306        // segment cache
307        let initial_offset = file_size - (deserializer.buffer().len() as u64);
308        self.populate_initial_segments(initial_offset, deserializer.buffer(), &footer);
309
310        Ok(footer)
311    }
312
313    /// Populate segments in the cache that were covered by the initial read.
314    fn populate_initial_segments(
315        &self,
316        initial_offset: u64,
317        initial_read: &ByteBuffer,
318        footer: &Footer,
319    ) {
320        let first_idx = footer
321            .segment_map()
322            .partition_point(|segment| segment.offset < initial_offset);
323
324        let mut initial_read_segments = self.initial_read_segments.write();
325
326        for idx in first_idx..footer.segment_map().len() {
327            let segment = &footer.segment_map()[idx];
328            let segment_id =
329                SegmentId::from(u32::try_from(idx).vortex_expect("Invalid segment ID"));
330            let offset =
331                usize::try_from(segment.offset - initial_offset).vortex_expect("Invalid offset");
332            let buffer = initial_read
333                .slice(offset..offset + (segment.length as usize))
334                .aligned(segment.alignment);
335            initial_read_segments.insert(segment_id, buffer);
336        }
337    }
338}
339
340#[cfg(feature = "object_store")]
341impl VortexOpenOptions {
342    pub async fn open_object_store(
343        self,
344        object_store: &Arc<dyn object_store::ObjectStore>,
345        path: &str,
346    ) -> VortexResult<VortexFile> {
347        use vortex_io::object_store::ObjectStoreReadAt;
348
349        let handle = self.session.handle();
350        let allocator = self.session.allocator();
351        let source = Arc::new(ObjectStoreReadAt::new_with_allocator(
352            Arc::clone(object_store),
353            path.into(),
354            handle,
355            allocator,
356        ));
357        self.open(source).await
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use std::sync::atomic::AtomicUsize;
364    use std::sync::atomic::Ordering;
365
366    use futures::future::BoxFuture;
367    use vortex_array::IntoArray;
368    use vortex_array::buffer::BufferHandle;
369    use vortex_array::dtype::session::DTypeSession;
370    use vortex_array::memory::DefaultHostAllocator;
371    use vortex_array::memory::HostAllocator;
372    use vortex_array::memory::MemorySessionExt;
373    use vortex_array::memory::WritableHostBuffer;
374    use vortex_array::scalar_fn::session::ScalarFnSession;
375    use vortex_array::session::ArraySession;
376    use vortex_buffer::Alignment;
377    use vortex_buffer::Buffer;
378    use vortex_buffer::ByteBuffer;
379    use vortex_buffer::ByteBufferMut;
380    use vortex_io::session::RuntimeSession;
381    use vortex_layout::session::LayoutSession;
382
383    use super::*;
384    use crate::WriteOptionsSessionExt;
385
386    #[derive(Clone)]
387    // Define CountingRead struct
388    struct CountingRead<R> {
389        inner: R,
390        total_read: Arc<AtomicUsize>,
391        first_read_len: Arc<AtomicUsize>,
392    }
393
394    impl<R: VortexReadAt + Clone> VortexReadAt for CountingRead<R> {
395        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
396            self.inner.size()
397        }
398
399        fn read_at(
400            &self,
401            offset: u64,
402            length: usize,
403            alignment: Alignment,
404        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
405            self.total_read.fetch_add(length, Ordering::Relaxed);
406            let _ = self.first_read_len.compare_exchange(
407                0,
408                length,
409                Ordering::Relaxed,
410                Ordering::Relaxed,
411            );
412            self.inner.read_at(offset, length, alignment)
413        }
414
415        fn concurrency(&self) -> usize {
416            self.inner.concurrency()
417        }
418    }
419
420    #[derive(Debug)]
421    struct CountingAllocator {
422        allocations: Arc<AtomicUsize>,
423    }
424
425    impl HostAllocator for CountingAllocator {
426        fn allocate(&self, len: usize, alignment: Alignment) -> VortexResult<WritableHostBuffer> {
427            self.allocations.fetch_add(1, Ordering::Relaxed);
428            DefaultHostAllocator.allocate(len, alignment)
429        }
430    }
431
432    #[tokio::test]
433    async fn test_initial_read_size() {
434        let session = VortexSession::empty()
435            .with::<DTypeSession>()
436            .with::<ArraySession>()
437            .with::<LayoutSession>()
438            .with::<ScalarFnSession>()
439            .with::<RuntimeSession>();
440
441        crate::register_default_encodings(&session);
442
443        // Create a large file (> 1MB)
444        let mut buf = ByteBufferMut::empty();
445
446        // 1.5M integers -> ~6MB. We use a pattern to avoid Sequence encoding.
447        let array = Buffer::from(
448            (0i32..1_500_000)
449                .map(|i| if i % 2 == 0 { i } else { -i })
450                .collect::<Vec<i32>>(),
451        )
452        .into_array();
453
454        session
455            .write_options()
456            .write(&mut buf, array.to_array_stream())
457            .await
458            .unwrap();
459
460        let buffer = ByteBuffer::from(buf);
461        assert!(
462            buffer.len() > 1024 * 1024,
463            "Buffer length is only {} bytes",
464            buffer.len()
465        );
466
467        let total_read = Arc::new(AtomicUsize::new(0));
468        let first_read_len = Arc::new(AtomicUsize::new(0));
469        let reader = CountingRead {
470            inner: buffer,
471            total_read: Arc::clone(&total_read),
472            first_read_len: Arc::clone(&first_read_len),
473        };
474
475        // Open the file
476        let _file = session.open_options().open_read(reader).await.unwrap();
477
478        // Assert that we read approximately the postscript size, not 1MB
479        let first = first_read_len.load(Ordering::Relaxed);
480        assert_eq!(
481            first,
482            MAX_POSTSCRIPT_SIZE as usize + EOF_SIZE,
483            "Read exactly the postscript size"
484        );
485        let read = total_read.load(Ordering::Relaxed);
486        assert!(read < 1024 * 1024, "Read {} bytes, expected < 1MB", read);
487    }
488
489    #[cfg(not(target_arch = "wasm32"))]
490    #[tokio::test]
491    async fn test_open_path_uses_memory_session_allocator() {
492        let session = VortexSession::empty()
493            .with::<DTypeSession>()
494            .with::<ArraySession>()
495            .with::<LayoutSession>()
496            .with::<ScalarFnSession>()
497            .with::<RuntimeSession>();
498
499        crate::register_default_encodings(&session);
500
501        let mut buf = ByteBufferMut::empty();
502        let array = Buffer::from((0i32..16_384).collect::<Vec<i32>>()).into_array();
503        session
504            .write_options()
505            .write(&mut buf, array.to_array_stream())
506            .await
507            .unwrap();
508
509        let file_path = std::env::temp_dir().join(format!(
510            "vortex-open-memory-session-{}.vx",
511            std::process::id()
512        ));
513        std::fs::write(&file_path, ByteBuffer::from(buf).as_slice()).unwrap();
514
515        let allocations = Arc::new(AtomicUsize::new(0));
516        session
517            .memory_mut()
518            .set_allocator(Arc::new(CountingAllocator {
519                allocations: Arc::clone(&allocations),
520            }));
521
522        let _file = session.open_options().open_path(&file_path).await.unwrap();
523        std::fs::remove_file(&file_path).unwrap();
524
525        assert!(
526            allocations.load(Ordering::Relaxed) > 0,
527            "expected at least one host allocation from MemorySession"
528        );
529    }
530}