You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/10/26 20:06:31 UTC

(opendal-yinyang) 01/02: test: keep interleaved content through a namespace-only rename receipt

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch xuanwo/managed-sync-reconciliation
in repository https://gitbox.apache.org/repos/asf/opendal-yinyang.git

commit 59a51bbcf5b6faf75596eae3c42a845d2fed3731
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Tue Oct 27 04:00:22 2026 +0800

    test: keep interleaved content through a namespace-only rename receipt
    
    Rename predicates guard links and entries, never inner file content, so an external v2 can land under a parked rename commit and be accepted with the rename. The regression requires in_sync to imply byte-identical real trees, keeps the unobserved v2 from being folded into the local v1 baseline, and accepts either eventual subtree convergence or a legal core conflict that retains both sides.
    
    Co-authored-by: Chojan Shang <ps...@apache.org>
---
 tests/directory_sync.rs | 192 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 192 insertions(+)

diff --git a/tests/directory_sync.rs b/tests/directory_sync.rs
index 7738fc6..dcdc373 100644
--- a/tests/directory_sync.rs
+++ b/tests/directory_sync.rs
@@ -873,3 +873,195 @@ async fn local_deletion_during_delayed_publication_receipt_stays_pending() {
     );
     assert_eq!(local_tree(&root), remote);
 }
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn namespace_only_rename_receipt_keeps_interleaved_content() {
+    let backend = TestBackend::default();
+    let authority: Arc<dyn Authority> = Arc::new(
+        Fs::create(backend.operator(), BackendProfile::Minio)
+            .await
+            .unwrap(),
+    );
+    let temp = tempfile::tempdir().unwrap();
+    let root = temp.path().join("payload");
+    let state = temp.path().join("state");
+    let dir = root.join("dir");
+    let file = dir.join("file");
+    std::fs::create_dir_all(&dir).unwrap();
+    std::fs::write(&file, b"v1").unwrap();
+
+    // A common baseline for the directory subtree first.
+    let mut sync = DirectorySync::init(authority.clone(), &state, &root)
+        .await
+        .unwrap();
+    let first = sync.run().await.unwrap();
+    assert_eq!(first.published_local, 2);
+    assert!(first.conflicts.is_empty());
+    assert_eq!(local_tree(&root), remote_tree(&authority).await);
+    drop(sync);
+
+    // The next run renames the directory locally while the remote is observed
+    // unchanged, then parks the rename commit before it executes.
+    std::fs::rename(&dir, root.join("moved")).unwrap();
+    let (committed_tx, mut committed_rx) = tokio::sync::mpsc::unbounded_channel();
+    let release = Arc::new(tokio::sync::Notify::new());
+    let held = Arc::new(HeldAuthority::new_before_commit(
+        authority.clone(),
+        committed_tx,
+        release.clone(),
+    ));
+    let mut sync = DirectorySync::open(held.clone(), &state).await.unwrap();
+    let run = sync.run();
+    let writer = async {
+        committed_rx
+            .recv()
+            .await
+            .expect("the rename commit must reach the pre-commit barrier");
+        // The interleaved content edit goes through the independent raw
+        // authority while the namespace-only rename is parked. The rename
+        // predicates guard links and entries, never the inner file content.
+        let external = tempfile::tempdir().unwrap();
+        let seed = Sync::open(authority.clone(), external.path().join("seed"))
+            .await
+            .unwrap();
+        let baseline = authority.observe_latest().await.unwrap();
+        let file = baseline.resolve("dir/file").await.unwrap().unwrap().id();
+        assert_eq!(
+            baseline
+                .open_file(file)
+                .await
+                .unwrap()
+                .read(0, 64)
+                .await
+                .unwrap(),
+            b"v1",
+            "the run must observe the remote unchanged before the interleave"
+        );
+        let edit = seed
+            .stage(
+                file,
+                baseline.revision(),
+                &mut &b"v2-was-never-seen-locally"[..],
+            )
+            .await
+            .unwrap();
+        seed.publish(edit).await.unwrap();
+        release.notify_one();
+    };
+    let run_result = tokio::time::timeout(std::time::Duration::from_secs(60), async {
+        tokio::join!(run, writer)
+    })
+    .await
+    .expect("the interleaved rename run must finish within the timeout")
+    .0;
+
+    if let Ok(outcome) = &run_result {
+        // Core invariant: in_sync may only ever claim byte-identical real
+        // trees, so the namespace rename receipt cannot fold the unobserved
+        // v2 into a common baseline with the local v1.
+        assert_eq!(
+            outcome.in_sync,
+            local_tree(&root) == remote_tree(&authority).await,
+            "in_sync must imply identical real trees"
+        );
+        assert!(
+            remote_tree(&authority).await.get("dir").is_none(),
+            "the accepted rename must move the directory out of the old name"
+        );
+        assert_eq!(
+            remote_tree(&authority).await["moved/file"],
+            TreeEntry::File {
+                executable: false,
+                bytes: b"v2-was-never-seen-locally".to_vec(),
+            },
+            "the namespace rename receipt must not swallow the interleaved external content"
+        );
+    } else {
+        // A legal core conflict retains both sides: the external bytes stay
+        // remote under one of the two names and the local v1 stays put.
+        let external = TreeEntry::File {
+            executable: false,
+            bytes: b"v2-was-never-seen-locally".to_vec(),
+        };
+        let remote = remote_tree(&authority).await;
+        assert!(
+            remote.get("dir/file") == Some(&external)
+                || remote.get("moved/file") == Some(&external),
+            "the interleaved external bytes must be retained"
+        );
+        assert_eq!(
+            local_tree(&root)["moved/file"],
+            TreeEntry::File {
+                executable: false,
+                bytes: b"v1".to_vec(),
+            },
+            "the local v1 must be retained on a legal conflict"
+        );
+    }
+
+    // After the namespace receipt, the machine either converges the subtree
+    // to the interleaved v2 or keeps a legal conflict; it may never report in
+    // sync while the real trees still differ.
+    let mut legal_conflict = run_result.is_err();
+    for _ in 0..8 {
+        let local = local_tree(&root);
+        let remote = remote_tree(&authority).await;
+        if local == remote {
+            break;
+        }
+        match sync.run().await {
+            Ok(outcome) => {
+                legal_conflict |= !outcome.conflicts.is_empty();
+                assert_eq!(
+                    outcome.in_sync,
+                    local_tree(&root) == remote_tree(&authority).await,
+                    "in_sync must imply identical real trees"
+                );
+            }
+            Err(_) => legal_conflict = true,
+        }
+    }
+    let local = local_tree(&root);
+    let remote = remote_tree(&authority).await;
+    if !legal_conflict {
+        assert_eq!(
+            local, remote,
+            "the moved subtree must converge to the interleaved external content"
+        );
+        assert_eq!(
+            local["moved/file"],
+            TreeEntry::File {
+                executable: false,
+                bytes: b"v2-was-never-seen-locally".to_vec(),
+            }
+        );
+        assert!(local.get("dir").is_none());
+        let stable = sync.run().await.unwrap();
+        assert_eq!(stable.published_local, 0);
+        assert_eq!(stable.materialized_remote, 0);
+        assert!(stable.conflicts.is_empty());
+        assert!(
+            stable.in_sync,
+            "the converged renamed subtree must report in sync"
+        );
+        assert_eq!(local_tree(&root), remote_tree(&authority).await);
+    } else {
+        let external = TreeEntry::File {
+            executable: false,
+            bytes: b"v2-was-never-seen-locally".to_vec(),
+        };
+        assert_eq!(
+            local["moved/file"],
+            TreeEntry::File {
+                executable: false,
+                bytes: b"v1".to_vec(),
+            },
+            "local v1 must survive a legal conflict"
+        );
+        assert!(
+            remote.get("dir/file") == Some(&external)
+                || remote.get("moved/file") == Some(&external),
+            "the interleaved external bytes must be retained on a legal conflict"
+        );
+    }
+}