Skip to main content

vortex_io/object_store/
read_at.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::io;
5use std::sync::Arc;
6
7use futures::FutureExt;
8use futures::SinkExt;
9use futures::StreamExt;
10use futures::channel::mpsc;
11use futures::future::BoxFuture;
12use futures::stream;
13use object_store::GetOptions;
14use object_store::GetRange;
15use object_store::GetResultPayload;
16use object_store::ObjectStore;
17use object_store::ObjectStoreExt;
18use object_store::path::Path as ObjectPath;
19use vortex_array::buffer::BufferHandle;
20use vortex_array::memory::BufferAllocatorRef;
21use vortex_buffer::Alignment;
22use vortex_error::VortexError;
23use vortex_error::VortexResult;
24use vortex_error::vortex_ensure;
25use vortex_error::vortex_ensure_eq;
26
27use crate::CoalesceConfig;
28use crate::ReadAtRequest;
29use crate::ReadAtStream;
30use crate::VortexReadAt;
31use crate::runtime::Handle;
32#[cfg(not(target_arch = "wasm32"))]
33use crate::std_file::read_exact_at;
34
35/// Default number of concurrent requests to allow.
36pub const DEFAULT_CONCURRENCY: usize = 192;
37
38/// An object store backed I/O source.
39pub struct ObjectStoreReadAt {
40    store: Arc<dyn ObjectStore>,
41    path: ObjectPath,
42    uri: Arc<str>,
43    handle: Handle,
44    allocator: BufferAllocatorRef,
45    concurrency: usize,
46    coalesce_config: Option<CoalesceConfig>,
47}
48
49impl ObjectStoreReadAt {
50    /// Create a new object store source.
51    pub fn new(store: Arc<dyn ObjectStore>, path: ObjectPath, handle: Handle) -> Self {
52        Self::new_with_allocator(
53            store,
54            path,
55            handle,
56            BufferAllocatorRef::statically_allocated(),
57        )
58    }
59
60    /// Create a new object store source with a custom writable buffer allocator.
61    pub fn new_with_allocator(
62        store: Arc<dyn ObjectStore>,
63        path: ObjectPath,
64        handle: Handle,
65        allocator: BufferAllocatorRef,
66    ) -> Self {
67        let uri = Arc::from(path.to_string());
68        Self {
69            store,
70            path,
71            uri,
72            handle,
73            allocator,
74            concurrency: DEFAULT_CONCURRENCY,
75            coalesce_config: Some(CoalesceConfig::object_storage()),
76        }
77    }
78
79    /// Set the concurrency for this source.
80    pub fn with_concurrency(mut self, concurrency: usize) -> Self {
81        self.concurrency = concurrency;
82        self
83    }
84
85    /// Set the coalesce config for this source.
86    pub fn with_coalesce_config(mut self, config: CoalesceConfig) -> Self {
87        self.coalesce_config = Some(config);
88        self
89    }
90}
91
92async fn read_object_store_range(
93    store: Arc<dyn ObjectStore>,
94    path: ObjectPath,
95    io_handle: Handle,
96    allocator: BufferAllocatorRef,
97    request: ReadAtRequest,
98) -> VortexResult<BufferHandle> {
99    let ReadAtRequest {
100        offset,
101        length,
102        alignment,
103    } = request;
104    let range = offset..(offset + length as u64);
105    let mut buffer = allocator.with_capacity_aligned::<u8>(length, alignment);
106    // SAFETY: each return path checks that every byte was initialized.
107    unsafe { buffer.set_len(length) };
108
109    let response = store
110        .get_opts(
111            &path,
112            GetOptions {
113                range: Some(GetRange::Bounded(range.clone())),
114                ..Default::default()
115            },
116        )
117        .await?;
118
119    let buffer = match response.payload {
120        #[cfg(not(target_arch = "wasm32"))]
121        GetResultPayload::File(file, _) => io_handle
122            .spawn_blocking(move || {
123                read_exact_at(&file, buffer.as_mut_slice(), range.start)?;
124                Ok::<_, io::Error>(buffer)
125            })
126            .await
127            .map_err(io::Error::other)?,
128        #[cfg(target_arch = "wasm32")]
129        GetResultPayload::File(..) => {
130            unreachable!("File payload not supported on wasm32")
131        }
132        GetResultPayload::Stream(mut byte_stream) => {
133            let mut written = 0usize;
134            while let Some(bytes) = byte_stream.next().await {
135                let bytes = bytes?;
136                let end = written + bytes.len();
137                vortex_ensure!(
138                    end <= length,
139                    "Object store stream returned too many bytes: {} > expected {} (range: {:?})",
140                    end,
141                    length,
142                    range
143                );
144                buffer.as_mut_slice()[written..end].copy_from_slice(&bytes);
145                written = end;
146            }
147
148            vortex_ensure_eq!(
149                written,
150                length,
151                "Object store stream returned too few bytes (range: {:?})",
152                range
153            );
154
155            buffer
156        }
157    };
158
159    Ok(BufferHandle::new_host(buffer.freeze()))
160}
161
162impl VortexReadAt for ObjectStoreReadAt {
163    fn uri(&self) -> Option<&Arc<str>> {
164        Some(&self.uri)
165    }
166
167    fn coalesce_config(&self) -> Option<CoalesceConfig> {
168        self.coalesce_config
169    }
170
171    fn concurrency(&self) -> usize {
172        self.concurrency
173    }
174
175    fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
176        let store = Arc::clone(&self.store);
177        let path = self.path.clone();
178        async move {
179            store
180                .head(&path)
181                .await
182                .map(|h| h.size)
183                .map_err(VortexError::from)
184        }
185        .boxed()
186    }
187
188    fn read_at(
189        &self,
190        offset: u64,
191        length: usize,
192        alignment: Alignment,
193    ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
194        let store = Arc::clone(&self.store);
195        let path = self.path.clone();
196        let handle = self.handle.clone();
197        let allocator = self.allocator.clone();
198        let io_handle = handle.clone();
199        handle
200            .spawn_io(read_object_store_range(
201                store,
202                path,
203                io_handle,
204                allocator,
205                ReadAtRequest::new(offset, length, alignment),
206            ))
207            .boxed()
208    }
209
210    fn read_ranges(&self, requests: Arc<[ReadAtRequest]>) -> ReadAtStream {
211        if requests.is_empty() {
212            return stream::empty().boxed();
213        }
214
215        let store = Arc::clone(&self.store);
216        let path = self.path.clone();
217        let handle = self.handle.clone();
218        let allocator = self.allocator.clone();
219        let concurrency = self.concurrency.max(1);
220        let (mut send, recv) = mpsc::channel(concurrency);
221        let io_handle = handle.clone();
222
223        // A single runtime task drives all GETs, avoiding one spawn per range. Do not use
224        // ObjectStore::get_ranges here: it returns one Vec after every range completes, whereas
225        // VortexReadAt::read_ranges must expose each result as soon as it is ready.
226        let task = handle.spawn_io(async move {
227            let reads = requests.iter().copied().map(|request| {
228                let store = Arc::clone(&store);
229                let path = path.clone();
230                let io_handle = io_handle.clone();
231                let allocator = allocator.clone();
232                async move {
233                    let result =
234                        read_object_store_range(store, path, io_handle, allocator, request).await;
235                    (request, result)
236                }
237            });
238
239            let mut reads = stream::iter(reads).buffer_unordered(concurrency);
240            while let Some(result) = reads.next().await {
241                if send.send(result).await.is_err() {
242                    break;
243                }
244            }
245        });
246
247        async_stream::stream! {
248            let mut recv = recv;
249            while let Some(result) = recv.next().await {
250                yield result;
251            }
252            task.await;
253        }
254        .boxed()
255    }
256}
257
258#[cfg(test)]
259mod tests {
260
261    use std::sync::atomic::AtomicUsize;
262    use std::sync::atomic::Ordering;
263
264    use object_store::PutPayload;
265    use object_store::memory::InMemory;
266
267    use super::*;
268    use crate::runtime::AbortHandle;
269    use crate::runtime::AbortHandleRef;
270    use crate::runtime::Executor;
271
272    const TEST_DATA: &[u8] = b"object store test data";
273
274    #[derive(Default)]
275    struct CountingExecutor {
276        spawn_count: AtomicUsize,
277        spawn_io_count: AtomicUsize,
278    }
279
280    impl Executor for CountingExecutor {
281        fn spawn(&self, fut: BoxFuture<'static, ()>) -> AbortHandleRef {
282            self.spawn_count.fetch_add(1, Ordering::SeqCst);
283            TokioAbortHandle::new_handle(tokio::spawn(fut).abort_handle())
284        }
285
286        fn spawn_io(&self, fut: BoxFuture<'static, ()>) -> AbortHandleRef {
287            self.spawn_io_count.fetch_add(1, Ordering::SeqCst);
288            TokioAbortHandle::new_handle(tokio::spawn(fut).abort_handle())
289        }
290
291        fn spawn_cpu(&self, task: Box<dyn FnOnce() + Send + 'static>) -> AbortHandleRef {
292            TokioAbortHandle::new_handle(tokio::spawn(async move { task() }).abort_handle())
293        }
294
295        fn spawn_blocking_io(&self, task: Box<dyn FnOnce() + Send + 'static>) -> AbortHandleRef {
296            TokioAbortHandle::new_handle(tokio::task::spawn_blocking(task).abort_handle())
297        }
298    }
299
300    struct TokioAbortHandle(tokio::task::AbortHandle);
301
302    impl TokioAbortHandle {
303        fn new_handle(handle: tokio::task::AbortHandle) -> AbortHandleRef {
304            Box::new(Self(handle))
305        }
306    }
307
308    impl AbortHandle for TokioAbortHandle {
309        fn abort(self: Box<Self>) {
310            self.0.abort();
311        }
312    }
313
314    #[tokio::test]
315    async fn read_at_uses_spawn_io() -> anyhow::Result<()> {
316        let executor = Arc::new(CountingExecutor::default());
317        let runtime = Arc::clone(&executor) as Arc<dyn Executor>;
318        let handle = Handle::new(Arc::downgrade(&runtime));
319
320        let store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
321        let path = ObjectPath::from("test.bin");
322        store.put(&path, PutPayload::from_static(TEST_DATA)).await?;
323
324        let reader = ObjectStoreReadAt::new(store, path, handle);
325        let buffer = reader.read_at(7, 5, Alignment::new(1)).await?;
326
327        assert_eq!(buffer.to_host().await.as_slice(), b"store");
328        assert_eq!(executor.spawn_io_count.load(Ordering::SeqCst), 1);
329        assert_eq!(executor.spawn_count.load(Ordering::SeqCst), 0);
330
331        Ok(())
332    }
333
334    #[tokio::test]
335    async fn read_ranges_uses_one_io_task() -> anyhow::Result<()> {
336        let executor = Arc::new(CountingExecutor::default());
337        let runtime = Arc::clone(&executor) as Arc<dyn Executor>;
338        let handle = Handle::new(Arc::downgrade(&runtime));
339
340        let store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
341        let path = ObjectPath::from("test.bin");
342        store.put(&path, PutPayload::from_static(TEST_DATA)).await?;
343
344        let reader = ObjectStoreReadAt::new(store, path, handle);
345        let requests: Arc<[ReadAtRequest]> = Arc::from([
346            ReadAtRequest::new(0, 6, Alignment::new(1)),
347            ReadAtRequest::new(7, 5, Alignment::new(1)),
348            ReadAtRequest::new(18, 4, Alignment::new(1)),
349        ]);
350        let results = reader.read_ranges(requests).collect::<Vec<_>>().await;
351
352        assert_eq!(results.len(), 3);
353        let mut ranges = Vec::new();
354        for (request, result) in results {
355            ranges.push((request.offset, request.length));
356            let buffer = result?;
357            let offset = usize::try_from(request.offset)?;
358            assert_eq!(buffer.len(), request.length);
359            assert_eq!(
360                buffer.to_host().await.as_slice(),
361                &TEST_DATA[offset..offset + request.length]
362            );
363        }
364        ranges.sort_unstable();
365        assert_eq!(ranges, [(0, 6), (7, 5), (18, 4)]);
366        assert_eq!(executor.spawn_io_count.load(Ordering::SeqCst), 1);
367        assert_eq!(executor.spawn_count.load(Ordering::SeqCst), 0);
368
369        Ok(())
370    }
371}