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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4f0a475b8 [spark][flink] Introduce orphan file cleaning local and 
distributed mode (#4285)
4f0a475b8 is described below

commit 4f0a475b8ec4dba00bfca5a5c05a2f8bcec4995b
Author: bknbkn <[email protected]>
AuthorDate: Tue Oct 8 17:13:51 2024 +0800

    [spark][flink] Introduce orphan file cleaning local and distributed mode 
(#4285)
---
 docs/content/flink/procedures.md                   |  9 ++-
 docs/content/spark/procedures.md                   |  4 +-
 .../paimon/operation/LocalOrphanFilesClean.java    | 26 +++++++--
 .../procedure/RemoveOrphanFilesProcedure.java      | 57 +++++++++++++-----
 .../action/RemoveOrphanFilesActionITCase.java      | 67 ++++++++++++++++++++++
 .../procedure/RemoveOrphanFilesProcedure.java      | 57 +++++++++++++-----
 .../procedure/RemoveOrphanFilesProcedureTest.scala | 47 +++++++++++++++
 7 files changed, 231 insertions(+), 36 deletions(-)

diff --git a/docs/content/flink/procedures.md b/docs/content/flink/procedures.md
index 430b42c3b..ce8c8043a 100644
--- a/docs/content/flink/procedures.md
+++ b/docs/content/flink/procedures.md
@@ -268,12 +268,13 @@ All available procedures are listed below.
       <td>remove_orphan_files</td>
       <td>
          -- Use named argument<br/>
-         CALL [catalog.]sys.remove_orphan_files(`table` => 'identifier', 
older_than => 'olderThan', dry_run => 'dryRun') <br/><br/>
+         CALL [catalog.]sys.remove_orphan_files(`table` => 'identifier', 
older_than => 'olderThan', dry_run => 'dryRun', mode => 'mode') <br/><br/>
          -- Use indexed argument<br/>
          CALL [catalog.]sys.remove_orphan_files('identifier')<br/><br/>
          CALL [catalog.]sys.remove_orphan_files('identifier', 
'olderThan')<br/><br/>
          CALL [catalog.]sys.remove_orphan_files('identifier', 'olderThan', 
'dryRun')<br/><br/>
-         CALL [catalog.]sys.remove_orphan_files('identifier', 'olderThan', 
'dryRun','parallelism')
+         CALL [catalog.]sys.remove_orphan_files('identifier', 'olderThan', 
'dryRun','parallelism')<br/><br/>
+         CALL [catalog.]sys.remove_orphan_files('identifier', 'olderThan', 
'dryRun','parallelism','mode')
       </td>
       <td>
          To remove the orphan data files and metadata files. Arguments:
@@ -283,11 +284,13 @@ All available procedures are listed below.
             </li>
             <li>dryRun: when true, view only orphan files, don't actually 
remove files. Default is false.</li>
             <li>parallelism: The maximum number of concurrent deleting files. 
By default is the number of processors available to the Java virtual 
machine.</li>
+            <li>mode: The mode of remove orphan clean procedure (local or 
distributed) . By default is distributed.</li>
       </td>
       <td>CALL remove_orphan_files(`table` => 'default.T', older_than => 
'2023-10-31 12:00:00')<br/><br/>
           CALL remove_orphan_files(`table` => 'default.*', older_than => 
'2023-10-31 12:00:00')<br/><br/>
           CALL remove_orphan_files(`table` => 'default.T', older_than => 
'2023-10-31 12:00:00', dry_run => true)<br/><br/>
-          CALL remove_orphan_files(`table` => 'default.T', older_than => 
'2023-10-31 12:00:00', dry_run => false, parallelism => '5')
+          CALL remove_orphan_files(`table` => 'default.T', older_than => 
'2023-10-31 12:00:00', dry_run => false, parallelism => '5')<br/><br/>
+          CALL remove_orphan_files(`table` => 'default.T', older_than => 
'2023-10-31 12:00:00', dry_run => false, parallelism => '5', mode => 'local')
       </td>
    </tr>
    <tr>
diff --git a/docs/content/spark/procedures.md b/docs/content/spark/procedures.md
index 1557fd6a7..d0d8f2397 100644
--- a/docs/content/spark/procedures.md
+++ b/docs/content/spark/procedures.md
@@ -188,12 +188,14 @@ This section introduce all available spark procedures 
about paimon.
             <li>older_than: to avoid deleting newly written files, this 
procedure only deletes orphan files older than 1 day by default. This argument 
can modify the interval.</li>
             <li>dry_run: when true, view only orphan files, don't actually 
remove files. Default is false.</li>
             <li>parallelism: The maximum number of concurrent deleting files. 
By default is the number of processors available to the Java virtual 
machine.</li>
+            <li>mode: The mode of remove orphan clean procedure (local or 
distributed) . By default is distributed.</li>
       </td>
       <td>
           CALL sys.remove_orphan_files(table => 'default.T', older_than => 
'2023-10-31 12:00:00')<br/><br/>
           CALL sys.remove_orphan_files(table => 'default.*', older_than => 
'2023-10-31 12:00:00')<br/><br/>
           CALL sys.remove_orphan_files(table => 'default.T', older_than => 
'2023-10-31 12:00:00', dry_run => true)<br/><br/>
-          CALL sys.remove_orphan_files(table => 'default.T', older_than => 
'2023-10-31 12:00:00', dry_run => true, parallelism => '5')
+          CALL sys.remove_orphan_files(table => 'default.T', older_than => 
'2023-10-31 12:00:00', dry_run => true, parallelism => '5')<br/><br/>
+          CALL sys.remove_orphan_files(table => 'default.T', older_than => 
'2023-10-31 12:00:00', dry_run => true, parallelism => '5', mode => 'local')
       </td>
     </tr>
     <tr>
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/LocalOrphanFilesClean.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/LocalOrphanFilesClean.java
index 91b23b9a5..a17434faa 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/LocalOrphanFilesClean.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/LocalOrphanFilesClean.java
@@ -56,15 +56,13 @@ import static 
org.apache.paimon.utils.ThreadPoolUtils.randomlyExecute;
 /**
  * Local {@link OrphanFilesClean}, it will use thread pool to execute deletion.
  *
- * <p>Note that, this class is not used any more since each engine should 
implement its own
- * distributed one. See `FlinkOrphanFilesClean` and `SparkOrphanFilesClean`.
+ * <p>Note that, this class will be used when the orphan clean mode is local, 
else orphan clean will
+ * use distributed one. See `FlinkOrphanFilesClean` and 
`SparkOrphanFilesClean`.
  */
 public class LocalOrphanFilesClean extends OrphanFilesClean {
 
     private final ThreadPoolExecutor executor;
 
-    private static final int SHOW_LIMIT = 200;
-
     private final List<Path> deleteFiles;
 
     public LocalOrphanFilesClean(FileStoreTable table) {
@@ -220,7 +218,23 @@ public class LocalOrphanFilesClean extends 
OrphanFilesClean {
         return orphanFilesCleans;
     }
 
-    public static String[] executeOrphanFilesClean(List<LocalOrphanFilesClean> 
tableCleans) {
+    public static long executeDatabaseOrphanFiles(
+            Catalog catalog,
+            String databaseName,
+            @Nullable String tableName,
+            long olderThanMillis,
+            SerializableConsumer<Path> fileCleaner,
+            @Nullable Integer parallelism)
+            throws Catalog.DatabaseNotExistException, 
Catalog.TableNotExistException {
+        List<LocalOrphanFilesClean> tableCleans =
+                createOrphanFilesCleans(
+                        catalog,
+                        databaseName,
+                        tableName,
+                        olderThanMillis,
+                        fileCleaner,
+                        parallelism);
+
         ExecutorService executorService =
                 
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
         List<Future<List<Path>>> tasks = new ArrayList<>();
@@ -241,6 +255,6 @@ public class LocalOrphanFilesClean extends OrphanFilesClean 
{
         }
 
         executorService.shutdownNow();
-        return showDeletedFiles(cleanOrphanFiles, SHOW_LIMIT).toArray(new 
String[0]);
+        return cleanOrphanFiles.size();
     }
 }
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RemoveOrphanFilesProcedure.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RemoveOrphanFilesProcedure.java
index 9bc20350d..10ad878e0 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RemoveOrphanFilesProcedure.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RemoveOrphanFilesProcedure.java
@@ -20,12 +20,15 @@ package org.apache.paimon.flink.procedure;
 
 import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.flink.orphan.FlinkOrphanFilesClean;
+import org.apache.paimon.operation.LocalOrphanFilesClean;
 
 import org.apache.flink.table.annotation.ArgumentHint;
 import org.apache.flink.table.annotation.DataTypeHint;
 import org.apache.flink.table.annotation.ProcedureHint;
 import org.apache.flink.table.procedure.ProcedureContext;
 
+import java.util.Locale;
+
 import static org.apache.paimon.operation.OrphanFilesClean.createFileCleaner;
 import static org.apache.paimon.operation.OrphanFilesClean.olderThanMillis;
 
@@ -55,29 +58,57 @@ public class RemoveOrphanFilesProcedure extends 
ProcedureBase {
                         type = @DataTypeHint("STRING"),
                         isOptional = true),
                 @ArgumentHint(name = "dry_run", type = 
@DataTypeHint("BOOLEAN"), isOptional = true),
-                @ArgumentHint(name = "parallelism", type = 
@DataTypeHint("INT"), isOptional = true)
+                @ArgumentHint(name = "parallelism", type = 
@DataTypeHint("INT"), isOptional = true),
+                @ArgumentHint(name = "mode", type = @DataTypeHint("STRING"), 
isOptional = true)
             })
     public String[] call(
             ProcedureContext procedureContext,
             String tableId,
             String olderThan,
             Boolean dryRun,
-            Integer parallelism)
+            Integer parallelism,
+            String mode)
             throws Exception {
         Identifier identifier = Identifier.fromString(tableId);
         String databaseName = identifier.getDatabaseName();
         String tableName = identifier.getObjectName();
-
-        long deleted =
-                FlinkOrphanFilesClean.executeDatabaseOrphanFiles(
-                        procedureContext.getExecutionEnvironment(),
-                        catalog,
-                        olderThanMillis(olderThan),
-                        createFileCleaner(catalog, dryRun),
-                        parallelism,
-                        databaseName,
-                        tableName);
-        return new String[] {String.valueOf(deleted)};
+        if (mode == null) {
+            mode = "DISTRIBUTED";
+        }
+        long deletedFiles;
+        try {
+            switch (mode.toUpperCase(Locale.ROOT)) {
+                case "DISTRIBUTED":
+                    deletedFiles =
+                            FlinkOrphanFilesClean.executeDatabaseOrphanFiles(
+                                    procedureContext.getExecutionEnvironment(),
+                                    catalog,
+                                    olderThanMillis(olderThan),
+                                    createFileCleaner(catalog, dryRun),
+                                    parallelism,
+                                    databaseName,
+                                    tableName);
+                    break;
+                case "LOCAL":
+                    deletedFiles =
+                            LocalOrphanFilesClean.executeDatabaseOrphanFiles(
+                                    catalog,
+                                    databaseName,
+                                    tableName,
+                                    olderThanMillis(olderThan),
+                                    createFileCleaner(catalog, dryRun),
+                                    parallelism);
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unknown mode: "
+                                    + mode
+                                    + ". Only 'DISTRIBUTED' and 'LOCAL' are 
supported.");
+            }
+            return new String[] {String.valueOf(deletedFiles)};
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
     }
 
     @Override
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/RemoveOrphanFilesActionITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/RemoveOrphanFilesActionITCase.java
index 82c09c667..938a8ce1b 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/RemoveOrphanFilesActionITCase.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/RemoveOrphanFilesActionITCase.java
@@ -271,4 +271,71 @@ public class RemoveOrphanFilesActionITCase extends 
ActionITCaseBase {
         ImmutableList<Row> actualDeleteFile = 
ImmutableList.copyOf(executeSQL(procedure));
         assertThat(actualDeleteFile).containsOnly(Row.of("4"));
     }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testRunWithMode(boolean isNamedArgument) throws Exception {
+        createTableAndWriteData(tableName);
+
+        List<String> args =
+                new ArrayList<>(
+                        Arrays.asList(
+                                "remove_orphan_files",
+                                "--warehouse",
+                                warehouse,
+                                "--database",
+                                database,
+                                "--table",
+                                tableName));
+        RemoveOrphanFilesAction action1 = 
createAction(RemoveOrphanFilesAction.class, args);
+        assertThatCode(action1::run).doesNotThrowAnyException();
+
+        args.add("--older_than");
+        args.add("2023-12-31 23:59:59");
+        RemoveOrphanFilesAction action2 = 
createAction(RemoveOrphanFilesAction.class, args);
+        assertThatCode(action2::run).doesNotThrowAnyException();
+
+        String withoutOlderThan =
+                String.format(
+                        isNamedArgument
+                                ? "CALL sys.remove_orphan_files(`table` => 
'%s.%s')"
+                                : "CALL sys.remove_orphan_files('%s.%s')",
+                        database,
+                        tableName);
+        CloseableIterator<Row> withoutOlderThanCollect = 
executeSQL(withoutOlderThan);
+        
assertThat(ImmutableList.copyOf(withoutOlderThanCollect)).containsOnly(Row.of("0"));
+
+        String withLocalMode =
+                String.format(
+                        isNamedArgument
+                                ? "CALL sys.remove_orphan_files(`table` => 
'%s.%s', older_than => '2999-12-31 23:59:59', dry_run => true, parallelism => 
5, mode => 'local')"
+                                : "CALL sys.remove_orphan_files('%s.%s', 
'2999-12-31 23:59:59', true, 5, 'local')",
+                        database,
+                        tableName);
+        ImmutableList<Row> actualLocalRunDeleteFile =
+                ImmutableList.copyOf(executeSQL(withLocalMode));
+        assertThat(actualLocalRunDeleteFile).containsOnly(Row.of("2"));
+
+        String withDistributedMode =
+                String.format(
+                        isNamedArgument
+                                ? "CALL sys.remove_orphan_files(`table` => 
'%s.%s', older_than => '2999-12-31 23:59:59', dry_run => true, parallelism => 
5, mode => 'distributed')"
+                                : "CALL sys.remove_orphan_files('%s.%s', 
'2999-12-31 23:59:59', true, 5, 'distributed')",
+                        database,
+                        tableName);
+        ImmutableList<Row> actualDistributedRunDeleteFile =
+                ImmutableList.copyOf(executeSQL(withDistributedMode));
+        assertThat(actualDistributedRunDeleteFile).containsOnly(Row.of("2"));
+
+        String withInvalidMode =
+                String.format(
+                        isNamedArgument
+                                ? "CALL sys.remove_orphan_files(`table` => 
'%s.%s', older_than => '2999-12-31 23:59:59', dry_run => true, parallelism => 
5, mode => 'unknown')"
+                                : "CALL sys.remove_orphan_files('%s.%s', 
'2999-12-31 23:59:59', true, 5, 'unknown')",
+                        database,
+                        tableName);
+        assertThatCode(() -> executeSQL(withInvalidMode))
+                .isInstanceOf(RuntimeException.class)
+                .hasMessageContaining("Unknown mode");
+    }
 }
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RemoveOrphanFilesProcedure.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RemoveOrphanFilesProcedure.java
index 4f442fbae..293e84ca1 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RemoveOrphanFilesProcedure.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RemoveOrphanFilesProcedure.java
@@ -19,6 +19,7 @@
 package org.apache.paimon.spark.procedure;
 
 import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.operation.LocalOrphanFilesClean;
 import org.apache.paimon.operation.OrphanFilesClean;
 import org.apache.paimon.spark.catalog.WithPaimonCatalog;
 import org.apache.paimon.spark.orphan.SparkOrphanFilesClean;
