1use 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
35pub const DEFAULT_CONCURRENCY: usize = 192;
37
38pub 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 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 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 pub fn with_concurrency(mut self, concurrency: usize) -> Self {
81 self.concurrency = concurrency;
82 self
83 }
84
85 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 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 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}