use std::sync::Arc;
use std::pin::Pin;
use std::io::{Result, Error, ErrorKind};
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::collections::BTreeMap;
use aws_sdk_s3::Client;
use crate::{segment::Segment, SegmentId};
use crate::s3commons::S3Ops;
use crate::s3uri::S3Uri;
use super::{WalReadWrite, WalChunkDesc};
pub(crate) struct S3Wal {
pub(crate) client: Client,
pub(crate) bucket: String,
pub(crate) root_path: String,
pub(crate) root_path_slash: String, #[allow(dead_code)]
pub(crate) data_block_size: usize,
pub(crate) last_segid: SegmentId,
pub(crate) seq: Arc<AtomicU64>,
}
impl S3Wal {
pub(crate) fn from_uri(uri: &str, data_block_size: usize, last_segid: SegmentId) -> Result<Option<Box<dyn WalReadWrite + Send>>> {
let Ok(s3uri) = S3Uri::parse(uri) else {
return Err(Error::new(ErrorKind::InvalidInput, "failed to parse wal config from uri"));
};
let bucket = s3uri.bucket.to_string();
let root_path = s3uri.key.trim_end_matches("/").to_string();
let root_path_slash = format!("{}/", root_path);
let config = tokio::task::block_in_place(move || {
tokio::runtime::Handle::current().block_on(async move {
aws_config::load_from_env().await
})
});
let client = Client::new(&config);
let s = Self {
client,
bucket,
root_path,
root_path_slash,
data_block_size,
last_segid,
seq: Arc::new(AtomicU64::new(0)),
};
Ok(Some(Box::new(s)))
}
#[inline]
fn next_seq(&self) -> u64 {
self.seq.fetch_add(1, Ordering::SeqCst)
}
#[inline]
fn reset_seq(&self) {
self.seq.store(0, Ordering::SeqCst);
}
#[inline]
fn encode(&mut self, segid: SegmentId, offset: usize, len: usize) -> String {
let seg_s = Segment::segid_to_staging_file_id(segid);
if self.last_segid != segid {
self.last_segid = segid;
self.reset_seq();
}
let seq = self.next_seq();
format!("{}/{}/{}_{}_{}", self.root_path, seg_s, seq, offset, len)
}
#[inline]
fn encode_static(&self, seq: usize, segid: SegmentId, offset: usize, len: usize) -> String {
let seg_s = Segment::segid_to_staging_file_id(segid);
format!("{}/{}/{}_{}_{}", self.root_path, seg_s, seq, offset, len)
}
#[inline]
fn decode(&self, objname: &str) -> Option<(usize, usize, usize)> {
let parts: Vec<&str> = objname.split('_').collect();
if parts.len() != 3 {
return None;
}
let Ok(seq) = usize::from_str(parts[0]) else {
return None;
};
let Ok(off) = usize::from_str(parts[1]) else {
return None;
};
let Ok(len) = usize::from_str(parts[2]) else {
return None;
};
Some((seq, off, len))
}
}
impl WalReadWrite for S3Wal {
fn write(&mut self, segid: SegmentId, offset: usize, buf: &[u8]) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>> {
let key = self.encode(segid, offset, buf.len());
let buf_dup = unsafe {
std::slice::from_raw_parts(buf.as_ptr() as *const u8, buf.len())
};
let client = self.client.clone();
let bucket = self.bucket.clone();
Box::pin(async move {
S3Ops::do_put_object(&client, &bucket, &key, buf_dup, &None).await.and(Ok(()))
})
}
fn write_zero(&mut self, segid: SegmentId, offset: usize, len: usize) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>> {
let key = self.encode(segid, offset, len);
let client = self.client.clone();
let bucket = self.bucket.clone();
Box::pin(async move {
let zero: Vec<u8> = Vec::new();
S3Ops::do_put_object(&client, &bucket, &key, &zero, &None).await.and(Ok(()))
})
}
fn read(&self, seq: usize, segid: SegmentId, offset: usize, len: usize) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + '_>> {
let key = self.encode_static(seq, segid, offset, len);
let client = self.client.clone();
let bucket = self.bucket.clone();
Box::pin(async move {
let mut buf = Vec::with_capacity(len);
buf.resize(len, 0);
let res = S3Ops::do_get_object(&client, &bucket, &key, &mut buf, None, false).await;
match res {
Ok(_) => Ok(buf),
Err(e) => Err(e),
}
})
}
fn list_segments(&self) -> Pin<Box<dyn Future<Output = Result<Vec<SegmentId>>> + Send + '_>> {
let client = self.client.clone();
let bucket = self.bucket.clone();
let root_path_slash = self.root_path_slash.clone();
Box::pin(async move {
let mut v = Vec::new();
let filter = |c: &aws_sdk_s3::types::CommonPrefix| {
if let Some(prefix) = c.prefix() {
let prefix = prefix.trim_end_matches('/');
let segid_str = prefix.trim_start_matches(&root_path_slash);
if let Ok(segid) = segid_str.parse::<u64>() {
v.push(segid);
}
}
};
let res = S3Ops::do_list_directory(&client, &bucket, &root_path_slash, filter).await;
match res {
Ok(_) => {
v.sort();
Ok(v)
},
Err(e) => Err(e),
}
})
}
fn list_chunks(&self, segid: SegmentId) -> Pin<Box<dyn Future<Output = Result<BTreeMap<usize, WalChunkDesc>>> + Send + '_>> {
let client = self.client.clone();
let bucket = self.bucket.clone();
let wal_segment_root_path = format!("{}{}/", self.root_path_slash, Segment::segid_to_staging_file_id(segid));
Box::pin(async move {
let mut map = BTreeMap::new();
let filter = |o: &aws_sdk_s3::types::Object| {
if let Some(key) = o.key() {
let objname = key.trim_start_matches(&wal_segment_root_path);
if let Some((seq, offset, len)) = self.decode(&objname) {
let ondisk_size = o.size().expect("unable to get object size");
let is_zero = if ondisk_size == 0 {
true
} else {
false
};
map.insert(seq, WalChunkDesc { seq, segid, key: key.to_string(), offset, len, is_zero });
}
}
};
let res = S3Ops::do_list_objects(&client, &bucket, &wal_segment_root_path, filter).await;
match res {
Ok(_) => Ok(map),
Err(e) => Err(e),
}
})
}
fn delete_segment(&self, segid: SegmentId) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>> {
let client = self.client.clone();
let bucket = self.bucket.clone();
let wal_segment_root_path = format!("{}{}/", self.root_path_slash, Segment::segid_to_staging_file_id(segid));
Box::pin(async move {
let mut keys = Vec::new();
{
let keys_ref = &mut keys;
let filter = |o: &aws_sdk_s3::types::Object| {
if let Some(key) = o.key() {
keys_ref.push(key.to_string());
}
};
S3Ops::do_list_objects(&client, &bucket, &wal_segment_root_path, filter).await?;
}
if keys.is_empty() {
return Ok(());
}
S3Ops::do_delete_objects(&client, &bucket, keys).await
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn wal_for_tests(root_path: &str, last_segid: SegmentId) -> S3Wal {
let sdk_config = aws_config::SdkConfig::builder()
.behavior_version(aws_config::BehaviorVersion::latest())
.build();
let client = Client::new(&sdk_config);
let root_path = root_path.to_string();
let root_path_slash = format!("{}/", root_path);
S3Wal {
client,
bucket: "test-bucket".to_string(),
root_path,
root_path_slash,
data_block_size: 4096,
last_segid,
seq: Arc::new(AtomicU64::new(0)),
}
}
#[test]
fn next_seq_increments_monotonically() {
let wal = wal_for_tests("root", 0);
assert_eq!(wal.next_seq(), 0);
assert_eq!(wal.next_seq(), 1);
assert_eq!(wal.next_seq(), 2);
assert_eq!(wal.next_seq(), 3);
}
#[test]
fn reset_seq_restarts_from_zero() {
let wal = wal_for_tests("root", 0);
let _ = wal.next_seq();
let _ = wal.next_seq();
let _ = wal.next_seq();
wal.reset_seq();
assert_eq!(wal.next_seq(), 0);
assert_eq!(wal.next_seq(), 1);
}
#[test]
fn encode_format_matches_expected_scheme() {
let mut wal = wal_for_tests("root", 0);
let key = wal.encode(0, 0, 4096);
assert_eq!(key, "root/0000000000/0_0_4096");
}
#[test]
fn encode_seq_advances_for_same_segid() {
let mut wal = wal_for_tests("root", 0);
assert_eq!(wal.encode(0, 0, 100), "root/0000000000/0_0_100");
assert_eq!(wal.encode(0, 100, 50), "root/0000000000/1_100_50");
assert_eq!(wal.encode(0, 200, 25), "root/0000000000/2_200_25");
}
#[test]
fn encode_resets_seq_when_segid_changes() {
let mut wal = wal_for_tests("root", 0);
let _ = wal.encode(0, 0, 100);
let _ = wal.encode(0, 100, 100);
assert_eq!(wal.encode(1, 0, 200), "root/0000000001/0_0_200");
assert_eq!(wal.encode(1, 200, 50), "root/0000000001/1_200_50");
}
#[test]
fn encode_static_ignores_internal_seq_state() {
let wal = wal_for_tests("root", 99);
let k1 = wal.encode_static(7, 42, 1024, 2048);
let k2 = wal.encode_static(7, 42, 1024, 2048);
assert_eq!(k1, k2);
assert_eq!(k1, "root/0000000042/7_1024_2048");
}
#[test]
fn decode_roundtrip_after_encode() {
let mut wal = wal_for_tests("root", 0);
let key = wal.encode(5, 16384, 8192);
let basename = key.rsplit('/').next().unwrap();
assert_eq!(wal.decode(basename), Some((0, 16384, 8192)));
}
#[test]
fn decode_rejects_malformed_names() {
let wal = wal_for_tests("root", 0);
assert_eq!(wal.decode("0_100"), None);
assert_eq!(wal.decode("0_100_200_extra"), None);
assert_eq!(wal.decode("a_100_200"), None);
assert_eq!(wal.decode("0_x_200"), None);
assert_eq!(wal.decode("0_100_z"), None);
assert_eq!(wal.decode(""), None);
}
#[test]
fn decode_accepts_zero_values() {
let wal = wal_for_tests("root", 0);
assert_eq!(wal.decode("0_0_0"), Some((0, 0, 0)));
}
#[test]
fn encode_does_not_reset_seq_when_same_segid_seen_twice_in_a_row() {
let mut wal = wal_for_tests("root", 0);
let _ = wal.encode(7, 0, 100); let _ = wal.encode(7, 100, 100); let k3 = wal.encode(7, 200, 100); assert!(k3.ends_with("/2_200_100"), "got: {}", k3);
}
}