use std::time::Duration;
use aws_sdk_s3::Client;
use uuid::Uuid;
use hyperdir::HyperDirLayout;
use hyperdir::ROOT_DIR_UUID;
use hyperdir::ScatterFirstInterceptor;
use hyperdir::hyper::HyperDir;
use hyperfile::file::flags::FileFlags;
use hyperfile::file::mode::FileMode;
use hyperfile::file::hyper::Hyper;
use hyperfile::staging::s3::S3Staging;
use hyperfile::staging::config::StagingConfig;
use hyperfile::config::HyperFileRuntimeConfig;
use hyperfile::inode::Inode;
use hyperfile::ondisk::InodeRaw;
fn bucket() -> String {
std::env::var("S3_BUCKET").expect("S3_BUCKET not set (source ../env.sh)")
}
async fn make_client() -> Client {
let region = std::env::var("S3_REGION").expect("S3_REGION not set");
let config = aws_config::from_env()
.region(aws_config::Region::new(region))
.load()
.await;
Client::new(&config)
}
fn dir_mode() -> FileMode {
FileMode::from(0o755)
}
async fn purge_prefix(client: &Client, bucket: &str, prefix: &str) {
let mut stream = client.list_objects_v2()
.bucket(bucket)
.prefix(prefix)
.into_paginator()
.send();
let mut keys: Vec<String> = Vec::new();
while let Some(page) = stream.next().await {
let page = page.expect("list for cleanup");
if let Some(objs) = page.contents {
for o in objs {
if let Some(k) = o.key { keys.push(k); }
}
}
}
for chunk in keys.chunks(1000) {
let ids: Vec<_> = chunk.iter().map(|k| {
aws_sdk_s3::types::ObjectIdentifier::builder().key(k).build().unwrap()
}).collect();
let del = aws_sdk_s3::types::Delete::builder()
.set_objects(Some(ids)).build().unwrap();
let _ = client.delete_objects().bucket(bucket).delete(del).send().await;
}
}
fn names(entries: &[hyperdir::file::DirFileEntry]) -> std::collections::BTreeSet<String> {
entries.iter().map(|e| e.name.clone()).collect()
}
#[tokio::test]
#[ignore]
async fn e2e_directory_lifecycle() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("e2e flow");
}
async fn run(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_a, _a_uuid) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "alpha", FileFlags::rdwr(), dir_mode()).await?;
let (_b, b_uuid) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "beta", FileFlags::rdwr(), dir_mode()).await?;
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
let stats = root.fs_compact().await?;
assert_eq!(stats.entries_added, 2, "two children consolidated");
assert_eq!(stats.tombstones_kept, 0);
drop(root);
let root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdonly()).await?;
let n = names(&root.fs_read_dir().await?);
assert!(n.contains("alpha") && n.contains("beta"), "got {:?}", n);
drop(root);
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
root.fs_rename("alpha", "gamma").await?;
let n = names(&root.fs_read_dir().await?);
assert!(n.contains("gamma") && !n.contains("alpha") && n.contains("beta"), "got {:?}", n);
drop(root);
let root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdonly()).await?;
let gamma_uuid = root.fs_read_entry("gamma").await?.uuid;
drop(root);
HyperDir::fs_rename_across(client, layout, bucket, &ROOT_DIR_UUID, "beta", &gamma_uuid, "beta2", None).await?;
let root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdonly()).await?;
let n = names(&root.fs_read_dir().await?);
assert!(!n.contains("beta"), "beta should have moved out of root, got {:?}", n);
drop(root);
let gamma = HyperDir::fs_open_dir(client, layout, bucket, &gamma_uuid, FileFlags::rdonly()).await?;
let gn = names(&gamma.fs_read_dir().await?);
assert!(gn.contains("beta2"), "beta2 should be under gamma, got {:?}", gn);
drop(gamma);
HyperDir::fs_rmdir(client, layout, bucket, &gamma_uuid, "beta2", &b_uuid, None).await?;
{
let mut gamma = HyperDir::fs_open_dir(client, layout, bucket, &gamma_uuid, FileFlags::rdwr()).await?;
gamma.fs_compact().await?;
let gn = names(&gamma.fs_read_dir().await?);
assert!(!gn.contains("beta2"), "beta2 removed, got {:?}", gn);
}
let stats = HyperDir::fs_gc(client, layout, bucket, &gamma_uuid).await?;
assert!(stats.tombstones_reclaimed >= 1, "gc reclaimed the tombstone: {:?}", stats);
Ok(())
}
async fn create_file(
client: &Client,
layout: &HyperDirLayout,
bucket: &str,
parent: &Uuid,
name: &str,
) -> std::io::Result<Uuid> {
let uuid = Uuid::new_v4();
let file_uri = layout.file_uri(bucket, &uuid);
let parent_uri = layout.dir_uri(bucket, parent);
let parent_staging = S3Staging::from(
client,
StagingConfig::new_s3_uri(&parent_uri, None),
HyperFileRuntimeConfig::default(),
).await?;
let interceptor = ScatterFirstInterceptor::new(parent_staging, name, uuid);
let mode = FileMode::from((libc::S_IFREG | 0o644) as libc::mode_t);
let mut f = Hyper::fs_create_with_interceptor(client, &file_uri, FileFlags::rdwr(), mode, interceptor).await?;
let _ = f.fs_release().await?;
Ok(uuid)
}
async fn file_nlink(client: &Client, layout: &HyperDirLayout, bucket: &str, uuid: &Uuid) -> std::io::Result<u64> {
let st = HyperDir::fs_getattr_fast(client, &layout.file_uri(bucket, uuid)).await?;
Ok(st.st_nlink)
}
async fn file_exists(client: &Client, layout: &HyperDirLayout, bucket: &str, uuid: &Uuid) -> bool {
HyperDir::fs_getattr_fast(client, &layout.file_uri(bucket, uuid)).await.is_ok()
}
#[tokio::test]
#[ignore]
async fn e2e_hardlink_nlink() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_hardlink(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("hardlink flow");
}
async fn run_hardlink(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let f = create_file(client, layout, bucket, &ROOT_DIR_UUID, "f1").await?;
{
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
root.fs_compact().await?;
assert!(names(&root.fs_read_dir().await?).contains("f1"));
}
assert_eq!(file_nlink(client, layout, bucket, &f).await?, 1, "fresh file nlink");
HyperDir::fs_link(client, layout, bucket, &ROOT_DIR_UUID, "f2", &f).await?;
assert_eq!(file_nlink(client, layout, bucket, &f).await?, 2, "after link");
{
let root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdonly()).await?;
assert_eq!(root.fs_read_entry("f1").await?.uuid, f);
assert_eq!(root.fs_read_entry("f2").await?.uuid, f);
}
let dup = HyperDir::fs_link(client, layout, bucket, &ROOT_DIR_UUID, "f2", &f).await;
assert_eq!(dup.unwrap_err().kind(), std::io::ErrorKind::AlreadyExists, "duplicate-name link -> AlreadyExists");
assert_eq!(file_nlink(client, layout, bucket, &f).await?, 2, "nlink unchanged after failed link");
HyperDir::fs_unlink(client, layout, bucket, &ROOT_DIR_UUID, "f1", &f, false, None).await?;
{
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
root.fs_compact().await?;
let n = names(&root.fs_read_dir().await?);
assert!(!n.contains("f1") && n.contains("f2"), "got {:?}", n);
}
assert_eq!(file_nlink(client, layout, bucket, &f).await?, 1, "after first unlink");
HyperDir::fs_gc(client, layout, bucket, &ROOT_DIR_UUID).await?;
assert!(file_exists(client, layout, bucket, &f).await, "file kept while a link remains");
HyperDir::fs_unlink(client, layout, bucket, &ROOT_DIR_UUID, "f2", &f, false, None).await?;
{
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
root.fs_compact().await?;
}
HyperDir::fs_gc(client, layout, bucket, &ROOT_DIR_UUID).await?;
assert!(!file_exists(client, layout, bucket, &f).await, "file reclaimed after last unlink");
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_retention() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_retention(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("retention flow");
}
async fn run_retention(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let f = create_file(client, layout, bucket, &ROOT_DIR_UUID, "keep").await?;
{
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
root.fs_compact().await?;
}
HyperDir::fs_unlink(client, layout, bucket, &ROOT_DIR_UUID, "keep", &f, false, Some(Duration::from_secs(3))).await?;
{
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
root.fs_compact().await?;
}
let s1 = HyperDir::fs_gc(client, layout, bucket, &ROOT_DIR_UUID).await?;
assert_eq!(s1.tombstones_reclaimed, 0, "nothing reclaimed before retention: {:?}", s1);
assert!(s1.tombstones_skipped_retention >= 1, "tombstone skipped: {:?}", s1);
assert!(file_exists(client, layout, bucket, &f).await, "file kept within retention");
tokio::time::sleep(Duration::from_secs(4)).await;
let s2 = HyperDir::fs_gc(client, layout, bucket, &ROOT_DIR_UUID).await?;
assert!(s2.tombstones_reclaimed >= 1, "reclaimed after expiry: {:?}", s2);
assert!(!file_exists(client, layout, bucket, &f).await, "file reclaimed after retention");
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_concurrent_compact() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_concurrent_compact(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("concurrent compact flow");
}
async fn run_concurrent_compact(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
for n in 0..5 {
let _ = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, &format!("d{n}"), FileFlags::rdwr(), dir_mode()).await?;
}
let mut a = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
let mut b = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
let (ra, rb) = tokio::join!(a.fs_compact(), b.fs_compact());
for r in [&ra, &rb] {
if let Err(e) = r {
assert!(
matches!(e.kind(), std::io::ErrorKind::ResourceBusy | std::io::ErrorKind::AlreadyExists),
"unexpected compact error: {e:?}"
);
}
}
assert!(ra.is_ok() || rb.is_ok(), "at least one compactor should succeed");
let root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdonly()).await?;
let n = names(&root.fs_read_dir().await?);
for i in 0..5 {
assert!(n.contains(&format!("d{i}")), "missing d{i}, got {:?}", n);
}
Ok(())
}
const COLLIDE_A: &str = "A6bZ)*$e1W{";
const COLLIDE_B: &str = "B%'#]%J G|K";
#[tokio::test]
#[ignore]
async fn e2e_crc64_collision() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_collision(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("collision flow");
}
async fn run_collision(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let h = |s: &str| { let mut c = crc64fast::Digest::new(); c.write(s.as_bytes()); c.sum64() };
assert_eq!(h(COLLIDE_A), h(COLLIDE_B), "test constants must collide");
assert_ne!(COLLIDE_A, COLLIDE_B);
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_a, ua) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, COLLIDE_A, FileFlags::rdwr(), dir_mode()).await?;
let (_b, ub) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, COLLIDE_B, FileFlags::rdwr(), dir_mode()).await?;
assert_ne!(ua, ub);
{
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
let stats = root.fs_compact().await?;
assert_eq!(stats.entries_added, 2, "both colliding entries consolidated: {:?}", stats);
}
{
let root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdonly()).await?;
let n = names(&root.fs_read_dir().await?);
assert!(n.contains(COLLIDE_A) && n.contains(COLLIDE_B), "got {:?}", n);
assert_eq!(root.fs_read_entry(COLLIDE_A).await?.uuid, ua);
assert_eq!(root.fs_read_entry(COLLIDE_B).await?.uuid, ub);
}
HyperDir::fs_rmdir(client, layout, bucket, &ROOT_DIR_UUID, COLLIDE_A, &ua, None).await?;
{
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
root.fs_compact().await?;
let n = names(&root.fs_read_dir().await?);
assert!(!n.contains(COLLIDE_A) && n.contains(COLLIDE_B), "after deleting A: {:?}", n);
assert_eq!(root.fs_read_entry(COLLIDE_B).await?.uuid, ub, "B still resolves after A removed");
}
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_rename_crash_recovery() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_rename_recovery(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("rename recovery flow");
}
async fn run_rename_recovery(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_a, ua) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "A", FileFlags::rdwr(), dir_mode()).await?;
let (_b, ub) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "B", FileFlags::rdwr(), dir_mode()).await?;
{
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
root.fs_compact().await?;
}
let (_x, ux) = HyperDir::fs_create_default(client, layout, bucket, &ua, "x", FileFlags::rdwr(), dir_mode()).await?;
{
let mut a = HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdwr()).await?;
a.fs_compact().await?;
assert!(names(&a.fs_read_dir().await?).contains("x"));
}
HyperDir::fs_rename_emit_intent_only(client, layout, bucket, &ua, "x", &ub, "y").await?;
{
let a = HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdonly()).await?;
assert!(names(&a.fs_read_dir().await?).contains("x"), "x still in A before recovery");
let b = HyperDir::fs_open_dir(client, layout, bucket, &ub, FileFlags::rdonly()).await?;
assert!(!names(&b.fs_read_dir().await?).contains("y"), "y not in B before recovery");
}
let n = HyperDir::fs_recover_renames(client, layout, bucket).await?;
assert!(n >= 1, "recovered at least one intent, got {}", n);
{
let mut a = HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdwr()).await?;
a.fs_compact().await?;
assert!(!names(&a.fs_read_dir().await?).contains("x"), "x removed from A after recovery");
let mut b = HyperDir::fs_open_dir(client, layout, bucket, &ub, FileFlags::rdwr()).await?;
b.fs_compact().await?;
let bn = names(&b.fs_read_dir().await?);
assert!(bn.contains("y"), "y present in B after recovery: {:?}", bn);
assert_eq!(b.fs_read_entry("y").await?.uuid, ux);
}
let n2 = HyperDir::fs_recover_renames(client, layout, bucket).await?;
assert_eq!(n2, 0, "no intents left on second recovery");
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_rename_crash_partial_apply() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_rename_partial_apply(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("rename partial-apply recovery flow");
}
async fn run_rename_partial_apply(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_a, ua) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "A", FileFlags::rdwr(), dir_mode()).await?;
let (_b, ub) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "B", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let ux = create_file(client, layout, bucket, &ua, "x").await?;
{ let mut a = HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdwr()).await?; a.fs_compact().await?; }
HyperDir::fs_rename_apply_partial(client, layout, bucket, &ua, "x", &ub, "y").await?;
{
let a = HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdonly()).await?;
assert!(names(&a.fs_read_dir().await?).contains("x"), "x still in A pre-recovery");
let b = HyperDir::fs_open_dir(client, layout, bucket, &ub, FileFlags::rdonly()).await?;
assert!(names(&b.fs_read_dir().await?).contains("y"), "y already in B pre-recovery");
}
assert_eq!(file_nlink(client, layout, bucket, &ux).await?, 1, "nlink unchanged by a move");
let n = HyperDir::fs_recover_renames(client, layout, bucket).await?;
assert!(n >= 1, "recovered the half-applied rename, got {}", n);
{
let mut a = HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdwr()).await?;
a.fs_compact().await?;
assert!(!names(&a.fs_read_dir().await?).contains("x"), "x removed from A after recovery");
let b = HyperDir::fs_open_dir(client, layout, bucket, &ub, FileFlags::rdonly()).await?;
assert_eq!(b.fs_read_entry("y").await?.uuid, ux, "y in B resolves to the moved child");
}
assert_eq!(file_nlink(client, layout, bucket, &ux).await?, 1, "still exactly one link after recovery");
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_reclaim_intent_recovery() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_reclaim_intent_recovery(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("reclaim intent recovery flow");
}
async fn run_reclaim_intent_recovery(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let kept = create_file(client, layout, bucket, &ROOT_DIR_UUID, "kept").await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let _ = HyperDir::fs_emit_reclaim_intent(client, layout, bucket, &ROOT_DIR_UUID, "kept", &kept, false).await?;
HyperDir::fs_recover_renames(client, layout, bucket).await?;
assert!(file_exists(client, layout, bucket, &kept).await, "a still-named child is not reclaimed");
let orphan = create_file(client, layout, bucket, &ROOT_DIR_UUID, "tmp").await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
HyperDir::fs_unlink(client, layout, bucket, &ROOT_DIR_UUID, "tmp", &orphan, false, None).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; } let _ = HyperDir::fs_emit_reclaim_intent(client, layout, bucket, &ROOT_DIR_UUID, "victim", &orphan, false).await?;
let n = HyperDir::fs_recover_renames(client, layout, bucket).await?;
assert!(n >= 1, "recovered the reclaim intent, got {}", n);
assert!(!file_exists(client, layout, bucket, &orphan).await, "the orphaned displaced child is reclaimed");
assert_eq!(HyperDir::fs_recover_renames(client, layout, bucket).await?, 0, "no intents left on second recovery");
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_unlink_crash_after_tombstone() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_unlink_crash_after_tombstone(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("unlink phase-2 crash flow");
}
async fn run_unlink_crash_after_tombstone(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let f = create_file(client, layout, bucket, &ROOT_DIR_UUID, "x").await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
HyperDir::fs_unlink_crash_after_tombstone(client, layout, bucket, &ROOT_DIR_UUID, "x", &f).await?;
assert_eq!(file_nlink(client, layout, bucket, &f).await?, 1, "nlink left stale-high (>0)");
let _ = HyperDir::fs_gc(client, layout, bucket, &ROOT_DIR_UUID).await?;
assert!(file_exists(client, layout, bucket, &f).await, "fs_gc must not reclaim a stale-high-nlink child");
let n = HyperDir::fs_gc_orphans(client, layout, bucket, Duration::from_millis(0)).await?;
assert!(n >= 1, "orphan sweep reclaimed the file, got {}", n);
assert!(!file_exists(client, layout, bucket, &f).await, "nameless stale-nlink file reclaimed by orphan GC");
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_link_crash_overcount() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_link_crash_overcount(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("link crash over-count flow");
}
async fn run_link_crash_overcount(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let f = create_file(client, layout, bucket, &ROOT_DIR_UUID, "x").await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
HyperDir::fs_link_bump_only(client, layout, bucket, &f).await?;
assert_eq!(file_nlink(client, layout, bucket, &f).await?, 2, "over-counted nlink");
HyperDir::fs_unlink(client, layout, bucket, &ROOT_DIR_UUID, "x", &f, false, None).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
assert_eq!(file_nlink(client, layout, bucket, &f).await?, 1, "stale-high nlink survives the unlink");
let _ = HyperDir::fs_gc(client, layout, bucket, &ROOT_DIR_UUID).await?;
assert!(file_exists(client, layout, bucket, &f).await, "fs_gc leaves the over-counted file");
let n = HyperDir::fs_gc_orphans(client, layout, bucket, Duration::from_millis(0)).await?;
assert!(n >= 1, "orphan sweep reclaimed the leak, got {}", n);
assert!(!file_exists(client, layout, bucket, &f).await, "link-crash over-count reclaimed by orphan GC");
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_orphan_dir_gc() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_orphan_dir_gc(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("orphan dir gc flow");
}
async fn run_orphan_dir_gc(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_kept, ukept) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "kept", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let orphan = Uuid::new_v4();
{
let mut d = HyperDir::fs_create(client, &layout.dir_uri(bucket, &orphan), FileFlags::rdwr(), dir_mode()).await?;
d.fs_flush().await?;
}
assert!(dir_exists(client, layout, bucket, &orphan).await, "orphan dir created");
let n = HyperDir::fs_gc_orphans(client, layout, bucket, Duration::from_millis(0)).await?;
assert!(n >= 1, "orphan sweep reclaimed the nameless dir, got {}", n);
assert!(!dir_exists(client, layout, bucket, &orphan).await, "nameless empty dir reclaimed");
assert!(dir_exists(client, layout, bucket, &ukept).await, "referenced dir left intact");
Ok(())
}
async fn dir_exists(client: &Client, layout: &HyperDirLayout, bucket: &str, uuid: &Uuid) -> bool {
HyperDir::fs_getattr_fast(client, &layout.dir_uri(bucket, uuid)).await.is_ok()
}
async fn object_exists(client: &Client, bucket: &str, key: &str) -> bool {
client.head_object().bucket(bucket).key(key).send().await.is_ok()
}
async fn keys_with_suffix(client: &Client, bucket: &str, prefix: &str, suffix: &str) -> Vec<String> {
let mut out = Vec::new();
let mut stream = client.list_objects_v2().bucket(bucket).prefix(prefix).into_paginator().send();
while let Some(page) = stream.next().await {
let page = page.expect("list");
if let Some(objs) = page.contents {
for o in objs { if let Some(k) = o.key { if k.ends_with(suffix) { out.push(k); } } }
}
}
out
}
#[tokio::test]
#[ignore]
async fn e2e_link_intent_recovery() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_link_intent_recovery(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("link intent recovery flow");
}
async fn run_link_intent_recovery(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
use hyperdir::Outcome;
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let f = create_file(client, layout, bucket, &ROOT_DIR_UUID, "x").await?;
let g = create_file(client, layout, bucket, &ROOT_DIR_UUID, "g").await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let fresh = HyperDir::fs_link_crash_after_intent(client, layout, bucket, &ROOT_DIR_UUID, "gf", &g, 0).await?;
assert_eq!(HyperDir::recover_link(client, layout, bucket, &fresh).await?, Outcome::Noop, "fresh intent deferred");
assert!(object_exists(client, bucket, &fresh).await, "fresh intent retained");
let key = HyperDir::fs_link_crash_after_intent(client, layout, bucket, &ROOT_DIR_UUID, "y", &f, 120_000).await?;
assert_eq!(file_nlink(client, layout, bucket, &f).await?, 2, "phantom bump present");
{
let r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdonly()).await?;
assert!(!names(&r.fs_read_dir().await?).contains("y"), "entry never committed");
}
let file_prefix = format!("{}/", layout.file_key(&f));
assert_eq!(HyperDir::recover_link(client, layout, bucket, &key).await?, Outcome::Done, "stale link recovered");
assert!(!object_exists(client, bucket, &key).await, "intent deleted");
assert_eq!(keys_with_suffix(client, bucket, &file_prefix, ".unref").await.len(), 1, "one .unref marker");
assert_eq!(HyperDir::reclaim_if_unreferenced(client, layout, bucket, &f).await?, Outcome::Noop, "still referenced");
assert!(file_exists(client, layout, bucket, &f).await, "file kept while named");
assert_eq!(HyperDir::recover_link(client, layout, bucket, &key).await?, Outcome::Noop, "recovery idempotent");
assert_eq!(keys_with_suffix(client, bucket, &file_prefix, ".unref").await.len(), 1, "marker count unchanged");
HyperDir::fs_unlink(client, layout, bucket, &ROOT_DIR_UUID, "x", &f, false, None).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
assert_eq!(HyperDir::reclaim_if_unreferenced(client, layout, bucket, &f).await?, Outcome::Done, "now unreferenced");
assert!(!file_exists(client, layout, bucket, &f).await, "file reclaimed via .unref arithmetic");
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_per_key_recovery() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_per_key_recovery(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("per-key recovery flow");
}
async fn run_per_key_recovery(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
use hyperdir::Outcome;
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_a, ua) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "A", FileFlags::rdwr(), dir_mode()).await?;
let (_b, ub) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "B", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let ux = create_file(client, layout, bucket, &ua, "x").await?;
{ let mut a = HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdwr()).await?; a.fs_compact().await?; }
HyperDir::fs_rename_emit_intent_only(client, layout, bucket, &ua, "x", &ub, "y").await?;
let ikey = keys_with_suffix(client, bucket, &layout.txn_prefix(), ".intent").await
.into_iter().next().expect("an .intent key");
assert_eq!(HyperDir::recover_intent(client, layout, bucket, &ikey).await?, Outcome::Done, "rename recovered by key");
{
let mut a = HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdwr()).await?; a.fs_compact().await?;
assert!(!names(&a.fs_read_dir().await?).contains("x"), "x removed from A");
let mut b = HyperDir::fs_open_dir(client, layout, bucket, &ub, FileFlags::rdwr()).await?; b.fs_compact().await?;
assert_eq!(b.fs_read_entry("y").await?.uuid, ux, "y in B resolves to the moved child");
}
assert_eq!(HyperDir::recover_intent(client, layout, bucket, &ikey).await?, Outcome::Noop, "recover_intent idempotent");
let orphan = create_file(client, layout, bucket, &ROOT_DIR_UUID, "tmp").await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
HyperDir::fs_unlink(client, layout, bucket, &ROOT_DIR_UUID, "tmp", &orphan, false, None).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let rkey = HyperDir::fs_emit_reclaim_intent(client, layout, bucket, &ROOT_DIR_UUID, "victim", &orphan, false).await?;
assert_eq!(HyperDir::process_reclaim(client, layout, bucket, &rkey).await?, Outcome::Done, "displaced orphan reclaimed by key");
assert!(!file_exists(client, layout, bucket, &orphan).await, "orphan storage gone");
assert_eq!(HyperDir::process_reclaim(client, layout, bucket, &rkey).await?, Outcome::Noop, "process_reclaim idempotent");
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_orphan_gc_failclosed_on_unreadable_dir() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_orphan_gc_failclosed(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("orphan gc fail-closed flow");
}
async fn run_orphan_gc_failclosed(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_a, ua) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "A", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let f = create_file(client, layout, bucket, &ua, "f").await?; { let mut a = HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdwr()).await?; a.fs_compact().await?; }
let dirs = [ROOT_DIR_UUID, ua];
let n = HyperDir::fs_gc_orphans_from(client, layout, bucket, &[f], &dirs, Duration::from_millis(0)).await?;
assert_eq!(n, 0, "clean scan reclaims nothing");
assert!(file_exists(client, layout, bucket, &f).await, "referenced file present after clean scan");
let a_inode = format!("{}/inode", layout.dir_key(&ua));
client.put_object().bucket(bucket).key(&a_inode)
.body(aws_sdk_s3::primitives::ByteStream::from_static(b"corrupt-not-an-inode"))
.send().await.expect("corrupt A inode");
assert!(HyperDir::fs_open_dir(client, layout, bucket, &ua, FileFlags::rdonly()).await.is_err(),
"precondition: A is now unreadable");
let res = HyperDir::fs_gc_orphans_from(client, layout, bucket, &[f], &dirs, Duration::from_millis(0)).await;
assert!(file_exists(client, layout, bucket, &f).await,
"data loss: orphan GC deleted a referenced file after its parent dir read failed (res={:?})", res);
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_reclaim_recovery_failclosed_on_unreadable_dst() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_reclaim_failclosed(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("reclaim recovery fail-closed flow");
}
async fn run_reclaim_failclosed(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_d, ud) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "D", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let f = create_file(client, layout, bucket, &ud, "victim").await?; { let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
let rkey = HyperDir::fs_emit_reclaim_intent(client, layout, bucket, &ud, "victim", &f, false).await?;
{
let d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await?;
assert!(d.fs_read_dir().await?.iter().any(|e| e.name == "victim" && e.uuid == f), "victim still names F");
}
assert!(file_exists(client, layout, bucket, &f).await, "F present before recovery");
assert!(object_exists(client, bucket, &rkey).await, "reclaim intent present before recovery");
let d_inode = format!("{}/inode", layout.dir_key(&ud));
client.put_object().bucket(bucket).key(&d_inode)
.body(aws_sdk_s3::primitives::ByteStream::from_static(b"corrupt-not-an-inode"))
.send().await.expect("corrupt D inode");
assert!(HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await.is_err(),
"precondition: D is now unreadable");
let res = HyperDir::process_reclaim(client, layout, bucket, &rkey).await;
assert!(file_exists(client, layout, bucket, &f).await,
"dangling: reclaim recovery deleted a still-named object after dst_parent read failed (res={:?})", res);
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_predelete_recreate_race_keeps_new_file() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_predelete_recreate_race(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("predelete/recreate race flow");
}
async fn run_predelete_recreate_race(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_d, ud) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "D", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let a = create_file(client, layout, bucket, &ud, "x").await?; { let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
HyperDir::fs_unlink(client, layout, bucket, &ud, "x", &a, false, None).await?;
{ let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact_phase1_only().await?; }
{
let d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await?;
assert!(!names(&d.fs_read_dir().await?).contains("x"), "x absent in the window");
}
let b = create_file(client, layout, bucket, &ud, "x").await?; assert_ne!(a, b);
HyperDir::fs_unlink_crash_after_tombstone(client, layout, bucket, &ud, "x", &a).await?;
{
let d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await?;
let entries = d.fs_read_dir().await?;
let x = entries.iter().find(|e| e.name == "x");
assert!(x.map(|e| e.uuid) == Some(b),
"recreated file B shadowed by a late tombstone for A (read_dir x = {:?})", x.map(|e| e.uuid));
}
{ let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
assert!(file_exists(client, layout, bucket, &b).await, "recreated file B lost after compaction");
{
let d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await?;
assert!(d.fs_read_dir().await?.iter().any(|e| e.name == "x" && e.uuid == b), "x must resolve to B after compaction");
}
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_concurrent_create_same_name_no_orphan() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_concurrent_create_no_orphan(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("concurrent same-name create flow");
}
async fn run_concurrent_create_no_orphan(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_d, ud) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "D", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let a = create_file(client, layout, bucket, &ud, "x").await?;
let b = create_file(client, layout, bucket, &ud, "x").await?;
assert_ne!(a, b);
assert!(file_exists(client, layout, bucket, &a).await && file_exists(client, layout, bucket, &b).await,
"both file objects exist right after the racing creates");
{ let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
let (winner, loser) = {
let d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await?;
let entries = d.fs_read_dir().await?;
let xs: Vec<_> = entries.iter().filter(|e| e.name == "x").collect();
assert_eq!(xs.len(), 1, "exactly one entry named x after compaction");
let w = xs[0].uuid;
assert!(w == a || w == b, "x resolves to one of the two created identities");
(w, if w == a { b } else { a })
};
assert!(file_exists(client, layout, bucket, &winner).await, "winner file object must survive");
assert!(!file_exists(client, layout, bucket, &loser).await,
"losing same-name create left a nameless orphan (file object {} still present)", loser);
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_create_over_committed_name_no_orphan() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_create_over_committed_no_orphan(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("create-over-committed flow");
}
async fn run_create_over_committed_no_orphan(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_d, ud) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "D", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let a = create_file(client, layout, bucket, &ud, "x").await?;
{ let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
assert!(names(&HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await?.fs_read_dir().await?).contains("x"));
let b = create_file(client, layout, bucket, &ud, "x").await?;
assert_ne!(a, b);
{ let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
{
let d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await?;
let entries = d.fs_read_dir().await?;
assert!(entries.iter().any(|e| e.name == "x" && e.uuid == b), "x must resolve to B");
}
assert!(file_exists(client, layout, bucket, &b).await, "winner B must survive");
assert!(!file_exists(client, layout, bucket, &a).await,
"displaced A left a nameless orphan (file object {} still present)", a);
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_rename_over_uncaught_target_no_orphan() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_rename_over_uncaught_target(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("rename-over-uncaught-target flow");
}
async fn run_rename_over_uncaught_target(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_s, us) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "S", FileFlags::rdwr(), dir_mode()).await?;
let (_d, ud) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "D", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let child = create_file(client, layout, bucket, &us, "x").await?;
{ let mut s = HyperDir::fs_open_dir(client, layout, bucket, &us, FileFlags::rdwr()).await?; s.fs_compact().await?; }
let c = create_file(client, layout, bucket, &ud, "y").await?;
{ let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
assert!(file_exists(client, layout, bucket, &c).await);
assert_ne!(child, c);
HyperDir::fs_rename_across(client, layout, bucket, &us, "x", &ud, "y", None).await?;
{ let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
{
let d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await?;
let entries = d.fs_read_dir().await?;
assert!(entries.iter().any(|e| e.name == "y" && e.uuid == child), "y must resolve to the moved child");
}
assert!(file_exists(client, layout, bucket, &child).await, "moved child must survive");
assert!(!file_exists(client, layout, bucket, &c).await,
"rename displaced target C left a nameless orphan (file object {} still present)", c);
{
let s = HyperDir::fs_open_dir(client, layout, bucket, &us, FileFlags::rdonly()).await?;
assert!(!names(&s.fs_read_dir().await?).contains("x"), "source name removed");
}
Ok(())
}
#[tokio::test]
#[ignore]
async fn e2e_rename_replace_committed_reclaims_once() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = run_rename_replace_committed(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("rename-replace-committed flow");
}
async fn run_rename_replace_committed(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let (_s, us) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "S", FileFlags::rdwr(), dir_mode()).await?;
let (_d, ud) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "D", FileFlags::rdwr(), dir_mode()).await?;
{ let mut r = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?; r.fs_compact().await?; }
let child = create_file(client, layout, bucket, &us, "x").await?;
{ let mut s = HyperDir::fs_open_dir(client, layout, bucket, &us, FileFlags::rdwr()).await?; s.fs_compact().await?; }
let dold = create_file(client, layout, bucket, &ud, "y").await?;
{ let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
assert_ne!(child, dold);
let rkey = HyperDir::fs_emit_reclaim_intent(client, layout, bucket, &ud, "y", &dold, false).await?;
HyperDir::fs_rename_across(client, layout, bucket, &us, "x", &ud, "y", Some(&rkey)).await?;
let _ = HyperDir::fs_delete_intent(client, bucket, &rkey).await;
{ let mut d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdwr()).await?; d.fs_compact().await?; }
{
let d = HyperDir::fs_open_dir(client, layout, bucket, &ud, FileFlags::rdonly()).await?;
assert!(d.fs_read_dir().await?.iter().any(|e| e.name == "y" && e.uuid == child), "y resolves to the moved child");
}
assert!(file_exists(client, layout, bucket, &child).await, "moved child survives");
assert!(!file_exists(client, layout, bucket, &dold).await, "replaced committed target must be reclaimed");
Ok(())
}
fn inline_inode(data: &[u8]) -> InodeRaw {
let mut raw = Inode::default_file().to_raw(unsafe { std::mem::zeroed() });
raw.i_mode = 0o100_644; raw.set_inline(data);
raw
}
#[tokio::test]
#[ignore]
async fn e2e_inline_survives_compaction_and_rename() {
let client = make_client().await;
let bucket = bucket();
let base = format!("hyperdir-e2e/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&base);
let result = inline_durability_flow(&client, &bucket, &layout).await;
purge_prefix(&client, &bucket, &format!("{}/", base)).await;
result.expect("inline durability flow");
}
async fn inline_durability_flow(client: &Client, bucket: &str, layout: &HyperDirLayout) -> std::io::Result<()> {
let _root = HyperDir::fs_create_root(client, layout, bucket, FileFlags::rdwr(), dir_mode()).await?;
let v1 = b"inline payload v1".to_vec();
let uuid = HyperDir::fs_create_inline(client, layout, bucket, &ROOT_DIR_UUID, "inl", &inline_inode(&v1)).await?;
let root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdonly()).await?;
let ent = root.fs_read_dir().await?.into_iter().find(|e| e.name == "inl")
.expect("inl present pre-compaction");
assert_eq!(ent.inline.as_deref(), Some(&v1[..]), "inline bytes visible pre-compaction");
drop(root);
let v2 = b"inline payload v2 (still tiny <=80B)".to_vec();
assert!(v2.len() <= InodeRaw::inline_cap());
HyperDir::fs_update_inline(client, layout, bucket, &ROOT_DIR_UUID, "inl", &uuid, &inline_inode(&v2)).await?;
let mut root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdwr()).await?;
root.fs_compact().await?;
drop(root);
let root = HyperDir::fs_open_root(client, layout, bucket, FileFlags::rdonly()).await?;
let ent = root.fs_read_dir().await?.into_iter().find(|e| e.name == "inl")
.expect("inl present post-compaction");
assert_eq!(ent.inline.as_deref(), Some(&v2[..]), "inline bytes survive compaction");
assert_eq!(ent.inode.to_stat(0, 0).st_size as usize, v2.len(), "inline size survives compaction");
drop(root);
let r = HyperDir::fs_getattr_fast(client, &layout.file_uri(bucket, &uuid)).await;
assert!(matches!(&r, Err(e) if e.kind() == std::io::ErrorKind::NotFound),
"inline file must have no FILE object, got {r:?}");
let (_d, d_uuid) = HyperDir::fs_create_default(client, layout, bucket, &ROOT_DIR_UUID, "sub", FileFlags::rdwr(), dir_mode()).await?;
HyperDir::fs_rename_across(client, layout, bucket, &ROOT_DIR_UUID, "inl", &d_uuid, "moved", None).await?;
let sub = HyperDir::fs_open_dir(client, layout, bucket, &d_uuid, FileFlags::rdonly()).await?;
let ent = sub.fs_read_dir().await?.into_iter().find(|e| e.name == "moved")
.expect("moved present after cross-dir rename");
assert_eq!(ent.inline.as_deref(), Some(&v2[..]), "inline bytes survive cross-dir rename");
drop(sub);
let r = HyperDir::fs_getattr_fast(client, &layout.file_uri(bucket, &uuid)).await;
assert!(matches!(&r, Err(e) if e.kind() == std::io::ErrorKind::NotFound),
"still inline (no FILE object) after rename, got {r:?}");
Ok(())
}