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;
25
26use crate::CoalesceConfig;
27use crate::ReadAtRequest;
28use crate::ReadAtStream;
29use crate::VortexReadAt;
30use crate::runtime::Handle;
31#[cfg(not(target_arch = "wasm32"))]
32use crate::std_file::read_exact_at;
33
34pub const DEFAULT_CONCURRENCY: usize = 192;
36
37pub struct ObjectStoreReadAt {
39 store: Arc<dyn ObjectStore>,
40 path: ObjectPath,
41 uri: Arc<str>,
42 handle: Handle,
43 allocator: BufferAllocatorRef,
44 concurrency: usize,
45 coalesce_config: Option<CoalesceConfig>,
46}
47
48impl ObjectStoreReadAt {
49 pub fn new(store: Arc<dyn ObjectStore>, path: ObjectPath, handle: Handle) -> Self {
51 Self::new_with_allocator(
52 store,
53 path,
54 handle,
55 BufferAllocatorRef::statically_allocated(),
56 )
57 }
58
59 pub fn new_with_allocator(
61 store: Arc<dyn ObjectStore>,
62 path: ObjectPath,
63 handle: Handle,
64 allocator: BufferAllocatorRef,
65 ) -> Self {
66 let uri = Arc::from(path.to_string());
67 Self {
68 store,
69 path,
70 uri,
71 handle,
72 allocator,
73 concurrency: DEFAULT_CONCURRENCY,
74 coalesce_config: Some(CoalesceConfig::object_storage()),
75 }
76 }
77
78 pub fn with_concurrency(mut self, concurrency: usize) -> Self {
80 self.concurrency = concurrency;
81 self
82 }
83
84 pub fn with_coalesce_config(mut self, config: CoalesceConfig) -> Self {
86 self.coalesce_config = Some(config);
87 self
88 }
89}
90
91async fn read_object_store_range(
92 store: Arc<dyn ObjectStore>,
93 path: ObjectPath,
94 io_handle: Handle,
95 allocator: BufferAllocatorRef,
96 request: ReadAtRequest,
97) -> VortexResult<BufferHandle> {
98 let ReadAtRequest {
99 offset,
100 length,
101 alignment,
102 } = request;
103 let range = offset..(offset + length as u64);
104 let mut buffer = allocator.with_capacity_aligned::<u8>(length, alignment);
105 unsafe { buffer.set_len(length) };
107
108 let response = store
109 .get_opts(
110 &path,
111 GetOptions {
112 range: Some(GetRange::Bounded(range.clone())),
113 ..Default::default()
114 },
115 )
116 .await?;
117
118 let buffer = match response.payload {
119 #[cfg(not(target_arch = "wasm32"))]
120 GetResultPayload::File(file, _) => io_handle
121 .spawn_blocking(move || {
122 read_exact_at(&file, buffer.as_mut_slice(), range.start)?;
123 Ok::<_, io::Error>(buffer)
124 })
125 .await
126 .map_err(io::Error::other)?,
127 #[cfg(target_arch = "wasm32")]
128 GetResultPayload::File(..) => {
129 unreachable!("File payload not supported on wasm32")
130 }
131 GetResultPayload::Stream(mut byte_stream) => {
132 let mut written = 0usize;
133 while let Some(bytes) = byte_stream.next().await {
134 let bytes = bytes?;
135 let end = written + bytes.len();
136 vortex_ensure!(
137 end <= length,
138 "Object store stream returned too many bytes: {} > expected {} (range: {:?})",
139 end,
140 length,
141 range
142 );
143 buffer.as_mut_slice()[written..end].copy_from_slice(&bytes);
144 written = end;
145 }
146
147 vortex_ensure!(
148 written == length,
149 "Object store stream returned {} bytes but expected {} bytes (range: {:?})",
150 written,
151 length,
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 for (request, result) in results {
354 let buffer = result?;
355 let offset = usize::try_from(request.offset)?;
356 assert_eq!(buffer.len(), request.length);
357 assert_eq!(
358 buffer.to_host().await.as_slice(),
359 &TEST_DATA[offset..offset + request.length]
360 );
361 }
362 assert_eq!(executor.spawn_io_count.load(Ordering::SeqCst), 1);
363 assert_eq!(executor.spawn_count.load(Ordering::SeqCst), 0);
364
365 Ok(())
366 }
367}