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

baiyangtx 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 ed882e171 [AMORO-2710] Support bulk operations for arctic file io 
(#2711)
ed882e171 is described below

commit ed882e171a57c3f0dff72b0eef37ade7a6919b21
Author: Xavier Bai <[email protected]>
AuthorDate: Thu Apr 11 19:33:22 2024 +0800

    [AMORO-2710] Support bulk operations for arctic file io (#2711)
    
    * Batch delete files when expire snapshots and clean orphan files
    
    * return files to delete
    
    * clean code
---
 .../maintainer/IcebergTableMaintainer.java         | 190 +++++++++++++--------
 .../java/com/netease/arctic/io/ArcticFileIO.java   |  15 ++
 .../com/netease/arctic/io/ArcticFileIOAdapter.java |  12 ++
 .../com/netease/arctic/io/ArcticHadoopFileIO.java  |  15 ++
 .../com/netease/arctic/utils/TableFileUtil.java    |  82 ++++++++-
 5 files changed, 243 insertions(+), 71 deletions(-)

diff --git 
a/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/IcebergTableMaintainer.java
 
b/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/IcebergTableMaintainer.java
index ae9f181e0..e417fe878 100644
--- 
a/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/IcebergTableMaintainer.java
+++ 
b/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/IcebergTableMaintainer.java
@@ -62,6 +62,7 @@ import 
org.apache.iceberg.relocated.com.google.common.collect.Sets;
 import org.apache.iceberg.types.Conversions;
 import org.apache.iceberg.types.Type;
 import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.ThreadPools;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -141,9 +142,8 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
     if (currentSnapshot == null) {
       return;
     }
-    java.util.Optional<String> totalDeleteFiles =
-        java.util.Optional.ofNullable(
-            
currentSnapshot.summary().get(SnapshotSummary.TOTAL_DELETE_FILES_PROP));
+    Optional<String> totalDeleteFiles =
+        
Optional.ofNullable(currentSnapshot.summary().get(SnapshotSummary.TOTAL_DELETE_FILES_PROP));
     if (totalDeleteFiles.isPresent() && Long.parseLong(totalDeleteFiles.get()) 
> 0) {
       // clear dangling delete files
       cleanDanglingDeleteFiles();
@@ -175,36 +175,37 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
   private void expireSnapshots(long olderThan, Set<String> exclude) {
     LOG.debug("start expire snapshots older than {}, the exclude is {}", 
olderThan, exclude);
     final AtomicInteger toDeleteFiles = new AtomicInteger(0);
-    final AtomicInteger deleteFiles = new AtomicInteger(0);
-    Set<String> parentDirectory = new HashSet<>();
+    Set<String> parentDirectories = new HashSet<>();
+    Set<String> expiredFiles = new HashSet<>();
     table
         .expireSnapshots()
         .retainLast(1)
         .expireOlderThan(olderThan)
         .deleteWith(
             file -> {
-              try {
-                if (exclude.isEmpty()) {
-                  arcticFileIO().deleteFile(file);
-                } else {
-                  String fileUriPath = TableFileUtil.getUriPath(file);
-                  if (!exclude.contains(fileUriPath)
-                      && !exclude.contains(new 
Path(fileUriPath).getParent().toString())) {
-                    arcticFileIO().deleteFile(file);
-                  }
+              if (exclude.isEmpty()) {
+                expiredFiles.add(file);
+              } else {
+                String fileUriPath = TableFileUtil.getUriPath(file);
+                if (!exclude.contains(fileUriPath)
+                    && !exclude.contains(new 
Path(fileUriPath).getParent().toString())) {
+                  expiredFiles.add(file);
                 }
-                parentDirectory.add(new Path(file).getParent().toString());
-                deleteFiles.incrementAndGet();
-              } catch (Throwable t) {
-                LOG.warn("failed to delete file " + file, t);
-              } finally {
-                toDeleteFiles.incrementAndGet();
               }
+
+              parentDirectories.add(new Path(file).getParent().toString());
+              toDeleteFiles.incrementAndGet();
             })
-        .cleanExpiredFiles(true)
+        .cleanExpiredFiles(
+            true) /* enable clean only for collecting the expired files, will 
delete them later */
         .commit();
 
-    parentDirectory.forEach(
+    // try to batch delete files
+    int deletedFiles =
+        TableFileUtil.parallelDeleteFiles(
+            arcticFileIO(), expiredFiles, ThreadPools.getWorkerPool());
+
+    parentDirectories.forEach(
         parent -> {
           try {
             TableFileUtil.deleteEmptyDirectory(arcticFileIO(), parent, 
exclude);
@@ -214,11 +215,14 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
           }
         });
 
-    LOG.info(
-        "to delete {} files in {}, success delete {} files",
-        toDeleteFiles.get(),
-        getTable().name(),
-        deleteFiles.get());
+    runWithCondition(
+        toDeleteFiles.get() > 0,
+        () ->
+            LOG.info(
+                "To delete {} files in {}, success delete {} files",
+                toDeleteFiles.get(),
+                getTable().name(),
+                deletedFiles));
   }
 
   @Override
@@ -297,21 +301,23 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
     // so acquire in advance
     // to prevent repeated acquisition
     Set<String> validFiles = orphanFileCleanNeedToExcludeFiles();
-    LOG.info("{} start clean content files of change store", table.name());
-    int deleteFilesCnt = clearInternalTableContentsFiles(lastTime, validFiles);
-    LOG.info("{} total delete {} files from change store", table.name(), 
deleteFilesCnt);
+    LOG.info("{} start cleaning orphan files in content", table.name());
+    clearInternalTableContentsFiles(lastTime, validFiles);
   }
 
   protected void cleanMetadata(long lastTime) {
     LOG.info("{} start clean metadata files", table.name());
-    int deleteFilesCnt = clearInternalTableMetadata(lastTime);
-    LOG.info("{} total delete {} metadata files", table.name(), 
deleteFilesCnt);
+    clearInternalTableMetadata(lastTime);
   }
 
   protected void cleanDanglingDeleteFiles() {
     LOG.info("{} start delete dangling delete files", table.name());
     int danglingDeleteFilesCnt = clearInternalTableDanglingDeleteFiles();
-    LOG.info("{} total delete {} dangling delete files", table.name(), 
danglingDeleteFilesCnt);
+    runWithCondition(
+        danglingDeleteFilesCnt > 0,
+        () ->
+            LOG.info(
+                "{} total delete {} dangling delete files", table.name(), 
danglingDeleteFilesCnt));
   }
 
   protected long mustOlderThan(TableRuntime tableRuntime, long now) {
@@ -344,18 +350,28 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
     return (ArcticFileIO) table.io();
   }
 
-  private int clearInternalTableContentsFiles(long lastTime, Set<String> 
exclude) {
+  private void clearInternalTableContentsFiles(long lastTime, Set<String> 
exclude) {
     String dataLocation = table.location() + File.separator + DATA_FOLDER_NAME;
+    int slated = 0, deleted = 0;
 
     try (ArcticFileIO io = arcticFileIO()) {
       // listPrefix will not return the directory and the orphan file clean 
should clean the empty
       // dir.
       if (io.supportFileSystemOperations()) {
         SupportsFileSystemOperations fio = io.asFileSystemIO();
-        return deleteInvalidFilesInFs(fio, dataLocation, lastTime, exclude);
+        Set<PathInfo> directories = new HashSet<>();
+        Set<String> filesToDelete =
+            deleteInvalidFilesInFs(fio, dataLocation, lastTime, exclude, 
directories);
+        slated = filesToDelete.size();
+        deleted = TableFileUtil.deleteFiles(io, filesToDelete);
+        /* delete empty directories */
+        deleteEmptyDirectories(fio, directories, lastTime, exclude);
       } else if (io.supportPrefixOperations()) {
         SupportsPrefixOperations pio = io.asPrefixFileIO();
-        return deleteInvalidFilesByPrefix(pio, dataLocation, lastTime, 
exclude);
+        Set<String> filesToDelete =
+            deleteInvalidFilesByPrefix(pio, dataLocation, lastTime, exclude);
+        slated = filesToDelete.size();
+        deleted = TableFileUtil.deleteFiles(io, filesToDelete);
       } else {
         LOG.warn(
             String.format(
@@ -364,10 +380,19 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
       }
     }
 
-    return 0;
+    final int finalCandidate = slated;
+    final int finalDeleted = deleted;
+    runWithCondition(
+        slated > 0,
+        () ->
+            LOG.info(
+                "{}: There were {} files slated for deletion and {} files were 
successfully deleted",
+                table.name(),
+                finalCandidate,
+                finalDeleted));
   }
 
-  private int clearInternalTableMetadata(long lastTime) {
+  private void clearInternalTableMetadata(long lastTime) {
     Set<String> validFiles = getValidMetadataFiles(table);
     LOG.info("{} table getRuntime {} valid files", table.name(), 
validFiles.size());
     Pattern excludeFileNameRegex = getExcludeFileNameRegex(table);
@@ -379,8 +404,19 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
     try (ArcticFileIO io = arcticFileIO()) {
       if (io.supportPrefixOperations()) {
         SupportsPrefixOperations pio = io.asPrefixFileIO();
-        return deleteInvalidMetadataFile(
-            pio, metadataLocation, lastTime, validFiles, excludeFileNameRegex);
+        Set<String> filesToDelete =
+            deleteInvalidMetadataFile(
+                pio, metadataLocation, lastTime, validFiles, 
excludeFileNameRegex);
+        int deleted = TableFileUtil.deleteFiles(io, filesToDelete);
+
+        runWithCondition(
+            !filesToDelete.isEmpty(),
+            () ->
+                LOG.info(
+                    "{}: There were {} metadata files to be deleted and {} 
metadata files were successfully deleted",
+                    table.name(),
+                    filesToDelete.size(),
+                    deleted));
       } else {
         LOG.warn(
             String.format(
@@ -388,7 +424,6 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
                 table.name()));
       }
     }
-    return 0;
   }
 
   private int clearInternalTableDanglingDeleteFiles() {
@@ -473,50 +508,61 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
     return snapshot.map(Snapshot::timestampMillis).orElse(Long.MAX_VALUE);
   }
 
-  private static int deleteInvalidFilesInFs(
-      SupportsFileSystemOperations fio, String location, long lastTime, 
Set<String> excludes) {
+  private Set<String> deleteInvalidFilesInFs(
+      SupportsFileSystemOperations fio,
+      String location,
+      long lastTime,
+      Set<String> excludes,
+      Set<PathInfo> directories) {
     if (!fio.exists(location)) {
-      return 0;
+      return Collections.emptySet();
     }
 
-    int deleteCount = 0;
+    Set<String> filesToDelete = new HashSet<>();
     for (PathInfo p : fio.listDirectory(location)) {
       String uriPath = TableFileUtil.getUriPath(p.location());
       if (p.isDirectory()) {
-        int deleted = deleteInvalidFilesInFs(fio, p.location(), lastTime, 
excludes);
-        deleteCount += deleted;
-        if (fio.exists(p.location())
-            && !p.location().endsWith(METADATA_FOLDER_NAME)
-            && !p.location().endsWith(DATA_FOLDER_NAME)
-            && p.createdAtMillis() < lastTime
-            && fio.isEmptyDirectory(p.location())) {
-          TableFileUtil.deleteEmptyDirectory(fio, p.location(), excludes);
-        }
+        directories.add(p);
+        filesToDelete.addAll(
+            deleteInvalidFilesInFs(fio, p.location(), lastTime, excludes, 
directories));
       } else {
         String parentLocation = TableFileUtil.getParent(p.location());
         String parentUriPath = TableFileUtil.getUriPath(parentLocation);
         if (!excludes.contains(uriPath)
             && !excludes.contains(parentUriPath)
             && p.createdAtMillis() < lastTime) {
-          fio.deleteFile(p.location());
-          deleteCount += 1;
+          filesToDelete.add(p.location());
         }
       }
     }
-    return deleteCount;
+    return filesToDelete;
+  }
+
+  private void deleteEmptyDirectories(
+      SupportsFileSystemOperations fio, Set<PathInfo> paths, long lastTime, 
Set<String> excludes) {
+    paths.forEach(
+        p -> {
+          if (fio.exists(p.location())
+              && !p.location().endsWith(METADATA_FOLDER_NAME)
+              && !p.location().endsWith(DATA_FOLDER_NAME)
+              && p.createdAtMillis() < lastTime
+              && fio.isEmptyDirectory(p.location())) {
+            TableFileUtil.deleteEmptyDirectory(fio, p.location(), excludes);
+          }
+        });
   }
 
-  private static int deleteInvalidFilesByPrefix(
+  private Set<String> deleteInvalidFilesByPrefix(
       SupportsPrefixOperations pio, String prefix, long lastTime, Set<String> 
excludes) {
-    int deleteCount = 0;
+    Set<String> filesToDelete = new HashSet<>();
     for (FileInfo fileInfo : pio.listPrefix(prefix)) {
       String uriPath = TableFileUtil.getUriPath(fileInfo.location());
       if (!excludes.contains(uriPath) && fileInfo.createdAtMillis() < 
lastTime) {
-        pio.deleteFile(fileInfo.location());
-        deleteCount += 1;
+        filesToDelete.add(fileInfo.location());
       }
     }
-    return deleteCount;
+
+    return filesToDelete;
   }
 
   private static Set<String> getValidMetadataFiles(Table internalTable) {
@@ -575,24 +621,24 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
     return null;
   }
 
-  private static int deleteInvalidMetadataFile(
+  private Set<String> deleteInvalidMetadataFile(
       SupportsPrefixOperations pio,
       String location,
       long lastTime,
       Set<String> exclude,
       Pattern excludeRegex) {
-    int count = 0;
+    Set<String> filesToDelete = new HashSet<>();
     for (FileInfo fileInfo : pio.listPrefix(location)) {
       String uriPath = TableFileUtil.getUriPath(fileInfo.location());
       if (!exclude.contains(uriPath)
           && fileInfo.createdAtMillis() < lastTime
           && (excludeRegex == null
               || 
!excludeRegex.matcher(TableFileUtil.getFileName(fileInfo.location())).matches()))
 {
-        pio.deleteFile(fileInfo.location());
-        count += 1;
+        filesToDelete.add(fileInfo.location());
       }
     }
-    return count;
+
+    return filesToDelete;
   }
 
   private static String formatTime(long timestamp) {
@@ -617,9 +663,7 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
     }
     long deleteFileCnt =
         Long.parseLong(
-            snapshot
-                .summary()
-                
.getOrDefault(org.apache.iceberg.SnapshotSummary.TOTAL_DELETE_FILES_PROP, "0"));
+            
snapshot.summary().getOrDefault(SnapshotSummary.TOTAL_DELETE_FILES_PROP, "0"));
     CloseableIterable<DataFile> dataFiles =
         CloseableIterable.transform(tasks, ContentScanTask::file);
     CloseableIterable<FileScanTask> hasDeleteTask =
@@ -936,4 +980,10 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
       return tsBound;
     }
   }
+
+  private void runWithCondition(boolean condition, Runnable fun) {
+    if (condition) {
+      fun.run();
+    }
+  }
 }
diff --git a/core/src/main/java/com/netease/arctic/io/ArcticFileIO.java 
b/core/src/main/java/com/netease/arctic/io/ArcticFileIO.java
index 3f3677855..c3b97deec 100644
--- a/core/src/main/java/com/netease/arctic/io/ArcticFileIO.java
+++ b/core/src/main/java/com/netease/arctic/io/ArcticFileIO.java
@@ -20,6 +20,7 @@ package com.netease.arctic.io;
 
 import org.apache.iceberg.io.FileIO;
 import org.apache.iceberg.io.InputFile;
+import org.apache.iceberg.io.SupportsBulkOperations;
 import org.apache.iceberg.io.SupportsPrefixOperations;
 
 import java.util.concurrent.Callable;
@@ -61,6 +62,20 @@ public interface ArcticFileIO extends FileIO {
     }
   }
 
+  /** Determine if the fileIO supports bulk operations. */
+  default boolean supportBulkOperations() {
+    return false;
+  }
+
+  /** Return this fileIO as a {@link SupportsBulkOperations} if it is an 
instance of that type. */
+  default SupportsBulkOperations asBulkFileIO() {
+    if (supportBulkOperations()) {
+      return (SupportsBulkOperations) this;
+    } else {
+      throw new IllegalStateException("Doesn't support bulk operations");
+    }
+  }
+
   /** Returns true if this tableIo is an {@link SupportsFileSystemOperations} 
*/
   default boolean supportFileSystemOperations() {
     return false;
diff --git a/core/src/main/java/com/netease/arctic/io/ArcticFileIOAdapter.java 
b/core/src/main/java/com/netease/arctic/io/ArcticFileIOAdapter.java
index 201845ee4..78a1507b8 100644
--- a/core/src/main/java/com/netease/arctic/io/ArcticFileIOAdapter.java
+++ b/core/src/main/java/com/netease/arctic/io/ArcticFileIOAdapter.java
@@ -21,6 +21,7 @@ package com.netease.arctic.io;
 import org.apache.iceberg.io.FileIO;
 import org.apache.iceberg.io.InputFile;
 import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.SupportsBulkOperations;
 import org.apache.iceberg.io.SupportsPrefixOperations;
 import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
 
@@ -67,6 +68,17 @@ public class ArcticFileIOAdapter implements ArcticFileIO {
     return (SupportsPrefixOperations) io;
   }
 
+  @Override
+  public boolean supportBulkOperations() {
+    return io instanceof SupportsBulkOperations;
+  }
+
+  @Override
+  public SupportsBulkOperations asBulkFileIO() {
+    Preconditions.checkArgument(supportBulkOperations());
+    return (SupportsBulkOperations) io;
+  }
+
   @Override
   public boolean supportFileSystemOperations() {
     return io instanceof ArcticFileIO && ((ArcticFileIO) 
io).supportFileSystemOperations();
diff --git a/core/src/main/java/com/netease/arctic/io/ArcticHadoopFileIO.java 
b/core/src/main/java/com/netease/arctic/io/ArcticHadoopFileIO.java
index 6f11a23fb..26297b8da 100644
--- a/core/src/main/java/com/netease/arctic/io/ArcticHadoopFileIO.java
+++ b/core/src/main/java/com/netease/arctic/io/ArcticHadoopFileIO.java
@@ -26,6 +26,7 @@ import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.fs.RemoteIterator;
 import org.apache.iceberg.hadoop.HadoopFileIO;
 import org.apache.iceberg.hadoop.Util;
+import org.apache.iceberg.io.BulkDeletionFailureException;
 import org.apache.iceberg.io.FileInfo;
 import org.apache.iceberg.io.InputFile;
 import org.apache.iceberg.io.OutputFile;
@@ -234,11 +235,25 @@ public class ArcticHadoopFileIO extends HadoopFileIO
         });
   }
 
+  @Override
+  public void deleteFiles(Iterable<String> pathsToDelete) throws 
BulkDeletionFailureException {
+    tableMetaStore.doAs(
+        () -> {
+          super.deleteFiles(pathsToDelete);
+          return null;
+        });
+  }
+
   @Override
   public boolean supportPrefixOperations() {
     return true;
   }
 
+  @Override
+  public boolean supportBulkOperations() {
+    return true;
+  }
+
   @Override
   public boolean supportFileSystemOperations() {
     return true;
diff --git a/core/src/main/java/com/netease/arctic/utils/TableFileUtil.java 
b/core/src/main/java/com/netease/arctic/utils/TableFileUtil.java
index 84053d28a..4ed345539 100644
--- a/core/src/main/java/com/netease/arctic/utils/TableFileUtil.java
+++ b/core/src/main/java/com/netease/arctic/utils/TableFileUtil.java
@@ -20,12 +20,16 @@ package com.netease.arctic.utils;
 
 import com.netease.arctic.io.ArcticFileIO;
 import org.apache.hadoop.fs.Path;
+import org.apache.iceberg.io.BulkDeletionFailureException;
+import org.apache.iceberg.util.Tasks;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.net.URI;
 import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
 
 public class TableFileUtil {
   private static final Logger LOG = 
LoggerFactory.getLogger(TableFileUtil.class);
@@ -62,7 +66,7 @@ public class TableFileUtil {
   public static void deleteEmptyDirectory(
       ArcticFileIO io, String directoryPath, Set<String> exclude) {
     if (!io.exists(directoryPath)) {
-      LOG.warn("The target directory {} does not exist or has been deleted", 
directoryPath);
+      LOG.debug("The target directory {} does not exist or has been deleted", 
directoryPath);
       return;
     }
     String parent = new Path(directoryPath).getParent().toString();
@@ -80,6 +84,82 @@ public class TableFileUtil {
     }
   }
 
+  /**
+   * Helper to delete files. Bulk deletion is used if possible.
+   *
+   * @param io arctic file io
+   * @param files files to delete
+   * @param workPool executor pool. Only applicable for non-bulk FileIO
+   * @return deleted file count
+   */
+  public static int deleteFiles(ArcticFileIO io, Set<String> files, 
ExecutorService workPool) {
+    if (files == null || files.isEmpty()) {
+      return 0;
+    }
+
+    AtomicInteger failedFileCnt = new AtomicInteger(0);
+    if (io.supportBulkOperations()) {
+      try {
+        io.asBulkFileIO().deleteFiles(files);
+      } catch (BulkDeletionFailureException e) {
+        failedFileCnt.set(e.numberFailedObjects());
+        LOG.warn("Failed to bulk delete {} files", e.numberFailedObjects(), e);
+      } catch (RuntimeException e) {
+        failedFileCnt.set(files.size());
+        LOG.warn("Failed to bulk delete files", e);
+      }
+    } else {
+      if (workPool != null) {
+        Tasks.foreach(files)
+            .executeWith(workPool)
+            .noRetry()
+            .suppressFailureWhenFinished()
+            .onFailure(
+                (file, exc) -> {
+                  failedFileCnt.addAndGet(1);
+                  LOG.warn("Failed to delete file {}", file, exc);
+                })
+            .run(io::deleteFile);
+      } else {
+        files.forEach(
+            f -> {
+              try {
+                io.deleteFile(f);
+              } catch (RuntimeException e) {
+                failedFileCnt.addAndGet(1);
+                LOG.warn("Failed to delete file {}", f, e);
+              }
+            });
+      }
+    }
+
+    return files.size() - failedFileCnt.get();
+  }
+
+  /**
+   * Helper to delete files sequentially
+   *
+   * @param io arctic file io
+   * @param files to deleted files
+   * @return deleted file count
+   */
+  public static int deleteFiles(ArcticFileIO io, Set<String> files) {
+    return deleteFiles(io, files, null);
+  }
+
+  /**
+   * Helper to delete files in parallel
+   *
+   * @param io arctic file io
+   * @param files to deleted files
+   * @param workPool executor pool
+   * @return deleted file count
+   */
+  public static int parallelDeleteFiles(
+      ArcticFileIO io, Set<String> files, ExecutorService workPool) {
+    return deleteFiles(io, files, workPool);
+  }
+
   /**
    * Get the file path after move file to target directory
    *

Reply via email to