Skip to main content

vortex_ipc/
stream.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::future::Future;
5use std::pin::Pin;
6use std::task::Poll;
7use std::task::ready;
8
9use bytes::Bytes;
10use bytes::BytesMut;
11use futures::AsyncRead;
12use futures::AsyncWrite;
13use futures::AsyncWriteExt;
14use futures::Stream;
15use futures::StreamExt;
16use futures::TryStreamExt;
17use pin_project_lite::pin_project;
18use vortex_array::ArrayRef;
19use vortex_array::dtype::DType;
20use vortex_array::stream::ArrayStream;
21use vortex_error::VortexResult;
22use vortex_error::vortex_bail;
23use vortex_error::vortex_err;
24use vortex_session::VortexSession;
25
26use crate::messages::AsyncMessageReader;
27use crate::messages::DecoderMessage;
28use crate::messages::EncoderMessage;
29use crate::messages::MessageEncoder;
30
31pin_project! {
32    /// An [`ArrayStream`] for reading messages off an async IPC stream.
33    pub struct AsyncIPCReader<R> {
34        #[pin]
35        reader: AsyncMessageReader<R>,
36        dtype: DType,
37        session: VortexSession,
38    }
39}
40
41impl<R: AsyncRead + Unpin> AsyncIPCReader<R> {
42    pub async fn try_new(read: R, session: &VortexSession) -> VortexResult<Self> {
43        let mut reader = AsyncMessageReader::new(read);
44
45        let dtype = match reader.next().await.transpose()? {
46            Some(msg) => match msg {
47                DecoderMessage::DType(dtype) => dtype,
48                msg => {
49                    vortex_bail!("Expected DType message, got {:?}", msg);
50                }
51            },
52            None => vortex_bail!("Expected DType message, got EOF"),
53        };
54
55        let dtype = DType::from_flatbuffer(dtype, session)?;
56
57        Ok(AsyncIPCReader {
58            reader,
59            dtype,
60            session: session.clone(),
61        })
62    }
63}
64
65impl<R: AsyncRead> ArrayStream for AsyncIPCReader<R> {
66    fn dtype(&self) -> &DType {
67        &self.dtype
68    }
69}
70
71impl<R: AsyncRead> Stream for AsyncIPCReader<R> {
72    type Item = VortexResult<ArrayRef>;
73
74    fn poll_next(
75        self: Pin<&mut Self>,
76        cx: &mut std::task::Context<'_>,
77    ) -> Poll<Option<Self::Item>> {
78        let this = self.project();
79
80        match ready!(this.reader.poll_next(cx)) {
81            None => Poll::Ready(None),
82            Some(msg) => match msg {
83                Ok(DecoderMessage::Array((array_parts, ctx, row_count))) => Poll::Ready(Some(
84                    array_parts
85                        .decode(this.dtype, row_count, &ctx, this.session)
86                        .and_then(|array| {
87                            if array.dtype() != this.dtype {
88                                Err(vortex_err!(
89                                    "Array data type mismatch: expected {:?}, got {:?}",
90                                    this.dtype,
91                                    array.dtype()
92                                ))
93                            } else {
94                                Ok(array)
95                            }
96                        }),
97                )),
98                Ok(msg) => Poll::Ready(Some(Err(vortex_err!(
99                    "Expected Array message, got {:?}",
100                    msg
101                )))),
102                Err(e) => Poll::Ready(Some(Err(e))),
103            },
104        }
105    }
106}
107
108/// A trait for converting an [`ArrayStream`] into IPC streams.
109pub trait ArrayStreamIPC {
110    fn into_ipc(self, session: &VortexSession) -> ArrayStreamIPCBytes
111    where
112        Self: Sized;
113
114    fn write_ipc<W: AsyncWrite + Unpin>(
115        self,
116        write: W,
117        session: &VortexSession,
118    ) -> impl Future<Output = VortexResult<W>>
119    where
120        Self: Sized;
121}
122
123impl<S: ArrayStream + 'static> ArrayStreamIPC for S {
124    fn into_ipc(self, session: &VortexSession) -> ArrayStreamIPCBytes
125    where
126        Self: Sized,
127    {
128        ArrayStreamIPCBytes {
129            stream: Box::pin(self),
130            encoder: MessageEncoder::new(session.clone()),
131            buffers: vec![],
132            written_dtype: false,
133        }
134    }
135
136    async fn write_ipc<W: AsyncWrite + Unpin>(
137        self,
138        mut write: W,
139        session: &VortexSession,
140    ) -> VortexResult<W>
141    where
142        Self: Sized,
143    {
144        let mut stream = self.into_ipc(session);
145        while let Some(chunk) = stream.next().await {
146            write.write_all(&chunk?).await?;
147        }
148        Ok(write)
149    }
150}
151
152pub struct ArrayStreamIPCBytes {
153    stream: Pin<Box<dyn ArrayStream + 'static>>,
154    encoder: MessageEncoder,
155    buffers: Vec<Bytes>,
156    written_dtype: bool,
157}
158
159impl ArrayStreamIPCBytes {
160    /// Collects the IPC bytes into a single `Bytes`.
161    pub async fn collect_to_buffer(self) -> VortexResult<Bytes> {
162        let buffers: Vec<Bytes> = self.try_collect().await?;
163        let mut buffer = BytesMut::with_capacity(buffers.iter().map(|b| b.len()).sum());
164        for buf in buffers {
165            buffer.extend_from_slice(buf.as_ref());
166        }
167        Ok(buffer.freeze())
168    }
169}
170
171impl Stream for ArrayStreamIPCBytes {
172    type Item = VortexResult<Bytes>;
173
174    fn poll_next(
175        self: Pin<&mut Self>,
176        cx: &mut std::task::Context<'_>,
177    ) -> Poll<Option<Self::Item>> {
178        let this = self.get_mut();
179
180        // If we haven't written the dtype yet, we write it
181        if !this.written_dtype {
182            let Ok(buffers) = this
183                .encoder
184                .encode(EncoderMessage::DType(this.stream.dtype()))
185            else {
186                return Poll::Ready(Some(Err(vortex_err!("Failed to encode DType message"))));
187            };
188            this.buffers.extend(buffers);
189            this.written_dtype = true;
190        }
191
192        // Try to flush any buffers we have
193        if !this.buffers.is_empty() {
194            return Poll::Ready(Some(Ok(this.buffers.remove(0))));
195        }
196
197        // Or else try to serialize the next array
198        match ready!(this.stream.poll_next_unpin(cx)) {
199            None => return Poll::Ready(None),
200            Some(chunk) => match chunk.and_then(|c| this.encoder.encode(EncoderMessage::Array(&c)))
201            {
202                Ok(buffers) => {
203                    this.buffers.extend(buffers);
204                }
205                Err(e) => return Poll::Ready(Some(Err(e))),
206            },
207        }
208
209        // Try to flush any buffers we have again
210        if !this.buffers.is_empty() {
211            return Poll::Ready(Some(Ok(this.buffers.remove(0))));
212        }
213
214        // Otherwise, we're done
215        Poll::Ready(None)
216    }
217}
218
219#[cfg(test)]
220mod test {
221    use std::io;
222    use std::pin::Pin;
223    use std::task::Context;
224    use std::task::Poll;
225
226    use futures::io::Cursor;
227    use vortex_array::IntoArray as _;
228    use vortex_array::VortexSessionExecute;
229    use vortex_array::assert_arrays_eq;
230    use vortex_array::stream::ArrayStream;
231    use vortex_array::stream::ArrayStreamExt;
232    use vortex_buffer::buffer;
233
234    use super::*;
235    use crate::test::SESSION;
236
237    #[tokio::test]
238    async fn test_async_stream() {
239        let array = buffer![1, 2, 3].into_array();
240        let ipc_buffer = array
241            .to_array_stream()
242            .into_ipc(&SESSION)
243            .collect_to_buffer()
244            .await
245            .unwrap();
246
247        let reader = AsyncIPCReader::try_new(Cursor::new(ipc_buffer), &SESSION)
248            .await
249            .unwrap();
250
251        assert_eq!(reader.dtype(), array.dtype());
252        let result = reader.read_all().await.unwrap();
253        assert_arrays_eq!(result, array, &mut SESSION.create_execution_ctx());
254    }
255
256    /// Wrapper that limits reads to small chunks to simulate network behavior
257    struct ChunkedReader<R> {
258        inner: R,
259        chunk_size: usize,
260    }
261
262    impl<R: AsyncRead + Unpin> AsyncRead for ChunkedReader<R> {
263        fn poll_read(
264            mut self: Pin<&mut Self>,
265            cx: &mut Context<'_>,
266            buf: &mut [u8],
267        ) -> Poll<io::Result<usize>> {
268            let chunk_size = self.chunk_size.min(buf.len());
269            Pin::new(&mut self.inner).poll_read(cx, &mut buf[..chunk_size])
270        }
271    }
272
273    #[tokio::test]
274    async fn test_async_stream_chunked() {
275        let array = buffer![1i32, 2, 3, 4, 5, 6, 7, 8, 9, 10].into_array();
276        let ipc_buffer = array
277            .to_array_stream()
278            .into_ipc(&SESSION)
279            .collect_to_buffer()
280            .await
281            .unwrap();
282
283        let chunked = ChunkedReader {
284            inner: Cursor::new(ipc_buffer),
285            chunk_size: 3,
286        };
287
288        let reader = AsyncIPCReader::try_new(chunked, &SESSION).await.unwrap();
289
290        let result = reader.read_all().await.unwrap();
291        let expected = buffer![1i32, 2, 3, 4, 5, 6, 7, 8, 9, 10].into_array();
292        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
293    }
294
295    /// Test with 1-byte chunks to stress-test partial read handling.
296    #[tokio::test]
297    async fn test_async_stream_single_byte_chunks() {
298        let array = buffer![42i64, -1, 0, i64::MAX, i64::MIN].into_array();
299        let ipc_buffer = array
300            .to_array_stream()
301            .into_ipc(&SESSION)
302            .collect_to_buffer()
303            .await
304            .unwrap();
305
306        let chunked = ChunkedReader {
307            inner: Cursor::new(ipc_buffer),
308            chunk_size: 1,
309        };
310
311        let reader = AsyncIPCReader::try_new(chunked, &SESSION).await.unwrap();
312
313        let result = reader.read_all().await.unwrap();
314        let expected = buffer![42i64, -1, 0, i64::MAX, i64::MIN].into_array();
315        assert_arrays_eq!(result, expected, &mut SESSION.create_execution_ctx());
316    }
317}