Skip to main content

vortex_file/
writer.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::io;
5use std::io::Write;
6use std::sync::Arc;
7use std::sync::atomic::AtomicU64;
8
9use futures::FutureExt;
10use futures::StreamExt;
11use futures::TryStreamExt;
12use futures::future::Fuse;
13use futures::future::LocalBoxFuture;
14use futures::future::ready;
15use futures::pin_mut;
16use futures::select;
17use itertools::Itertools;
18use vortex_array::ArrayContext;
19use vortex_array::ArrayRef;
20use vortex_array::dtype::DType;
21use vortex_array::expr::stats::Stat;
22use vortex_array::iter::ArrayIterator;
23use vortex_array::iter::ArrayIteratorExt;
24use vortex_array::session::ArraySessionExt;
25use vortex_array::stats::PRUNING_STATS;
26use vortex_array::stream::ArrayStream;
27use vortex_array::stream::ArrayStreamAdapter;
28use vortex_array::stream::ArrayStreamExt;
29use vortex_array::stream::SendableArrayStream;
30use vortex_buffer::ByteBuffer;
31use vortex_error::VortexError;
32use vortex_error::VortexExpect;
33use vortex_error::VortexResult;
34use vortex_error::vortex_bail;
35use vortex_error::vortex_err;
36use vortex_io::IoBuf;
37use vortex_io::VortexWrite;
38use vortex_io::kanal_ext::KanalExt;
39use vortex_io::runtime::BlockingRuntime;
40use vortex_io::session::RuntimeSessionExt;
41use vortex_layout::LayoutStrategy;
42use vortex_layout::layouts::file_stats::accumulate_stats;
43use vortex_layout::sequence::SequenceId;
44use vortex_layout::sequence::SequentialStreamAdapter;
45use vortex_layout::sequence::SequentialStreamExt;
46use vortex_session::SessionExt;
47use vortex_session::VortexSession;
48use vortex_session::registry::ReadContext;
49
50use crate::Footer;
51use crate::MAGIC_BYTES;
52use crate::WriteStrategyBuilder;
53use crate::counting::CountingVortexWrite;
54use crate::footer::FileStatistics;
55use crate::segments::writer::BufferedSegmentSink;
56
57/// Configure a new writer, which can eventually be used to write an [`ArrayStream`] into a sink
58/// that implements [`VortexWrite`].
59///
60/// Unless overridden, the default [write strategy][crate::WriteStrategyBuilder] will be used with no
61/// additional configuration.
62pub struct VortexWriteOptions {
63    session: VortexSession,
64    strategy: Arc<dyn LayoutStrategy>,
65    exclude_dtype: bool,
66    max_variable_length_statistics_size: usize,
67    file_statistics: Vec<Stat>,
68}
69
70pub trait WriteOptionsSessionExt: SessionExt {
71    /// Create [`VortexWriteOptions`] for writing to a Vortex file.
72    fn write_options(&self) -> VortexWriteOptions {
73        let session = self.session();
74        VortexWriteOptions {
75            strategy: WriteStrategyBuilder::default().build(),
76            session,
77            exclude_dtype: false,
78            file_statistics: PRUNING_STATS.to_vec(),
79            max_variable_length_statistics_size: 64,
80        }
81    }
82}
83impl<S: SessionExt> WriteOptionsSessionExt for S {}
84
85impl VortexWriteOptions {
86    /// Create a new [`VortexWriteOptions`] with the given session.
87    pub fn new(session: VortexSession) -> Self {
88        VortexWriteOptions {
89            strategy: WriteStrategyBuilder::default().build(),
90            session,
91            exclude_dtype: false,
92            file_statistics: PRUNING_STATS.to_vec(),
93            max_variable_length_statistics_size: 64,
94        }
95    }
96
97    /// Replace the default layout strategy with the provided one.
98    pub fn with_strategy(mut self, strategy: Arc<dyn LayoutStrategy>) -> Self {
99        self.strategy = strategy;
100        self
101    }
102
103    /// Exclude the DType from the Vortex file. You must provide the DType to the reader.
104    // TODO(ngates): Should we store some sort of DType checksum to make sure the one passed at
105    //  read-time is sane? I guess most layouts will have some reasonable validation.
106    pub fn exclude_dtype(mut self) -> Self {
107        self.exclude_dtype = true;
108        self
109    }
110
111    /// Configure which statistics to compute at the file-level.
112    pub fn with_file_statistics(mut self, file_statistics: Vec<Stat>) -> Self {
113        self.file_statistics = file_statistics;
114        self
115    }
116}
117
118impl VortexWriteOptions {
119    /// Drop into the blocking writer API using the given runtime.
120    pub fn blocking<B: BlockingRuntime>(self, runtime: &B) -> BlockingWrite<'_, B> {
121        BlockingWrite {
122            options: self,
123            runtime,
124        }
125    }
126
127    /// Write an [`ArrayStream`] as a Vortex file.
128    ///
129    /// Note that buffers are flushed as soon as they are available with no buffering, the caller
130    /// is responsible for deciding how to configure buffering on the underlying `Write` sink.
131    pub async fn write<W: VortexWrite + Unpin, S: ArrayStream + Send + 'static>(
132        self,
133        write: W,
134        stream: S,
135    ) -> VortexResult<WriteSummary> {
136        self.write_internal(write, ArrayStreamExt::boxed(stream))
137            .await
138    }
139
140    async fn write_internal<W: VortexWrite + Unpin>(
141        self,
142        mut write: W,
143        stream: SendableArrayStream,
144    ) -> VortexResult<WriteSummary> {
145        // NOTE(os): Setup an array context that already has all known encodings pre-populated.
146        // This is preferred for now over having an empty context here, because only the
147        // serialised array order is deterministic. The serialisation of arrays are done
148        // parallel and with an empty context they can register their encodings to the context
149        // in different order, changing the written bytes from run to run.
150        let ctx = ArrayContext::new(self.session.arrays().registry().ids().sorted().collect())
151            // Configure a registry just to ensure only known encodings are interned.
152            .with_registry(self.session.arrays().registry().clone());
153        let dtype = stream.dtype().clone();
154
155        let (mut ptr, eof) = SequenceId::root().split();
156
157        let stream = SequentialStreamAdapter::new(
158            dtype.clone(),
159            stream
160                .try_filter(|chunk| ready(!chunk.is_empty()))
161                .map(move |result| result.map(|chunk| (ptr.advance(), chunk))),
162        )
163        .sendable();
164        let (file_stats, stream) = accumulate_stats(
165            stream,
166            self.file_statistics.clone().into(),
167            self.max_variable_length_statistics_size,
168        );
169
170        // First, write the magic bytes.
171        write.write_all(ByteBuffer::copy_from(MAGIC_BYTES)).await?;
172        let mut position = MAGIC_BYTES.len() as u64;
173
174        // Create a channel to send buffers from the segment sink to the output stream.
175        let (send, recv) = kanal::bounded_async(1);
176
177        let segments = Arc::new(BufferedSegmentSink::new(send, position));
178
179        // We spawn the layout future so it is driven in the background while we write the
180        // buffer stream, so we don't need to poll it until all buffers have been drained.
181        let ctx2 = ctx.clone();
182        let session = self.session.clone();
183        let layout_fut = self.session.handle().spawn_nested(move |h| async move {
184            let session = session.with_handle(h);
185            let layout = self
186                .strategy
187                .write_stream(
188                    ctx2,
189                    Arc::<BufferedSegmentSink>::clone(&segments),
190                    stream,
191                    eof,
192                    &session,
193                )
194                .await?;
195            Ok::<_, VortexError>((layout, segments.segment_specs()))
196        });
197
198        // Flush buffers as they arrive
199        let recv_stream = recv.into_stream();
200        pin_mut!(recv_stream);
201        while let Some(buffer) = recv_stream.next().await {
202            if buffer.is_empty() {
203                continue;
204            }
205            position += buffer.len() as u64;
206            write.write_all(buffer).await?;
207        }
208
209        let (layout, segment_specs) = layout_fut.await?;
210
211        // Assemble the Footer object now that we have all the segments.
212        let mut footer = Footer::new(
213            Arc::clone(&layout),
214            segment_specs,
215            if self.file_statistics.is_empty() {
216                None
217            } else {
218                Some(FileStatistics::new_with_dtype(
219                    file_stats.stats_sets().into(),
220                    &dtype,
221                ))
222            },
223            ReadContext::new(ctx.to_ids()),
224        );
225
226        // Emit the footer buffers and EOF.
227        let footer_buffers = footer
228            .clone()
229            .into_serializer()
230            .with_offset(position)
231            .with_exclude_dtype(self.exclude_dtype)
232            .serialize()?;
233
234        // Update the approx footer size in the footer object, so it can be used for caching and
235        // memory management in the future.
236        footer = footer.with_approx_byte_size(footer_buffers.iter().map(|b| b.len()).sum());
237
238        for buffer in footer_buffers {
239            position += buffer.len() as u64;
240            write.write_all(buffer).await?;
241        }
242
243        write.flush().await?;
244
245        Ok(WriteSummary {
246            footer,
247            size: position,
248        })
249    }
250
251    /// Create a push-based [`Writer`] that can be used to incrementally write arrays to the file.
252    pub fn writer<'w, W: VortexWrite + Unpin + 'w>(self, write: W, dtype: DType) -> Writer<'w> {
253        // Create a channel for sending arrays to the layout task.
254        let (arrays_send, arrays_recv) = kanal::bounded_async(1);
255
256        let arrays =
257            ArrayStreamExt::boxed(ArrayStreamAdapter::new(dtype, arrays_recv.into_stream()));
258
259        let write = CountingVortexWrite::new(write);
260        let bytes_written = write.counter();
261        let strategy = Arc::clone(&self.strategy);
262        let future = self.write(write, arrays).boxed_local().fuse();
263
264        Writer {
265            arrays: Some(arrays_send),
266            future,
267            bytes_written,
268            strategy,
269        }
270    }
271}
272
273/// An async API for writing Vortex files.
274pub struct Writer<'w> {
275    // The input channel for sending arrays to the writer.
276    arrays: Option<kanal::AsyncSender<VortexResult<ArrayRef>>>,
277    // The writer task that ultimately produces the footer.
278    future: Fuse<LocalBoxFuture<'w, VortexResult<WriteSummary>>>,
279    // The bytes written so far.
280    bytes_written: Arc<AtomicU64>,
281    // The layout strategy that is being used for the write.
282    strategy: Arc<dyn LayoutStrategy>,
283}
284
285impl Writer<'_> {
286    /// Push a new chunk into the writer.
287    pub async fn push(&mut self, chunk: ArrayRef) -> VortexResult<()> {
288        let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
289        let send_fut = async move { arrays.send(Ok(chunk)).await }.fuse();
290        pin_mut!(send_fut);
291
292        // We poll the writer future to continue writing bytes to the output, while waiting for
293        // enough room to push the next chunk into the channel.
294        select! {
295            result = send_fut => {
296                // If the send future failed, the writer has failed or panicked.
297                if result.is_err() {
298                    return Err(self.handle_failed_task().await);
299                }
300            },
301            result = &mut self.future => {
302                // Under normal operation, the writer future should never complete until
303                // finish() is called. Therefore, we can assume the writer has failed.
304                // The writer future has failed, we need to propagate the error.
305                match result {
306                    Ok(_) => vortex_bail!("Internal error: writer future completed early"),
307                    Err(e) => return Err(e),
308                }
309            }
310        }
311
312        Ok(())
313    }
314
315    /// Push an entire [`ArrayStream`] into the writer, consuming it.
316    ///
317    /// A task is spawned to consume the stream and push it into the writer, with the current
318    /// thread being used to write buffers to the output.
319    pub async fn push_stream(&mut self, mut stream: SendableArrayStream) -> VortexResult<()> {
320        let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
321        let stream_fut = async move {
322            while let Some(chunk) = stream.next().await {
323                arrays.send(chunk).await?;
324            }
325            Ok::<_, kanal::SendError>(())
326        }
327        .fuse();
328        pin_mut!(stream_fut);
329
330        // We poll the writer future to continue writing bytes to the output, while waiting for
331        // enough room to push the stream into the channel.
332        select! {
333            result = stream_fut => {
334                if let Err(_send_err) = result {
335                    // If the send future failed, the writer has failed or panicked.
336                    return Err(self.handle_failed_task().await);
337                }
338            }
339
340            result = &mut self.future => {
341                // Under normal operation, the writer future should never complete until
342                // finish() is called. Therefore, we can assume the writer has failed.
343                // The writer future has failed, we need to propagate the error.
344                match result {
345                    Ok(_) => vortex_bail!("Internal error: writer future completed early"),
346                    Err(e) => return Err(e),
347                }
348            }
349        }
350
351        Ok(())
352    }
353
354    /// Returns the number of bytes written to the file so far.
355    pub fn bytes_written(&self) -> u64 {
356        self.bytes_written
357            .load(std::sync::atomic::Ordering::Relaxed)
358    }
359
360    /// Returns the number of bytes currently buffered by the layout writers.
361    pub fn buffered_bytes(&self) -> u64 {
362        self.strategy.buffered_bytes()
363    }
364
365    /// Finish writing the Vortex file, flushing any remaining buffers and returning the
366    /// new file's footer.
367    pub async fn finish(mut self) -> VortexResult<WriteSummary> {
368        // Drop the input channel to signal EOF.
369        drop(self.arrays.take());
370
371        // Await the future task.
372        self.future.await
373    }
374
375    /// Assuming the writer task has failed, await it to get the error.
376    async fn handle_failed_task(&mut self) -> VortexError {
377        match (&mut self.future).await {
378            Ok(_) => vortex_err!(
379                "Internal error: writer task completed successfully but write future finished early"
380            ),
381            Err(e) => e,
382        }
383    }
384}
385
386/// A blocking API for writing Vortex files.
387pub struct BlockingWrite<'rt, B: BlockingRuntime> {
388    options: VortexWriteOptions,
389    runtime: &'rt B,
390}
391
392impl<'rt, B: BlockingRuntime> BlockingWrite<'rt, B> {
393    /// Write a Vortex file into the given `Write` sink.
394    pub fn write<W: Write + Unpin>(
395        self,
396        write: W,
397        iter: impl ArrayIterator + Send + 'static,
398    ) -> VortexResult<WriteSummary> {
399        self.runtime.block_on(async move {
400            self.options
401                .write(BlockingWriteAdapter(write), iter.into_array_stream())
402                .await
403        })
404    }
405
406    pub fn writer<'w, W: Write + Unpin + 'w>(
407        self,
408        write: W,
409        dtype: DType,
410    ) -> BlockingWriter<'rt, 'w, B> {
411        BlockingWriter {
412            writer: self.options.writer(BlockingWriteAdapter(write), dtype),
413            runtime: self.runtime,
414        }
415    }
416}
417
418/// A blocking adapter around a [`Writer`], allowing incremental writing of arrays to a Vortex file.
419pub struct BlockingWriter<'rt, 'w, B: BlockingRuntime> {
420    runtime: &'rt B,
421    writer: Writer<'w>,
422}
423
424impl<B: BlockingRuntime> BlockingWriter<'_, '_, B> {
425    pub fn push(&mut self, chunk: ArrayRef) -> VortexResult<()> {
426        self.runtime.block_on(self.writer.push(chunk))
427    }
428
429    pub fn bytes_written(&self) -> u64 {
430        self.writer.bytes_written()
431    }
432
433    pub fn buffered_bytes(&self) -> u64 {
434        self.writer.buffered_bytes()
435    }
436
437    pub fn finish(self) -> VortexResult<WriteSummary> {
438        self.runtime.block_on(self.writer.finish())
439    }
440}
441
442// TODO(ngates): this blocking API may change, for now we just run blocking I/O inline.
443struct BlockingWriteAdapter<W>(W);
444
445impl<W: Write + Unpin> VortexWrite for BlockingWriteAdapter<W> {
446    async fn write_all<B: IoBuf>(&mut self, buffer: B) -> io::Result<B> {
447        self.0.write_all(buffer.as_slice())?;
448        Ok(buffer)
449    }
450
451    fn flush(&mut self) -> impl Future<Output = io::Result<()>> {
452        ready(self.0.flush())
453    }
454
455    fn shutdown(&mut self) -> impl Future<Output = io::Result<()>> {
456        ready(Ok(()))
457    }
458}
459
460pub struct WriteSummary {
461    footer: Footer,
462    size: u64,
463    // TODO(ngates): add a checksum
464}
465
466impl WriteSummary {
467    /// The footer of the written Vortex file.
468    pub fn footer(&self) -> &Footer {
469        &self.footer
470    }
471
472    /// The total size of the written Vortex file in bytes.
473    pub fn size(&self) -> u64 {
474        self.size
475    }
476
477    /// The total number of rows in the written Vortex file.
478    pub fn row_count(&self) -> u64 {
479        self.footer.row_count()
480    }
481}