use std::{io::Cursor, sync::Arc, time::Duration};
use arrow_array::RecordBatch;
use bytes::Bytes;
use object_store::{
BackoffConfig, ObjectStore, ObjectStoreExt, ObjectStoreScheme, PutPayload, RetryConfig,
aws::AmazonS3Builder, azure::MicrosoftAzureBuilder, gcp::GoogleCloudStorageBuilder,
http::HttpBuilder, path::Path as StorePath,
};
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use url::Url;
use crate::{
error::{GraphArError, Result},
types::FileType,
};
#[derive(Debug, Clone)]
pub struct RetryOptions {
pub max_retries: usize,
pub init_backoff: Duration,
pub max_backoff: Duration,
pub retry_timeout: Duration,
}
impl Default for RetryOptions {
fn default() -> Self {
Self {
max_retries: 10,
init_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(15),
retry_timeout: Duration::from_secs(3 * 60),
}
}
}
impl RetryOptions {
pub fn none() -> Self {
Self {
max_retries: 0,
..Self::default()
}
}
pub fn new(max_retries: usize, init_backoff: Duration, max_backoff: Duration) -> Self {
Self {
max_retries,
init_backoff,
max_backoff,
..Self::default()
}
}
}
impl From<RetryOptions> for RetryConfig {
fn from(o: RetryOptions) -> Self {
RetryConfig {
backoff: BackoffConfig {
init_backoff: o.init_backoff,
max_backoff: o.max_backoff,
base: 2.0,
},
max_retries: o.max_retries,
retry_timeout: o.retry_timeout,
}
}
}
pub const DEFAULT_MULTIPART_THRESHOLD: usize = 16 * 1024 * 1024;
pub const DEFAULT_MULTIPART_PART_SIZE: usize = 8 * 1024 * 1024;
#[derive(Debug, Clone, Copy)]
pub struct MultipartOptions {
pub threshold: usize,
pub part_size: usize,
}
impl Default for MultipartOptions {
fn default() -> Self {
Self {
threshold: DEFAULT_MULTIPART_THRESHOLD,
part_size: DEFAULT_MULTIPART_PART_SIZE,
}
}
}
impl MultipartOptions {
pub fn new(threshold: usize, part_size: usize) -> Self {
Self {
threshold,
part_size: part_size.max(1),
}
}
}
pub fn read_chunk_bytes(bytes: Bytes, file_type: &FileType) -> Result<Vec<RecordBatch>> {
match file_type {
FileType::Parquet => {
let reader = ParquetRecordBatchReaderBuilder::try_new(bytes)?.build()?;
reader.map(|b| Ok(b?)).collect()
}
FileType::Csv => {
let format = arrow_csv::reader::Format::default().with_header(true);
let (schema, _) = format.infer_schema(Cursor::new(&bytes), Some(100))?;
let reader = arrow_csv::ReaderBuilder::new(Arc::new(schema))
.with_header(true)
.build(Cursor::new(&bytes))?;
reader.map(|b| Ok(b?)).collect()
}
FileType::Json => {
let mut cursor = Cursor::new(&bytes);
let (schema, _) = arrow::json::reader::infer_json_schema(&mut cursor, None)?;
cursor.set_position(0);
let reader = arrow::json::ReaderBuilder::new(Arc::new(schema)).build(cursor)?;
reader.map(|b| Ok(b?)).collect()
}
FileType::Orc => {
let reader = orc_rust::ArrowReaderBuilder::try_new(bytes)?.build();
reader.map(|b| Ok(b?)).collect()
}
FileType::ArrowIpc => {
let reader = arrow::ipc::reader::FileReader::try_new(Cursor::new(bytes), None)?;
reader.map(|b| Ok(b?)).collect()
}
}
}
pub fn write_chunk_bytes(batches: &[RecordBatch], file_type: &FileType) -> Result<Vec<u8>> {
if batches.is_empty() {
return Ok(Vec::new());
}
let schema = batches[0].schema();
let mut out = Vec::new();
match file_type {
FileType::Parquet => {
let mut writer = parquet::arrow::ArrowWriter::try_new(&mut out, schema, None)?;
for batch in batches {
writer.write(batch)?;
}
writer.close()?;
}
FileType::Csv => {
let mut writer = arrow_csv::WriterBuilder::new()
.with_header(true)
.build(&mut out);
for batch in batches {
writer.write(batch)?;
}
}
FileType::Json => {
let mut writer = arrow::json::LineDelimitedWriter::new(&mut out);
writer.write_batches(&batches.iter().collect::<Vec<_>>())?;
writer.finish()?;
}
FileType::Orc => {
let mut writer = orc_rust::ArrowWriterBuilder::new(&mut out, schema).try_build()?;
for batch in batches {
writer.write(batch)?;
}
writer.close()?;
}
FileType::ArrowIpc => {
let mut writer = arrow::ipc::writer::FileWriter::try_new(&mut out, &schema)?;
for batch in batches {
writer.write(batch)?;
}
writer.finish()?;
}
}
Ok(out)
}
#[derive(Clone)]
pub struct ChunkStore {
store: Arc<dyn ObjectStore>,
prefix: StorePath,
multipart: MultipartOptions,
}
impl ChunkStore {
pub fn new(store: Arc<dyn ObjectStore>, prefix: impl Into<StorePath>) -> Self {
Self {
store,
prefix: prefix.into(),
multipart: MultipartOptions::default(),
}
}
pub fn with_multipart(mut self, opts: MultipartOptions) -> Self {
self.multipart = opts;
self
}
pub fn multipart_options(&self) -> MultipartOptions {
self.multipart
}
pub fn from_url(url: &str) -> Result<Self> {
Self::from_url_with_retry(url, RetryOptions::default())
}
pub fn from_url_with_retry(url: &str, retry: RetryOptions) -> Result<Self> {
let url = Url::parse(url).map_err(|e| GraphArError::Other(e.to_string()))?;
let (scheme, prefix) = ObjectStoreScheme::parse(&url)
.map_err(|e| GraphArError::Other(format!("unrecognised object-store url: {e}")))?;
let retry: RetryConfig = retry.into();
let env = || {
std::env::vars().filter(|(k, _)| {
k.starts_with("AWS_") || k.starts_with("GOOGLE_") || k.starts_with("AZURE_")
})
};
let store: Arc<dyn ObjectStore> = match scheme {
ObjectStoreScheme::AmazonS3 => {
let mut b = AmazonS3Builder::new()
.with_url(url.as_str())
.with_retry(retry);
for (k, v) in env() {
if let Ok(key) = k.to_ascii_lowercase().parse() {
b = b.with_config(key, v);
}
}
Arc::new(b.build()?)
}
ObjectStoreScheme::GoogleCloudStorage => {
let mut b = GoogleCloudStorageBuilder::new()
.with_url(url.as_str())
.with_retry(retry);
for (k, v) in env() {
if let Ok(key) = k.to_ascii_lowercase().parse() {
b = b.with_config(key, v);
}
}
Arc::new(b.build()?)
}
ObjectStoreScheme::MicrosoftAzure => {
let mut b = MicrosoftAzureBuilder::new()
.with_url(url.as_str())
.with_retry(retry);
for (k, v) in env() {
if let Ok(key) = k.to_ascii_lowercase().parse() {
b = b.with_config(key, v);
}
}
Arc::new(b.build()?)
}
ObjectStoreScheme::Http => {
let base = &url[..url::Position::BeforePath];
let mut b = HttpBuilder::new()
.with_url(base.to_string())
.with_retry(retry);
for (k, v) in env() {
if let Ok(key) = k.to_ascii_lowercase().parse() {
b = b.with_config(key, v);
}
}
Arc::new(b.build()?)
}
_ => {
let (store, _) = object_store::parse_url(&url)?;
Arc::from(store)
}
};
Ok(Self {
store,
prefix,
multipart: MultipartOptions::default(),
})
}
pub fn local(root: impl AsRef<std::path::Path>) -> Result<Self> {
let store = object_store::local::LocalFileSystem::new_with_prefix(root)?;
Ok(Self {
store: Arc::new(store),
prefix: StorePath::default(),
multipart: MultipartOptions::default(),
})
}
pub fn s3_compatible(
endpoint: &str,
bucket: &str,
access_key: &str,
secret_key: &str,
prefix: &str,
) -> Result<Self> {
Self::s3_compatible_with_retry(
endpoint,
bucket,
access_key,
secret_key,
prefix,
RetryOptions::default(),
)
}
pub fn s3_compatible_with_retry(
endpoint: &str,
bucket: &str,
access_key: &str,
secret_key: &str,
prefix: &str,
retry: RetryOptions,
) -> Result<Self> {
let store = AmazonS3Builder::new()
.with_endpoint(endpoint)
.with_bucket_name(bucket)
.with_access_key_id(access_key)
.with_secret_access_key(secret_key)
.with_region("us-east-1")
.with_allow_http(true)
.with_virtual_hosted_style_request(false)
.with_retry(retry.into())
.build()?;
Ok(Self {
store: Arc::new(store),
prefix: StorePath::from(prefix),
multipart: MultipartOptions::default(),
})
}
fn full_path(&self, rel: &str) -> StorePath {
if self.prefix.as_ref().is_empty() {
StorePath::from(rel)
} else {
StorePath::from(format!("{}/{}", self.prefix, rel))
}
}
pub async fn read_chunk(&self, rel: &str, file_type: &FileType) -> Result<Vec<RecordBatch>> {
let bytes = self.store.get(&self.full_path(rel)).await?.bytes().await?;
read_chunk_bytes(bytes, file_type)
}
pub async fn write_chunk(
&self,
rel: &str,
batches: &[RecordBatch],
file_type: &FileType,
) -> Result<()> {
let result = async {
let payload = write_chunk_bytes(batches, file_type)?;
self.put_object(rel, payload).await
}
.await;
crate::functional_status(
"graphar/chunk_store",
"write_chunk",
result.is_ok(),
rel,
);
result
}
pub async fn get_bytes(&self, rel: &str) -> Result<Bytes> {
Ok(self.store.get(&self.full_path(rel)).await?.bytes().await?)
}
pub async fn put_bytes(&self, rel: &str, bytes: Vec<u8>) -> Result<()> {
self.put_object(rel, bytes).await
}
async fn put_object(&self, rel: &str, bytes: Vec<u8>) -> Result<()> {
let path = self.full_path(rel);
if bytes.len() <= self.multipart.threshold {
self.store.put(&path, bytes.into()).await?;
return Ok(());
}
let part_size = self.multipart.part_size.max(1);
let mut upload = self.store.put_multipart(&path).await?;
let mut offset = 0;
while offset < bytes.len() {
let end = (offset + part_size).min(bytes.len());
let part = PutPayload::from(bytes[offset..end].to_vec());
if let Err(e) = upload.put_part(part).await {
let _ = upload.abort().await;
return Err(e.into());
}
offset = end;
}
if let Err(e) = upload.complete().await {
let _ = upload.abort().await;
return Err(e.into());
}
Ok(())
}
}