178 lines
6.0 KiB
Rust
178 lines
6.0 KiB
Rust
//! Stress test: multipart upload and verify huge files (4–16 GiB).
|
||
//!
|
||
//! Uses the SDK's multipart_upload convenience method which splits data into
|
||
//! parts and uploads them sequentially via CreateMultipartUpload / UploadPart /
|
||
//! CompleteMultipartUpload.
|
||
//!
|
||
//! Prerequisites: post3-server running on localhost:9000
|
||
//! mise run up && mise run dev
|
||
//!
|
||
//! Run:
|
||
//! cargo run -p post3-sdk --example multipart_upload --release
|
||
//!
|
||
//! Or with custom sizes (in MB) and part size:
|
||
//! POST3_SIZES=4096,8192,16384 POST3_PART_SIZE=64 cargo run -p post3-sdk --example multipart_upload --release
|
||
|
||
use post3_sdk::Post3Client;
|
||
use std::time::Instant;
|
||
|
||
fn generate_data(size_bytes: usize) -> Vec<u8> {
|
||
let mut data = Vec::with_capacity(size_bytes);
|
||
let mut state: u64 = 0xdeadbeef;
|
||
while data.len() < size_bytes {
|
||
state ^= state << 13;
|
||
state ^= state >> 7;
|
||
state ^= state << 17;
|
||
data.extend_from_slice(&state.to_le_bytes());
|
||
}
|
||
data.truncate(size_bytes);
|
||
data
|
||
}
|
||
|
||
fn format_size(bytes: usize) -> String {
|
||
if bytes >= 1024 * 1024 * 1024 {
|
||
format!("{:.1} GiB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
|
||
} else if bytes >= 1024 * 1024 {
|
||
format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
|
||
} else if bytes >= 1024 {
|
||
format!("{:.1} KiB", bytes as f64 / 1024.0)
|
||
} else {
|
||
format!("{} B", bytes)
|
||
}
|
||
}
|
||
|
||
fn format_throughput(bytes: usize, duration: std::time::Duration) -> String {
|
||
let secs = duration.as_secs_f64();
|
||
if secs == 0.0 {
|
||
return "∞".to_string();
|
||
}
|
||
let mb_per_sec = bytes as f64 / (1024.0 * 1024.0) / secs;
|
||
format!("{:.1} MiB/s", mb_per_sec)
|
||
}
|
||
|
||
#[tokio::main]
|
||
async fn main() -> anyhow::Result<()> {
|
||
let endpoint = std::env::var("POST3_ENDPOINT")
|
||
.unwrap_or_else(|_| "http://localhost:9000".to_string());
|
||
let client = Post3Client::new(&endpoint);
|
||
|
||
let sizes_mb: Vec<usize> = std::env::var("POST3_SIZES")
|
||
.unwrap_or_else(|_| "100,1024,4096,8192,16384".to_string())
|
||
.split(',')
|
||
.filter_map(|s| s.trim().parse().ok())
|
||
.collect();
|
||
|
||
// Part size in MiB (default 64 MiB — good balance of part count vs memory)
|
||
let part_size_mb: usize = std::env::var("POST3_PART_SIZE")
|
||
.unwrap_or_else(|_| "64".to_string())
|
||
.parse()
|
||
.unwrap_or(64);
|
||
let part_size = part_size_mb * 1024 * 1024;
|
||
|
||
println!("=== post3 Multipart Upload Stress Test ===");
|
||
println!("Endpoint: {}", endpoint);
|
||
println!("Sizes: {:?} MB", sizes_mb);
|
||
println!("Part size: {} MiB", part_size_mb);
|
||
println!();
|
||
|
||
client.create_bucket("mp-stress").await?;
|
||
|
||
for size_mb in &sizes_mb {
|
||
let size_bytes = size_mb * 1024 * 1024;
|
||
let key = format!("mp-test-{}mb.bin", size_mb);
|
||
let num_parts = (size_bytes + part_size - 1) / part_size;
|
||
|
||
println!("--- {} ({} parts of {} each) ---",
|
||
format_size(size_bytes),
|
||
num_parts,
|
||
format_size(part_size.min(size_bytes)),
|
||
);
|
||
|
||
// Generate data
|
||
print!(" Generating data... ");
|
||
let gen_start = Instant::now();
|
||
let data = generate_data(size_bytes);
|
||
println!("done ({:.1}s)", gen_start.elapsed().as_secs_f64());
|
||
|
||
// Multipart upload
|
||
print!(" Uploading (multipart)... ");
|
||
let upload_start = Instant::now();
|
||
match client.multipart_upload("mp-stress", &key, &data, part_size).await {
|
||
Ok(()) => {
|
||
let upload_dur = upload_start.elapsed();
|
||
println!(
|
||
"done ({:.1}s, {})",
|
||
upload_dur.as_secs_f64(),
|
||
format_throughput(size_bytes, upload_dur)
|
||
);
|
||
}
|
||
Err(e) => {
|
||
println!("FAILED: {}", e);
|
||
println!(" Skipping remaining sizes");
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Head (verify metadata)
|
||
let head = client.head_object("mp-stress", &key).await?;
|
||
if let Some(info) = &head {
|
||
println!(
|
||
" Head: size={}, etag={:?}",
|
||
format_size(info.size as usize),
|
||
info.etag
|
||
);
|
||
// Verify the compound ETag format (md5-N)
|
||
if let Some(ref etag) = info.etag {
|
||
let stripped = etag.trim_matches('"');
|
||
if stripped.contains('-') {
|
||
let parts_str = stripped.split('-').last().unwrap_or("?");
|
||
println!(" ETag format: compound ({} parts)", parts_str);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Download and verify
|
||
print!(" Downloading... ");
|
||
let download_start = Instant::now();
|
||
match client.get_object("mp-stress", &key).await {
|
||
Ok(downloaded) => {
|
||
let download_dur = download_start.elapsed();
|
||
println!(
|
||
"done ({:.1}s, {})",
|
||
download_dur.as_secs_f64(),
|
||
format_throughput(size_bytes, download_dur)
|
||
);
|
||
|
||
print!(" Verifying... ");
|
||
if downloaded.len() != data.len() {
|
||
println!(
|
||
"FAILED: size mismatch (expected {}, got {})",
|
||
data.len(),
|
||
downloaded.len()
|
||
);
|
||
} else if downloaded.as_ref() == data.as_slice() {
|
||
println!("OK (byte-for-byte match)");
|
||
} else {
|
||
let pos = data
|
||
.iter()
|
||
.zip(downloaded.iter())
|
||
.position(|(a, b)| a != b)
|
||
.unwrap_or(0);
|
||
println!("FAILED: mismatch at byte {}", pos);
|
||
}
|
||
}
|
||
Err(e) => {
|
||
println!("FAILED: {}", e);
|
||
}
|
||
}
|
||
|
||
// Cleanup
|
||
client.delete_object("mp-stress", &key).await?;
|
||
println!();
|
||
}
|
||
|
||
client.delete_bucket("mp-stress").await?;
|
||
println!("=== Done ===");
|
||
Ok(())
|
||
}
|