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

adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 0cd7a9735c9 HDDS-14600. [FSO] Non-recursive dir deletion fails with 
"Directory is not empty" despite all children deleted until double buffer is 
flushed (#9739)
0cd7a9735c9 is described below

commit 0cd7a9735c990bc3556f83b00c7344b198c6b3f3
Author: Siyao Meng <[email protected]>
AuthorDate: Thu Feb 12 02:56:27 2026 -0800

    HDDS-14600. [FSO] Non-recursive dir deletion fails with "Directory is not 
empty" despite all children deleted until double buffer is flushed (#9739)
---
 .../ozone/AbstractOzoneFileSystemTestWithFSO.java  | 41 ++++++++++++
 .../ozone/om/request/file/OMFileRequest.java       | 38 +++++++++--
 .../request/key/TestOMKeyDeleteRequestWithFSO.java | 74 ++++++++++++++++++++++
 3 files changed, 148 insertions(+), 5 deletions(-)

diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractOzoneFileSystemTestWithFSO.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractOzoneFileSystemTestWithFSO.java
index 3d41012a98a..a1bb004746c 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractOzoneFileSystemTestWithFSO.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractOzoneFileSystemTestWithFSO.java
@@ -42,6 +42,7 @@
 import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
 import org.apache.hadoop.ozone.om.helpers.OmDirectoryInfo;
 import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.ratis.OzoneManagerDoubleBuffer;
 import org.apache.ozone.test.GenericTestUtils;
 import org.apache.ozone.test.GenericTestUtils.LogCapturer;
 import org.junit.jupiter.api.MethodOrderer;
@@ -552,4 +553,44 @@ long verifyDirKey(long volumeId, long bucketId, long 
parentId,
     return dirInfo.getObjectID();
   }
 
+  /**
+   * Test to reproduce "Directory Not Empty" bug using public FileSystem API.
+   * Tests both checkSubDirectoryExists() and checkSubFileExists() paths.
+   * Creates child directory and file, deletes them, then tries to delete 
parent.
+   */
+  @Test
+  public void testDeleteParentAfterChildDeleted() throws Exception {
+    Path parent = new Path("/parent");
+    Path childDir = new Path(parent, "childDir");
+    Path childFile = new Path(parent, "childFile");
+
+    // Create parent directory
+    assertTrue(getFs().mkdirs(parent));
+    // Create child directory (tests checkSubDirectoryExists path)
+    assertTrue(getFs().mkdirs(childDir));
+    // Create child file (tests checkSubFileExists path)
+    ContractTestUtils.touch(getFs(), childFile);
+
+    // Pause double buffer to prevent flushing deleted entries to DB
+    // This makes the bug reproduce deterministically
+    OzoneManagerDoubleBuffer doubleBuffer = getCluster().getOzoneManager()
+        .getOmRatisServer().getOmStateMachine().getOzoneManagerDoubleBuffer();
+    doubleBuffer.pause();
+
+    try {
+      // Delete child directory
+      assertTrue(getFs().delete(childDir, false), "Child directory delete 
should succeed");
+      // Delete child file
+      assertTrue(getFs().delete(childFile, false), "Child file delete should 
succeed");
+
+      // Try to delete parent directory (should succeed but may fail with the 
bug)
+      // Without the fix, this fails because deleted children are still in DB
+      boolean parentDeleted = getFs().delete(parent, false);
+      assertTrue(parentDeleted, "Parent delete should succeed after children 
deleted");
+    } finally {
+      // Unpause double buffer to avoid affecting other tests
+      doubleBuffer.unpause();
+    }
+  }
+
 }
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/file/OMFileRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/file/OMFileRequest.java
index 8403828f395..84722ede629 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/file/OMFileRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/file/OMFileRequest.java
@@ -889,11 +889,25 @@ private static boolean checkSubDirectoryExists(OmKeyInfo 
omKeyInfo,
         Table.KeyValue<String, OmDirectoryInfo>>
             iterator = dirTable.iterator(seekDirInDB)) {
 
-      if (iterator.hasNext()) {
+      while (iterator.hasNext()) {
         Table.KeyValue<String, OmDirectoryInfo> entry = iterator.next();
+        String dbKey = entry.getKey();
         OmDirectoryInfo dirInfo = entry.getValue();
-        return isImmediateChild(dirInfo.getParentObjectID(),
+        boolean isChild = isImmediateChild(dirInfo.getParentObjectID(),
             omKeyInfo.getObjectID());
+
+        if (!isChild) {
+          return false;
+        }
+
+        // If child found in DB, check if it's marked as deleted in cache
+        CacheValue<OmDirectoryInfo> cacheValue = dirTable.getCacheValue(new 
CacheKey<>(dbKey));
+        if (cacheValue != null && cacheValue.getCacheValue() == null) {
+          // Entry is in DB but marked for deletion in cache, ignore it and 
check next entry
+          continue;
+        }
+
+        return true;
       }
 
     }
@@ -933,11 +947,25 @@ private static boolean checkSubFileExists(OmKeyInfo 
omKeyInfo,
     try (TableIterator<String, ? extends Table.KeyValue<String, OmKeyInfo>>
             iterator = fileTable.iterator(seekFileInDB)) {
 
-      if (iterator.hasNext()) {
+      while (iterator.hasNext()) {
         Table.KeyValue<String, OmKeyInfo> entry = iterator.next();
+        String dbKey = entry.getKey();
         OmKeyInfo fileInfo = entry.getValue();
-        return isImmediateChild(fileInfo.getParentObjectID(),
-            omKeyInfo.getObjectID()); // found a sub path file
+        boolean isChild = isImmediateChild(fileInfo.getParentObjectID(),
+            omKeyInfo.getObjectID());
+
+        if (!isChild) {
+          return false;
+        }
+
+        // If child found in DB, check if it's marked as deleted in cache
+        CacheValue<OmKeyInfo> cacheValue = fileTable.getCacheValue(new 
CacheKey<>(dbKey));
+        if (cacheValue != null && cacheValue.getCacheValue() == null) {
+          // Entry is in DB but marked for deletion in cache, ignore it and 
check next entry
+          continue;
+        }
+
+        return true; // found a sub path file
       }
     }
     return false; // no sub paths found
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyDeleteRequestWithFSO.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyDeleteRequestWithFSO.java
index eace03a8a1c..c537b09c85b 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyDeleteRequestWithFSO.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMKeyDeleteRequestWithFSO.java
@@ -29,6 +29,7 @@
 import java.io.IOException;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.UUID;
 import org.apache.hadoop.hdds.client.RatisReplicationConfig;
 import org.apache.hadoop.ozone.om.OzonePrefixPathImpl;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
@@ -39,6 +40,8 @@
 import org.apache.hadoop.ozone.om.request.OMRequestTestUtils;
 import org.apache.hadoop.ozone.om.response.OMClientResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DeleteKeyRequest;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.KeyArgs;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import org.apache.hadoop.ozone.security.acl.OzonePrefixPath;
 import org.junit.jupiter.api.Test;
@@ -259,4 +262,75 @@ public void testDeleteDirectoryWithColonInFSOBucket() 
throws Exception {
     assertEquals(OzoneManagerProtocolProtos.Status.OK, 
response.getOMResponse().getStatus());
     assertNull(omMetadataManager.getDirectoryTable().get(dirName));
   }
+
+  private OMRequest createDeleteKeyRequest(String keyPath, boolean recursive) {
+    KeyArgs keyArgs = KeyArgs.newBuilder()
+        .setBucketName(bucketName)
+        .setVolumeName(volumeName)
+        .setKeyName(keyPath)
+        .setRecursive(recursive)
+        .build();
+
+    DeleteKeyRequest deleteKeyRequest =
+        DeleteKeyRequest.newBuilder().setKeyArgs(keyArgs).build();
+
+    return OMRequest.newBuilder()
+        .setDeleteKeyRequest(deleteKeyRequest)
+        .setCmdType(OzoneManagerProtocolProtos.Type.DeleteKey)
+        .setClientId(UUID.randomUUID().toString())
+        .build();
+  }
+
+  /**
+   * Minimal test to reproduce "Directory Not Empty" bug with Ratis.
+   * Tests both checkSubDirectoryExists() and checkSubFileExists() paths.
+   * Creates child directory and file, deletes them, then tries to delete 
parent.
+   * This test exposes a Ratis transaction visibility issue where deleted
+   * entries are in cache but not yet flushed to DB via double buffer.
+   */
+  @Test
+  public void testDeleteParentAfterChildDeleted() throws Exception {
+    OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName, 
omMetadataManager, getBucketLayout());
+
+    String parentDir = "parent";
+    long parentId = OMRequestTestUtils.addParentsToDirTable(volumeName, 
bucketName, parentDir, omMetadataManager);
+
+    // Create a child directory (tests checkSubDirectoryExists path)
+    OMRequestTestUtils.addParentsToDirTable(volumeName, bucketName, parentDir 
+ "/childDir", omMetadataManager);
+
+    // Create a child file (tests checkSubFileExists path)
+    String fileName = "childFile";
+    OmKeyInfo fileInfo = OMRequestTestUtils.createOmKeyInfo(volumeName,
+        bucketName, parentDir + "/" + fileName, 
RatisReplicationConfig.getInstance(ONE))
+        .setObjectID(parentId + 2)
+        .setParentObjectID(parentId)
+        .setUpdateID(50L)
+        .build();
+    fileInfo.setKeyName(fileName);
+    OMRequestTestUtils.addFileToKeyTable(false, false, fileName, fileInfo, -1, 
50, omMetadataManager);
+
+    // Delete the child directory
+    long txnId = 1000L;
+    OMRequest deleteChildDirRequest = 
doPreExecute(createDeleteKeyRequest(parentDir + "/childDir", true));
+    OMKeyDeleteRequest deleteChildDirKeyRequest = 
getOmKeyDeleteRequest(deleteChildDirRequest);
+    OMClientResponse deleteChildDirResponse = 
deleteChildDirKeyRequest.validateAndUpdateCache(ozoneManager, txnId++);
+    assertEquals(OzoneManagerProtocolProtos.Status.OK, 
deleteChildDirResponse.getOMResponse().getStatus(),
+        "Child directory delete should succeed");
+
+    // Delete the child file
+    OMRequest deleteChildFileRequest = 
doPreExecute(createDeleteKeyRequest(parentDir + "/" + fileName, false));
+    OMKeyDeleteRequest deleteChildFileKeyRequest = 
getOmKeyDeleteRequest(deleteChildFileRequest);
+    OMClientResponse deleteChildFileResponse = 
deleteChildFileKeyRequest.validateAndUpdateCache(ozoneManager, txnId++);
+    assertEquals(OzoneManagerProtocolProtos.Status.OK, 
deleteChildFileResponse.getOMResponse().getStatus(),
+        "Child file delete should succeed");
+
+    // Try to delete parent (should succeed but fails without fix)
+    OMRequest deleteParentRequest = 
doPreExecute(createDeleteKeyRequest(parentDir, false));
+    OMKeyDeleteRequest deleteParentKeyRequest = 
getOmKeyDeleteRequest(deleteParentRequest);
+    OMClientResponse response = 
deleteParentKeyRequest.validateAndUpdateCache(ozoneManager, txnId);
+
+    // This should succeed after the fix
+    assertEquals(OzoneManagerProtocolProtos.Status.OK, 
response.getOMResponse().getStatus(),
+        "Parent delete should succeed after children deleted");
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to