You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ozone.apache.org by si...@apache.org on 2026/10/21 07:15:04 UTC

(ozone) branch master updated: HDDS-16438. FSO lifecycle scan can skip an unscanned directory's expired keys after a mid scan crash (#11558)

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

smengcl 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 2cb1b6dd4e9 HDDS-16438. FSO lifecycle scan can skip an unscanned directory's expired keys after a mid scan crash (#11558)
2cb1b6dd4e9 is described below

commit 2cb1b6dd4e9ace32c9c0c52ce80d1c3d3a1ffd8a
Author: Sammi Chen <sa...@apache.org>
AuthorDate: Wed Oct 21 15:14:58 2026 +0800

    HDDS-16438. FSO lifecycle scan can skip an unscanned directory's expired keys after a mid scan crash (#11558)
---
 .../ozone/om/service/KeyLifecycleService.java      | 82 +++++++++++++++++-----
 .../ozone/om/service/TestKeyLifecycleService.java  | 43 ++++++++++++
 2 files changed, 107 insertions(+), 18 deletions(-)

diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyLifecycleService.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyLifecycleService.java
index d74c220e8e4..416e2d4115b 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyLifecycleService.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/KeyLifecycleService.java
@@ -58,6 +58,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.IntPredicate;
 import java.util.stream.Collectors;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hdds.conf.ConfigurationSource;
