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