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

JingsongLi 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 9bcbbc4a67 [common] Keep the shared _temporary directory while other 
writers stage there (#8900)
9bcbbc4a67 is described below

commit 9bcbbc4a67f44cf52803b5578d6fe330ad3ee3ab
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Thu Jul 30 12:01:48 2026 +0800

    [common] Keep the shared _temporary directory while other writers stage 
there (#8900)
---
 .../paimon/fs/RenamingTwoPhaseOutputStream.java    |  7 ++-
 .../org/apache/paimon/fs/TwoPhaseOutputStream.java | 14 +++++-
 .../fs/RenamingTwoPhaseOutputStreamTest.java       | 57 ++++++++++++++++++++++
 .../paimon/spark/sql/FormatTableTestBase.scala     |  6 ++-
 4 files changed, 80 insertions(+), 4 deletions(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/fs/RenamingTwoPhaseOutputStream.java
 
b/paimon-common/src/main/java/org/apache/paimon/fs/RenamingTwoPhaseOutputStream.java
index f5dcdf7a1b..a8547bd167 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/fs/RenamingTwoPhaseOutputStream.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/fs/RenamingTwoPhaseOutputStream.java
@@ -29,6 +29,7 @@ import java.util.UUID;
  */
 @Public
 public class RenamingTwoPhaseOutputStream extends TwoPhaseOutputStream {
+
     private static final String TEMP_DIR_NAME = "_temporary";
 
     private final Path targetPath;
@@ -136,7 +137,11 @@ public class RenamingTwoPhaseOutputStream extends 
TwoPhaseOutputStream {
 
         @Override
         public void clean(FileIO fileIO) {
-            fileIO.deleteDirectoryQuietly(tempPath.getParent());
+            // Only what this committer staged. '_temporary' is shared with 
every other writer of
+            // this directory, Paimon or not, and it is theirs to remove: 
seeing it empty does not
+            // mean it is unused, because a writer that has just created it 
has not staged its file
+            // yet, and deleting it from under that writer fails its open.
+            fileIO.deleteQuietly(tempPath);
         }
     }
 }
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/fs/TwoPhaseOutputStream.java 
b/paimon-common/src/main/java/org/apache/paimon/fs/TwoPhaseOutputStream.java
index 5f85b087d4..931969ec68 100644
--- a/paimon-common/src/main/java/org/apache/paimon/fs/TwoPhaseOutputStream.java
+++ b/paimon-common/src/main/java/org/apache/paimon/fs/TwoPhaseOutputStream.java
@@ -46,7 +46,8 @@ public abstract class TwoPhaseOutputStream extends 
PositionOutputStream {
         void commit(FileIO fileIO) throws IOException;
 
         /**
-         * Discards the written data, cleaning up any temporary files or 
resources.
+         * Discards the written data, cleaning up any temporary files or 
resources. Called instead
+         * of {@link #commit} when the write is given up.
          *
          * @throws IOException if an I/O error occurs during discard
          */
@@ -54,6 +55,17 @@ public abstract class TwoPhaseOutputStream extends 
PositionOutputStream {
 
         Path targetPath();
 
+        /**
+         * Releases what this committer staged and no longer needs, after 
{@link #commit} has
+         * succeeded. May do nothing.
+         *
+         * <p>Only resources this committer created itself. A staging 
directory is shared with every
+         * other writer of the same location, Paimon or not, and removing it 
is theirs to decide:
+         * finding it empty does not mean it is unused, because a writer that 
has just created it
+         * has not staged its file in it yet.
+         *
+         * @throws IOException if an I/O error occurs during cleaning
+         */
         void clean(FileIO fileIO) throws IOException;
     }
 }
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/fs/RenamingTwoPhaseOutputStreamTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/fs/RenamingTwoPhaseOutputStreamTest.java
index 2b929f18ca..2fc4fef14f 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/fs/RenamingTwoPhaseOutputStreamTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/fs/RenamingTwoPhaseOutputStreamTest.java
@@ -71,6 +71,63 @@ public class RenamingTwoPhaseOutputStreamTest {
         assertThat(new String(content)).isEqualTo(testData);
     }
 
+    @Test
+    void testCleanKeepsAStagingDirectoryHoldingAnotherWritersFile() throws 
IOException {
+        RenamingTwoPhaseOutputStream stream =
+                new RenamingTwoPhaseOutputStream(fileIO, targetPath, false);
+        stream.write("Some data".getBytes());
+        TwoPhaseOutputStream.Committer committer = stream.closeForCommit();
+
+        // A MapReduce-style writer with a task attempt still pending in the 
same directory.
+        Path otherWriterPending =
+                new Path(targetPath.getParent(), 
"_temporary/attempt_0001_m_000010_15/part-00010");
+        fileIO.writeFile(otherWriterPending, "concurrent", false);
+
+        committer.commit(fileIO);
+        committer.clean(fileIO);
+
+        assertThat(fileIO.exists(targetPath)).isTrue();
+        assertThat(fileIO.exists(otherWriterPending)).isTrue();
+        assertThat(fileIO.exists(new Path(targetPath.getParent(), 
"_temporary"))).isTrue();
+    }
+
+    @Test
+    void testCleanKeepsAStagingDirectoryThatIsEmpty() throws IOException {
+        RenamingTwoPhaseOutputStream stream =
+                new RenamingTwoPhaseOutputStream(fileIO, targetPath, false);
+        stream.write("Some data".getBytes());
+        TwoPhaseOutputStream.Committer committer = stream.closeForCommit();
+        Path stagingDir = new Path(targetPath.getParent(), "_temporary");
+
+        committer.commit(fileIO);
+        committer.clean(fileIO);
+
+        // Empty is not the same as unused: a writer that has just created 
'_temporary' has not
+        // staged its file in it yet, and removing the directory would fail 
that writer's open.
+        // exists(), not listStatus(): listStatus answers with no entries for 
a directory that is
+        // gone as much as for one that is empty, so it cannot tell the two 
apart.
+        assertThat(fileIO.exists(targetPath)).isTrue();
+        assertThat(fileIO.exists(stagingDir)).isTrue();
+        assertThat(fileIO.listStatus(stagingDir)).isEmpty();
+    }
+
+    @Test
+    void testCleanRemovesTheFileItStagedWhenThereWasNoCommit() throws 
IOException {
+        RenamingTwoPhaseOutputStream stream =
+                new RenamingTwoPhaseOutputStream(fileIO, targetPath, false);
+        stream.write("Some data".getBytes());
+        TwoPhaseOutputStream.Committer committer = stream.closeForCommit();
+
+        // No commit renamed it away, so clean() is what keeps the staged file 
from being left
+        // behind for good.
+        committer.clean(fileIO);
+
+        Path stagingDir = new Path(targetPath.getParent(), "_temporary");
+        assertThat(fileIO.exists(targetPath)).isFalse();
+        assertThat(fileIO.exists(stagingDir)).isTrue();
+        assertThat(fileIO.listStatus(stagingDir)).isEmpty();
+    }
+
     @Test
     void testDiscard() throws IOException {
         RenamingTwoPhaseOutputStream stream =
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
index 720d283204..652fb9be4d 100644
--- 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
@@ -25,7 +25,7 @@ import org.apache.paimon.spark.{PaimonFormatTableScan, 
PaimonHiveTestBase, Paimo
 import org.apache.paimon.spark.PaimonHiveTestBase.hiveUri
 import org.apache.paimon.table.FormatTable
 import org.apache.paimon.table.source.Split
-import org.apache.paimon.utils.CompressUtils
+import org.apache.paimon.utils.{CompressUtils, PartitionPathUtils}
 
 import org.apache.spark.sql.Row
 import org.apache.spark.sql.connector.read.InputPartition
@@ -214,7 +214,9 @@ abstract class FormatTableTestBase extends 
PaimonHiveTestBase with AdaptiveSpark
         val fileIO = table.fileIO()
         val file = fileIO
           .listStatus(new Path(table.location()))
-          .filter(file => !file.getPath.getName.startsWith("."))
+          // The same rule the reader applies: a writer's staging directory 
stays behind, and it
+          // is not a data file.
+          .filter(file => 
!PartitionPathUtils.isHiddenName(file.getPath.getName))
           .head
           .getPath
           .toUri

Reply via email to