Skip to main content

lance_io/
object_reader.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::fs::File;
5use std::ops::Range;
6use std::sync::Arc;
7
8use crate::local::join_local_io;
9#[cfg(windows)]
10use crate::local::read_exact_at;
11#[cfg(unix)]
12use std::os::unix::fs::FileExt;
13
14use bytes::Bytes;
15use futures::{
16    FutureExt,
17    future::{BoxFuture, Shared},
18    stream::{self, StreamExt},
19};
20use lance_core::deepsize::DeepSizeOf;
21use lance_core::{Error, Result, error::CloneableError};
22use object_store::ObjectStoreExt;
23use object_store::{GetOptions, GetResult, ObjectStore, Result as OSResult, path::Path};
24use tokio::sync::OnceCell;
25use tracing::instrument;
26
27use crate::{
28    object_store::DEFAULT_CLOUD_IO_PARALLELISM,
29    traits::{ByteStream, Reader},
30};
31
32trait StaticGetRange {
33    fn path(&self) -> &Path;
34    fn get_range(&self) -> BoxFuture<'static, OSResult<GetResult>>;
35}
36
37/// A wrapper around an object store and a path that implements a static
38/// get_range method by assuming self is stored in an Arc.
39struct GetRequest {
40    object_store: Arc<dyn ObjectStore>,
41    path: Path,
42    options: GetOptions,
43}
44
45impl StaticGetRange for Arc<GetRequest> {
46    fn path(&self) -> &Path {
47        &self.path
48    }
49
50    fn get_range(&self) -> BoxFuture<'static, OSResult<GetResult>> {
51        let store_and_path = self.clone();
52        Box::pin(async move {
53            store_and_path
54                .object_store
55                .get_opts(&store_and_path.path, store_and_path.options.clone())
56                .await
57        })
58    }
59}
60
61/// Object Reader
62///
63/// Object Store + Base Path
64#[derive(Debug)]
65pub struct CloudObjectReader {
66    // Object Store.
67    pub object_store: Arc<dyn ObjectStore>,
68    // File path
69    pub path: Path,
70    // File size, if known.
71    size: OnceCell<usize>,
72
73    block_size: usize,
74    io_parallelism: usize,
75    download_retry_count: usize,
76}
77
78impl DeepSizeOf for CloudObjectReader {
79    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
80        // Skipping object_store because there is no easy way to do that and it shouldn't be too big
81        self.path.as_ref().deep_size_of_children(context)
82    }
83}
84
85impl CloudObjectReader {
86    /// Create an ObjectReader from URI
87    pub fn new(
88        object_store: Arc<dyn ObjectStore>,
89        path: Path,
90        block_size: usize,
91        known_size: Option<usize>,
92        download_retry_count: usize,
93    ) -> Result<Self> {
94        Ok(Self {
95            object_store,
96            path,
97            size: OnceCell::new_with(known_size),
98            block_size,
99            io_parallelism: DEFAULT_CLOUD_IO_PARALLELISM,
100            download_retry_count,
101        })
102    }
103
104    /// Override the I/O parallelism this reader advertises.
105    ///
106    /// `ObjectStore::open` / `open_with_size` pass their normalized effective
107    /// parallelism (`LANCE_IO_THREADS` override applied, at least 1) so
108    /// consumers that size concurrency windows off the reader honor the
109    /// configured request limit instead of the hardcoded cloud default.
110    pub fn with_io_parallelism(mut self, io_parallelism: usize) -> Self {
111        self.io_parallelism = io_parallelism;
112        self
113    }
114}
115
116// Retries for the initial request are handled by object store, but
117// there are no retries for failures that occur during the streaming
118// of the response body. Thus we add an outer retry loop here.
119async fn do_with_retry<'a, O>(f: impl Fn() -> BoxFuture<'a, OSResult<O>> + Clone) -> OSResult<O> {
120    let mut retries = 3;
121    loop {
122        let f = f.clone();
123        match f().await {
124            Ok(val) => return Ok(val),
125            Err(err) => {
126                if retries == 0 {
127                    return Err(err);
128                }
129                retries -= 1;
130            }
131        }
132    }
133}
134
135// We have a separate retry loop here.  This is because object_store does not
136// attempt retries on downloads that fail during streaming of the response body.
137//
138// However, this failure is pretty common (e.g. timeout) and we want to retry in these
139// situations.  In addition, we provide additional logging information in these
140// failures cases.
141async fn do_get_with_outer_retry(
142    download_retry_count: usize,
143    get_request: Arc<GetRequest>,
144    desc: impl Fn() -> String,
145) -> OSResult<Bytes> {
146    let mut retries = download_retry_count;
147    loop {
148        let get_request_clone = get_request.clone();
149        let get_result = do_with_retry(move || get_request_clone.get_range()).await?;
150        match get_result.bytes().await {
151            Ok(bytes) => return Ok(bytes),
152            Err(err) => {
153                if retries == 0 {
154                    log::warn!(
155                        "Failed to download {} from {} after {} attempts.  This may indicate that cloud storage is overloaded or your timeout settings are too restrictive.  Error details: {:?}",
156                        desc(),
157                        get_request.path(),
158                        download_retry_count,
159                        err
160                    );
161                    return Err(err);
162                }
163                log::debug!(
164                    "Retrying {} from {} (remaining retries: {}).  Error details: {:?}",
165                    desc(),
166                    get_request.path(),
167                    retries,
168                    err
169                );
170                retries -= 1;
171            }
172        }
173    }
174}
175
176impl Reader for CloudObjectReader {
177    fn path(&self) -> &Path {
178        &self.path
179    }
180
181    fn block_size(&self) -> usize {
182        self.block_size
183    }
184
185    fn io_parallelism(&self) -> usize {
186        self.io_parallelism
187    }
188
189    /// Object/File Size.
190    fn size(&self) -> BoxFuture<'_, object_store::Result<usize>> {
191        Box::pin(async move {
192            self.size
193                .get_or_try_init(|| async move {
194                    let meta =
195                        do_with_retry(|| Box::pin(self.object_store.head(&self.path))).await?;
196                    Ok(meta.size as usize)
197                })
198                .await
199                .cloned()
200        })
201    }
202
203    #[instrument(level = "debug", skip(self))]
204    fn get_range(&self, range: Range<usize>) -> BoxFuture<'static, OSResult<Bytes>> {
205        let object_store = self.object_store.clone();
206        let path = self.path.clone();
207        let get_range = Range {
208            start: range.start as u64,
209            end: range.end as u64,
210        };
211        Box::pin(async move {
212            let bytes = do_with_retry(move || {
213                let object_store = object_store.clone();
214                let path = path.clone();
215                let get_range = get_range.clone();
216                Box::pin(async move { object_store.get_ranges(&path, &[get_range]).await })
217            })
218            .await?;
219
220            bytes
221                .into_iter()
222                .next()
223                .ok_or_else(|| object_store::Error::Generic {
224                    store: "CloudObjectReader",
225                    source: "get_ranges returned no bytes".into(),
226                })
227        })
228    }
229
230    #[instrument(level = "debug", skip_all)]
231    fn get_all(&self) -> BoxFuture<'_, OSResult<Bytes>> {
232        let get_request = Arc::new(GetRequest {
233            object_store: self.object_store.clone(),
234            path: self.path.clone(),
235            options: GetOptions::default(),
236        });
237        Box::pin(async move {
238            do_get_with_outer_retry(self.download_retry_count, get_request, || {
239                "read_all".to_string()
240            })
241            .await
242        })
243    }
244
245    fn get_stream(&self) -> BoxFuture<'_, OSResult<ByteStream>> {
246        let get_request = Arc::new(GetRequest {
247            object_store: self.object_store.clone(),
248            path: self.path.clone(),
249            options: GetOptions::default(),
250        });
251        Box::pin(async move {
252            let get_request_clone = get_request.clone();
253            let get_result = do_with_retry(move || get_request_clone.get_range()).await?;
254            Ok(get_result.into_stream())
255        })
256    }
257
258    fn get_range_stream(&self, range: Range<usize>) -> BoxFuture<'_, OSResult<ByteStream>> {
259        let get_request = Arc::new(GetRequest {
260            object_store: self.object_store.clone(),
261            path: self.path.clone(),
262            options: GetOptions {
263                range: Some(
264                    Range {
265                        start: range.start as u64,
266                        end: range.end as u64,
267                    }
268                    .into(),
269                ),
270                ..Default::default()
271            },
272        });
273        Box::pin(async move {
274            let get_request_clone = get_request.clone();
275            let get_result = do_with_retry(move || get_request_clone.get_range()).await?;
276            Ok(get_result.into_stream())
277        })
278    }
279}
280
281#[derive(Debug)]
282pub struct SmallReaderInner {
283    path: Path,
284    size: usize,
285    state: std::sync::Mutex<SmallReaderState>,
286}
287
288/// A reader for a file so small, we just eagerly read it all into memory.
289///
290/// When created, it represents a future that will read the whole file into memory.
291///
292/// On the first read call, it will start the read. Multiple threads can call read at the same time.
293///
294/// Once the read is complete, any thread can call read again to get the result.
295#[derive(Clone, Debug)]
296pub struct SmallReader {
297    inner: Arc<SmallReaderInner>,
298}
299
300enum SmallReaderState {
301    Loading(Shared<BoxFuture<'static, std::result::Result<Bytes, CloneableError>>>),
302    Finished(std::result::Result<Bytes, CloneableError>),
303}
304
305impl std::fmt::Debug for SmallReaderState {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        match self {
308            Self::Loading(_) => write!(f, "Loading"),
309            Self::Finished(Ok(data)) => {
310                write!(f, "Finished({} bytes)", data.len())
311            }
312            Self::Finished(Err(err)) => {
313                write!(f, "Finished({})", err.0)
314            }
315        }
316    }
317}
318
319impl SmallReader {
320    pub fn new(
321        store: Arc<dyn ObjectStore>,
322        path: Path,
323        download_retry_count: usize,
324        size: usize,
325    ) -> Self {
326        let path_ref = path.clone();
327        let state = SmallReaderState::Loading(
328            Box::pin(async move {
329                let object_reader =
330                    CloudObjectReader::new(store, path_ref, 0, None, download_retry_count)
331                        .map_err(CloneableError)?;
332                object_reader
333                    .get_all()
334                    .await
335                    .map_err(|err| CloneableError(Error::from(err)))
336            })
337            .boxed()
338            .shared(),
339        );
340        Self {
341            inner: Arc::new(SmallReaderInner {
342                path,
343                size,
344                state: std::sync::Mutex::new(state),
345            }),
346        }
347    }
348}
349
350impl SmallReaderInner {
351    async fn wait(&self) -> OSResult<Bytes> {
352        let future = {
353            let state = self.state.lock().unwrap();
354            match &*state {
355                SmallReaderState::Loading(future) => future.clone(),
356                SmallReaderState::Finished(result) => {
357                    return result.clone().map_err(|err| err.0.into());
358                }
359            }
360        };
361
362        let result = future.await;
363        let result_to_return = result.clone().map_err(|err| err.0.into());
364        let mut state = self.state.lock().unwrap();
365        if matches!(*state, SmallReaderState::Loading(_)) {
366            *state = SmallReaderState::Finished(result);
367        }
368        result_to_return
369    }
370}
371
372impl Reader for SmallReader {
373    fn path(&self) -> &Path {
374        &self.inner.path
375    }
376
377    fn block_size(&self) -> usize {
378        64 * 1024
379    }
380
381    fn io_parallelism(&self) -> usize {
382        1024
383    }
384
385    /// Object/File Size.
386    fn size(&self) -> BoxFuture<'_, OSResult<usize>> {
387        let size = self.inner.size;
388        Box::pin(async move { Ok(size) })
389    }
390
391    fn get_range(&self, range: Range<usize>) -> BoxFuture<'static, OSResult<Bytes>> {
392        let inner = self.inner.clone();
393        Box::pin(async move {
394            let bytes = inner.wait().await?;
395            let start = range.start;
396            let end = range.end;
397            if start >= bytes.len() || end > bytes.len() {
398                return Err(object_store::Error::Generic {
399                    store: "memory",
400                    source: format!(
401                        "Invalid range {}..{} for object of size {} bytes",
402                        start,
403                        end,
404                        bytes.len()
405                    )
406                    .into(),
407                });
408            }
409            Ok(bytes.slice(range))
410        })
411    }
412
413    fn get_all(&self) -> BoxFuture<'_, OSResult<Bytes>> {
414        Box::pin(async move { self.inner.wait().await })
415    }
416}
417
418pub(crate) fn stream_local_range(
419    file: Arc<File>,
420    path: Path,
421    io_tracker: Arc<crate::utils::tracking_store::IOTracker>,
422    range: Range<usize>,
423    chunk_size: usize,
424) -> ByteStream {
425    stream::try_unfold(
426        (file, path, io_tracker, range.start, range.end),
427        move |state| async move {
428            let (file, path, io_tracker, start, end) = state;
429            if start >= end {
430                return Ok(None);
431            }
432
433            let next = (start + chunk_size).min(end);
434            let file_clone = file.clone();
435            let path_clone = path.clone();
436            let num_bytes = (next - start) as u64;
437            let metrics = io_tracker.begin_io("get");
438            let result = join_local_io(tokio::task::spawn_blocking(move || {
439                let mut buf = bytes::BytesMut::with_capacity(next - start);
440                // Safety: buffer capacity matches the exact number of bytes we read below.
441                unsafe { buf.set_len(next - start) };
442                #[cfg(unix)]
443                file_clone.read_exact_at(buf.as_mut(), start as u64)?;
444                #[cfg(windows)]
445                read_exact_at(file_clone, buf.as_mut(), start as u64)?;
446                Ok::<_, std::io::Error>(buf.freeze())
447            }))
448            .await;
449            metrics.record(&result, num_bytes);
450            let bytes = result?;
451
452            io_tracker.record_read(
453                "get_range_stream",
454                path_clone,
455                num_bytes,
456                Some(start as u64..next as u64),
457            );
458
459            Ok(Some((bytes, (file, path, io_tracker, next, end))))
460        },
461    )
462    .boxed()
463}
464
465impl DeepSizeOf for SmallReader {
466    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
467        let mut size = self.inner.path.as_ref().deep_size_of_children(context);
468
469        if let Ok(guard) = self.inner.state.try_lock()
470            && let SmallReaderState::Finished(Ok(data)) = &*guard
471        {
472            size += data.len();
473        }
474
475        size
476    }
477}