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

zhoujinsong 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 0704a1ee9 [AMORO-4281]Fix cleanning process trigger issue for mixed 
format tables (#4282)
0704a1ee9 is described below

commit 0704a1ee92df82bd923074d6714cb7612b45d964
Author: ZhouJinsong <[email protected]>
AuthorDate: Wed Jul 22 19:28:00 2026 +0800

    [AMORO-4281]Fix cleanning process trigger issue for mixed format tables 
(#4282)
    
    * Fix expire process trigger issue
    
    * Add some method to reuse some codes
    
    * Fix some issues of comments
    
    ---------
    
    Co-authored-by: baiyangtx <[email protected]>
---
 .../amoro/server/process/ProcessService.java       | 143 +++++++++++++---
 .../process/TestProcessServiceMultiFormat.java     | 188 +++++++++++++++++++++
 2 files changed, 312 insertions(+), 19 deletions(-)

diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/process/ProcessService.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/process/ProcessService.java
index 68446db8d..b166ada7a 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/process/ProcessService.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/process/ProcessService.java
@@ -41,10 +41,12 @@ import 
org.apache.amoro.shade.guava32.com.google.common.annotations.VisibleForTe
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -59,7 +61,7 @@ public class ProcessService extends PersistentBase {
   private static final Logger LOG = 
LoggerFactory.getLogger(ProcessService.class);
   private final TableService tableService;
 
-  private final Map<String, ActionCoordinatorScheduler> actionCoordinators =
+  private final Map<String, List<ActionCoordinatorScheduler>> 
actionCoordinators =
       new ConcurrentHashMap<>();
   private final Map<String, ExecuteEngine> executeEngines = new 
ConcurrentHashMap<>();
 
@@ -120,13 +122,70 @@ public class ProcessService extends PersistentBase {
     activeTableProcess.clear();
   }
 
+  /**
+   * Find the scheduler that supports the given table format for the specified 
action.
+   *
+   * @param actionName action name
+   * @param format table format
+   * @return matching scheduler, or null if not found
+   */
+  private ActionCoordinatorScheduler findScheduler(String actionName, 
TableFormat format) {
+    List<ActionCoordinatorScheduler> schedulers = 
actionCoordinators.get(actionName);
+    if (schedulers == null) {
+      return null;
+    }
+    for (ActionCoordinatorScheduler scheduler : schedulers) {
+      if (scheduler.formatSupported(format)) {
+        return scheduler;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Find all schedulers that support the given table format across all 
actions.
+   *
+   * @param format table format
+   * @return list of matching schedulers
+   */
+  private List<ActionCoordinatorScheduler> findSchedulersByFormat(TableFormat 
format) {
+    List<ActionCoordinatorScheduler> result = new ArrayList<>();
+    for (List<ActionCoordinatorScheduler> schedulers : 
actionCoordinators.values()) {
+      for (ActionCoordinatorScheduler scheduler : schedulers) {
+        if (scheduler.formatSupported(format)) {
+          result.add(scheduler);
+        }
+      }
+    }
+    return result;
+  }
+
   private void initialize(List<TableRuntime> tableRuntimes) {
     LOG.info("Initializing process service");
     // Pre-configured coordinators built from TableRuntimeFactory / 
ProcessFactory
     for (ActionCoordinator actionCoordinator : actionCoordinatorList) {
-      actionCoordinators.put(
-          actionCoordinator.action().getName(),
-          new ActionCoordinatorScheduler(actionCoordinator, tableService, 
ProcessService.this));
+      ActionCoordinatorScheduler scheduler =
+          new ActionCoordinatorScheduler(actionCoordinator, tableService, 
ProcessService.this);
+      List<ActionCoordinatorScheduler> schedulers =
+          actionCoordinators.computeIfAbsent(
+              actionCoordinator.action().getName(), k -> new 
CopyOnWriteArrayList<>());
+      // Validate (action, format) uniqueness: warn if a scheduler with 
overlapping format support
+      // already exists for the same action
+      for (ActionCoordinatorScheduler existing : schedulers) {
+        for (TableFormat format : TableFormat.values()) {
+          if (existing.getCoordinator().formatSupported(format)
+              && actionCoordinator.formatSupported(format)) {
+            LOG.warn(
+                "Duplicate ActionCoordinator for action {} and format {}: 
existing={}, new={}. "
+                    + "The later one will shadow the former in findScheduler.",
+                actionCoordinator.action().getName(),
+                format,
+                existing.getCoordinator().name(),
+                actionCoordinator.name());
+          }
+        }
+      }
+      schedulers.add(scheduler);
     }
     executeEngineManager
         .installedPlugins()
@@ -135,7 +194,7 @@ public class ProcessService extends PersistentBase {
               executeEngines.put(executeEngine.name(), executeEngine);
             });
     recoverProcesses(tableRuntimes);
-    actionCoordinators.values().forEach(s -> s.initialize(tableRuntimes));
+    actionCoordinators.values().forEach(list -> list.forEach(s -> 
s.initialize(tableRuntimes)));
   }
 
   /**
@@ -153,10 +212,24 @@ public class ProcessService extends PersistentBase {
     activeProcesses.forEach(
         processMeta -> {
           TableRuntime tableRuntime = 
tableIdToRuntimes.get(processMeta.getTableId());
-          ActionCoordinatorScheduler scheduler =
-              actionCoordinators.get(processMeta.getProcessType());
-          if (tableRuntime != null && scheduler != null) {
-            recoverProcess(tableRuntime, scheduler, processMeta);
+          if (tableRuntime != null) {
+            ActionCoordinatorScheduler scheduler =
+                findScheduler(processMeta.getProcessType(), 
tableRuntime.getFormat());
+            if (scheduler != null) {
+              recoverProcess(tableRuntime, scheduler, processMeta);
+            } else {
+              LOG.warn(
+                  "No ActionCoordinatorScheduler found for process {} 
(type={}, format={}), "
+                      + "skipping recovery. The process record will remain in 
its current state.",
+                  processMeta.getProcessId(),
+                  processMeta.getProcessType(),
+                  tableRuntime.getFormat());
+            }
+          } else {
+            LOG.warn(
+                "Table runtime not found for process {} (tableId={}), skipping 
recovery.",
+                processMeta.getProcessId(),
+                processMeta.getTableId());
           }
         });
   }
@@ -250,8 +323,11 @@ public class ProcessService extends PersistentBase {
     TableProcessExecutor executor = new TableProcessExecutor(process, store, 
executeEngine);
     executor.onProcessFinished(
         () -> {
-          ActionCoordinatorScheduler scheduler =
-              actionCoordinators.get(store.getAction().getName());
+          ActionCoordinatorScheduler scheduler = null;
+          if (process.getTableRuntime() != null) {
+            scheduler =
+                findScheduler(store.getAction().getName(), 
process.getTableRuntime().getFormat());
+          }
           if (scheduler != null
               && store.getStatus() == ProcessStatus.FAILED
               && store.getRetryNumber() < 
ActionCoordinatorScheduler.PROCESS_MAX_RETRY_NUMBER
@@ -265,6 +341,14 @@ public class ProcessService extends PersistentBase {
                 process.getSummary());
             executeOrTraceProcess(store, process);
           } else {
+            if (store.getStatus() == ProcessStatus.FAILED && 
process.getTableRuntime() != null) {
+              LOG.info(
+                  "FAILED process {} will not be retried: scheduler={}, 
retryNumber={}, maxRetry={}",
+                  store.getProcessId(),
+                  scheduler != null ? "found" : "not found",
+                  store.getRetryNumber(),
+                  ActionCoordinatorScheduler.PROCESS_MAX_RETRY_NUMBER);
+            }
             untrackTableProcessInstance(
                 process.getTableRuntime().getTableIdentifier(), 
store.getProcessId());
           }
@@ -389,7 +473,7 @@ public class ProcessService extends PersistentBase {
    * @return coordinators map
    */
   @VisibleForTesting
-  public Map<String, ActionCoordinatorScheduler> getActionCoordinators() {
+  public Map<String, List<ActionCoordinatorScheduler>> getActionCoordinators() 
{
     return actionCoordinators;
   }
 
@@ -467,9 +551,26 @@ public class ProcessService extends PersistentBase {
 
   @VisibleForTesting
   public void installActionCoordinator(ActionCoordinator actionCoordinator) {
-    this.actionCoordinators.put(
-        actionCoordinator.action().getName(),
-        new ActionCoordinatorScheduler(actionCoordinator, tableService, 
ProcessService.this));
+    ActionCoordinatorScheduler scheduler =
+        new ActionCoordinatorScheduler(actionCoordinator, tableService, 
ProcessService.this);
+    List<ActionCoordinatorScheduler> schedulers =
+        this.actionCoordinators.computeIfAbsent(
+            actionCoordinator.action().getName(), k -> new 
CopyOnWriteArrayList<>());
+    // Validate (action, format) uniqueness
+    for (ActionCoordinatorScheduler existing : schedulers) {
+      for (TableFormat format : TableFormat.values()) {
+        if (existing.getCoordinator().formatSupported(format)
+            && actionCoordinator.formatSupported(format)) {
+          LOG.warn(
+              "Duplicate ActionCoordinator for action {} and format {}: 
existing={}, new={}.",
+              actionCoordinator.action().getName(),
+              format,
+              existing.getCoordinator().name(),
+              actionCoordinator.name());
+        }
+      }
+    }
+    schedulers.add(scheduler);
   }
 
   @VisibleForTesting
@@ -509,7 +610,8 @@ public class ProcessService extends PersistentBase {
      */
     @Override
     protected void handleStatusChanged(TableRuntime tableRuntime, 
OptimizingStatus originalStatus) {
-      actionCoordinators.values().forEach(s -> 
s.handleStatusChanged(tableRuntime, originalStatus));
+      findSchedulersByFormat(tableRuntime.getFormat())
+          .forEach(s -> s.handleStatusChanged(tableRuntime, originalStatus));
     }
 
     /**
@@ -521,7 +623,8 @@ public class ProcessService extends PersistentBase {
     @Override
     protected void handleConfigChanged(
         TableRuntime tableRuntime, TableConfiguration originalConfig) {
-      actionCoordinators.values().forEach(s -> 
s.handleConfigChanged(tableRuntime, originalConfig));
+      findSchedulersByFormat(tableRuntime.getFormat())
+          .forEach(s -> s.handleConfigChanged(tableRuntime, originalConfig));
     }
 
     /**
@@ -532,7 +635,8 @@ public class ProcessService extends PersistentBase {
      */
     @Override
     protected void handleTableAdded(AmoroTable<?> table, TableRuntime 
tableRuntime) {
-      actionCoordinators.values().forEach(s -> s.handleTableAdded(table, 
tableRuntime));
+      findSchedulersByFormat(tableRuntime.getFormat())
+          .forEach(s -> s.handleTableAdded(table, tableRuntime));
     }
 
     /**
@@ -542,7 +646,8 @@ public class ProcessService extends PersistentBase {
      */
     @Override
     protected void handleTableRemoved(TableRuntime tableRuntime) {
-      actionCoordinators.values().forEach(s -> 
s.handleTableRemoved(tableRuntime));
+      findSchedulersByFormat(tableRuntime.getFormat())
+          .forEach(s -> s.handleTableRemoved(tableRuntime));
       List<TableProcessHolder> processes =
           
getTableProcessInstances(tableRuntime.getTableIdentifier()).values().stream()
               .collect(Collectors.toList());
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/process/TestProcessServiceMultiFormat.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/process/TestProcessServiceMultiFormat.java
new file mode 100644
index 000000000..d0eab0992
--- /dev/null
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/process/TestProcessServiceMultiFormat.java
@@ -0,0 +1,188 @@
+/*
+ * 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.process;
+
+import org.apache.amoro.Action;
+import org.apache.amoro.TableFormat;
+import org.apache.amoro.TableRuntime;
+import org.apache.amoro.process.ActionCoordinator;
+import org.apache.amoro.process.TableProcess;
+import org.apache.amoro.process.TableProcessStore;
+import org.apache.amoro.server.process.ProcessService.ExecuteEngineManager;
+import org.apache.amoro.server.table.TableService;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Tests for {@link ProcessService} multi-format ActionCoordinator isolation. 
Ensures that
+ * coordinators sharing the same action name but targeting different formats 
are not silently
+ * overwritten (regression test for AMORO-4281).
+ */
+public class TestProcessServiceMultiFormat {
+
+  private static final Action TEST_ACTION = 
Action.register("test-expire-snapshots");
+
+  /** A coordinator that only supports a specific format. */
+  static class FormatSpecificCoordinator implements ActionCoordinator {
+    private final TableFormat format;
+    private final String name;
+
+    FormatSpecificCoordinator(TableFormat format) {
+      this.format = format;
+      this.name = "coordinator-" + format.name();
+    }
+
+    @Override
+    public boolean formatSupported(TableFormat format) {
+      return this.format.equals(format);
+    }
+
+    @Override
+    public int parallelism() {
+      return 1;
+    }
+
+    @Override
+    public Action action() {
+      return TEST_ACTION;
+    }
+
+    @Override
+    public long getNextExecutingTime(TableRuntime tableRuntime) {
+      return 1000L;
+    }
+
+    @Override
+    public boolean enabled(TableRuntime tableRuntime) {
+      return formatSupported(tableRuntime.getFormat());
+    }
+
+    @Override
+    public long getExecutorDelay() {
+      return 1000L;
+    }
+
+    @Override
+    public Optional<TableProcess> trigger(TableRuntime tableRuntime) {
+      return Optional.empty();
+    }
+
+    @Override
+    public TableProcess recoverTableProcess(
+        TableRuntime tableRuntime, TableProcessStore processStore) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void open(Map<String, String> properties) {}
+
+    @Override
+    public void close() {}
+
+    @Override
+    public String name() {
+      return name;
+    }
+
+    TableFormat supportedFormat() {
+      return format;
+    }
+  }
+
+  /**
+   * Two coordinators with the same action name but different formats should 
both be registered
+   * without one overwriting the other. This is the core regression test for 
AMORO-4281.
+   */
+  @Test
+  public void testMultipleCoordinatorsWithSameActionDifferentFormats() {
+    FormatSpecificCoordinator icebergCoordinator =
+        new FormatSpecificCoordinator(TableFormat.ICEBERG);
+    FormatSpecificCoordinator paimonCoordinator = new 
FormatSpecificCoordinator(TableFormat.PAIMON);
+
+    TableService tableService = Mockito.mock(TableService.class);
+    ProcessService processService =
+        new ProcessService(tableService, Collections.emptyList(), new 
ExecuteEngineManager());
+
+    processService.installActionCoordinator(icebergCoordinator);
+    processService.installActionCoordinator(paimonCoordinator);
+
+    Map<String, List<ActionCoordinatorScheduler>> coordinators =
+        processService.getActionCoordinators();
+    Assertions.assertEquals(1, coordinators.size(), "Should have one action 
entry");
+    List<ActionCoordinatorScheduler> schedulers = 
coordinators.get(TEST_ACTION.getName());
+    Assertions.assertNotNull(schedulers, "Schedulers list should not be null");
+    Assertions.assertEquals(2, schedulers.size(), "Both coordinators should be 
registered");
+
+    // Verify both formats are represented
+    boolean hasIceberg =
+        schedulers.stream().anyMatch(s -> 
s.getCoordinator().formatSupported(TableFormat.ICEBERG));
+    boolean hasPaimon =
+        schedulers.stream().anyMatch(s -> 
s.getCoordinator().formatSupported(TableFormat.PAIMON));
+    Assertions.assertTrue(hasIceberg, "ICEBERG coordinator should be present");
+    Assertions.assertTrue(hasPaimon, "PAIMON coordinator should be present");
+  }
+
+  /**
+   * Installing coordinators via installActionCoordinator should also not 
overwrite existing ones
+   * with the same action name.
+   */
+  @Test
+  public void testInstallActionCoordinatorDoesNotOverwrite() {
+    FormatSpecificCoordinator icebergCoordinator =
+        new FormatSpecificCoordinator(TableFormat.ICEBERG);
+    FormatSpecificCoordinator paimonCoordinator = new 
FormatSpecificCoordinator(TableFormat.PAIMON);
+
+    TableService tableService = Mockito.mock(TableService.class);
+    ProcessService processService =
+        new ProcessService(tableService, Collections.emptyList(), new 
ExecuteEngineManager());
+
+    processService.installActionCoordinator(icebergCoordinator);
+    processService.installActionCoordinator(paimonCoordinator);
+
+    Map<String, List<ActionCoordinatorScheduler>> coordinators =
+        processService.getActionCoordinators();
+    List<ActionCoordinatorScheduler> schedulers = 
coordinators.get(TEST_ACTION.getName());
+    Assertions.assertNotNull(schedulers);
+    Assertions.assertEquals(2, schedulers.size(), "Both coordinators should be 
registered");
+  }
+
+  /**
+   * The getActionCoordinators return type should be Map<String, 
List<ActionCoordinatorScheduler>>.
+   */
+  @Test
+  public void testGetActionCoordinatorsReturnType() {
+    FormatSpecificCoordinator coordinator = new 
FormatSpecificCoordinator(TableFormat.ICEBERG);
+    TableService tableService = Mockito.mock(TableService.class);
+    ProcessService processService =
+        new ProcessService(tableService, Collections.emptyList(), new 
ExecuteEngineManager());
+
+    processService.installActionCoordinator(coordinator);
+
+    Map<String, List<ActionCoordinatorScheduler>> coordinators =
+        processService.getActionCoordinators();
+    Assertions.assertNotNull(coordinators);
+    Assertions.assertTrue(coordinators.containsKey(TEST_ACTION.getName()));
+  }
+}

Reply via email to