use std::fs::File;
use std::ops::Range;
use std::sync::Arc;
use crate::local::join_local_io;
#[cfg(windows)]
use crate::local::read_exact_at;
#[cfg(unix)]
use std::os::unix::fs::FileExt;
use bytes::Bytes;
use futures::{
FutureExt,
future::{BoxFuture, Shared},
stream::{self, StreamExt},
};
use lance_core::deepsize::DeepSizeOf;
use lance_core::{Error, Result, error::CloneableError};
use object_store::ObjectStoreExt;
use object_store::{GetOptions, GetResult, ObjectStore, Result as OSResult, path::Path};
use tokio::sync::OnceCell;
use tracing::instrument;
use crate::{
object_store::DEFAULT_CLOUD_IO_PARALLELISM,
traits::{ByteStream, Reader},
};
trait StaticGetRange {
fn path(&self) -> &Path;
fn get_range(&self) -> BoxFuture<'static, OSResult<GetResult>>;
}
struct GetRequest {
object_store: Arc<dyn ObjectStore>,
path: Path,
options: GetOptions,
}
impl StaticGetRange for Arc<GetRequest> {
fn path(&self) -> &Path {
&self.path
}
fn get_range(&self) -> BoxFuture<'static, OSResult<GetResult>> {
let store_and_path = self.clone();
Box::pin(async move {
store_and_path
.object_store
.get_opts(&store_and_path.path, store_and_path.options.clone())
.await
})
}
}
#[derive(Debug)]
pub struct CloudObjectReader {
pub object_store: Arc<dyn ObjectStore>,
pub path: Path,
size: OnceCell<usize>,
block_size: usize,
io_parallelism: usize,
download_retry_count: usize,
}
impl DeepSizeOf for CloudObjectReader {
fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
self.path.as_ref().deep_size_of_children(context)
}
}
impl CloudObjectReader {
pub fn new(
object_store: Arc<dyn ObjectStore>,
path: Path,
block_size: usize,
known_size: Option<usize>,
download_retry_count: usize,
) -> Result<Self> {
Ok(Self {
object_store,
path,
size: OnceCell::new_with(known_size),
block_size,
io_parallelism: DEFAULT_CLOUD_IO_PARALLELISM,
download_retry_count,
})
}
pub fn with_io_parallelism(mut self, io_parallelism: usize) -> Self {
self.io_parallelism = io_parallelism;
self
}
}
async fn do_with_retry<'a, O>(f: impl Fn() -> BoxFuture<'a, OSResult<O>> + Clone) -> OSResult<O> {
let mut retries = 3;
loop {
let f = f.clone();
match f().await {
Ok(val) => return Ok(val),
Err(err) => {
if retries == 0 {
return Err(err);
}
retries -= 1;
}
}
}
}
async fn do_get_with_outer_retry(
download_retry_count: usize,
get_request: Arc<GetRequest>,
desc: impl Fn() -> String,
) -> OSResult<Bytes> {
let mut retries = download_retry_count;
loop {
let get_request_clone = get_request.clone();
let get_result = do_with_retry(move || get_request_clone.get_range()).await?;
match get_result.bytes().await {
Ok(bytes) => return Ok(bytes),
Err(err) => {
if retries == 0 {
log::warn!(
"Failed to download {} from {} after {} attempts. This may indicate that cloud storage is overloaded or your timeout settings are too restrictive. Error details: {:?}",
desc(),
get_request.path(),
download_retry_count,
err
);
return Err(err);
}
log::debug!(
"Retrying {} from {} (remaining retries: {}). Error details: {:?}",
desc(),
get_request.path(),
retries,
err
);
retries -= 1;
}
}
}
}
impl Reader for CloudObjectReader {
fn path(&self) -> &Path {
&self.path
}
fn block_size(&self) -> usize {
self.block_size
}
fn io_parallelism(&self) -> usize {
self.io_parallelism
}
fn size(&self) -> BoxFuture<'_, object_store::Result<usize>> {
Box::pin(async move {
self.size
.get_or_try_init(|| async move {
let meta =
do_with_retry(|| Box::pin(self.object_store.head(&self.path))).await?;
Ok(meta.size as usize)
})
.await
.cloned()
})
}
#[instrument(level = "debug", skip(self))]
fn get_range(&self, range: Range<usize>) -> BoxFuture<'static, OSResult<Bytes>> {
let range = Range {
start: range.start as u64,
end: range.end as u64,
};
let get_request = Arc::new(GetRequest {
object_store: self.object_store.clone(),
path: self.path.clone(),
options: GetOptions {
range: Some(range.clone().into()),
..Default::default()
},
});
let download_retry_count = self.download_retry_count;
Box::pin(async move {
do_get_with_outer_retry(download_retry_count, get_request, move || {
format!("range {}..{}", range.start, range.end)
})
.await
})
}
#[instrument(level = "debug", skip_all)]
fn get_all(&self) -> BoxFuture<'_, OSResult<Bytes>> {
let get_request = Arc::new(GetRequest {
object_store: self.object_store.clone(),
path: self.path.clone(),
options: GetOptions::default(),
});
Box::pin(async move {
do_get_with_outer_retry(self.download_retry_count, get_request, || {
"read_all".to_string()
})
.await
})
}
fn get_stream(&self) -> BoxFuture<'_, OSResult<ByteStream>> {
let get_request = Arc::new(GetRequest {
object_store: self.object_store.clone(),
path: self.path.clone(),
options: GetOptions::default(),
});
Box::pin(async move {
let get_request_clone = get_request.clone();
let get_result = do_with_retry(move || get_request_clone.get_range()).await?;
Ok(get_result.into_stream())
})
}
fn get_range_stream(&self, range: Range<usize>) -> BoxFuture<'_, OSResult<ByteStream>> {
let get_request = Arc::new(GetRequest {
object_store: self.object_store.clone(),
path: self.path.clone(),
options: GetOptions {
range: Some(
Range {
start: range.start as u64,
end: range.end as u64,
}
.into(),
),
..Default::default()
},
});
Box::pin(async move {
let get_request_clone = get_request.clone();
let get_result = do_with_retry(move || get_request_clone.get_range()).await?;
Ok(get_result.into_stream())
})
}
}
#[derive(Debug)]
pub struct SmallReaderInner {
path: Path,
size: usize,
state: std::sync::Mutex<SmallReaderState>,
}
#[derive(Clone, Debug)]
pub struct SmallReader {
inner: Arc<SmallReaderInner>,
}
enum SmallReaderState {
Loading(Shared<BoxFuture<'static, std::result::Result<Bytes, CloneableError>>>),
Finished(std::result::Result<Bytes, CloneableError>),
}
impl std::fmt::Debug for SmallReaderState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Loading(_) => write!(f, "Loading"),
Self::Finished(Ok(data)) => {
write!(f, "Finished({} bytes)", data.len())
}
Self::Finished(Err(err)) => {
write!(f, "Finished({})", err.inner())
}
}
}
}
impl SmallReader {
pub fn new(
store: Arc<dyn ObjectStore>,
path: Path,
download_retry_count: usize,
size: usize,
) -> Self {
let path_ref = path.clone();
let state = SmallReaderState::Loading(
Box::pin(async move {
let object_reader =
CloudObjectReader::new(store, path_ref, 0, None, download_retry_count)
.map_err(CloneableError::new)?;
object_reader
.get_all()
.await
.map_err(|err| CloneableError::new(Error::from(err)))
})
.boxed()
.shared(),
);
Self {
inner: Arc::new(SmallReaderInner {
path,
size,
state: std::sync::Mutex::new(state),
}),
}
}
}
impl SmallReaderInner {
async fn wait(&self) -> OSResult<Bytes> {
let future = {
let state = self.state.lock().unwrap();
match &*state {
SmallReaderState::Loading(future) => future.clone(),
SmallReaderState::Finished(result) => {
return result.clone().map_err(|err| err.into_inner().into());
}
}
};
let result = future.await;
let result_to_return = result.clone().map_err(|err| err.into_inner().into());
let mut state = self.state.lock().unwrap();
if matches!(*state, SmallReaderState::Loading(_)) {
*state = SmallReaderState::Finished(result);
}
result_to_return
}
}
impl Reader for SmallReader {
fn path(&self) -> &Path {
&self.inner.path
}
fn block_size(&self) -> usize {
64 * 1024
}
fn io_parallelism(&self) -> usize {
1024
}
fn size(&self) -> BoxFuture<'_, OSResult<usize>> {
let size = self.inner.size;
Box::pin(async move { Ok(size) })
}
fn get_range(&self, range: Range<usize>) -> BoxFuture<'static, OSResult<Bytes>> {
let inner = self.inner.clone();
Box::pin(async move {
let bytes = inner.wait().await?;
let start = range.start;
let end = range.end;
if start >= bytes.len() || end > bytes.len() {
return Err(object_store::Error::Generic {
store: "memory",
source: format!(
"Invalid range {}..{} for object of size {} bytes",
start,
end,
bytes.len()
)
.into(),
});
}
Ok(bytes.slice(range))
})
}
fn get_all(&self) -> BoxFuture<'_, OSResult<Bytes>> {
Box::pin(async move { self.inner.wait().await })
}
}
pub(crate) fn stream_local_range(
file: Arc<File>,
path: Path,
io_tracker: Arc<crate::utils::tracking_store::IOTracker>,
range: Range<usize>,
chunk_size: usize,
) -> ByteStream {
stream::try_unfold(
(file, path, io_tracker, range.start, range.end),
move |state| async move {
let (file, path, io_tracker, start, end) = state;
if start >= end {
return Ok(None);
}
let next = (start + chunk_size).min(end);
let file_clone = file.clone();
let path_clone = path.clone();
let num_bytes = (next - start) as u64;
let metrics = io_tracker.begin_io("get");
let result = join_local_io(tokio::task::spawn_blocking(move || {
let mut buf = bytes::BytesMut::with_capacity(next - start);
unsafe { buf.set_len(next - start) };
#[cfg(unix)]
file_clone.read_exact_at(buf.as_mut(), start as u64)?;
#[cfg(windows)]
read_exact_at(file_clone, buf.as_mut(), start as u64)?;
Ok::<_, std::io::Error>(buf.freeze())
}))
.await;
metrics.record(&result, num_bytes);
let bytes = result?;
io_tracker.record_read(
"get_range_stream",
path_clone,
num_bytes,
Some(start as u64..next as u64),
);
Ok(Some((bytes, (file, path, io_tracker, next, end))))
},
)
.boxed()
}
impl DeepSizeOf for SmallReader {
fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
let mut size = self.inner.path.as_ref().deep_size_of_children(context);
if let Ok(guard) = self.inner.state.try_lock()
&& let SmallReaderState::Finished(Ok(data)) = &*guard
{
size += data.len();
}
size
}
}
#[cfg(test)]
mod tests {
use std::fmt::{Display, Formatter};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use futures::stream::BoxStream;
use lance_core::utils::testing::{ProxyObjectStore, ProxyObjectStorePolicy};
use object_store::memory::InMemory;
use object_store::{
Attributes, CopyOptions, GetResult, GetResultPayload, ListResult, MultipartUpload,
ObjectMeta, ObjectStore, PutMultipartOptions, PutOptions, PutPayload, PutResult,
Result as OSResult, path::Path,
};
use rstest::rstest;
use super::*;
use crate::utils::tracking_store::{IoStats, IoTrackingStore};
#[derive(Debug)]
struct PartialBodyStore;
impl Display for PartialBodyStore {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "PartialBodyStore")
}
}
#[async_trait::async_trait]
impl ObjectStore for PartialBodyStore {
async fn put_opts(
&self,
_location: &Path,
_payload: PutPayload,
_opts: PutOptions,
) -> OSResult<PutResult> {
unimplemented!("put_opts is not used by these tests")
}
async fn put_multipart_opts(
&self,
_location: &Path,
_opts: PutMultipartOptions,
) -> OSResult<Box<dyn MultipartUpload>> {
unimplemented!("put_multipart_opts is not used by these tests")
}
async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
let range = options
.range
.expect("the reader asks for a range")
.as_range(1024)
.expect("the range fits the object");
Ok(GetResult {
payload: GetResultPayload::Stream(
futures::stream::once(async {
Err(object_store::Error::Generic {
store: "PartialBodyStore",
source: "connection reset".into(),
})
})
.boxed(),
),
meta: ObjectMeta {
location: location.clone(),
last_modified: Default::default(),
size: 1024,
e_tag: None,
version: None,
},
range,
attributes: Attributes::default(),
extensions: Default::default(),
})
}
fn delete_stream(
&self,
_locations: BoxStream<'static, OSResult<Path>>,
) -> BoxStream<'static, OSResult<Path>> {
unimplemented!("delete_stream is not used by these tests")
}
fn list(&self, _prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
unimplemented!("list is not used by these tests")
}
async fn list_with_delimiter(&self, _prefix: Option<&Path>) -> OSResult<ListResult> {
unimplemented!("list_with_delimiter is not used by these tests")
}
async fn copy_opts(&self, _from: &Path, _to: &Path, _opts: CopyOptions) -> OSResult<()> {
unimplemented!("copy_opts is not used by these tests")
}
}
fn reader(store: Arc<dyn ObjectStore>, download_retry_count: usize) -> CloudObjectReader {
CloudObjectReader::new(
store,
Path::from("test/data.lance"),
4096,
None,
download_retry_count,
)
.unwrap()
}
#[rstest]
#[case::no_retries(0, 1)]
#[case::one_retry(1, 2)]
#[case::default(3, 4)]
#[case::raised(10, 11)]
#[tokio::test]
async fn get_range_retries_a_partial_download_as_configured(
#[case] download_retry_count: usize,
#[case] expected_requests: u64,
) {
let stats = Arc::new(Mutex::new(IoStats::default()));
let store = IoTrackingStore::new(Arc::new(PartialBodyStore), stats.clone());
let reader = reader(Arc::new(store), download_retry_count);
reader.get_range(0..128).await.unwrap_err();
assert_eq!(stats.lock().unwrap().read_iops, expected_requests);
}
#[tokio::test]
async fn get_range_leaves_the_request_retries_alone() {
let requests = Arc::new(AtomicUsize::new(0));
let counted = requests.clone();
let mut policy = ProxyObjectStorePolicy::new();
policy.set_before_policy(
"fail_every_request",
Arc::new(move |_, _| {
counted.fetch_add(1, Ordering::Relaxed);
Err(Error::io("connection reset"))
}),
);
let store = ProxyObjectStore::new(Arc::new(InMemory::new()), Arc::new(Mutex::new(policy)));
let reader = reader(Arc::new(store), 10);
reader.get_range(0..128).await.unwrap_err();
assert_eq!(requests.load(Ordering::Relaxed), 4);
}
}