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