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

jojochuang 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 0df1bc25212 HDDS-15997. Fix QuotaRepairTask scan hang and partial 
counts on worker failure (#10884)
0df1bc25212 is described below

commit 0df1bc25212984ee057b1c212d9557b92395f7f5
Author: Chi-Hsuan Huang <[email protected]>
AuthorDate: Fri Aug 28 05:13:23 2026 +0800

    HDDS-15997. Fix QuotaRepairTask scan hang and partial counts on worker 
failure (#10884)
    
    Generated-by: Codex (GPT-5)
---
 .../hadoop/ozone/om/service/QuotaRepairTask.java   | 123 +++++++++++++++++----
 .../ozone/om/service/TestQuotaRepairTask.java      | 113 +++++++++++++++++++
 2 files changed, 214 insertions(+), 22 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 2805f84c837..443b997340a 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
@@ -23,6 +23,7 @@
 import static org.apache.hadoop.ozone.OzoneConsts.OM_KEY_PREFIX;
 import static 
org.apache.hadoop.ozone.om.helpers.SnapshotInfo.SnapshotStatus.SNAPSHOT_ACTIVE;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.protobuf.ServiceException;
 import java.io.File;
@@ -49,6 +50,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
 import org.apache.commons.io.FileUtils;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.server.JsonUtils;
@@ -82,7 +84,8 @@
 public class QuotaRepairTask {
   private static final Logger LOG = LoggerFactory.getLogger(
       QuotaRepairTask.class);
-  private static final int BATCH_SIZE = 5000;
+  @VisibleForTesting
+  static final int BATCH_SIZE = 5000;
   private static final int TASK_THREAD_CNT = 3;
   /**
    * Parallel full-table scans: OBS keys, FSO files, dirs, active deleted 
keys/dirs,
@@ -363,8 +366,10 @@ private void repairCount(
         }
       }));
 
-      for (Future<?> f : tasks) {
-        f.get();
+      // await every scan before propagating a failure, so no scan outlives 
the checkpoint
+      Exception scanFailure = awaitAll(tasks, null);
+      if (scanFailure != null) {
+        throw scanFailure;
       }
     } catch (UncheckedIOException ex) {
       LOG.error("quota repair failure", ex.getCause());
@@ -576,6 +581,21 @@ private <VALUE> void recalculateUsages(
       Table<String, VALUE> table, Map<String, CountPair> prefixUsageMap,
       String strType, boolean haveValue) throws UncheckedIOException,
       UncheckedExecutionException {
+    try (Table.KeyValueIterator<String, VALUE> keyIter
+        = table.iterator(null, haveValue ? KEY_AND_VALUE : KEY_ONLY)) {
+      scanTableInBatches(executor, keyIter, strType,
+          kv -> extractCount(kv, prefixUsageMap, haveValue));
+    } catch (IOException ex) {
+      throw new UncheckedIOException(ex);
+    }
+  }
+
+  @VisibleForTesting
+  static <VALUE> void scanTableInBatches(
+      ExecutorService executor,
+      Table.KeyValueIterator<String, VALUE> keyIter, String strType,
+      Consumer<Table.KeyValue<String, VALUE>> kvConsumer)
+      throws UncheckedIOException, UncheckedExecutionException {
     LOG.info("Starting recalculate {}", strType);
 
     List<Table.KeyValue<String, VALUE>> kvList = new ArrayList<>(BATCH_SIZE);
@@ -584,53 +604,112 @@ private <VALUE> void recalculateUsages(
     List<Future<?>> tasks = new ArrayList<>();
     AtomicBoolean isRunning = new AtomicBoolean(true);
     for (int i = 0; i < TASK_THREAD_CNT; ++i) {
-      tasks.add(executor.submit(() -> captureCount(
-          prefixUsageMap, q, isRunning, haveValue)));
+      tasks.add(executor.submit(() -> captureCount(q, isRunning, kvConsumer)));
     }
     int count = 0;
     long startTime = Time.monotonicNow();
-    try (Table.KeyValueIterator<String, VALUE> keyIter
-        = table.iterator(null, haveValue ? KEY_AND_VALUE : KEY_ONLY)) {
+    Exception failure = null;
+    try {
       while (keyIter.hasNext()) {
         count++;
         kvList.add(keyIter.next());
         if (kvList.size() == BATCH_SIZE) {
-          q.put(kvList);
+          putBatch(q, kvList, tasks);
           kvList = new ArrayList<>(BATCH_SIZE);
         }
       }
-      q.put(kvList);
-      isRunning.set(false);
-      for (Future<?> f : tasks) {
-        f.get();
+      if (!kvList.isEmpty()) {
+        putBatch(q, kvList, tasks);
       }
-      LOG.info("Recalculate {} completed, count {} time {}ms", strType,
-          count, (Time.monotonicNow() - startTime));
-    } catch (IOException ex) {
-      throw new UncheckedIOException(ex);
     } catch (InterruptedException ex) {
       Thread.currentThread().interrupt();
-    } catch (ExecutionException ex) {
-      throw new UncheckedExecutionException(ex);
+      failure = ex;
+      q.clear();
+    } catch (ExecutionException | RuntimeException ex) {
+      failure = ex;
+      q.clear();
+    } finally {
+      isRunning.set(false);
+    }
+    // always await workers so none outlives this scan and touches a closed 
table
+    failure = awaitAll(tasks, failure);
+    if (failure != null) {
+      throw new UncheckedExecutionException(failure);
     }
+    LOG.info("Recalculate {} completed, count {} time {}ms", strType,
+        count, (Time.monotonicNow() - startTime));
   }
-  
+
+  /**
+   * Awaits every task, retrying an interrupted wait so the interrupted future 
is not
+   * abandoned mid-run; an interrupt seen while waiting is restored before 
returning.
+   * Returns the passed-in failure or the first failure seen, with later ones 
suppressed.
+   */
+  private static Exception awaitAll(List<Future<?>> tasks, Exception failure) {
+    boolean interrupted = false;
+    for (Future<?> f : tasks) {
+      boolean done = false;
+      while (!done) {
+        try {
+          f.get();
+          done = true;
+        } catch (InterruptedException ex) {
+          interrupted = true;
+          if (failure == null) {
+            failure = ex;
+          }
+        } catch (ExecutionException ex) {
+          done = true;
+          if (failure == null) {
+            failure = ex;
+          } else {
+            failure.addSuppressed(ex);
+          }
+        }
+      }
+    }
+    if (interrupted) {
+      Thread.currentThread().interrupt();
+    }
+    return failure;
+  }
+
+  /**
+   * Blocks until the batch is queued. Fails fast if a worker has already 
exited,
+   * otherwise a failed worker set could leave the producer blocked forever on 
a full queue.
+   */
+  private static <VALUE> void putBatch(
+      BlockingQueue<List<Table.KeyValue<String, VALUE>>> q,
+      List<Table.KeyValue<String, VALUE>> kvList,
+      List<Future<?>> tasks) throws InterruptedException, ExecutionException {
+    while (!q.offer(kvList, 100, TimeUnit.MILLISECONDS)) {
+      for (Future<?> f : tasks) {
+        if (f.isDone()) {
+          f.get();
+          throw new IllegalStateException("quota repair scan worker exited 
prematurely");
+        }
+      }
+    }
+  }
+
   private static <VALUE> void captureCount(
-      Map<String, CountPair> prefixUsageMap,
       BlockingQueue<List<Table.KeyValue<String, VALUE>>> q,
-      AtomicBoolean isRunning, boolean haveValue) throws UncheckedIOException {
+      AtomicBoolean isRunning,
+      Consumer<Table.KeyValue<String, VALUE>> kvConsumer) throws 
UncheckedIOException {
     try {
       while (isRunning.get() || !q.isEmpty()) {
         List<Table.KeyValue<String, VALUE>> kvList
             = q.poll(100, TimeUnit.MILLISECONDS);
         if (null != kvList) {
           for (Table.KeyValue<String, VALUE> kv : kvList) {
-            extractCount(kv, prefixUsageMap, haveValue);
+            kvConsumer.accept(kv);
           }
         }
       }
     } catch (InterruptedException ex) {
       Thread.currentThread().interrupt();
+      // fail the scan instead of returning partial counts as success
+      throw new IllegalStateException("quota repair scan worker interrupted", 
ex);
     }
   }
   
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 a956fb3cb21..00b01b18c44 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
@@ -20,7 +20,10 @@
 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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -28,12 +31,19 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import com.google.common.util.concurrent.UncheckedExecutionException;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.hadoop.hdds.client.RatisReplicationConfig;
 import org.apache.hadoop.hdds.utils.db.BatchOperation;
+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.om.helpers.BucketLayout;
@@ -336,4 +346,107 @@ private void zeroOutBucketUsedBytes(String volumeName, 
String bucketName,
             CacheValue.get(trxnLogIndex, bucketInfo));
     omMetadataManager.getBucketTable().put(dbKey, bucketInfo);
   }
+
+  @Test
+  public void testScanTableInBatchesFailsFastOnWorkerFailure() throws 
Exception {
+    int totalEntries = QuotaRepairTask.BATCH_SIZE * 8;
+    AtomicInteger remaining = new AtomicInteger(totalEntries);
+    @SuppressWarnings("unchecked")
+    Table.KeyValueIterator<String, OmKeyInfo> keyIter = 
mock(Table.KeyValueIterator.class);
+    when(keyIter.hasNext()).thenAnswer(inv -> remaining.get() > 0);
+    when(keyIter.next()).thenAnswer(inv -> {
+      remaining.decrementAndGet();
+      return Table.newKeyValue("/vol/bucket/key", null);
+    });
+    ExecutorService executor = Executors.newFixedThreadPool(3);
+    try {
+      UncheckedExecutionException ex = 
assertThrows(UncheckedExecutionException.class,
+          () -> QuotaRepairTask.scanTableInBatches(executor, keyIter, "worker 
failure test", kv -> {
+            throw new UncheckedIOException(new IOException("injected worker 
failure"));
+          }));
+      assertInstanceOf(UncheckedIOException.class, ex.getCause().getCause());
+      // no worker may outlive the scan
+      executor.shutdown();
+      assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+    } finally {
+      executor.shutdownNow();
+    }
+  }
+
+  @Test
+  public void testScanTableInBatchesFailsOnProducerInterrupt() throws 
Exception {
+    @SuppressWarnings("unchecked")
+    Table.KeyValueIterator<String, OmKeyInfo> keyIter = 
mock(Table.KeyValueIterator.class);
+    when(keyIter.hasNext()).thenReturn(true);
+    when(keyIter.next()).thenAnswer(inv -> 
Table.newKeyValue("/vol/bucket/key", null));
+    ExecutorService executor = Executors.newFixedThreadPool(3);
+    AtomicReference<Throwable> thrown = new AtomicReference<>();
+    Thread producer = new Thread(() -> {
+      try {
+        QuotaRepairTask.scanTableInBatches(executor, keyIter, "interrupt 
test", kv -> { });
+      } catch (Throwable t) {
+        thrown.set(t);
+      }
+    });
+    try {
+      producer.start();
+      producer.interrupt();
+      producer.join(TimeUnit.SECONDS.toMillis(60));
+      assertFalse(producer.isAlive());
+      UncheckedExecutionException ex = 
assertInstanceOf(UncheckedExecutionException.class, thrown.get());
+      assertInstanceOf(InterruptedException.class, ex.getCause());
+      executor.shutdown();
+      assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+    } finally {
+      executor.shutdownNow();
+    }
+  }
+
+  @Test
+  public void testInterruptedScanStillAwaitsWorkers() throws Exception {
+    AtomicInteger remaining = new AtomicInteger(QuotaRepairTask.BATCH_SIZE);
+    @SuppressWarnings("unchecked")
+    Table.KeyValueIterator<String, OmKeyInfo> keyIter = 
mock(Table.KeyValueIterator.class);
+    when(keyIter.hasNext()).thenAnswer(inv -> remaining.get() > 0);
+    when(keyIter.next()).thenAnswer(inv -> {
+      remaining.decrementAndGet();
+      return Table.newKeyValue("/vol/bucket/key", null);
+    });
+    CountDownLatch workerStarted = new CountDownLatch(1);
+    CountDownLatch releaseWorker = new CountDownLatch(1);
+    ExecutorService executor = Executors.newFixedThreadPool(3);
+    AtomicReference<Throwable> thrown = new AtomicReference<>();
+    Thread producer = new Thread(() -> {
+      try {
+        QuotaRepairTask.scanTableInBatches(executor, keyIter, "await workers 
test", kv -> {
+          workerStarted.countDown();
+          try {
+            releaseWorker.await();
+          } catch (InterruptedException ex) {
+            throw new IllegalStateException(ex);
+          }
+        });
+      } catch (Throwable t) {
+        thrown.set(t);
+      }
+    });
+    try {
+      producer.start();
+      assertTrue(workerStarted.await(30, TimeUnit.SECONDS));
+      producer.interrupt();
+      // the interrupted producer must keep waiting for the blocked worker 
instead of exiting
+      producer.join(TimeUnit.SECONDS.toMillis(1));
+      assertTrue(producer.isAlive());
+      releaseWorker.countDown();
+      producer.join(TimeUnit.SECONDS.toMillis(60));
+      assertFalse(producer.isAlive());
+      UncheckedExecutionException ex = 
assertInstanceOf(UncheckedExecutionException.class, thrown.get());
+      assertInstanceOf(InterruptedException.class, ex.getCause());
+      executor.shutdown();
+      assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+    } finally {
+      releaseWorker.countDown();
+      executor.shutdownNow();
+    }
+  }
 }


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

Reply via email to