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 4066b0e8e23 HDDS-15991. Speed up deleted table scans in quota repair 
(#11386)
4066b0e8e23 is described below

commit 4066b0e8e237375196664599e50b5b55d7b48b83
Author: Chi-Hsuan Huang <[email protected]>
AuthorDate: Mon Oct 5 23:25:55 2026 +0800

    HDDS-15991. Speed up deleted table scans in quota repair (#11386)
    
    Generated-by: Claude Code (Claude Opus 5.5)
---
 .../hadoop/ozone/om/service/QuotaRepairTask.java   |  65 ++++++++---
 .../ozone/om/service/TestQuotaRepairTask.java      | 119 +++++++++++++++++++++
 2 files changed, 168 insertions(+), 16 deletions(-)

diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/QuotaRepairTask.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/QuotaRepairTask.java
index bc800b5a5b2..10c9a4ca864 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/QuotaRepairTask.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/QuotaRepairTask.java
@@ -19,6 +19,7 @@
 
 import static org.apache.hadoop.hdds.utils.db.IteratorType.KEY_AND_VALUE;
 import static org.apache.hadoop.hdds.utils.db.IteratorType.KEY_ONLY;
+import static org.apache.hadoop.hdds.utils.db.IteratorType.VALUE_ONLY;
 import static org.apache.hadoop.ozone.OzoneConsts.OLD_QUOTA_DEFAULT;
 import static org.apache.hadoop.ozone.OzoneConsts.OM_KEY_PREFIX;
 import static 
org.apache.hadoop.ozone.om.helpers.SnapshotInfo.SnapshotStatus.SNAPSHOT_ACTIVE;
@@ -55,9 +56,13 @@
 import org.apache.commons.io.FileUtils;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.server.JsonUtils;
+import org.apache.hadoop.hdds.utils.db.ByteArrayCodec;
+import org.apache.hadoop.hdds.utils.db.Codec;
 import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
+import org.apache.hadoop.hdds.utils.db.StringCodec;
 import org.apache.hadoop.hdds.utils.db.Table;
 import org.apache.hadoop.hdds.utils.db.TableIterator;
+import org.apache.hadoop.hdds.utils.db.cache.TableCache.CacheType;
 import org.apache.hadoop.ozone.om.OMMetadataManager;
 import org.apache.hadoop.ozone.om.OmMetadataManagerImpl;
 import org.apache.hadoop.ozone.om.OmSnapshot;
@@ -65,6 +70,7 @@
 import org.apache.hadoop.ozone.om.OzoneManager;
 import org.apache.hadoop.ozone.om.SnapshotChainInfo;
 import org.apache.hadoop.ozone.om.SnapshotChainManager;
+import org.apache.hadoop.ozone.om.codec.OMDBDefinition;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.BucketLayout;
 import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
@@ -362,9 +368,8 @@ private void repairCount(
 
       Map<Long, OmBucketInfo> bucketById = 
buildBucketByObjectId(idBucketInfoMap);
 
-      tasks.add(executor.submit(() -> recalculateDeletedKeyUsages(
-          metadataManager.getDeletedTable(), bucketById, snapshotDeletedKeyMap,
-          "active DB checkpoint")));
+      tasks.add(executor.submit(() -> recalculateActiveDeletedKeyUsages(
+          metadataManager, bucketById, snapshotDeletedKeyMap)));
       tasks.add(executor.submit(() -> recalculateDeletedDirNamespace(
           metadataManager.getDeletedDirTable(), snapshotDeletedDirMap,
           "active DB checkpoint")));
@@ -503,6 +508,28 @@ private void scanDeletedTables(
         snapshotDeletedDirMap, sourceLabel);
   }
 
+  private void recalculateActiveDeletedKeyUsages(
+      OMMetadataManager metadataManager,
+      Map<Long, OmBucketInfo> bucketById,
+      Map<String, CountPair> snapshotCountByBucketNameKey)
+      throws UncheckedIOException, UncheckedExecutionException {
+    Codec<RepeatedOmKeyInfo> codec = 
OMDBDefinition.DELETED_TABLE_DEF.getValueCodec();
+    // a typed iterator decodes on this thread, so keep values raw and let the 
workers decode
+    try (Table.KeyValueIterator<String, byte[]> keyIter = 
metadataManager.getStore()
+        .getTable(OMDBDefinition.DELETED_TABLE, StringCodec.get(), 
ByteArrayCodec.get(), CacheType.NO_CACHE)
+        .iterator(null, VALUE_ONLY)) {
+      scanTableInBatches(executor, keyIter, "snapshot usages from deletedTable 
(active DB checkpoint)", kv -> {
+        try {
+          countDeletedKey(codec.fromPersistedFormat(kv.getValue()), 
bucketById, snapshotCountByBucketNameKey);
+        } catch (IOException ex) {
+          throw new UncheckedIOException(ex);
+        }
+      });
+    } catch (IOException ex) {
+      throw new UncheckedIOException(ex);
+    }
+  }
+
   private void recalculateDeletedKeyUsages(
       Table<String, RepeatedOmKeyInfo> deletedTable,
       Map<Long, OmBucketInfo> bucketById,
@@ -518,18 +545,7 @@ private void recalculateDeletedKeyUsages(
       while (keyIter.hasNext()) {
         Table.KeyValue<String, RepeatedOmKeyInfo> kv = keyIter.next();
         count++;
-        RepeatedOmKeyInfo val = kv.getValue();
-        OmBucketInfo bucket = bucketById.get(val.getBucketId());
-        if (bucket == null) {
-          continue;
-        }
-        String nameKey = buildNamePath(bucket.getVolumeName(), 
bucket.getBucketName());
-        CountPair usage = snapshotCountByBucketNameKey.get(nameKey);
-        if (usage == null) {
-          continue;
-        }
-        usage.incrSpace(val.getTotalSize().getRight());
-        usage.incrNamespace(val.getOmKeyInfoList().size());
+        countDeletedKey(kv.getValue(), bucketById, 
snapshotCountByBucketNameKey);
       }
       LOG.info("Recalculate snapshot usages from deletedTable ({}) completed, 
count {} time {}ms",
           sourceLabel, count, (Time.monotonicNow() - startTime));
@@ -538,6 +554,23 @@ private void recalculateDeletedKeyUsages(
     }
   }
 
+  private static void countDeletedKey(
+      RepeatedOmKeyInfo val,
+      Map<Long, OmBucketInfo> bucketById,
+      Map<String, CountPair> snapshotCountByBucketNameKey) {
+    OmBucketInfo bucket = bucketById.get(val.getBucketId());
+    if (bucket == null) {
+      return;
+    }
+    String nameKey = buildNamePath(bucket.getVolumeName(), 
bucket.getBucketName());
+    CountPair usage = snapshotCountByBucketNameKey.get(nameKey);
+    if (usage == null) {
+      return;
+    }
+    usage.incrSpace(val.getTotalSize().getRight());
+    usage.incrNamespace(val.getOmKeyInfoList().size());
+  }
+
   private void recalculateDeletedDirNamespace(
       Table<String, OmKeyInfo> deletedDirTable,
       Map<String, CountPair> snapshotDirNsByIdPrefix,
@@ -549,7 +582,7 @@ private void recalculateDeletedDirNamespace(
     int count = 0;
     long startTime = Time.monotonicNow();
     try (Table.KeyValueIterator<String, OmKeyInfo> keyIter
-        = deletedDirTable.iterator()) {
+        = deletedDirTable.iterator(null, KEY_ONLY)) {
       while (keyIter.hasNext()) {
         Table.KeyValue<String, OmKeyInfo> kv = keyIter.next();
         count++;
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestQuotaRepairTask.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestQuotaRepairTask.java
index 3565484b4c5..3f47ed06bd9 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestQuotaRepairTask.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestQuotaRepairTask.java
@@ -19,6 +19,7 @@
 
 import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE;
 import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.THREE;
+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.assertInstanceOf;
@@ -52,11 +53,14 @@
 import org.apache.hadoop.hdds.client.ReplicationConfig;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.hdds.utils.db.ByteArrayCodec;
+import org.apache.hadoop.hdds.utils.db.StringCodec;
 import org.apache.hadoop.hdds.utils.db.Table;
 import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
 import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
 import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.codec.OMDBDefinition;
 import org.apache.hadoop.ozone.om.helpers.BucketLayout;
 import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
 import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
@@ -355,6 +359,99 @@ public void testQuotaRepairSnapshotDbDeletedTableQuota() 
throws Exception {
     assertEquals(1, repaired.getSnapshotUsedNamespace());
   }
 
+  @Test
+  public void testQuotaRepairCountsDeletedDirectories() throws Exception {
+    AtomicReference<OzoneManagerProtocolProtos.OMRequest> request = 
mockQuotaRepairRequest();
+    OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+        omMetadataManager, BucketLayout.FILE_SYSTEM_OPTIMIZED);
+    long volumeId = omMetadataManager.getVolumeId(volumeName);
+    long bucketId = omMetadataManager.getBucketTable()
+        .get(omMetadataManager.getBucketKey(volumeName, 
bucketName)).getObjectID();
+
+    int dirs = 3;
+    for (int i = 0; i < dirs; i++) {
+      String dirName = "dir" + i;
+      long objectId = 100L + i;
+      OmKeyInfo dirInfo = OMRequestTestUtils.createOmKeyInfo(volumeName, 
bucketName, dirName, RATIS_ONE)
+          .setObjectID(objectId).setParentObjectID(bucketId).build();
+      
omMetadataManager.getDeletedDirTable().put(omMetadataManager.getOzoneDeletePathKey(objectId,
+          omMetadataManager.getOzonePathKey(volumeId, bucketId, bucketId, 
dirName)), dirInfo);
+    }
+    // the scan reads keys only, so an undecodable value is still counted
+    omMetadataManager.getStore().getTable(OMDBDefinition.DELETED_DIR_TABLE, 
StringCodec.get(), ByteArrayCodec.get())
+        .put(omMetadataManager.getOzoneDeletePathKey(200L,
+            omMetadataManager.getOzonePathKey(volumeId, bucketId, bucketId, 
"badDir")), new byte[] {0});
+    String bucketKey = corruptSnapshotUsage(bucketName, 1L);
+
+    applyQuotaRepair(request, 2L, bucketKey);
+
+    OmBucketInfo repaired = omMetadataManager.getBucketTable().get(bucketKey);
+    assertEquals(0, repaired.getSnapshotUsedBytes());
+    assertEquals(dirs + 1, repaired.getSnapshotUsedNamespace());
+  }
+
+  @Test
+  public void testQuotaRepairCountsDeletedKeysAcrossBatches() throws Exception 
{
+    AtomicReference<OzoneManagerProtocolProtos.OMRequest> request = 
mockQuotaRepairRequest();
+    OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+        omMetadataManager, BucketLayout.OBJECT_STORE);
+    long bucketId = omMetadataManager.getBucketTable()
+        .get(omMetadataManager.getBucketKey(volumeName, 
bucketName)).getObjectID();
+
+    // a partial last batch, so a dropped tail batch would show up in the 
totals
+    int rows = QuotaRepairTask.BATCH_SIZE * 2 + 1;
+    for (int i = 0; i < rows; i++) {
+      putDeletedKey(bucketName, bucketId, "key" + i, i == 0 ? 2 : 1, 
RATIS_ONE);
+    }
+    // replicated size differs from data size only with more than one replica
+    putDeletedKey(bucketName, bucketId, "threeReplicaKey", 1, 
RatisReplicationConfig.getInstance(THREE));
+    String bucketKey = corruptSnapshotUsage(bucketName, 1L);
+
+    applyQuotaRepair(request, 2L, bucketKey);
+
+    OmBucketInfo repaired = omMetadataManager.getBucketTable().get(bucketKey);
+    assertEquals((rows + 1) * 1000L + 3000L, repaired.getSnapshotUsedBytes());
+    assertEquals(rows + 2, repaired.getSnapshotUsedNamespace());
+  }
+
+  @Test
+  public void testQuotaRepairSkipsDeletedKeysOfRecreatedBucket() throws 
Exception {
+    AtomicReference<OzoneManagerProtocolProtos.OMRequest> request = 
mockQuotaRepairRequest();
+    OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+        omMetadataManager, BucketLayout.OBJECT_STORE);
+    long bucketId = omMetadataManager.getBucketTable()
+        .get(omMetadataManager.getBucketKey(volumeName, 
bucketName)).getObjectID();
+
+    putDeletedKey(bucketName, bucketId, "liveKey", 1, RATIS_ONE);
+    // left by an earlier bucket with the same name: the key prefix matches, 
the bucket id does not
+    putDeletedKey(bucketName, bucketId + 1000, "staleKey", 1, RATIS_ONE);
+    // written before deletedTable rows carried a bucket id, so it decodes to 0
+    putDeletedKey(bucketName, 0, "legacyKey", 1, RATIS_ONE);
+    String bucketKey = corruptSnapshotUsage(bucketName, 1L);
+
+    applyQuotaRepair(request, 2L, bucketKey);
+
+    OmBucketInfo repaired = omMetadataManager.getBucketTable().get(bucketKey);
+    assertEquals(1000L, repaired.getSnapshotUsedBytes());
+    assertEquals(1, repaired.getSnapshotUsedNamespace());
+  }
+
+  @Test
+  public void testQuotaRepairFailsOnUndecodableDeletedKey() throws Exception {
+    AtomicReference<OzoneManagerProtocolProtos.OMRequest> request = 
mockQuotaRepairRequest();
+    OMRequestTestUtils.addVolumeAndBucketToDB(volumeName, bucketName,
+        omMetadataManager, BucketLayout.OBJECT_STORE);
+    // a zero tag is never valid protobuf
+    String ozoneKey = omMetadataManager.getOzoneKey(volumeName, bucketName, 
"badKey");
+    omMetadataManager.getStore().getTable(OMDBDefinition.DELETED_TABLE, 
StringCodec.get(), ByteArrayCodec.get())
+        .put(omMetadataManager.getOzoneDeletePathKey(1L, ozoneKey), new byte[] 
{0});
+    corruptSnapshotUsage(bucketName, 1L);
+
+    assertFalse(awaitRepair(new QuotaRepairTask(ozoneManager).repair()));
+    assertThat(QuotaRepairTask.getStatus()).contains("CodecException");
+    assertNull(request.get());
+  }
+
   private void zeroOutBucketUsedBytes(String volumeName, String bucketName,
                                       long trxnLogIndex)
       throws IOException {
@@ -706,6 +803,28 @@ private String corruptBucketUsage(String bucket, long 
usedBytes, long usedNamesp
     return bucketKey;
   }
 
+  private String corruptSnapshotUsage(String bucket, long transactionIndex) 
throws IOException {
+    String bucketKey = omMetadataManager.getBucketKey(volumeName, bucket);
+    OmBucketInfo corrupted = 
omMetadataManager.getBucketTable().get(bucketKey).toBuilder()
+        .setSnapshotUsedBytes(7L).setSnapshotUsedNamespace(99L).build();
+    omMetadataManager.getBucketTable().put(bucketKey, corrupted);
+    omMetadataManager.getBucketTable().addCacheEntry(
+        new CacheKey<>(bucketKey), CacheValue.get(transactionIndex, 
corrupted));
+    return bucketKey;
+  }
+
+  // repair scans a RocksDB checkpoint, so rows must be written to the table, 
not the cache
+  private void putDeletedKey(String bucket, long bucketId, String keyName, int 
versions,
+      ReplicationConfig replicationConfig) throws IOException {
+    RepeatedOmKeyInfo row = new RepeatedOmKeyInfo(bucketId);
+    for (int i = 0; i < versions; i++) {
+      row.addOmKeyInfo(OMRequestTestUtils.createOmKeyInfo(volumeName, bucket, 
keyName, replicationConfig)
+          .setObjectID(i + 1L).build());
+    }
+    String ozoneKey = omMetadataManager.getOzoneKey(volumeName, bucket, 
keyName);
+    
omMetadataManager.getDeletedTable().put(omMetadataManager.getOzoneDeletePathKey(1L,
 ozoneKey), row);
+  }
+
   private void 
applyQuotaRepair(AtomicReference<OzoneManagerProtocolProtos.OMRequest> request,
       long transactionIndex, String... buckets) throws Exception {
     QuotaRepairTask quotaRepairTask = new QuotaRepairTask(ozoneManager);


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

Reply via email to