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 5be644f5edf HDDS-15114. Replace misconfigured ThreadPoolExecutor with 
Executors factory methods (#10133)
5be644f5edf is described below

commit 5be644f5edf49a1ca4bf5c9472782bcb17e3ff7d
Author: Rishabh Patel <[email protected]>
AuthorDate: Thu Jun 4 01:39:21 2026 -0700

    HDDS-15114. Replace misconfigured ThreadPoolExecutor with Executors factory 
methods (#10133)
---
 .../common/src/main/resources/ozone-default.xml    |  10 ++
 .../hadoop/ozone/recon/ReconServerConfigKeys.java  |   5 +
 .../apache/hadoop/ozone/recon/TarExtractor.java    |   8 +-
 .../ozone/recon/api/DataNodeMetricsService.java    |  28 +++--
 .../spi/impl/OzoneManagerServiceProviderImpl.java  |   2 +-
 .../hadoop/ozone/recon/TestTarExtractor.java       | 131 +++++++++++++++++++++
 6 files changed, 165 insertions(+), 19 deletions(-)

diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml 
b/hadoop-hdds/common/src/main/resources/ozone-default.xml
index f9dce0b1e97..f93b14e9c23 100644
--- a/hadoop-hdds/common/src/main/resources/ozone-default.xml
+++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml
@@ -3668,6 +3668,16 @@
       If it exceeds pending tasks will be cancelled.
     </description>
   </property>
+  <property>
+    <name>ozone.recon.dn.metrics.collection.thread.count</name>
+    <value>0</value>
+    <tag>OZONE, RECON, DN</tag>
+    <description>
+      Size of the thread pool Recon uses to collect JMX metrics from DataNodes.
+      A value of 0 (or any non-positive value) means "auto" and selects
+      2 x Runtime.availableProcessors() at startup.
+    </description>
+  </property>
   <property>
     <name>ozone.scm.datanode.admin.monitor.interval</name>
     <value>30s</value>
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServerConfigKeys.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServerConfigKeys.java
index c6a99508d80..633d2077621 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServerConfigKeys.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServerConfigKeys.java
@@ -233,6 +233,11 @@ public final class  ReconServerConfigKeys {
       "ozone.recon.dn.metrics.collection.timeout";
   public static final String OZONE_RECON_DN_METRICS_COLLECTION_TIMEOUT_DEFAULT 
= "10m";
 
+  public static final String OZONE_RECON_DN_METRICS_COLLECTION_THREAD_COUNT =
+      "ozone.recon.dn.metrics.collection.thread.count";
+  public static final int 
OZONE_RECON_DN_METRICS_COLLECTION_THREAD_COUNT_DEFAULT =
+      Runtime.getRuntime().availableProcessors() * 2;
+
   /**
    * Application-level ceiling on the number of ContainerIDs fetched from SCM
    * per RPC call during container sync. The effective batch size is
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/TarExtractor.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/TarExtractor.java
index b3bd17bdece..49f2cdc40a3 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/TarExtractor.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/TarExtractor.java
@@ -34,10 +34,9 @@
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
@@ -73,7 +72,7 @@ public class TarExtractor {
   public TarExtractor(int threadPoolSize, String threadNamePrefix) {
     this.threadPoolSize = threadPoolSize;
     this.threadFactory =
-        new ThreadFactoryBuilder().setNameFormat("FetchOMDBTar-%d" + 
threadNamePrefix)
+        new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + 
"FetchOMDBTar-%d")
             .build();
   }
 
@@ -163,8 +162,7 @@ private void writeFile(Path outputDir, String fileName, 
byte[] fileData) {
 
   public void start() {
     if (executorServiceStarted.compareAndSet(false, true)) {
-      this.executor =
-          new ThreadPoolExecutor(0, threadPoolSize, 60L, TimeUnit.SECONDS, new 
LinkedBlockingQueue<>(), threadFactory);
+      this.executor = Executors.newFixedThreadPool(threadPoolSize, 
threadFactory);
     }
   }
 
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/DataNodeMetricsService.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/DataNodeMetricsService.java
index 558883ee87e..369c5514d31 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/DataNodeMetricsService.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/api/DataNodeMetricsService.java
@@ -19,6 +19,8 @@
 
 import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DN_METRICS_COLLECTION_MINIMUM_API_DELAY;
 import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DN_METRICS_COLLECTION_MINIMUM_API_DELAY_DEFAULT;
+import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DN_METRICS_COLLECTION_THREAD_COUNT;
+import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DN_METRICS_COLLECTION_THREAD_COUNT_DEFAULT;
 import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DN_METRICS_COLLECTION_TIMEOUT;
 import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DN_METRICS_COLLECTION_TIMEOUT_DEFAULT;
 
@@ -31,9 +33,10 @@
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
@@ -60,11 +63,9 @@
 public class DataNodeMetricsService {
   
   private static final Logger LOG = 
LoggerFactory.getLogger(DataNodeMetricsService.class);
-  private static final int MAX_POOL_SIZE = 500;
-  private static final int KEEP_ALIVE_TIME = 5;
   private static final int POLL_INTERVAL_MS = 200;
 
-  private final ThreadPoolExecutor executorService;
+  private final ExecutorService executorService;
   private final ReconNodeManager reconNodeManager;
   private final boolean httpsEnabled;
   private final int minimumApiDelayMs;
@@ -95,14 +96,15 @@ public DataNodeMetricsService(
         OZONE_RECON_DN_METRICS_COLLECTION_TIMEOUT_DEFAULT, 
TimeUnit.MILLISECONDS);
     this.metricsServiceProviderFactory = metricsServiceProviderFactory;
     this.lastCollectionEndTime.set(-minimumApiDelayMs);
-    int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
-    this.executorService = new ThreadPoolExecutor(
-        corePoolSize, MAX_POOL_SIZE,
-        KEEP_ALIVE_TIME, TimeUnit.SECONDS,
-        new LinkedBlockingQueue<>(),
-        new ThreadFactoryBuilder()
-            .setNameFormat("DataNodeMetricsCollector-%d")
-            .build());
+    int corePoolSize = 
config.getInt(OZONE_RECON_DN_METRICS_COLLECTION_THREAD_COUNT,
+        OZONE_RECON_DN_METRICS_COLLECTION_THREAD_COUNT_DEFAULT);
+    corePoolSize = corePoolSize > 0
+        ? corePoolSize
+        : OZONE_RECON_DN_METRICS_COLLECTION_THREAD_COUNT_DEFAULT;
+    ThreadFactory threadFactory = new ThreadFactoryBuilder()
+        .setNameFormat("DataNodeMetricsCollector-%d")
+        .build();
+    this.executorService = Executors.newFixedThreadPool(corePoolSize, 
threadFactory);
   }
 
   /**
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/OzoneManagerServiceProviderImpl.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/OzoneManagerServiceProviderImpl.java
index dca33c759b8..cd62b2160da 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/OzoneManagerServiceProviderImpl.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/OzoneManagerServiceProviderImpl.java
@@ -229,7 +229,7 @@ public OzoneManagerServiceProviderImpl(
         new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + 
"SyncOM-%d")
             .build();
     // Number of parallel workers
-    int omDBTarProcessorThreadCount = Math.max(64, 
Runtime.getRuntime().availableProcessors());
+    int omDBTarProcessorThreadCount = Math.min(64, 
Runtime.getRuntime().availableProcessors());
     this.reconContext = reconContext;
     this.taskStatusUpdaterManager = taskStatusUpdaterManager;
     this.omDBLagThreshold = 
configuration.getLong(RECON_OM_DELTA_UPDATE_LAG_THRESHOLD,
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/TestTarExtractor.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/TestTarExtractor.java
new file mode 100644
index 00000000000..5a21a2f2032
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/TestTarExtractor.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.recon;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+
+/**
+ * Tests for {@link TarExtractor}.
+ */
+public class TestTarExtractor {
+
+  @Test
+  public void testStartCreatesFixedThreadPoolWithConfiguredSize() {
+    int poolSize = 8;
+    String threadPrefix = "TestPrefix-";
+    ExecutorService mockExecutor = mock(ExecutorService.class);
+
+    // Construct outside mockStatic block so ThreadFactoryBuilder can
+    // use the real Executors.defaultThreadFactory() internally.
+    TarExtractor extractor = new TarExtractor(poolSize, threadPrefix);
+
+    try (MockedStatic<Executors> executorsMock = mockStatic(Executors.class)) {
+      executorsMock.when(() -> Executors.newFixedThreadPool(
+          eq(poolSize), any(ThreadFactory.class))).thenReturn(mockExecutor);
+
+      extractor.start();
+
+      executorsMock.verify(() -> Executors.newFixedThreadPool(
+          eq(poolSize), any(ThreadFactory.class)));
+    }
+  }
+
+  @Test
+  public void testThreadFactoryUsesConfiguredPrefix() {
+    int poolSize = 4;
+    String threadPrefix = "MyCustomPrefix-";
+    ExecutorService mockExecutor = mock(ExecutorService.class);
+    TarExtractor extractor = new TarExtractor(poolSize, threadPrefix);
+
+    ArgumentCaptor<ThreadFactory> factoryCaptor =
+        ArgumentCaptor.forClass(ThreadFactory.class);
+
+    try (MockedStatic<Executors> executorsMock = mockStatic(Executors.class)) {
+      executorsMock.when(() -> Executors.newFixedThreadPool(
+          eq(poolSize), any(ThreadFactory.class))).thenReturn(mockExecutor);
+
+      extractor.start();
+
+      executorsMock.verify(() -> Executors.newFixedThreadPool(
+          eq(poolSize), factoryCaptor.capture()));
+
+      ThreadFactory capturedFactory = factoryCaptor.getValue();
+      Thread thread = capturedFactory.newThread(() -> {
+      });
+      assertTrue(thread.getName().startsWith(threadPrefix),
+          "Thread name should start with configured prefix, but was: "
+              + thread.getName());
+    }
+  }
+
+  @Test
+  public void testStopShutsDownExecutor() throws InterruptedException {
+    int poolSize = 4;
+    String threadPrefix = "ShutdownTest-";
+    ExecutorService mockExecutor = mock(ExecutorService.class);
+
+    // Construct outside mockStatic block.
+    TarExtractor extractor = new TarExtractor(poolSize, threadPrefix);
+
+    try (MockedStatic<Executors> executorsMock = mockStatic(Executors.class)) {
+      executorsMock.when(() -> Executors.newFixedThreadPool(
+          eq(poolSize), any(ThreadFactory.class))).thenReturn(mockExecutor);
+      when(mockExecutor.awaitTermination(60, TimeUnit.SECONDS))
+          .thenReturn(true);
+
+      extractor.start();
+      extractor.stop();
+
+      verify(mockExecutor).shutdown();
+    }
+  }
+
+  @Test
+  public void testStartIsIdempotent() {
+    int poolSize = 4;
+    String threadPrefix = "IdempotentTest-";
+    ExecutorService mockExecutor = mock(ExecutorService.class);
+    TarExtractor extractor = new TarExtractor(poolSize, threadPrefix);
+
+    try (MockedStatic<Executors> executorsMock = mockStatic(Executors.class)) {
+      executorsMock.when(() -> Executors.newFixedThreadPool(
+          eq(poolSize), any(ThreadFactory.class))).thenReturn(mockExecutor);
+
+      extractor.start();
+      extractor.start(); // second call should be a no-op
+
+      // newFixedThreadPool should only be called once
+      executorsMock.verify(() -> Executors.newFixedThreadPool(
+          eq(poolSize), any(ThreadFactory.class)));
+    }
+  }
+}


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

Reply via email to