use std::io::{Result, Error, ErrorKind};
use std::time::SystemTime;
use bytes::Bytes;
use log::{warn, error};
use ulid::Ulid;
use uuid::Uuid;
use aws_sdk_s3::primitives::SdkBody;
use hyperfile::staging::config::StagingConfig;
use hyperfile::staging::s3::S3Staging;
use super::{DirStaging, DirScatterInode, DirScatterInodeOp, CompactLeaseGuard};
use super::{DEFAULT_DIR_INODE_SCATTER_FOLDER, DEFAULT_COMPACT_LEASE_FILE};
use super::{DEFAULT_DIR_INODE_SUFFIX, DEFAULT_DIR_TOMBSTONE_SUFFIX};
use super::{format_lease_body, parse_lease_body, unix_now_ms};
use aws_sdk_s3::error::SdkError;
pub(crate) fn s3_error_kind<E>(err: &SdkError<E>) -> ErrorKind {
match err {
SdkError::TimeoutError(_) => ErrorKind::TimedOut,
SdkError::DispatchFailure(_) => ErrorKind::ConnectionReset,
SdkError::ConstructionFailure(_) => ErrorKind::InvalidInput,
_ => match err.raw_response().map(|r| r.status().as_u16()) {
Some(404) => ErrorKind::NotFound,
Some(403) => ErrorKind::PermissionDenied,
Some(408) => ErrorKind::TimedOut,
Some(409 | 412) => ErrorKind::AlreadyExists,
Some(429 | 500 | 502 | 503 | 504) => ErrorKind::ResourceBusy,
Some(400 | 405 | 411 | 416 | 501) => ErrorKind::InvalidInput,
_ => ErrorKind::Other,
},
}
}
async fn list_scatter_with_prefix(client: &aws_sdk_s3::Client, bucket: &str, prefix: &str)
-> std::io::Result<Vec<DirScatterInode>>
{
let mut stream = client.list_objects_v2()
.bucket(bucket)
.prefix(prefix)
.into_paginator()
.send();
let mut scatters = Vec::new();
while let Some(page) = stream.next().await {
let list_res = page.map_err(|e| {
let kind = s3_error_kind(&e);
Error::new(kind, format!("ListObjectsV2 s3://{}/{}: {}", bucket, prefix, e))
})?;
if let Some(objects) = list_res.contents {
for obj in objects.iter() {
if let Some(key) = obj.key() {
if key.ends_with(DEFAULT_DIR_INODE_SUFFIX)
|| key.ends_with(DEFAULT_DIR_TOMBSTONE_SUFFIX)
{
let st = SystemTime::try_from(
obj.last_modified.expect("unable to get last_modified from object")
).expect("unable to convert DateTime to SystemTime");
scatters.push(DirScatterInode::path_decode(key, st));
}
}
}
}
}
Ok(scatters)
}
impl DirStaging for S3Staging {
async fn list_scatter_inodes(&self) -> Result<Vec<DirScatterInode>> {
list_scatter_with_prefix(&self.client, &self.bucket, &self.scatter_inodes_path()).await
}
async fn list_scatter_inodes_for_name(&self, filename: &str) -> Result<Vec<DirScatterInode>> {
let prefix = format!("{}{}/", self.scatter_inodes_path(), DirScatterInode::encode_filename(filename));
list_scatter_with_prefix(&self.client, &self.bucket, &prefix).await
}
async fn collect_scatter_inodes(&self, v_scatters: Vec<DirScatterInode>) -> Result<Vec<(DirScatterInode, Bytes)>> {
let mut v_res = Vec::new();
for scatter in v_scatters.into_iter() {
let key = &scatter.key;
let res = self.client
.get_object()
.bucket(&self.bucket)
.key(key)
.send()
.await;
match res {
Ok(output) => {
let agg_bytes = output.body.collect().await?;
v_res.push((scatter, agg_bytes.into_bytes()));
},
Err(sdk_err) => {
if sdk_err.as_service_error().is_some_and(|e| e.is_no_such_key()) {
warn!("scatter {} vanished (compacted concurrently), skipping", key);
continue;
}
let err_str = format!("GetObject s3://{}/{} error: {}", self.bucket, key, sdk_err);
error!("{}", err_str);
return Err(Error::new(s3_error_kind(&sdk_err), err_str));
},
}
}
Ok(v_res)
}
async fn remove_scatter_inodes(&self, delete_keys: Vec<String>) -> Result<()> {
let mut err = false;
for keys in delete_keys.chunks(1000) {
let obj_ids = keys.iter().map(|k| {
aws_sdk_s3::types::ObjectIdentifier::builder()
.key(k.to_string())
.build()
.unwrap()
}).collect::<Vec<aws_sdk_s3::types::ObjectIdentifier>>();
let delete = aws_sdk_s3::types::Delete::builder()
.set_objects(Some(obj_ids))
.quiet(true)
.build()
.unwrap();
match self.client.delete_objects()
.bucket(&self.bucket)
.delete(delete)
.send()
.await
{
Ok(output) => {
let failed = output.errors();
if !failed.is_empty() {
err = true;
for e in failed {
error!("delete objects: key={:?} code={:?} message={:?}",
e.key(), e.code(), e.message());
}
}
},
Err(sdk_err) => {
err = true;
error!("delete objects error: {}", sdk_err);
}
}
}
if err {
return Err(Error::new(ErrorKind::Interrupted, "at least one of delete objects op failed"));
}
Ok(())
}
fn to_dir_staging_config(config: &StagingConfig) -> StagingConfig {
let mut config = config.clone();
assert!(!config.root_uri.ends_with('/'));
config.inode_file_uri = format!("{}/inode", config.root_uri);
config
}
async fn emit_scatter_event(&self, filename: &str, child_uuid: &Uuid, buf: &[u8], op: DirScatterInodeOp) -> Result<()> {
let key = DirScatterInode::path_encode(&self.root_path, filename, child_uuid, op.clone() as u8);
let mut builder = self.client
.put_object()
.bucket(&self.bucket)
.key(&key);
builder = match op {
DirScatterInodeOp::Create | DirScatterInodeOp::Update | DirScatterInodeOp::Delete => {
let body = SdkBody::from(buf);
builder.body(body.into())
},
DirScatterInodeOp::PreDelete => {
let body = SdkBody::from(buf);
builder.body(body.into())
},
DirScatterInodeOp::Unknown => {
return Err(Error::new(ErrorKind::InvalidInput,
format!("emit_scatter_event called with Unknown op for s3://{}/{}",
self.bucket, key)));
},
};
builder = builder.if_none_match('*');
match builder.send().await {
Ok(_) => Ok(()),
Err(sdk_err) => {
if let Some(resp) = sdk_err.raw_response() {
let st = resp.status().as_u16();
if st == 412 || st == 409 {
warn!("scatter PUT s3://{}/{} returned {} (treating as already committed)",
self.bucket, key, st);
return Ok(());
}
}
let err_str = format!("PutObject s3://{}/{} error: {}", self.bucket, key, sdk_err);
error!("{}", err_str);
Err(Error::new(s3_error_kind(&sdk_err), err_str))
},
}
}
fn scatter_inodes_path(&self) -> String {
format!("{}/{DEFAULT_DIR_INODE_SCATTER_FOLDER}/", self.root_path)
}
fn compact_lease_path(&self) -> String {
format!("{}/{DEFAULT_COMPACT_LEASE_FILE}", self.root_path)
}
async fn acquire_compact_lease(&self, ttl_ms: u64) -> Result<CompactLeaseGuard> {
let lease_key = self.compact_lease_path();
let holder_id = Ulid::new().to_string();
let now_ms = unix_now_ms();
let expires_at_unix_ms = now_ms.saturating_add(ttl_ms as i64);
let body_str = format_lease_body(&holder_id, expires_at_unix_ms);
let body = SdkBody::from(body_str.as_bytes());
let res = self.client
.put_object()
.bucket(&self.bucket)
.key(&lease_key)
.body(body.into())
.if_none_match('*')
.send()
.await;
match res {
Ok(output) => {
let etag = output.e_tag.unwrap_or_default().replace('"', "");
Ok(CompactLeaseGuard { holder_id, etag, lease_key, expires_at_unix_ms })
},
Err(sdk_err) => {
let is_conflict = sdk_err.raw_response()
.map(|r| matches!(r.status().as_u16(), 412 | 409))
.unwrap_or(false);
if is_conflict {
return try_take_over_compact_lease(
self, &lease_key, &holder_id, ttl_ms, now_ms).await;
}
let err_str = format!(
"PutObject (acquire_compact_lease) s3://{}/{} error: {}",
self.bucket, lease_key, sdk_err);
error!("{}", err_str);
Err(Error::new(s3_error_kind(&sdk_err), err_str))
},
}
}
async fn release_compact_lease(&self, guard: CompactLeaseGuard) -> Result<()> {
let res = self.client
.delete_object()
.bucket(&self.bucket)
.key(&guard.lease_key)
.if_match(&guard.etag)
.send()
.await;
match res {
Ok(_) => Ok(()),
Err(sdk_err) => {
if let Some(resp) = sdk_err.raw_response() {
if resp.status().as_u16() == 412 {
warn!("compact lease s3://{}/{} was taken over before release (holder={})",
self.bucket, guard.lease_key, guard.holder_id);
return Ok(());
}
}
warn!("DeleteObject (release_compact_lease) s3://{}/{} error: {} (lease will expire on TTL)",
self.bucket, guard.lease_key, sdk_err);
Ok(())
},
}
}
}
async fn try_take_over_compact_lease(
staging: &S3Staging,
lease_key: &str,
holder_id: &str,
ttl_ms: u64,
now_ms: i64,
) -> Result<CompactLeaseGuard> {
let res = staging.client
.get_object()
.bucket(&staging.bucket)
.key(lease_key)
.send()
.await;
let (existing_etag, existing_body) = match res {
Ok(out) => {
let etag = out.e_tag.clone().unwrap_or_default().replace('"', "");
let body = out.body.collect().await
.map_err(|e| Error::other(
format!("read compact lease body s3://{}/{}: {}", staging.bucket, lease_key, e)))?
.into_bytes();
(etag, body)
},
Err(sdk_err) => {
let err_str = format!(
"GetObject (try_take_over_compact_lease) s3://{}/{} error: {}",
staging.bucket, lease_key, sdk_err);
warn!("{}", err_str);
return Err(Error::new(ErrorKind::ResourceBusy, err_str));
},
};
let (existing_holder, existing_expires) = parse_lease_body(&existing_body)?;
if existing_expires > now_ms {
return Err(Error::new(ErrorKind::ResourceBusy,
format!("compact lease s3://{}/{} held by {} until {}ms",
staging.bucket, lease_key, existing_holder, existing_expires)));
}
let new_expires = now_ms.saturating_add(ttl_ms as i64);
let body_str = format_lease_body(holder_id, new_expires);
let body = SdkBody::from(body_str.as_bytes());
let res = staging.client
.put_object()
.bucket(&staging.bucket)
.key(lease_key)
.body(body.into())
.if_match(&existing_etag)
.send()
.await;
match res {
Ok(output) => {
let new_etag = output.e_tag.unwrap_or_default().replace('"', "");
warn!("compact lease s3://{}/{} taken over from expired holder {} (new holder {})",
staging.bucket, lease_key, existing_holder, holder_id);
Ok(CompactLeaseGuard {
holder_id: holder_id.to_string(),
etag: new_etag,
lease_key: lease_key.to_string(),
expires_at_unix_ms: new_expires,
})
},
Err(sdk_err) => {
let is_conflict = sdk_err.raw_response()
.map(|r| matches!(r.status().as_u16(), 412 | 409))
.unwrap_or(false);
if is_conflict {
return Err(Error::new(ErrorKind::ResourceBusy,
format!("lost race to take over expired compact lease s3://{}/{}",
staging.bucket, lease_key)));
}
let err_str = format!(
"PutObject (take_over_compact_lease) s3://{}/{} error: {}",
staging.bucket, lease_key, sdk_err);
error!("{}", err_str);
Err(Error::new(s3_error_kind(&sdk_err), err_str))
},
}
}