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

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


The following commit(s) were added to refs/heads/master by this push:
     new 5a11a4fce206 fix(storage-format): ignore temporary LSM timeline 
manifests (#19659)
5a11a4fce206 is described below

commit 5a11a4fce20620afac143358bc771dfe2d14d3c8
Author: Danny Chan <[email protected]>
AuthorDate: Wed Aug 19 15:37:36 2026 +0800

    fix(storage-format): ignore temporary LSM timeline manifests (#19659)
    
    * fix(storage-format): ignore temporary LSM timeline manifests
    * fix(storage): handle immutable file cleanup failures
---
 .../hudi/common/table/timeline/LSMTimeline.java    |  11 +-
 .../common/table/timeline/TestLSMTimeline.java     |  58 +++++++++++
 .../storage/hadoop/TestHoodieHadoopStorage.java    | 114 +++++++++++++++++++++
 .../org/apache/hudi/storage/HoodieStorage.java     |  76 ++++++--------
 .../hudi/io/storage/TestHoodieStorageBase.java     |  19 ++++
 5 files changed, 232 insertions(+), 46 deletions(-)

diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/LSMTimeline.java
 
b/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/LSMTimeline.java
index 4a6680904f0b..4e216f451524 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/LSMTimeline.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/LSMTimeline.java
@@ -109,8 +109,7 @@ public class LSMTimeline {
 
   private static final String VERSION_FILE_NAME = "_version_";    // _version_
   private static final String MANIFEST_FILE_PREFIX = "manifest_"; // 
manifest_[N]
-
-  private static final String TEMP_FILE_SUFFIX = ".tmp";
+  private static final Pattern MANIFEST_FILE_PATTERN = Pattern.compile("^" + 
MANIFEST_FILE_PREFIX + "(\\d+)$");
 
   private static final Pattern ARCHIVE_FILE_PATTERN =
       Pattern.compile("^(\\d+)_(\\d+)_(\\d)\\.parquet");
@@ -241,7 +240,11 @@ public class LSMTimeline {
    * Parse the snapshot version from the manifest file name.
    */
   public static int getManifestVersion(String fileName) {
-    return Integer.parseInt(fileName.split("_")[1]);
+    Matcher fileMatcher = MANIFEST_FILE_PATTERN.matcher(fileName);
+    if (fileMatcher.matches()) {
+      return Integer.parseInt(fileMatcher.group(1));
+    }
+    throw new HoodieException("Unexpected manifest file name: " + fileName);
   }
 
   /**
@@ -297,6 +300,6 @@ public class LSMTimeline {
    * Returns a path filter for the manifest files.
    */
   public static StoragePathFilter getManifestFilePathFilter() {
-    return path -> path.getName().startsWith(MANIFEST_FILE_PREFIX) && 
!path.getName().endsWith(TEMP_FILE_SUFFIX);
+    return path -> MANIFEST_FILE_PATTERN.matcher(path.getName()).matches();
   }
 }
diff --git 
a/hudi-common/src/test/java/org/apache/hudi/common/table/timeline/TestLSMTimeline.java
 
b/hudi-common/src/test/java/org/apache/hudi/common/table/timeline/TestLSMTimeline.java
index 009c926fa226..f8506962ad47 100644
--- 
a/hudi-common/src/test/java/org/apache/hudi/common/table/timeline/TestLSMTimeline.java
+++ 
b/hudi-common/src/test/java/org/apache/hudi/common/table/timeline/TestLSMTimeline.java
@@ -19,13 +19,30 @@
 
 package org.apache.hudi.common.table.timeline;
 
+import org.apache.hudi.common.table.HoodieTableMetaClient;
 import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.storage.StoragePathFilter;
+import org.apache.hudi.storage.StoragePathInfo;
 
 import org.junit.jupiter.api.Test;
 
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 /**
  * Test cases for {@link LSMTimeline}.
@@ -54,4 +71,45 @@ public class TestLSMTimeline {
     assertThat(layer, is(0));
     assertThat("for invalid file name, returns 0", 
LSMTimeline.getFileLayer("invalid_file_name.parquet"), is(0));
   }
+
+  @Test
+  void testManifestFileValidation() {
+    StoragePathFilter filter = LSMTimeline.getManifestFilePathFilter();
+
+    assertTrue(filter.accept(new StoragePath("manifest_1")));
+    assertTrue(filter.accept(new StoragePath("manifest_114")));
+    assertThat(LSMTimeline.getManifestVersion("manifest_114"), is(114));
+
+    assertFalse(filter.accept(new StoragePath("manifest_114.tmp")));
+    assertFalse(filter.accept(new 
StoragePath("manifest_114.a5e022a6-e5c9-4450-b9e5-9296262329b5")));
+    assertFalse(filter.accept(new StoragePath("manifest_invalid")));
+    assertFalse(filter.accept(new StoragePath("manifest_")));
+    assertThrows(HoodieException.class,
+        () -> 
LSMTimeline.getManifestVersion("manifest_114.a5e022a6-e5c9-4450-b9e5-9296262329b5"));
+  }
+
+  @Test
+  void testLatestSnapshotVersionFallbackIgnoresTemporaryManifest() throws 
IOException {
+    StoragePath archivePath = new 
StoragePath("/table/.hoodie/timeline/history");
+    HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+    HoodieStorage storage = mock(HoodieStorage.class);
+    List<StoragePathInfo> manifestFiles = Arrays.asList(
+        manifestFile(archivePath, "manifest_113"),
+        manifestFile(archivePath, "manifest_114"),
+        manifestFile(archivePath, 
"manifest_114.a5e022a6-e5c9-4450-b9e5-9296262329b5"));
+
+    when(metaClient.getStorage()).thenReturn(storage);
+    
when(storage.open(LSMTimeline.getVersionFilePath(archivePath))).thenThrow(new 
FileNotFoundException());
+    when(storage.listDirectEntries(eq(archivePath), 
any(StoragePathFilter.class))).thenAnswer(invocation -> {
+      StoragePathFilter filter = invocation.getArgument(1);
+      return manifestFiles.stream().filter(file -> 
filter.accept(file.getPath())).collect(Collectors.toList());
+    });
+
+    assertThat(LSMTimeline.latestSnapshotVersion(metaClient, archivePath), 
is(114));
+    assertThat(LSMTimeline.allSnapshotVersions(metaClient, archivePath), 
is(Arrays.asList(113, 114)));
+  }
+
+  private static StoragePathInfo manifestFile(StoragePath archivePath, String 
fileName) {
+    return new StoragePathInfo(new StoragePath(archivePath, fileName), 0, 
false, (short) 1, 0, 0);
+  }
 }
diff --git 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/storage/hadoop/TestHoodieHadoopStorage.java
 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/storage/hadoop/TestHoodieHadoopStorage.java
index 8d0f64d0c6bd..0b060e144ae1 100644
--- 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/storage/hadoop/TestHoodieHadoopStorage.java
+++ 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/storage/hadoop/TestHoodieHadoopStorage.java
@@ -19,17 +19,28 @@
 
 package org.apache.hudi.storage.hadoop;
 
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.exception.HoodieIOException;
 import org.apache.hudi.hadoop.fs.HadoopFSUtils;
 import org.apache.hudi.io.storage.TestHoodieStorageBase;
+import org.apache.hudi.storage.HoodieInstantWriter;
 import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
 
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileSystem;
 import org.junit.jupiter.api.Test;
 
+import java.io.FilterOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Tests {@link HoodieHadoopStorage}.
@@ -70,4 +81,107 @@ public class TestHoodieHadoopStorage extends 
TestHoodieStorageBase {
     assertSame(fileSystem, storage.getFileSystem());
     assertSame(fileSystem, HadoopFSUtils.getFs(getTempDir(), conf, true));
   }
+
+  @Test
+  void testCreateImmutableFileCleansTemporaryFileAfterCloseFailure() throws 
IOException {
+    Configuration conf = new Configuration();
+    FileSystem fileSystem = HadoopFSUtils.getFs(getTempDir(), conf, true);
+    HoodieStorage storage = new CloseFailingHoodieHadoopStorage(fileSystem);
+    StoragePath directory = new StoragePath(getTempDir(), 
"testImmutableFileCloseFailure");
+    StoragePath path = new StoragePath(directory, "1.file");
+    storage.createDirectory(directory);
+
+    HoodieIOException exception = assertThrows(HoodieIOException.class,
+        () -> storage.createImmutableFileInPath(path,
+            Option.of(HoodieInstantWriter.convertByteArrayToWriter(new byte[] 
{42}))));
+
+    assertTrue(exception.getMessage().startsWith("Failed to create immutable 
file "));
+    assertFalse(storage.exists(path));
+    assertTrue(storage.listDirectEntries(directory).isEmpty());
+  }
+
+  @Test
+  void testCreateImmutableFileCleansTemporaryFileAfterUncheckedRenameFailure() 
throws IOException {
+    Configuration conf = new Configuration();
+    FileSystem fileSystem = HadoopFSUtils.getFs(getTempDir(), conf, true);
+    HoodieStorage storage = new RenameFailingHoodieHadoopStorage(fileSystem);
+    StoragePath directory = new StoragePath(getTempDir(), 
"testImmutableFileRenameFailure");
+    StoragePath path = new StoragePath(directory, "1.file");
+    storage.createDirectory(directory);
+
+    HoodieException exception = assertThrows(HoodieException.class,
+        () -> storage.createImmutableFileInPath(path,
+            Option.of(HoodieInstantWriter.convertByteArrayToWriter(new byte[] 
{42}))));
+
+    assertEquals("rename failure", exception.getMessage());
+    assertFalse(storage.exists(path));
+    assertTrue(storage.listDirectEntries(directory).isEmpty());
+  }
+
+  @Test
+  void testCreateImmutableFileSuppressesUncheckedCleanupFailure() throws 
IOException {
+    Configuration conf = new Configuration();
+    FileSystem fileSystem = HadoopFSUtils.getFs(getTempDir(), conf, true);
+    CleanupFailingHoodieHadoopStorage storage = new 
CleanupFailingHoodieHadoopStorage(fileSystem);
+    StoragePath directory = new StoragePath(getTempDir(), 
"testImmutableFileCleanupFailure");
+    StoragePath path = new StoragePath(directory, "1.file");
+    storage.createDirectory(directory);
+
+    HoodieIOException exception = assertThrows(HoodieIOException.class,
+        () -> storage.createImmutableFileInPath(path, Option.of(outputStream 
-> {
+          outputStream.write(42);
+          throw new IOException("write failure");
+        })));
+
+    assertEquals("write failure", exception.getCause().getMessage());
+    assertEquals(1, exception.getSuppressed().length);
+    assertEquals("cleanup failure", exception.getSuppressed()[0].getMessage());
+    assertFalse(storage.exists(path));
+    assertEquals(1, storage.listDirectEntries(directory).size());
+    StoragePath temporaryPath = 
storage.listDirectEntries(directory).get(0).getPath();
+    storage.deleteTemporaryFileAfterTest(temporaryPath);
+  }
+
+  private static class CloseFailingHoodieHadoopStorage extends 
HoodieHadoopStorage {
+    CloseFailingHoodieHadoopStorage(FileSystem fileSystem) {
+      super(fileSystem);
+    }
+
+    @Override
+    public OutputStream create(StoragePath path, boolean overwrite) throws 
IOException {
+      return new FilterOutputStream(super.create(path, overwrite)) {
+        @Override
+        public void close() throws IOException {
+          super.close();
+          throw new IOException("close failure");
+        }
+      };
+    }
+  }
+
+  private static class RenameFailingHoodieHadoopStorage extends 
HoodieHadoopStorage {
+    RenameFailingHoodieHadoopStorage(FileSystem fileSystem) {
+      super(fileSystem);
+    }
+
+    @Override
+    public boolean rename(StoragePath oldPath, StoragePath newPath) {
+      throw new HoodieException("rename failure");
+    }
+  }
+
+  private static class CleanupFailingHoodieHadoopStorage extends 
HoodieHadoopStorage {
+    CleanupFailingHoodieHadoopStorage(FileSystem fileSystem) {
+      super(fileSystem);
+    }
+
+    @Override
+    public boolean deleteFile(StoragePath path) {
+      throw new HoodieIOException("cleanup failure");
+    }
+
+    void deleteTemporaryFileAfterTest(StoragePath path) throws IOException {
+      super.deleteFile(path);
+    }
+  }
 }
diff --git a/hudi-io/src/main/java/org/apache/hudi/storage/HoodieStorage.java 
b/hudi-io/src/main/java/org/apache/hudi/storage/HoodieStorage.java
index 2ec0d048b45d..13fdd1c932ad 100644
--- a/hudi-io/src/main/java/org/apache/hudi/storage/HoodieStorage.java
+++ b/hudi-io/src/main/java/org/apache/hudi/storage/HoodieStorage.java
@@ -23,6 +23,7 @@ import org.apache.hudi.ApiMaturityLevel;
 import org.apache.hudi.PublicAPIClass;
 import org.apache.hudi.PublicAPIMethod;
 import org.apache.hudi.common.util.Option;
+import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieIOException;
 import org.apache.hudi.io.SeekableDataInputStream;
 
@@ -329,58 +330,49 @@ public abstract class HoodieStorage implements Closeable {
   public final void createImmutableFileInPath(StoragePath path,
                                               Option<HoodieInstantWriter> 
contentWriter,
                                               boolean needTempFile) throws 
HoodieIOException {
-    OutputStream fsout = null;
     StoragePath tmpPath = null;
+    StoragePath pathToCreate = path;
+    if (contentWriter.isPresent() && needTempFile) {
+      StoragePath parent = path.getParent();
+      tmpPath = new StoragePath(parent, path.getName() + "." + 
UUID.randomUUID());
+      pathToCreate = tmpPath;
+    }
 
+    boolean fileCreated = false;
+    HoodieException failure = null;
     try {
-      if (!contentWriter.isPresent()) {
-        fsout = create(path, false);
-      }
-
-      if (contentWriter.isPresent() && needTempFile) {
-        StoragePath parent = path.getParent();
-        tmpPath = new StoragePath(parent, path.getName() + "." + 
UUID.randomUUID());
-        fsout = create(tmpPath, false);
-        contentWriter.get().writeToStream(fsout);
+      try (OutputStream fsout = create(pathToCreate, false)) {
+        if (contentWriter.isPresent()) {
+          contentWriter.get().writeToStream(fsout);
+        }
       }
-
-      if (contentWriter.isPresent() && !needTempFile) {
-        fsout = create(path, false);
-        contentWriter.get().writeToStream(fsout);
+      fileCreated = tmpPath == null || rename(tmpPath, path);
+    } catch (Throwable t) {
+      if (t instanceof HoodieException) {
+        failure = (HoodieException) t;
+      } else if (t instanceof IOException) {
+        failure = new HoodieIOException("Failed to create immutable file " + 
path, (IOException) t);
+      } else {
+        failure = new HoodieException("Failed to create immutable file " + 
path, t);
       }
-    } catch (IOException e) {
-      String errorMsg = "Failed to create file " + (tmpPath != null ? tmpPath 
: path);
-      throw new HoodieIOException(errorMsg, e);
+      throw failure;
     } finally {
-      try {
-        if (null != fsout) {
-          fsout.close();
-        }
-      } catch (IOException e) {
-        String errorMsg = "Failed to close file " + (needTempFile ? tmpPath : 
path);
-        throw new HoodieIOException(errorMsg, e);
-      }
-
-      boolean renameSuccess = false;
-      try {
-        if (null != tmpPath) {
-          renameSuccess = rename(tmpPath, path);
-        }
-      } catch (IOException e) {
-        throw new HoodieIOException(
-            "Failed to rename " + tmpPath + " to the target " + path,
-            e);
-      } finally {
-        if (!renameSuccess && null != tmpPath) {
-          try {
-            deleteFile(tmpPath);
-            LOG.debug("Failed to rename {} to {}, target file exists: {}", 
tmpPath, path, exists(path));
-          } catch (IOException e) {
-            throw new HoodieIOException("Failed to delete tmp file " + 
tmpPath, e);
+      if (tmpPath != null && !fileCreated) {
+        try {
+          deleteFile(tmpPath);
+        } catch (Throwable t) {
+          if (failure != null) {
+            failure.addSuppressed(t);
+          } else {
+            throw new HoodieException("Failed to delete temporary file " + 
tmpPath, t);
           }
         }
       }
     }
+
+    if (!fileCreated) {
+      LOG.debug("Failed to rename {} to {}; target file may already exist", 
tmpPath, path);
+    }
   }
 
   /**
diff --git 
a/hudi-io/src/test/java/org/apache/hudi/io/storage/TestHoodieStorageBase.java 
b/hudi-io/src/test/java/org/apache/hudi/io/storage/TestHoodieStorageBase.java
index 090e3f956787..380967dea129 100644
--- 
a/hudi-io/src/test/java/org/apache/hudi/io/storage/TestHoodieStorageBase.java
+++ 
b/hudi-io/src/test/java/org/apache/hudi/io/storage/TestHoodieStorageBase.java
@@ -20,6 +20,7 @@
 package org.apache.hudi.io.storage;
 
 import org.apache.hudi.common.util.Option;
+import org.apache.hudi.exception.HoodieIOException;
 import org.apache.hudi.io.SeekableDataInputStream;
 import org.apache.hudi.io.util.FileIOUtils;
 import org.apache.hudi.storage.HoodieInstantWriter;
@@ -151,6 +152,24 @@ public abstract class TestHoodieStorageBase {
     assertTrue(storage.createDirectory(path4));
   }
 
+  @Test
+  public void testImmutableFileIsNotPublishedOnWriteFailure() throws 
IOException {
+    HoodieStorage storage = getStorage();
+    StoragePath directory = new StoragePath(getTempDir(), 
"testImmutableFileWriteFailure");
+    StoragePath path = new StoragePath(directory, "1.file");
+    storage.createDirectory(directory);
+
+    HoodieIOException exception = assertThrows(HoodieIOException.class,
+        () -> storage.createImmutableFileInPath(path, Option.of(outputStream 
-> {
+          outputStream.write(42);
+          throw new IOException("write failure");
+        })));
+
+    assertEquals("write failure", exception.getCause().getMessage());
+    assertFalse(storage.exists(path));
+    assertTrue(storage.listDirectEntries(directory).isEmpty());
+  }
+
   @Test
   public void testSeekable() throws IOException {
     HoodieStorage storage = getStorage();

Reply via email to