@@ -760,30 +761,25 @@ private void evaluateKeyAndDirTable(OmBucketInfo bucket, long volumeObjId, Table
 
         if (item.isFirstEvaluate()) {
           // filter sub directory list
-          if (!subDirSummary.getSubDirList().isEmpty()) {
-            Iterator<OmDirectoryInfo> iterator = subDirSummary.getSubDirList().iterator();
-            while (iterator.hasNext()) {
-              OmDirectoryInfo subDir = iterator.next();
-              String subDirPath = currentDirPath.isEmpty() ? subDir.getName() :
-                  currentDirPath + OM_KEY_PREFIX + subDir.getName();
+          if (!subDirSummary.isEmpty()) {
+            subDirSummary.removeIf(i -> {
+              OmDirectoryInfo subDir = subDirSummary.getSubDirList().get(i);
+              String subDirPath = currentDirPath.isEmpty() ? subDir.getName()
+                  : currentDirPath + OM_KEY_PREFIX + subDir.getName();
               if (subDirPath.startsWith(TRASH_PREFIX)) {
-                iterator.remove();
-                continue;
+                return true;
               }
-              boolean matched = false;
               for (OmLCRule rule : ruleList) {
-                if (rule.getEffectivePrefix() != null && subDirPath.startsWith(rule.getEffectivePrefix())) {
-                  matched = true;
-                  break;
+                if (rule.getEffectivePrefix() != null
+                    && subDirPath.startsWith(rule.getEffectivePrefix())) {
+                  return false;
                 }
               }
-              if (!matched) {
-                iterator.remove();
-              }
-            }
+              return true;
+            });
           }
 
-          if (!subDirSummary.getSubDirList().isEmpty()) {
+          if (!subDirSummary.isEmpty()) {
             item.setDirectoryList(subDirSummary);
             item.setFirstEvaluate(false);
             try {
@@ -954,6 +950,7 @@ private DirectoryList getSubDirectory(long dirObjID, String prefix, OMMetadataMa
       Iterator<Map.Entry<CacheKey<String>, CacheValue<OmDirectoryInfo>>>
           cacheIter = dirTable.cacheIterator();
       HashSet<String> deletedDirSet = new HashSet();
+      HashSet<String> dirSet = new HashSet();
       while (cacheIter.hasNext()) {
         Map.Entry<CacheKey<String>, CacheValue<OmDirectoryInfo>> entry =
             cacheIter.next();
@@ -964,6 +961,7 @@ private DirectoryList getSubDirectory(long dirObjID, String prefix, OMMetadataMa
           continue;
         }
         if (cacheOmDirInfo.getParentObjectID() == dirObjID) {
+          dirSet.add(entry.getKey().getCacheKey());
           subDirList.addSubDir(entry.getKey().getCacheKey(), cacheOmDirInfo, cacheOmDirInfo.getName());
         }
       }
@@ -975,7 +973,7 @@ private DirectoryList getSubDirectory(long dirObjID, String prefix, OMMetadataMa
           numDirIterated++;
           Table.KeyValue<String, OmDirectoryInfo> entry = iterator.next();
           OmDirectoryInfo dir = entry.getValue();
-          if (deletedDirSet.contains(entry.getKey())) {
+          if (deletedDirSet.contains(entry.getKey()) || dirSet.contains(entry.getKey())) {
             continue;
           }
           if (dir.getParentObjectID() == dirObjID) {
@@ -983,6 +981,12 @@ private DirectoryList getSubDirectory(long dirObjID, String prefix, OMMetadataMa
           }
         }
       }
+      // Cache children (hash order) and table children (ascending) are appended in that order; sort
+      // by name so the DFS stack pops siblings in strictly descending name order, which canSkipDir
+      // relies on to decide a resumed directory was already scanned.
+      if (!dirSet.isEmpty()) {
+        subDirList.sortByName();
+      }
       return subDirList;
     }
 
@@ -2001,6 +2005,48 @@ public void addSubDir(String key, OmDirectoryInfo dir, String dirPath) {
       subDirCount++;
     }
 
+    /** Removes the entry at {@code index} from all parallel lists. */
+    void removeAt(int index) {
+      subDirList.remove(index);
+      subDirKeyList.remove(index);
+      subDirKeyPathList.remove(index);
+      subDirCount--;
+    }
+
+    /** Removes the entry if shouldRemoveIndex returns true. */
+    void removeIf(IntPredicate shouldRemoveIndex) {
+      for (int i = subDirCount - 1; i >= 0; i--) {
+        if (shouldRemoveIndex.test(i)) {
+          removeAt(i);
+        }
+      }
+    }
+
+    public void sortByName() {
+      if (subDirCount <= 1) {
+        return;
+      }
+      List<Integer> order = new ArrayList<>(subDirCount);
+      for (int i = 0; i < subDirCount; i++) {
+        order.add(i);
+      }
+      order.sort(Comparator.comparing((Integer idx) -> subDirList.get(idx).getName()));
+      List<OmDirectoryInfo> sortedDirs = new ArrayList<>(subDirCount);
+      List<String> sortedKeys = new ArrayList<>(subDirCount);
+      List<String> sortedPaths = new ArrayList<>(subDirCount);
+      for (int idx : order) {
+        sortedDirs.add(subDirList.get(idx));
+        sortedKeys.add(subDirKeyList.get(idx));
+        sortedPaths.add(subDirKeyPathList.get(idx));
+      }
+      subDirList.clear();
+      subDirList.addAll(sortedDirs);
+      subDirKeyList.clear();
+      subDirKeyList.addAll(sortedKeys);
+      subDirKeyPathList.clear();
+      subDirKeyPathList.addAll(sortedPaths);
+    }
+
     public OmDirectoryInfo getLastSubDir() {
       return subDirCount > 0 ? subDirList.get(subDirCount - 1) : null;
     }
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java
index 1da98e26775..ed51e2e4649 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestKeyLifecycleService.java
@@ -45,6 +45,7 @@
 import static org.apache.hadoop.ozone.om.helpers.BucketLayout.FILE_SYSTEM_OPTIMIZED;
 import static org.apache.hadoop.ozone.om.helpers.BucketLayout.OBJECT_STORE;
 import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.ALL;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -262,6 +263,48 @@ private void createSubject() throws Exception {
     metadataManager = omTestManagers.getMetadataManager();
   }
 
+  private static OmDirectoryInfo dirInfoWithName(String name) {
+    return OmDirectoryInfo.newBuilder().setName(name)
+        .setObjectID(Math.abs((long) name.hashCode()) + 1).setParentObjectID(1L).build();
+  }
+
+  @Test
+  void testDirectoryListSortByName() {
+    KeyLifecycleService.DirectoryList list = new KeyLifecycleService.DirectoryList();
+    // Add children out of order, as a cache-resident dir (lower name) followed by table siblings would.
+    list.addSubDir("keyC", dirInfoWithName("dirC"), "root/dirC");
+    list.addSubDir("keyA", dirInfoWithName("dirA"), "root/dirA");
+    list.addSubDir("keyB", dirInfoWithName("dirB"), "root/dirB");
+
+    list.sortByName();
+
+    // Ascending name order means the LIFO stack pops siblings in strictly descending name order,
+    // which is the property canSkipDir depends on when deciding a resumed directory was already scanned.
+    assertThat(list.getSubDirList())
+        .extracting(OmDirectoryInfo::getName)
+        .containsExactly("dirA", "dirB", "dirC");
+    // Parallel key list must remain index-aligned with the sorted directories.
+    assertThat(list.getSubDirKeyList()).containsExactly("keyA", "keyB", "keyC");
+  }
+
+  @Test
+  void testDirectoryListRemoveIf() {
+    KeyLifecycleService.DirectoryList list = new KeyLifecycleService.DirectoryList();
+    list.addSubDir("keyA", dirInfoWithName("dirA"), "root/dirA");
+    list.addSubDir("keyB", dirInfoWithName("dirB"), "root/dirB");
+    list.addSubDir("keyC", dirInfoWithName("dirC"), "root/dirC");
+
+    // Remove first and last (multiple backward removals), as prefix filtering might.
+    list.removeIf(i -> i == 0 || i == 2);
+
+    assertThat(list.getSubDirCount()).isEqualTo(1);
+    assertThat(list.getSubDirList()).hasSize(1);
+    assertThat(list.getSubDirKeyList()).hasSize(1);
+    assertThat(list.getSubDirList().get(0).getName()).isEqualTo("dirB");
+    assertThat(list.getSubDirKeyList().get(0)).isEqualTo("keyB");
+    assertThat(list.getLastSubDirPath()).isEqualTo("root/dirB");
+  }
+
   /**
    * Tests happy path.
    */


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@ozone.apache.org
For additional commands, e-mail: commits-help@ozone.apache.org