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