@@ -32,6 +33,8 @@ import org.apache.spark.sql.types.StructType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Locale;
+
 import static org.apache.spark.sql.types.DataTypes.BooleanType;
 import static org.apache.spark.sql.types.DataTypes.IntegerType;
 import static org.apache.spark.sql.types.DataTypes.LongType;
@@ -56,7 +59,8 @@ public class RemoveOrphanFilesProcedure extends BaseProcedure 
{
                 ProcedureParameter.required("table", StringType),
                 ProcedureParameter.optional("older_than", StringType),
                 ProcedureParameter.optional("dry_run", BooleanType),
-                ProcedureParameter.optional("parallelism", IntegerType)
+                ProcedureParameter.optional("parallelism", IntegerType),
+                ProcedureParameter.optional("mode", StringType)
             };
 
     private static final StructType OUTPUT_TYPE =
@@ -98,18 +102,45 @@ public class RemoveOrphanFilesProcedure extends 
BaseProcedure {
         LOG.info("identifier is {}.", identifier);
 
         Catalog catalog = ((WithPaimonCatalog) tableCatalog()).paimonCatalog();
-        long deletedFiles =
-                SparkOrphanFilesClean.executeDatabaseOrphanFiles(
-                        catalog,
-                        identifier.getDatabaseName(),
-                        identifier.getTableName(),
-                        OrphanFilesClean.olderThanMillis(
-                                args.isNullAt(1) ? null : args.getString(1)),
-                        OrphanFilesClean.createFileCleaner(
-                                catalog, !args.isNullAt(2) && 
args.getBoolean(2)),
-                        args.isNullAt(3) ? null : args.getInt(3));
-
-        return new InternalRow[] {newInternalRow(deletedFiles)};
+        String mode = args.isNullAt(4) ? "DISTRIBUTED" : args.getString(4);
+
+        long deletedFiles;
+        try {
+            switch (mode.toUpperCase(Locale.ROOT)) {
+                case "LOCAL":
+                    deletedFiles =
+                            LocalOrphanFilesClean.executeDatabaseOrphanFiles(
+                                    catalog,
+                                    identifier.getDatabaseName(),
+                                    identifier.getTableName(),
+                                    OrphanFilesClean.olderThanMillis(
+                                            args.isNullAt(1) ? null : 
args.getString(1)),
+                                    OrphanFilesClean.createFileCleaner(
+                                            catalog, !args.isNullAt(2) && 
args.getBoolean(2)),
+                                    args.isNullAt(3) ? null : args.getInt(3));
+                    break;
+                case "DISTRIBUTED":
+                    deletedFiles =
+                            SparkOrphanFilesClean.executeDatabaseOrphanFiles(
+                                    catalog,
+                                    identifier.getDatabaseName(),
+                                    identifier.getTableName(),
+                                    OrphanFilesClean.olderThanMillis(
+                                            args.isNullAt(1) ? null : 
args.getString(1)),
+                                    OrphanFilesClean.createFileCleaner(
+                                            catalog, !args.isNullAt(2) && 
args.getBoolean(2)),
+                                    args.isNullAt(3) ? null : args.getInt(3));
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unknown mode: "
+                                    + mode
+                                    + ". Only 'DISTRIBUTED' and 'LOCAL' are 
supported.");
+            }
+            return new InternalRow[] {newInternalRow(deletedFiles)};
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
     }
 
     public static ProcedureBuilder builder() {
diff --git 
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/procedure/RemoveOrphanFilesProcedureTest.scala
 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/procedure/RemoveOrphanFilesProcedureTest.scala
index c414515f1..d9d738112 100644
--- 
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/procedure/RemoveOrphanFilesProcedureTest.scala
+++ 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/procedure/RemoveOrphanFilesProcedureTest.scala
@@ -172,4 +172,51 @@ class RemoveOrphanFilesProcedureTest extends 
PaimonSparkTestBase {
     checkAnswer(spark.sql(s"CALL sys.remove_orphan_files(table => 'test.*')"), 
Row(0) :: Nil)
   }
 
+  test("Paimon procedure: remove orphan files with mode") {
+    spark.sql(s"""
+                 |CREATE TABLE T (id STRING, name STRING)
+                 |USING PAIMON
+                 |TBLPROPERTIES ('primary-key'='id')
+                 |""".stripMargin)
+
+    spark.sql(s"INSERT INTO T VALUES ('1', 'a'), ('2', 'b')")
+
+    val table = loadTable("T")
+    val fileIO = table.fileIO()
+    val tablePath = table.location()
+
+    val orphanFile1 = new Path(tablePath, ORPHAN_FILE_1)
+    val orphanFile2 = new Path(tablePath, ORPHAN_FILE_2)
+
+    fileIO.tryToWriteAtomic(orphanFile1, "a")
+    Thread.sleep(2000)
+    fileIO.tryToWriteAtomic(orphanFile2, "b")
+
+    // by default, no file deleted
+    checkAnswer(spark.sql(s"CALL sys.remove_orphan_files(table => 'T')"), 
Row(0) :: Nil)
+
+    val orphanFile2ModTime = 
fileIO.getFileStatus(orphanFile2).getModificationTime
+    val older_than1 = DateTimeUtils.formatLocalDateTime(
+      DateTimeUtils.toLocalDateTime(
+        orphanFile2ModTime -
+          TimeUnit.SECONDS.toMillis(1)),
+      3)
+
+    checkAnswer(
+      spark.sql(
+        s"CALL sys.remove_orphan_files(table => 'T', older_than => 
'$older_than1', mode => 'diSTributed')"),
+      Row(1) :: Nil)
+
+    val older_than2 = DateTimeUtils.formatLocalDateTime(
+      DateTimeUtils.toLocalDateTime(System.currentTimeMillis()),
+      3)
+
+    checkAnswer(
+      spark.sql(
+        s"CALL sys.remove_orphan_files(table => 'T', older_than => 
'$older_than2', mode => 'local')"),
+      Row(1) :: Nil)
+
+    checkAnswer(spark.sql(s"CALL sys.remove_orphan_files(table => 'T')"), 
Row(0) :: Nil)
+  }
+
 }

Reply via email to