use std::sync::Arc;
use crate::common::chunk;
use futures::stream::{self, TryStreamExt};
use object_store::path::Path;
use object_store::DynObjectStore;
use reqwest::Url;
use ux_serde::u63;
#[derive(Debug, Clone)]
pub struct ObjectStoreClient(Arc<ObjectStoreClientInner>);
#[derive(Debug)]
pub struct ObjectStoreClientInner {
url: Box<str>,
object_store: Box<DynObjectStore>,
base_path: Path,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChunkType {
Input,
Output,
}
impl std::fmt::Display for ChunkType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChunkType::Input => write!(f, "in"),
ChunkType::Output => write!(f, "out"),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ChunkRetrievalError {
#[error("Failed to retrieve chunk ({submission_prefix}, {chunk_index}, {chunk_type}) from object store: {source}")]
ObjectStoreError {
source: object_store::Error,
submission_prefix: Box<str>,
chunk_index: chunk::ChunkIndex,
chunk_type: ChunkType,
},
}
#[derive(thiserror::Error, Debug)]
pub enum ChunkStorageError {
#[error("Failed to store chunk ({submission_prefix}, {chunk_index}, {chunk_type}) to object store: {source}")]
ObjectStoreError {
source: object_store::Error,
submission_prefix: Box<str>,
chunk_index: chunk::ChunkIndex,
chunk_type: ChunkType,
},
#[error("Failed to read chunk element from stream/iterator at index {chunk_index}: ")]
ChunkContentsEvalError {
submission_prefix: Box<str>,
chunk_index: chunk::ChunkIndex,
chunk_type: ChunkType,
source: anyhow::Error,
},
}
#[derive(thiserror::Error, Debug)]
pub enum ChunksStorageError {
#[error(transparent)]
ChunkStorageError(#[from] ChunkStorageError),
#[error("Failed to read chunk element from stream/iterator: {source}")]
ChunkContentsEvalError {
submission_prefix: Box<str>,
chunk_type: ChunkType,
source: anyhow::Error,
},
}
#[derive(thiserror::Error, Debug)]
pub enum NewObjectStoreClientError {
#[error("Failed to parse URL: {0}")]
UrlParseFailure(#[from] url::ParseError),
#[error("URL is not valid object store URL {0}")]
ObjectStoreUrlFailure(#[from] object_store::Error),
}
impl ObjectStoreClient {
pub fn new(
object_store_url: &str,
options: Vec<(String, String)>,
) -> Result<Self, NewObjectStoreClientError> {
let url = Url::parse(object_store_url)?;
let (object_store, base_path) = object_store::parse_url_opts(&url, options)?;
Ok(ObjectStoreClient(Arc::new(ObjectStoreClientInner {
url: object_store_url.into(),
object_store,
base_path,
})))
}
pub async fn store_chunks(
&self,
submission_prefix: &str,
chunk_type: ChunkType,
chunk_contents: impl TryStreamExt<Ok = Vec<u8>, Error = anyhow::Error>,
) -> Result<u63, ChunksStorageError> {
use ChunksStorageError::*;
let chunk_count = chunk_contents
.try_fold(u63::new(0), |chunk_index, chunk_content| async move {
self.store_chunk(
submission_prefix,
chunk_index.into(),
chunk_type,
chunk_content,
)
.await?;
tracing::debug!(
"Upladed chunk {}",
self.chunk_path(submission_prefix, chunk_index.into(), chunk_type)
);
Ok(chunk_index + u63::new(1))
})
.await
.map_err(|e| ChunkContentsEvalError {
source: e,
submission_prefix: submission_prefix.into(),
chunk_type,
})?;
tracing::debug!(
"Finished uploading all {} chunks for submission prefix {}",
chunk_count,
submission_prefix
);
Ok(chunk_count)
}
pub async fn store_chunk(
&self,
submission_prefix: &str,
chunk_index: chunk::ChunkIndex,
chunk_type: ChunkType,
content: Vec<u8>,
) -> Result<(), ChunkStorageError> {
use ChunkStorageError::*;
let path = self.chunk_path(submission_prefix, chunk_index, chunk_type);
self.0
.object_store
.put(&path, content.into())
.await
.map_err(|e| ObjectStoreError {
source: e,
submission_prefix: submission_prefix.into(),
chunk_index,
chunk_type,
})?;
Ok(())
}
pub async fn retrieve_chunk(
&self,
submission_prefix: &str,
chunk_index: chunk::ChunkIndex,
chunk_type: ChunkType,
) -> Result<Vec<u8>, ChunkRetrievalError> {
use ChunkRetrievalError::*;
let res = async move {
let bytes = self
.0
.object_store
.get(&self.chunk_path(submission_prefix, chunk_index, chunk_type))
.await?
.bytes()
.await?
.into();
Ok(bytes)
}
.await;
res.map_err(|e| ObjectStoreError {
source: e,
submission_prefix: submission_prefix.into(),
chunk_index,
chunk_type,
})
}
pub async fn retrieve_chunks<Prefix: Into<String>>(
&self,
submission_prefix: Prefix,
chunk_count: u63,
chunk_type: ChunkType,
) -> impl TryStreamExt<Ok = Vec<u8>, Error = ChunkRetrievalError> + 'static {
let submission_prefix: String = submission_prefix.into();
let initial_state = (self.clone(), submission_prefix, u63::new(0));
stream::unfold(initial_state, move |(client, prefix, index)| async move {
if index >= chunk_count {
return None;
}
let element = client
.retrieve_chunk(&prefix, index.into(), chunk_type)
.await;
let new_state = (client, prefix, index + u63::new(1));
Some((element, new_state))
})
}
pub fn base_path(&self) -> &Path {
&self.0.base_path
}
fn chunk_path(
&self,
submission_prefix: &str,
chunk_index: chunk::ChunkIndex,
chunk_type: ChunkType,
) -> Path {
Path::from(format!(
"{}/{}/{}-{}.bin",
self.0.base_path, submission_prefix, chunk_index, chunk_type
))
}
pub fn url(&self) -> &str {
&self.0.url
}
}