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

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


The following commit(s) were added to refs/heads/master by this push:
     new eca5e883a70a  feat: support configurable parallelism for partition TTL 
stats collection (#19137)
eca5e883a70a is described below

commit eca5e883a70ae3932cc78b875f0f2a2d92ca15ac
Author: wangxianghu <[email protected]>
AuthorDate: Thu Jul 2 15:47:48 2026 +0800

     feat: support configurable parallelism for partition TTL stats collection 
(#19137)
---
 .../org/apache/hudi/config/HoodieTTLConfig.java    | 15 +++++
 .../org/apache/hudi/config/HoodieWriteConfig.java  |  9 +++
 .../action/ttl/strategy/KeepByTimeStrategy.java    |  2 +-
 .../apache/hudi/config/TestHoodieWriteConfig.java  | 22 ++++++++
 .../ttl/strategy/TestKeepByTimeStrategy.java       | 64 ++++++++++++++++++++++
 5 files changed, 111 insertions(+), 1 deletion(-)

diff --git 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieTTLConfig.java
 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieTTLConfig.java
index 1f9a4e40e983..6411081d5a9b 100644
--- 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieTTLConfig.java
+++ 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieTTLConfig.java
@@ -84,6 +84,16 @@ public class HoodieTTLConfig extends HoodieConfig {
       .sinceVersion("1.0.0")
       .withDocumentation("max partitions to delete in partition ttl 
management");
 
+  public static final ConfigProperty<Integer> STATS_MAX_PARALLELISM = 
ConfigProperty
+      .key(PARTITION_TTL_STRATEGY_PARAM_PREFIX + "stats.max.parallelism")
+      .defaultValue(200)
+      .markAdvanced()
+      .sinceVersion("1.3.0")
+      .withDocumentation("Max parallelism used to collect the last commit time 
for candidate partitions "
+          + "during partition ttl management. The effective parallelism is the 
smaller of the candidate "
+          + "partition count and this value. When a table enables partition 
ttl for the first time, there "
+          + "may be a large number of historical partitions, so a higher value 
than the default may be desired.");
+
   public static class Builder {
     private final HoodieTTLConfig ttlConfig = new HoodieTTLConfig();
 
@@ -97,6 +107,11 @@ public class HoodieTTLConfig extends HoodieConfig {
       return this;
     }
 
+    public HoodieTTLConfig.Builder withTTLStatsMaxParallelism(Integer 
statsMaxParallelism) {
+      ttlConfig.setValue(STATS_MAX_PARALLELISM, 
statsMaxParallelism.toString());
+      return this;
+    }
+
     public HoodieTTLConfig.Builder enableInlinePartitionTTL(Boolean enable) {
       ttlConfig.setValue(INLINE_PARTITION_TTL, enable.toString());
       return this;
diff --git 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieWriteConfig.java
 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieWriteConfig.java
index e0bce7985cbb..94a075d4288b 100644
--- 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieWriteConfig.java
+++ 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieWriteConfig.java
@@ -3058,6 +3058,10 @@ public class HoodieWriteConfig extends HoodieConfig {
     return getInt(HoodieTTLConfig.MAX_PARTITION_TO_DELETE);
   }
 
+  public Integer getPartitionTTLStatsMaxParallelism() {
+    return getInt(HoodieTTLConfig.STATS_MAX_PARALLELISM);
+  }
+
   public boolean isSecondaryIndexEnabled() {
     return metadataConfig.isSecondaryIndexEnabled();
   }
@@ -3873,6 +3877,11 @@ public class HoodieWriteConfig extends HoodieConfig {
       checkArgument(lookbackCommits >= 0,
           String.format("%s must be non-negative, but was %d",
               ROLLING_METADATA_TIMELINE_LOOKBACK_COMMITS.key(), 
lookbackCommits));
+
+      int ttlStatsMaxParallelism = 
writeConfig.getInt(HoodieTTLConfig.STATS_MAX_PARALLELISM);
+      checkArgument(ttlStatsMaxParallelism > 0,
+          String.format("%s must be positive, but was %d",
+              HoodieTTLConfig.STATS_MAX_PARALLELISM.key(), 
ttlStatsMaxParallelism));
     }
 
     public HoodieWriteConfig build() {
diff --git 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/ttl/strategy/KeepByTimeStrategy.java
 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/ttl/strategy/KeepByTimeStrategy.java
index 8968d69f70bd..41c8d32174f7 100644
--- 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/ttl/strategy/KeepByTimeStrategy.java
+++ 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/ttl/strategy/KeepByTimeStrategy.java
@@ -85,7 +85,7 @@ public class KeepByTimeStrategy extends PartitionTTLStrategy {
       log.info("Candidate partition paths list is empty, skip TTL stats 
collection");
       return Collections.emptyMap();
     }
-    int statsParallelism = Math.min(partitionPaths.size(), 200);
+    int statsParallelism = Math.min(partitionPaths.size(), 
writeConfig.getPartitionTTLStatsMaxParallelism());
     return hoodieTable.getContext().map(partitionPaths, partitionPath -> {
       Option<String> partitionLastModifiedTime = hoodieTable.getHoodieView()
           .getLatestFileSlicesBeforeOrOn(partitionPath, instantTime, true)
diff --git 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/config/TestHoodieWriteConfig.java
 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/config/TestHoodieWriteConfig.java
index 044e41ed89d2..42e37cf835de 100644
--- 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/config/TestHoodieWriteConfig.java
+++ 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/config/TestHoodieWriteConfig.java
@@ -50,6 +50,7 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
@@ -124,6 +125,27 @@ public class TestHoodieWriteConfig {
         });
   }
 
+  @Test
+  public void testPartitionTTLStatsMaxParallelismMustBePositive() {
+    // A non-positive value (0 or negative) is a misconfiguration and must 
fail fast at build time,
+    // rather than being silently coerced when TTL stats collection runs.
+    for (int invalid : new int[] {0, -5}) {
+      IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+          () -> HoodieWriteConfig.newBuilder().withPath("/tmp")
+              .withProps(Collections.singletonMap(
+                  HoodieTTLConfig.STATS_MAX_PARALLELISM.key(), 
String.valueOf(invalid)))
+              .build());
+      
assertTrue(e.getMessage().contains(HoodieTTLConfig.STATS_MAX_PARALLELISM.key()),
+          "Validation error should mention the offending config key");
+    }
+
+    // A positive value builds successfully.
+    assertEquals(8, HoodieWriteConfig.newBuilder().withPath("/tmp")
+        .withProps(java.util.Collections.singletonMap(
+            HoodieTTLConfig.STATS_MAX_PARALLELISM.key(), "8"))
+        .build().getPartitionTTLStatsMaxParallelism());
+  }
+
   @Test
   public void testDefaultIndexAccordingToEngineType() {
     testEngineSpecificConfig(HoodieWriteConfig::getIndexType,
diff --git 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/table/action/ttl/strategy/TestKeepByTimeStrategy.java
 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/table/action/ttl/strategy/TestKeepByTimeStrategy.java
index 309eab2d6013..4c9658a157bb 100644
--- 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/table/action/ttl/strategy/TestKeepByTimeStrategy.java
+++ 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/table/action/ttl/strategy/TestKeepByTimeStrategy.java
@@ -18,15 +18,23 @@
 
 package org.apache.hudi.table.action.ttl.strategy;
 
+import org.apache.hudi.common.engine.HoodieEngineContext;
+import org.apache.hudi.common.function.SerializableFunction;
 import org.apache.hudi.config.HoodieWriteConfig;
 import org.apache.hudi.table.HoodieTable;
 
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -59,4 +67,60 @@ public class TestKeepByTimeStrategy {
     // Crucial: we must never reach the engine map call with parallelism=0.
     verify(hoodieTable, never()).getContext();
   }
+
+  /**
+   * When the candidate partition count exceeds the configured max parallelism,
+   * the stats collection must be capped at the configured value.
+   */
+  @Test
+  public void testStatsParallelism_cappedByConfiguredMax() {
+    int configuredMax = 500;
+    List<String> partitions = IntStream.range(0, 1000)
+        .mapToObj(i -> "p" + i)
+        .collect(Collectors.toList());
+
+    int captured = captureStatsParallelism(configuredMax, partitions);
+
+    assertEquals(configuredMax, captured,
+        "Parallelism should be capped at the configured max when partitions 
exceed it");
+  }
+
+  /**
+   * When the candidate partition count is below the configured max 
parallelism,
+   * the effective parallelism should be the partition count (avoid 
over-provisioning).
+   */
+  @Test
+  public void testStatsParallelism_boundedByPartitionCount() {
+    int configuredMax = 500;
+    List<String> partitions = IntStream.range(0, 50)
+        .mapToObj(i -> "p" + i)
+        .collect(Collectors.toList());
+
+    int captured = captureStatsParallelism(configuredMax, partitions);
+
+    assertEquals(partitions.size(), captured,
+        "Parallelism should equal the partition count when it is below the 
configured max");
+  }
+
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  private int captureStatsParallelism(int configuredMax, List<String> 
partitions) {
+    HoodieTable hoodieTable = mock(HoodieTable.class);
+    HoodieWriteConfig writeConfig = mock(HoodieWriteConfig.class);
+    HoodieEngineContext context = mock(HoodieEngineContext.class);
+    when(hoodieTable.getConfig()).thenReturn(writeConfig);
+    when(writeConfig.getPartitionTTLStrategyDaysRetain()).thenReturn(10);
+    
when(writeConfig.getPartitionTTLStatsMaxParallelism()).thenReturn(configuredMax);
+    when(hoodieTable.getContext()).thenReturn(context);
+    // Return an empty result so no expired partitions are produced; we only 
care
+    // about the parallelism argument handed to the engine.
+    when(context.map(any(List.class), any(SerializableFunction.class), 
anyInt()))
+        .thenReturn(Collections.emptyList());
+
+    KeepByTimeStrategy strategy = new KeepByTimeStrategy(hoodieTable, 
"20240101000000000");
+    strategy.getExpiredPartitionsForTimeStrategy(partitions);
+
+    ArgumentCaptor<Integer> parallelismCaptor = 
ArgumentCaptor.forClass(Integer.class);
+    verify(context).map(any(List.class), any(SerializableFunction.class), 
parallelismCaptor.capture());
+    return parallelismCaptor.getValue();
+  }
 }

Reply via email to