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::ALLOWED_ENCODINGS;
51use crate::Footer;
52use crate::MAGIC_BYTES;
53use crate::WriteStrategyBuilder;
54use crate::counting::CountingVortexWrite;
55use crate::footer::FileStatistics;
56use crate::segments::writer::BufferedSegmentSink;
57
58/// Configure a new writer, which can eventually be used to write an [`ArrayStream`] into a sink
59/// that implements [`VortexWrite`].
60///
61/// Unless overridden, the default [write strategy][crate::WriteStrategyBuilder] will be used with no
62/// additional configuration.
63pub struct VortexWriteOptions {
64    session: VortexSession,
65    strategy: Arc<dyn LayoutStrategy>,
66    exclude_dtype: bool,
67    max_variable_length_statistics_size: usize,
68    file_statistics: Vec<Stat>,
69}
70
71pub trait WriteOptionsSessionExt: SessionExt {
72    /// Create [`VortexWriteOptions`] for writing to a Vortex file.
73    fn write_options(&self) -> VortexWriteOptions {
74        let session = self.session();
75        VortexWriteOptions {
76            strategy: WriteStrategyBuilder::default().build(),
77            session,
78            exclude_dtype: false,
79            file_statistics: PRUNING_STATS.to_vec(),
80            max_variable_length_statistics_size: 64,
81        }
82    }
83}
84impl<S: SessionExt> WriteOptionsSessionExt for S {}
85
86impl VortexWriteOptions {
87    /// Create a new [`VortexWriteOptions`] with the given session.
88    pub fn new(session: VortexSession) -> Self {
89        VortexWriteOptions {
90            strategy: WriteStrategyBuilder::default().build(),
91            session,
92            exclude_dtype: false,
93            file_statistics: PRUNING_STATS.to_vec(),
94            max_variable_length_statistics_size: 64,
95        }
96    }
97
98    /// Replace the default layout strategy with the provided one.
99    pub fn with_strategy(mut self, strategy: Arc<dyn LayoutStrategy>) -> Self {
100        self.strategy = strategy;
101        self
102    }
103
104    /// Exclude the DType from the Vortex file. You must provide the DType to the reader.
105    // TODO(ngates): Should we store some sort of DType checksum to make sure the one passed at
106    //  read-time is sane? I guess most layouts will have some reasonable validation.
107    pub fn exclude_dtype(mut self) -> Self {
108        self.exclude_dtype = true;
109        self
110    }
111
112    /// Configure which statistics to compute at the file-level.
113    pub fn with_file_statistics(mut self, file_statistics: Vec<Stat>) -> Self {
114        self.file_statistics = file_statistics;
115        self
116    }
117}
118
119impl VortexWriteOptions {
120    /// Drop into the blocking writer API using the given runtime.
121    pub fn blocking<B: BlockingRuntime>(self, runtime: &B) -> BlockingWrite<'_, B> {
122        BlockingWrite {
123            options: self,
124            runtime,
125        }
126    }
127
128    /// Write an [`ArrayStream`] as a Vortex file.
129    ///
130    /// Note that buffers are flushed as soon as they are available with no buffering, the caller
131    /// is responsible for deciding how to configure buffering on the underlying `Write` sink.
132    pub async fn write<W: VortexWrite + Unpin, S: ArrayStream + Send + 'static>(
133        self,
134        write: W,
135        stream: S,
136    ) -> VortexResult<WriteSummary> {
137        self.write_internal(write, ArrayStreamExt::boxed(stream))
138            .await
139    }
140
141    async fn write_internal<W: VortexWrite + Unpin>(
142        self,
143        mut write: W,
144        stream: SendableArrayStream,
145    ) -> VortexResult<WriteSummary> {
146        // NOTE(os): Setup an array context that already has all known encodings pre-populated.
147        // This is preferred for now over having an empty context here, because only the
148        // serialised array order is deterministic. The serialisation of arrays are done
149        // parallel and with an empty context they can register their encodings to the context
150        // in different order, changing the written bytes from run to run.
151        let ctx = ArrayContext::new(ALLOWED_ENCODINGS.iter().cloned().sorted().collect())
152            // Configure a registry just to ensure only known encodings are interned.
153            .with_registry(self.session.arrays().registry().clone());
154        let dtype = stream.dtype().clone();
155
156        let (mut ptr, eof) = SequenceId::root().split();
157
158        let stream = SequentialStreamAdapter::new(
159            dtype.clone(),
160            stream
161                .try_filter(|chunk| ready(!chunk.is_empty()))
162                .map(move |result| result.map(|chunk| (ptr.advance(), chunk))),
163        )
164        .sendable();
165        let (file_stats, stream) = accumulate_stats(
166            stream,
167            self.file_statistics.clone().into(),
168            self.max_variable_length_statistics_size,
169            &self.session,
170        );
171
172        // First, write the magic bytes.
173        write.write_all(ByteBuffer::copy_from(MAGIC_BYTES)).await?;
174        let mut position = MAGIC_BYTES.len() as u64;
175
176        // Create a channel to send buffers from the segment sink to the output stream.
177        let (send, recv) = kanal::bounded_async(1);
178
179        let segments = Arc::new(BufferedSegmentSink::new(send, position));
180
181        // We spawn the layout future so it is driven in the background while we write the
182        // buffer stream, so we don't need to poll it until all buffers have been drained.
183        let ctx2 = ctx.clone();
184        let session = self.session.clone();
185        let layout_fut = self.session.handle().spawn_nested(move |h| async move {
186            let session = session.with_handle(h);
187            let layout = self
188                .strategy
189                .write_stream(
190                    ctx2,
191                    Arc::<BufferedSegmentSink>::clone(&segments),
192                    stream,
193                    eof,
194                    &session,
195                )
196                .await?;
197            Ok::<_, VortexError>((layout, segments.segment_specs()))
198        });
199
200        // Flush buffers as they arrive
201        let recv_stream = recv.into_stream();
202        pin_mut!(recv_stream);
203        while let Some(buffer) = recv_stream.next().await {
204            if buffer.is_empty() {
205                continue;
206            }
207            position += buffer.len() as u64;
208            write.write_all(buffer).await?;
209        }
210
211        let (layout, segment_specs) = layout_fut.await?;
212
213        // Assemble the Footer object now that we have all the segments.
214        let mut footer = Footer::new(
215            Arc::clone(&layout),
216            segment_specs,
217            if self.file_statistics.is_empty() {
218                None
219            } else {
220                Some(FileStatistics::new_with_dtype(
221                    file_stats.stats_sets().into(),
222                    &dtype,
223                ))
224            },
225            ReadContext::new(ctx.to_ids()),
226        );
227
228        // Emit the footer buffers and EOF.
229        let footer_buffers = footer
230            .clone()
231            .into_serializer()
232            .with_offset(position)
233            .with_exclude_dtype(self.exclude_dtype)
234            .serialize()?;
235
236        // Update the approx footer size in the footer object, so it can be used for caching and
237        // memory management in the future.
238        footer = footer.with_approx_byte_size(footer_buffers.iter().map(|b| b.len()).sum());
239
240        for buffer in footer_buffers {
241            position += buffer.len() as u64;
242            write.write_all(buffer).await?;
243        }
244
245        write.flush().await?;
246
247        Ok(WriteSummary {
248            footer,
249            size: position,
250        })
251    }
252
253    /// Create a push-based [`Writer`] that can be used to incrementally write arrays to the file.
254    pub fn writer<'w, W: VortexWrite + Unpin + 'w>(self, write: W, dtype: DType) -> Writer<'w> {
255        // Create a channel for sending arrays to the layout task.
256        let (arrays_send, arrays_recv) = kanal::bounded_async(1);
257
258        let arrays =
259            ArrayStreamExt::boxed(ArrayStreamAdapter::new(dtype, arrays_recv.into_stream()));
260
261        let write = CountingVortexWrite::new(write);
262        let bytes_written = write.counter();
263        let strategy = Arc::clone(&self.strategy);
264        let future = self.write(write, arrays).boxed_local().fuse();
265
266        Writer {
267            arrays: Some(arrays_send),
268            future,
269            bytes_written,
270            strategy,
271        }
272    }
273}
274
275/// An async API for writing Vortex files.
276pub struct Writer<'w> {
277    // The input channel for sending arrays to the writer.
278    arrays: Option<kanal::AsyncSender<VortexResult<ArrayRef>>>,
279    // The writer task that ultimately produces the footer.
280    future: Fuse<LocalBoxFuture<'w, VortexResult<WriteSummary>>>,
281    // The bytes written so far.
282    bytes_written: Arc<AtomicU64>,
283    // The layout strategy that is being used for the write.
284    strategy: Arc<dyn LayoutStrategy>,
285}
286
287impl Writer<'_> {
288    /// Push a new chunk into the writer.
289    pub async fn push(&mut self, chunk: ArrayRef) -> VortexResult<()> {
290        let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
291        let send_fut = async move { arrays.send(Ok(chunk)).await }.fuse();
292        pin_mut!(send_fut);
293
294        // We poll the writer future to continue writing bytes to the output, while waiting for
295        // enough room to push the next chunk into the channel.
296        select! {
297            result = send_fut => {
298                // If the send future failed, the writer has failed or panicked.
299                if result.is_err() {
300                    return Err(self.handle_failed_task().await);
301                }
302            },
303            result = &mut self.future => {
304                // Under normal operation, the writer future should never complete until
305                // finish() is called. Therefore, we can assume the writer has failed.
306                // The writer future has failed, we need to propagate the error.
307                match result {
308                    Ok(_) => vortex_bail!("Internal error: writer future completed early"),
309                    Err(e) => return Err(e),
310                }
311            }
312        }
313
314        Ok(())
315    }
316
317    /// Push an entire [`ArrayStream`] into the writer, consuming it.
318    ///
319    /// A task is spawned to consume the stream and push it into the writer, with the current
320    /// thread being used to write buffers to the output.
321    pub async fn push_stream(&mut self, mut stream: SendableArrayStream) -> VortexResult<()> {
322        let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
323        let stream_fut = async move {
324            while let Some(chunk) = stream.next().await {
325                arrays.send(chunk).await?;
326            }
327            Ok::<_, kanal::SendError>(())
328        }
329        .fuse();
330        pin_mut!(stream_fut);
331
332        // We poll the writer future to continue writing bytes to the output, while waiting for
333        // enough room to push the stream into the channel.
334        select! {
335            result = stream_fut => {
336                if let Err(_send_err) = result {
337                    // If the send future failed, the writer has failed or panicked.
338                    return Err(self.handle_failed_task().await);
339                }
340            }
341
342            result = &mut self.future => {
343                // Under normal operation, the writer future should never complete until
344                // finish() is called. Therefore, we can assume the writer has failed.
345                // The writer future has failed, we need to propagate the error.
346                match result {
347                    Ok(_) => vortex_bail!("Internal error: writer future completed early"),
348                    Err(e) => return Err(e),
349                }
350            }
351        }
352
353        Ok(())
354    }
355
356    /// Returns the number of bytes written to the file so far.
357    pub fn bytes_written(&self) -> u64 {
358        self.bytes_written
359            .load(std::sync::atomic::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}