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

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


The following commit(s) were added to refs/heads/master by this push:
     new 5c8990523 [hotfix] Implement an independent process history cleanup 
mechanism and migrate the configuration item to the Duration type. (#4161)
5c8990523 is described below

commit 5c8990523f56f11102fd813f8fa90f90afb99631
Author: ConradJam <[email protected]>
AuthorDate: Tue Apr 7 21:06:28 2026 +0800

    [hotfix] Implement an independent process history cleanup mechanism and 
migrate the configuration item to the Duration type. (#4161)
---
 .../apache/amoro/server/AmoroManagementConf.java   |  40 ++++-
 .../apache/amoro/server/AmoroServiceContainer.java |   2 +-
 .../persistence/mapper/TableProcessMapper.java     |   7 +
 .../scheduler/inline/InlineTableExecutors.java     |  30 +++-
 ...cutor.java => ProcessDataExpiringExecutor.java} |  56 +++---
 .../amoro/server/TestAmoroManagementConf.java      |  26 +++
 .../inline/TestConfigurableIntervalExecutors.java  |  58 ++++++
 .../inline/TestProcessDataExpiringExecutor.java    | 194 +++++++++++++++++++++
 .../src/test/resources/config-with-units.yaml      |   5 +
 dist/src/main/amoro-bin/conf/config.yaml           |   7 +-
 docs/configuration/ams-config.md                   |   8 +-
 11 files changed, 397 insertions(+), 36 deletions(-)

diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
index 3a31933bf..98331ad33 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
@@ -479,19 +479,37 @@ public class AmoroManagementConf {
           .defaultValue(10)
           .withDescription("The number of threads that self-optimizing uses to 
submit results.");
 
+  /** @deprecated Use {@link #OPTIMIZING_RUNTIME_DATA_KEEP_TIME} instead. */
+  @Deprecated
   public static final ConfigOption<Integer> OPTIMIZING_RUNTIME_DATA_KEEP_DAYS =
       ConfigOptions.key("self-optimizing.runtime-data-keep-days")
           .intType()
           .defaultValue(30)
           .withDescription(
-              "The number of days that self-optimizing runtime data keeps the 
runtime.");
+              "Deprecated: use 'self-optimizing.runtime-data-keep-time' 
instead. "
+                  + "The number of days that self-optimizing runtime data 
keeps the runtime.");
 
+  /** @deprecated Use {@link #OPTIMIZING_RUNTIME_DATA_EXPIRE_INTERVAL} 
instead. */
+  @Deprecated
   public static final ConfigOption<Integer> 
OPTIMIZING_RUNTIME_DATA_EXPIRE_INTERVAL_HOURS =
       ConfigOptions.key("self-optimizing.runtime-data-expire-interval-hours")
           .intType()
           .defaultValue(1)
           .withDescription(
-              "The number of hours that self-optimizing runtime data expire 
interval.");
+              "Deprecated: use 'self-optimizing.runtime-data-expire-interval' 
instead. "
+                  + "The number of hours that self-optimizing runtime data 
expire interval.");
+
+  public static final ConfigOption<Duration> OPTIMIZING_RUNTIME_DATA_KEEP_TIME 
=
+      ConfigOptions.key("self-optimizing.runtime-data-keep-time")
+          .durationType()
+          .defaultValue(Duration.ofDays(30))
+          .withDescription("Duration that self-optimizing runtime data is 
retained.");
+
+  public static final ConfigOption<Duration> 
OPTIMIZING_RUNTIME_DATA_EXPIRE_INTERVAL =
+      ConfigOptions.key("self-optimizing.runtime-data-expire-interval")
+          .durationType()
+          .defaultValue(Duration.ofHours(1))
+          .withDescription("Interval between self-optimizing runtime data 
expiration runs.");
 
   public static final ConfigOption<Boolean> 
OPTIMIZING_BREAK_QUOTA_LIMIT_ENABLED =
       ConfigOptions.key("self-optimizing.break-quota-limit-enabled")
@@ -500,6 +518,24 @@ public class AmoroManagementConf {
           .withDescription(
               "Allow the table to break the quota limit when the resource is 
sufficient.");
 
+  /** @deprecated Use {@link #PROCESS_HISTORY_DATA_KEEP_TIME} instead. */
+  @Deprecated
+  public static final ConfigOption<Integer> PROCESS_HISTORY_DATA_KEEP_DAYS =
+      ConfigOptions.key("process.history-data-keep-days")
+          .intType()
+          .defaultValue(7)
+          .withDescription(
+              "Deprecated: use 'process.history-data-keep-time' instead. "
+                  + "The number of days that process history data is 
retained.");
+
+  public static final ConfigOption<Duration> PROCESS_HISTORY_DATA_KEEP_TIME =
+      ConfigOptions.key("process.history-data-keep-time")
+          .durationType()
+          .defaultValue(Duration.ofDays(7))
+          .withDescription(
+              "Duration that process history data is retained. "
+                  + "Expired terminal process records will be deleted 
automatically.");
+
   public static final ConfigOption<Duration> OVERVIEW_CACHE_REFRESH_INTERVAL =
       ConfigOptions.key("overview-cache.refresh-interval")
           .durationType()
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java
index 0fa2b917e..768dab89f 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java
@@ -293,7 +293,7 @@ public class AmoroServiceContainer {
     
addHandlerChain(InlineTableExecutors.getInstance().getOrphanFilesCleaningExecutor());
     
addHandlerChain(InlineTableExecutors.getInstance().getDanglingDeleteFilesCleaningExecutor());
     
addHandlerChain(InlineTableExecutors.getInstance().getOptimizingCommitExecutor());
-    
addHandlerChain(InlineTableExecutors.getInstance().getOptimizingExpiringExecutor());
+    
addHandlerChain(InlineTableExecutors.getInstance().getProcessDataExpiringExecutor());
     
addHandlerChain(InlineTableExecutors.getInstance().getBlockerExpiringExecutor());
     
addHandlerChain(InlineTableExecutors.getInstance().getHiveCommitSyncExecutor());
     
addHandlerChain(InlineTableExecutors.getInstance().getTableRefreshingExecutor());
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/persistence/mapper/TableProcessMapper.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/persistence/mapper/TableProcessMapper.java
index ed6192452..bbf4019c7 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/persistence/mapper/TableProcessMapper.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/persistence/mapper/TableProcessMapper.java
@@ -42,6 +42,13 @@ public interface TableProcessMapper {
   @Delete("DELETE FROM table_process WHERE process_id <= #{processId} AND 
table_id = #{tableId}")
   void deleteBefore(@Param("tableId") long tableId, @Param("processId") long 
processId);
 
+  @Delete(
+      "DELETE FROM table_process WHERE process_id <= #{minProcessId} "
+          + "AND table_id = #{tableId} "
+          + "AND status NOT IN ('SUBMITTED', 'RUNNING', 'PENDING', 
'CANCELING')")
+  void deleteExpiredProcesses(
+      @Param("tableId") long tableId, @Param("minProcessId") long 
minProcessId);
+
   @Insert(
       "INSERT INTO table_process "
           + "(process_id, table_id, external_process_identifier, status, 
process_type, process_stage, execution_engine, retry_number, "
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/InlineTableExecutors.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/InlineTableExecutors.java
index 17eb72e39..173c0259e 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/InlineTableExecutors.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/InlineTableExecutors.java
@@ -22,6 +22,8 @@ import org.apache.amoro.config.Configurations;
 import org.apache.amoro.server.AmoroManagementConf;
 import org.apache.amoro.server.table.TableService;
 
+import java.time.Duration;
+
 public class InlineTableExecutors {
 
   private static final InlineTableExecutors instance = new 
InlineTableExecutors();
@@ -30,7 +32,7 @@ public class InlineTableExecutors {
   private DanglingDeleteFilesCleaningExecutor 
danglingDeleteFilesCleaningExecutor;
   private BlockerExpiringExecutor blockerExpiringExecutor;
   private OptimizingCommitExecutor optimizingCommitExecutor;
-  private OptimizingExpiringExecutor optimizingExpiringExecutor;
+  private ProcessDataExpiringExecutor processDataExpiringExecutor;
   private HiveCommitSyncExecutor hiveCommitSyncExecutor;
   private TagsAutoCreatingExecutor tagsAutoCreatingExecutor;
   private DataExpiringExecutor dataExpiringExecutor;
@@ -57,11 +59,23 @@ public class InlineTableExecutors {
     this.optimizingCommitExecutor =
         new OptimizingCommitExecutor(
             tableService, 
conf.getInteger(AmoroManagementConf.OPTIMIZING_COMMIT_THREAD_COUNT));
-    this.optimizingExpiringExecutor =
-        new OptimizingExpiringExecutor(
-            tableService,
-            
conf.getInteger(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_KEEP_DAYS),
-            
conf.getInteger(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_EXPIRE_INTERVAL_HOURS));
+    Duration optimizingKeepTime =
+        conf.contains(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_KEEP_TIME)
+            ? conf.get(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_KEEP_TIME)
+            : Duration.ofDays(
+                
conf.getInteger(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_KEEP_DAYS));
+    Duration expireInterval =
+        
conf.contains(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_EXPIRE_INTERVAL)
+            ? 
conf.get(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_EXPIRE_INTERVAL)
+            : Duration.ofHours(
+                
conf.getInteger(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_EXPIRE_INTERVAL_HOURS));
+    Duration processKeepTime =
+        conf.contains(AmoroManagementConf.PROCESS_HISTORY_DATA_KEEP_TIME)
+            ? conf.get(AmoroManagementConf.PROCESS_HISTORY_DATA_KEEP_TIME)
+            : 
Duration.ofDays(conf.getInteger(AmoroManagementConf.PROCESS_HISTORY_DATA_KEEP_DAYS));
+    this.processDataExpiringExecutor =
+        new ProcessDataExpiringExecutor(
+            tableService, optimizingKeepTime, expireInterval, processKeepTime);
     this.blockerExpiringExecutor = new BlockerExpiringExecutor(tableService);
     if (conf.getBoolean(AmoroManagementConf.SYNC_HIVE_TABLES_ENABLED)) {
       this.hiveCommitSyncExecutor =
@@ -110,8 +124,8 @@ public class InlineTableExecutors {
     return optimizingCommitExecutor;
   }
 
-  public OptimizingExpiringExecutor getOptimizingExpiringExecutor() {
-    return optimizingExpiringExecutor;
+  public ProcessDataExpiringExecutor getProcessDataExpiringExecutor() {
+    return processDataExpiringExecutor;
   }
 
   public HiveCommitSyncExecutor getHiveCommitSyncExecutor() {
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/OptimizingExpiringExecutor.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/ProcessDataExpiringExecutor.java
similarity index 56%
rename from 
amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/OptimizingExpiringExecutor.java
rename to 
amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/ProcessDataExpiringExecutor.java
index 329afc8ef..1bc4e9ac7 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/OptimizingExpiringExecutor.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/scheduler/inline/ProcessDataExpiringExecutor.java
@@ -28,22 +28,30 @@ import org.apache.amoro.server.utils.SnowflakeIdGenerator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class OptimizingExpiringExecutor extends PeriodicTableScheduler {
-  private static final Logger LOG = 
LoggerFactory.getLogger(OptimizingExpiringExecutor.class);
+import java.time.Duration;
+
+public class ProcessDataExpiringExecutor extends PeriodicTableScheduler {
+  private static final Logger LOG = 
LoggerFactory.getLogger(ProcessDataExpiringExecutor.class);
 
   private final Persistency persistency = new Persistency();
-  private final long keepTime;
-  private final long interval;
+  private final long optimizingKeepTimeMs;
+  private final long processKeepTimeMs;
+  private final long expireIntervalMs;
 
-  public OptimizingExpiringExecutor(TableService tableService, int keepDays, 
int intervalHours) {
+  public ProcessDataExpiringExecutor(
+      TableService tableService,
+      Duration optimizingKeepTime,
+      Duration expireInterval,
+      Duration processKeepTime) {
     super(tableService, 1);
-    this.keepTime = keepDays * 24 * 60 * 60 * 1000L;
-    this.interval = intervalHours * 60 * 60 * 1000L;
+    this.optimizingKeepTimeMs = optimizingKeepTime.toMillis();
+    this.processKeepTimeMs = processKeepTime.toMillis();
+    this.expireIntervalMs = expireInterval.toMillis();
   }
 
   @Override
   protected long getNextExecutingTime(TableRuntime tableRuntime) {
-    return interval;
+    return expireIntervalMs;
   }
 
   @Override
@@ -68,32 +76,38 @@ public class OptimizingExpiringExecutor extends 
PeriodicTableScheduler {
 
   private class Persistency extends PersistentBase {
     public void doExpiring(TableRuntime tableRuntime) {
-      long expireTime = System.currentTimeMillis() - keepTime;
-      long minProcessId = SnowflakeIdGenerator.getMinSnowflakeId(expireTime);
+      long tableId = tableRuntime.getTableIdentifier().getId();
+      long now = System.currentTimeMillis();
+
+      // 1. Expire optimizing runtime data (optimizingKeepTimeMs, e.g. 30d)
+      long optimizingMinId = SnowflakeIdGenerator.getMinSnowflakeId(now - 
optimizingKeepTimeMs);
       doAsTransaction(
           () ->
               doAs(
                   TableProcessMapper.class,
-                  mapper ->
-                      
mapper.deleteBefore(tableRuntime.getTableIdentifier().getId(), minProcessId)),
+                  mapper -> mapper.deleteBefore(tableId, optimizingMinId)),
           () ->
               doAs(
                   OptimizingProcessMapper.class,
-                  mapper ->
-                      mapper.deleteProcessStateBefore(
-                          tableRuntime.getTableIdentifier().getId(), 
minProcessId)),
+                  mapper -> mapper.deleteProcessStateBefore(tableId, 
optimizingMinId)),
           () ->
               doAs(
                   OptimizingProcessMapper.class,
-                  mapper ->
-                      mapper.deleteTaskRuntimesBefore(
-                          tableRuntime.getTableIdentifier().getId(), 
minProcessId)),
+                  mapper -> mapper.deleteTaskRuntimesBefore(tableId, 
optimizingMinId)),
           () ->
               doAs(
                   OptimizingProcessMapper.class,
-                  mapper ->
-                      mapper.deleteOptimizingQuotaBefore(
-                          tableRuntime.getTableIdentifier().getId(), 
minProcessId)));
+                  mapper -> mapper.deleteOptimizingQuotaBefore(tableId, 
optimizingMinId)));
+
+      // 2. Expire process history terminal records (processKeepTimeMs, e.g. 
7d)
+      //    Only deletes terminal records in the window between 
processKeepTime and keepTime,
+      //    since records older than keepTime are already removed by step 1.
+      if (processKeepTimeMs < optimizingKeepTimeMs) {
+        long processMinId = SnowflakeIdGenerator.getMinSnowflakeId(now - 
processKeepTimeMs);
+        doAs(
+            TableProcessMapper.class,
+            mapper -> mapper.deleteExpiredProcesses(tableId, processMinId));
+      }
     }
   }
 }
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/TestAmoroManagementConf.java 
b/amoro-ams/src/test/java/org/apache/amoro/server/TestAmoroManagementConf.java
index 7b0fcc9d4..99e8b451a 100644
--- 
a/amoro-ams/src/test/java/org/apache/amoro/server/TestAmoroManagementConf.java
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/TestAmoroManagementConf.java
@@ -77,6 +77,32 @@ public class TestAmoroManagementConf {
     assertTimeRelatedConfigs(serviceConfig, expectedConfig);
   }
 
+  @Test
+  void testNewDurationConfigDefaults() {
+    Configurations serviceConfig = new Configurations();
+    Assertions.assertEquals(
+        Duration.ofDays(30),
+        
serviceConfig.get(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_KEEP_TIME));
+    Assertions.assertEquals(
+        Duration.ofHours(1),
+        
serviceConfig.get(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_EXPIRE_INTERVAL));
+    Assertions.assertEquals(
+        Duration.ofDays(7), 
serviceConfig.get(AmoroManagementConf.PROCESS_HISTORY_DATA_KEEP_TIME));
+  }
+
+  @Test
+  void testDeprecatedIntegerConfigDefaults() {
+    Configurations serviceConfig = new Configurations();
+    Assertions.assertEquals(
+        30, 
serviceConfig.getInteger(AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_KEEP_DAYS));
+    Assertions.assertEquals(
+        1,
+        serviceConfig.getInteger(
+            
AmoroManagementConf.OPTIMIZING_RUNTIME_DATA_EXPIRE_INTERVAL_HOURS));
+    Assertions.assertEquals(
+        7, 
serviceConfig.getInteger(AmoroManagementConf.PROCESS_HISTORY_DATA_KEEP_DAYS));
+  }
+
   @Test
   void testParsingDefaultStorageRelatedConfigs() {
     Configurations serviceConfig = new Configurations();
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/scheduler/inline/TestConfigurableIntervalExecutors.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/scheduler/inline/TestConfigurableIntervalExecutors.java
index a3d7b2515..5b50245b6 100644
--- 
a/amoro-ams/src/test/java/org/apache/amoro/server/scheduler/inline/TestConfigurableIntervalExecutors.java
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/scheduler/inline/TestConfigurableIntervalExecutors.java
@@ -64,4 +64,62 @@ public class TestConfigurableIntervalExecutors {
     // 5 hours ago - should not execute
     Assert.assertFalse(executor.shouldExecute(now - 
Duration.ofHours(5).toMillis()));
   }
+
+  @Test
+  public void testSnapshotsExpiringDefaultInterval() {
+    Duration interval = Duration.ofHours(1);
+    SnapshotsExpiringExecutor executor = new SnapshotsExpiringExecutor(null, 
1, interval);
+
+    TableRuntime tableRuntime = Mockito.mock(TableRuntime.class);
+    Assert.assertEquals(
+        Duration.ofHours(1).toMillis(), 
executor.getNextExecutingTime(tableRuntime));
+  }
+
+  @Test
+  public void testSnapshotsExpiringCustomInterval() {
+    Duration interval = Duration.ofMinutes(30);
+    SnapshotsExpiringExecutor executor = new SnapshotsExpiringExecutor(null, 
1, interval);
+
+    TableRuntime tableRuntime = Mockito.mock(TableRuntime.class);
+    Assert.assertEquals(
+        Duration.ofMinutes(30).toMillis(), 
executor.getNextExecutingTime(tableRuntime));
+  }
+
+  @Test
+  public void testProcessDataExpiringDefaultInterval() {
+    Duration optimizingKeepTime = Duration.ofDays(30);
+    Duration expireInterval = Duration.ofHours(1);
+    Duration processKeepTime = Duration.ofDays(7);
+    ProcessDataExpiringExecutor executor =
+        new ProcessDataExpiringExecutor(null, optimizingKeepTime, 
expireInterval, processKeepTime);
+
+    TableRuntime tableRuntime = Mockito.mock(TableRuntime.class);
+    Assert.assertEquals(
+        Duration.ofHours(1).toMillis(), 
executor.getNextExecutingTime(tableRuntime));
+  }
+
+  @Test
+  public void testProcessDataExpiringCustomInterval() {
+    Duration optimizingKeepTime = Duration.ofDays(15);
+    Duration expireInterval = Duration.ofMinutes(30);
+    Duration processKeepTime = Duration.ofDays(3);
+    ProcessDataExpiringExecutor executor =
+        new ProcessDataExpiringExecutor(null, optimizingKeepTime, 
expireInterval, processKeepTime);
+
+    TableRuntime tableRuntime = Mockito.mock(TableRuntime.class);
+    Assert.assertEquals(
+        Duration.ofMinutes(30).toMillis(), 
executor.getNextExecutingTime(tableRuntime));
+  }
+
+  @Test
+  public void testSnapshotsExpiringShouldExecuteAfterInterval() {
+    Duration interval = Duration.ofHours(2);
+    SnapshotsExpiringExecutor executor = new SnapshotsExpiringExecutor(null, 
1, interval);
+
+    long now = System.currentTimeMillis();
+    // 3 hours ago - should execute
+    Assert.assertTrue(executor.shouldExecute(now - 
Duration.ofHours(3).toMillis()));
+    // 1 hour ago - should not execute
+    Assert.assertFalse(executor.shouldExecute(now - 
Duration.ofHours(1).toMillis()));
+  }
 }
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/scheduler/inline/TestProcessDataExpiringExecutor.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/scheduler/inline/TestProcessDataExpiringExecutor.java
new file mode 100644
index 000000000..bd251b02d
--- /dev/null
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/scheduler/inline/TestProcessDataExpiringExecutor.java
@@ -0,0 +1,194 @@
+/*
+ * 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.amoro.server.scheduler.inline;
+
+import org.apache.amoro.ServerTableIdentifier;
+import org.apache.amoro.TableFormat;
+import org.apache.amoro.process.ProcessStatus;
+import org.apache.amoro.server.AMSServiceTestBase;
+import org.apache.amoro.server.persistence.PersistentBase;
+import org.apache.amoro.server.persistence.mapper.TableProcessMapper;
+import org.apache.amoro.server.process.TableProcessMeta;
+import org.apache.amoro.server.table.DefaultTableRuntime;
+import org.apache.amoro.server.table.TableService;
+import org.apache.amoro.server.utils.SnowflakeIdGenerator;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+
+public class TestProcessDataExpiringExecutor extends AMSServiceTestBase {
+
+  private static final long TABLE_ID = 1L;
+  private static final ServerTableIdentifier TABLE_IDENTIFIER =
+      ServerTableIdentifier.of(
+          TABLE_ID, "test_catalog", "test_db", "test_table_expiring", 
TableFormat.MIXED_ICEBERG);
+
+  private final Persistency persistency = new Persistency();
+  private DefaultTableRuntime tableRuntime;
+  private TableService tableService;
+
+  @Before
+  public void mock() {
+    tableRuntime = Mockito.mock(DefaultTableRuntime.class);
+    tableService = Mockito.mock(TableService.class);
+    
Mockito.when(tableRuntime.getTableIdentifier()).thenReturn(TABLE_IDENTIFIER);
+    // Clean up any leftover data
+    persistency.cleanAll(TABLE_ID);
+  }
+
+  @Test
+  public void testProcessHistoryExpiringWhenShorterThanKeepTime() {
+    // optimizingKeepTime=30d, processKeepTime=7d
+    Duration optimizingKeepTime = Duration.ofDays(30);
+    Duration expireInterval = Duration.ofHours(1);
+    Duration processKeepTime = Duration.ofDays(7);
+
+    long now = System.currentTimeMillis();
+
+    // Insert a terminal (SUCCESS) process at 10 days ago - should be deleted 
by processKeepTime
+    long tenDaysAgo = now - Duration.ofDays(10).toMillis();
+    long processId10d = SnowflakeIdGenerator.getMinSnowflakeId(tenDaysAgo) + 1;
+    persistency.insertProcess(TABLE_ID, processId10d, ProcessStatus.SUCCESS, 
tenDaysAgo);
+
+    // Insert a terminal (SUCCESS) process at 3 days ago - should be kept 
(within processKeepTime)
+    long threeDaysAgo = now - Duration.ofDays(3).toMillis();
+    long processId3d = SnowflakeIdGenerator.getMinSnowflakeId(threeDaysAgo) + 
1;
+    persistency.insertProcess(TABLE_ID, processId3d, ProcessStatus.SUCCESS, 
threeDaysAgo);
+
+    Assert.assertEquals(2, persistency.listProcesses(TABLE_ID).size());
+
+    ProcessDataExpiringExecutor executor =
+        new ProcessDataExpiringExecutor(
+            tableService, optimizingKeepTime, expireInterval, processKeepTime);
+    executor.execute(tableRuntime);
+
+    List<TableProcessMeta> remaining = persistency.listProcesses(TABLE_ID);
+    Assert.assertEquals(1, remaining.size());
+    Assert.assertEquals(processId3d, remaining.get(0).getProcessId());
+  }
+
+  @Test
+  public void testProcessHistoryNotExpiringWhenEqualToKeepTime() {
+    // When processKeepTime >= optimizingKeepTime, the extra process cleanup 
should not trigger
+    Duration optimizingKeepTime = Duration.ofDays(7);
+    Duration expireInterval = Duration.ofHours(1);
+    Duration processKeepTime = Duration.ofDays(7);
+
+    long now = System.currentTimeMillis();
+
+    // Insert a terminal process at 5 days ago - within both 
optimizingKeepTime and processKeepTime
+    long fiveDaysAgo = now - Duration.ofDays(5).toMillis();
+    long processId5d = SnowflakeIdGenerator.getMinSnowflakeId(fiveDaysAgo) + 1;
+    persistency.insertProcess(TABLE_ID, processId5d, ProcessStatus.SUCCESS, 
fiveDaysAgo);
+
+    Assert.assertEquals(1, persistency.listProcesses(TABLE_ID).size());
+
+    ProcessDataExpiringExecutor executor =
+        new ProcessDataExpiringExecutor(
+            tableService, optimizingKeepTime, expireInterval, processKeepTime);
+    executor.execute(tableRuntime);
+
+    // The process should still exist - not expired by either mechanism
+    Assert.assertEquals(1, persistency.listProcesses(TABLE_ID).size());
+  }
+
+  @Test
+  public void testDeleteExpiredProcessesSkipsActiveStatuses() {
+    // optimizingKeepTime=30d, processKeepTime=7d
+    Duration optimizingKeepTime = Duration.ofDays(30);
+    Duration expireInterval = Duration.ofHours(1);
+    Duration processKeepTime = Duration.ofDays(7);
+
+    long now = System.currentTimeMillis();
+    long tenDaysAgo = now - Duration.ofDays(10).toMillis();
+
+    // Insert active-status processes at 10 days ago - should NOT be deleted
+    long pidRunning = SnowflakeIdGenerator.getMinSnowflakeId(tenDaysAgo) + 1;
+    persistency.insertProcess(TABLE_ID, pidRunning, ProcessStatus.RUNNING, 
tenDaysAgo);
+
+    long pidSubmitted = SnowflakeIdGenerator.getMinSnowflakeId(tenDaysAgo) + 2;
+    persistency.insertProcess(TABLE_ID, pidSubmitted, ProcessStatus.SUBMITTED, 
tenDaysAgo);
+
+    long pidPending = SnowflakeIdGenerator.getMinSnowflakeId(tenDaysAgo) + 3;
+    persistency.insertProcess(TABLE_ID, pidPending, ProcessStatus.PENDING, 
tenDaysAgo);
+
+    long pidCanceling = SnowflakeIdGenerator.getMinSnowflakeId(tenDaysAgo) + 4;
+    persistency.insertProcess(TABLE_ID, pidCanceling, ProcessStatus.CANCELING, 
tenDaysAgo);
+
+    // Insert a terminal process at 10 days ago - SHOULD be deleted
+    long pidSuccess = SnowflakeIdGenerator.getMinSnowflakeId(tenDaysAgo) + 5;
+    persistency.insertProcess(TABLE_ID, pidSuccess, ProcessStatus.SUCCESS, 
tenDaysAgo);
+
+    long pidFailed = SnowflakeIdGenerator.getMinSnowflakeId(tenDaysAgo) + 6;
+    persistency.insertProcess(TABLE_ID, pidFailed, ProcessStatus.FAILED, 
tenDaysAgo);
+
+    Assert.assertEquals(6, persistency.listProcesses(TABLE_ID).size());
+
+    ProcessDataExpiringExecutor executor =
+        new ProcessDataExpiringExecutor(
+            tableService, optimizingKeepTime, expireInterval, processKeepTime);
+    executor.execute(tableRuntime);
+
+    List<TableProcessMeta> remaining = persistency.listProcesses(TABLE_ID);
+    // Active statuses (RUNNING, SUBMITTED, PENDING, CANCELING) should survive
+    Assert.assertEquals(4, remaining.size());
+    for (TableProcessMeta meta : remaining) {
+      Assert.assertTrue(
+          "Expected active status but got: " + meta.getStatus(),
+          meta.getStatus() == ProcessStatus.RUNNING
+              || meta.getStatus() == ProcessStatus.SUBMITTED
+              || meta.getStatus() == ProcessStatus.PENDING
+              || meta.getStatus() == ProcessStatus.CANCELING);
+    }
+  }
+
+  private static class Persistency extends PersistentBase {
+
+    public void insertProcess(long tableId, long processId, ProcessStatus 
status, long createTime) {
+      doAs(
+          TableProcessMapper.class,
+          mapper ->
+              mapper.insertProcess(
+                  tableId,
+                  processId,
+                  "",
+                  status,
+                  "TEST",
+                  status.name(),
+                  "LOCAL",
+                  0,
+                  createTime,
+                  Collections.emptyMap(),
+                  Collections.emptyMap()));
+    }
+
+    public List<TableProcessMeta> listProcesses(long tableId) {
+      return getAs(TableProcessMapper.class, mapper -> 
mapper.listProcessMeta(tableId, null, null));
+    }
+
+    public void cleanAll(long tableId) {
+      doAs(TableProcessMapper.class, mapper -> mapper.deleteBefore(tableId, 
Long.MAX_VALUE));
+    }
+  }
+}
diff --git a/amoro-ams/src/test/resources/config-with-units.yaml 
b/amoro-ams/src/test/resources/config-with-units.yaml
index 9f72540a9..7f307de7d 100644
--- a/amoro-ams/src/test/resources/config-with-units.yaml
+++ b/amoro-ams/src/test/resources/config-with-units.yaml
@@ -46,10 +46,15 @@ ams:
     interval: 1min
     max-pending-partition-count: 100 # default 100
 
+  process:
+    history-data-keep-time: 7d
+
   self-optimizing:
     commit-thread-count: 10
     runtime-data-keep-days: 30
     runtime-data-expire-interval-hours: 1
+    runtime-data-keep-time: 30d
+    runtime-data-expire-interval: 1h
     refresh-group-interval: 30s
     break-quota-limit-enabled: true
 
diff --git a/dist/src/main/amoro-bin/conf/config.yaml 
b/dist/src/main/amoro-bin/conf/config.yaml
index 27d3e5eb4..65446fe2b 100644
--- a/dist/src/main/amoro-bin/conf/config.yaml
+++ b/dist/src/main/amoro-bin/conf/config.yaml
@@ -81,10 +81,13 @@ ams:
     interval: 1min # 60000
     max-pending-partition-count: 100 # default 100
 
+  process:
+    history-data-keep-time: 7d
+
   self-optimizing:
     commit-thread-count: 10
-    runtime-data-keep-days: 30
-    runtime-data-expire-interval-hours: 1
+    runtime-data-keep-time: 30d
+    runtime-data-expire-interval: 1h
     refresh-group-interval: 30s
     break-quota-limit-enabled: true
 
diff --git a/docs/configuration/ams-config.md b/docs/configuration/ams-config.md
index c929ef2a8..3285e57e6 100644
--- a/docs/configuration/ams-config.md
+++ b/docs/configuration/ams-config.md
@@ -111,6 +111,8 @@ table td:last-child, table th:last-child { width: 40%; 
word-break: break-all; }
 | optimizer.task-execute-timeout | 2147483647 s | Timeout duration for task 
execution, default to Integer.MAX_VALUE seconds(about 24,855 days). |
 | overview-cache.max-size | 3360 | Max size of overview cache. |
 | overview-cache.refresh-interval | 3 min | Interval for refreshing overview 
cache. |
+| process.history-data-keep-days | 7 | Deprecated: use 
'process.history-data-keep-time' instead. The number of days that process 
history data is retained. |
+| process.history-data-keep-time | 7 d | Duration that process history data is 
retained. Expired terminal process records will be deleted automatically. |
 | refresh-external-catalogs.interval | 3 min | Interval to refresh the 
external catalog. |
 | refresh-external-catalogs.queue-size | 1000000 | The queue size of the 
executors of the external catalog explorer. |
 | refresh-external-catalogs.thread-count | 10 | The number of threads used for 
discovering tables in external catalogs. |
@@ -122,8 +124,10 @@ table td:last-child, table th:last-child { width: 40%; 
word-break: break-all; }
 | self-optimizing.commit-thread-count | 10 | The number of threads that 
self-optimizing uses to submit results. |
 | self-optimizing.plan-manifest-io-thread-count | 10 | Sets the size of the 
worker pool. The worker pool limits the number of tasks concurrently processing 
manifests in the base table implementation across all concurrent planning 
operations. |
 | self-optimizing.refresh-group-interval | 30 s | Optimizer group refresh 
interval. |
-| self-optimizing.runtime-data-expire-interval-hours | 1 | The number of hours 
that self-optimizing runtime data expire interval. |
-| self-optimizing.runtime-data-keep-days | 30 | The number of days that 
self-optimizing runtime data keeps the runtime. |
+| self-optimizing.runtime-data-expire-interval | 1 h | Interval between 
self-optimizing runtime data expiration runs. |
+| self-optimizing.runtime-data-expire-interval-hours | 1 | Deprecated: use 
'self-optimizing.runtime-data-expire-interval' instead. The number of hours 
that self-optimizing runtime data expire interval. |
+| self-optimizing.runtime-data-keep-days | 30 | Deprecated: use 
'self-optimizing.runtime-data-keep-time' instead. The number of days that 
self-optimizing runtime data keeps the runtime. |
+| self-optimizing.runtime-data-keep-time | 30 d | Duration that 
self-optimizing runtime data is retained. |
 | server-bind-host | 0.0.0.0 | The host bound to the server. |
 | server-expose-host |  | The exposed host of the server. |
 | sync-hive-tables.enabled | false | Enable synchronizing Hive tables. |

Reply via email to