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 5bcdd3fb3c [common] Resolve the atomic rename under the Kerberos 
FileSystem wrapper (#9652)
5bcdd3fb3c is described below

commit 5bcdd3fb3cb140384dde1dad9a971e69844df060
Author: YangJie <[email protected]>
AuthorDate: Fri Sep 11 03:08:50 2026 -0400

    [common] Resolve the atomic rename under the Kerberos FileSystem wrapper 
(#9652)
---
 .../org/apache/paimon/fs/hadoop/HadoopFileIO.java  |  54 ++++++++-
 .../paimon/fs/hadoop/HadoopSecuredFileSystem.java  |  15 +++
 .../fs/hadoop/HadoopSecuredFileSystemTest.java     | 127 +++++++++++++++++++++
 3 files changed, 191 insertions(+), 5 deletions(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/fs/hadoop/HadoopFileIO.java 
b/paimon-common/src/main/java/org/apache/paimon/fs/hadoop/HadoopFileIO.java
index 3ff241d6c8..3114baad2e 100644
--- a/paimon-common/src/main/java/org/apache/paimon/fs/hadoop/HadoopFileIO.java
+++ b/paimon-common/src/main/java/org/apache/paimon/fs/hadoop/HadoopFileIO.java
@@ -400,6 +400,15 @@ public class HadoopFileIO implements FileIO, 
HadoopOptionsProvider {
         org.apache.hadoop.fs.Path hadoopDst = path(dst);
         FileSystem fs = getFileSystem(hadoopDst);
 
+        // HadoopSecuredFileSystem cannot override FileSystem's protected 
3-arg rename, so
+        // reflection has to find it on the file system underneath the wrapper.
+        final FileSystem renameTarget;
+        if (fs instanceof HadoopSecuredFileSystem) {
+            renameTarget = ((HadoopSecuredFileSystem) fs).unwrap();
+        } else {
+            renameTarget = fs;
+        }
+
         if (renameMethodRef == null) {
             synchronized (this) {
                 if (renameMethodRef == null) {
@@ -409,7 +418,7 @@ public class HadoopFileIO implements FileIO, 
HadoopOptionsProvider {
                     // DistributedFileSystem and ViewFileSystem override the 
rename method to public
                     // and implement correct renaming
                     try {
-                        method = ReflectionUtils.getMethod(fs.getClass(), 
"rename", 3);
+                        method = 
ReflectionUtils.getMethod(renameTarget.getClass(), "rename", 3);
                     } catch (NoSuchMethodException e) {
                         method = null;
                     }
@@ -435,15 +444,29 @@ public class HadoopFileIO implements FileIO, 
HadoopOptionsProvider {
                 writer.flush();
             }
 
-            renameMethod.invoke(
-                    fs, hadoopTemp, hadoopDst, new Options.Rename[] 
{Options.Rename.OVERWRITE});
+            Options.Rename[] renameOptions = new Options.Rename[] 
{Options.Rename.OVERWRITE};
+            if (fs instanceof HadoopSecuredFileSystem) {
+                // the call has to stay inside the wrapper's doAs, or the 
rename runs as
+                // whoever the current thread is rather than the login user
+                ((HadoopSecuredFileSystem) fs)
+                        .callAsLoginUser(
+                                () -> {
+                                    invokeRename(
+                                            renameMethod,
+                                            renameTarget,
+                                            hadoopTemp,
+                                            hadoopDst,
+                                            renameOptions);
+                                    return null;
+                                });
+            } else {
+                invokeRename(renameMethod, renameTarget, hadoopTemp, 
hadoopDst, renameOptions);
+            }
             renameDone = true;
             // TODO: this is a workaround of HADOOP-16255 - remove this when 
HADOOP-16255 is
             // resolved
             tryRemoveCrcFile(hadoopTemp);
             return true;
-        } catch (InvocationTargetException | IllegalAccessException e) {
-            throw new IOException(e);
         } finally {
             if (!renameDone) {
                 deleteQuietly(tempPath);
@@ -451,6 +474,27 @@ public class HadoopFileIO implements FileIO, 
HadoopOptionsProvider {
         }
     }
 
+    /**
+     * Invokes the reflective 3-arg rename and translates reflective failures 
to {@link
+     * IOException}. Inside {@link HadoopSecuredFileSystem}'s {@code doAs}, an 
escaping {@link
+     * InvocationTargetException} is rewrapped as {@code 
UndeclaredThrowableException} and would
+     * surface as a {@link RuntimeException}, bypassing the {@code 
IOException} retry in {@code
+     * HintFileUtils.commitHint}.
+     */
+    private static void invokeRename(
+            Method renameMethod,
+            FileSystem renameTarget,
+            org.apache.hadoop.fs.Path src,
+            org.apache.hadoop.fs.Path dst,
+            Options.Rename[] renameOptions)
+            throws IOException {
+        try {
+            renameMethod.invoke(renameTarget, src, dst, renameOptions);
+        } catch (InvocationTargetException | IllegalAccessException e) {
+            throw new IOException(e);
+        }
+    }
+
     /** @throws IOException if a fatal exception occurs. Will try to ignore 
most exceptions. */
     @SuppressWarnings("CatchMayIgnoreException")
     private void tryRemoveCrcFile(org.apache.hadoop.fs.Path path) throws 
IOException {
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/fs/hadoop/HadoopSecuredFileSystem.java
 
b/paimon-common/src/main/java/org/apache/paimon/fs/hadoop/HadoopSecuredFileSystem.java
index cbfca1b6d9..45bf32ce87 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/fs/hadoop/HadoopSecuredFileSystem.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/fs/hadoop/HadoopSecuredFileSystem.java
@@ -196,6 +196,21 @@ public class HadoopSecuredFileSystem extends FileSystem {
         }
     }
 
+    /**
+     * The underlying {@link FileSystem} this secured wrapper delegates to. 
Callers that reach past
+     * the wrapper for a method it cannot override, such as {@link 
FileSystem}'s protected
+     * three-argument {@code rename}, have to run the call through {@link 
#callAsLoginUser} so it
+     * still happens as the login user.
+     */
+    public FileSystem unwrap() {
+        return fileSystem;
+    }
+
+    /** Runs the callable as the login user, like every delegating method here 
does. */
+    public <T> T callAsLoginUser(Callable<T> callable) throws IOException {
+        return runSecuredWithIOException(callable);
+    }
+
     public static FileSystem trySecureFileSystem(
             FileSystem fileSystem, Options options, Configuration 
configuration)
             throws IOException {
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/fs/hadoop/HadoopSecuredFileSystemTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/fs/hadoop/HadoopSecuredFileSystemTest.java
index 54de46dc7c..12a267501b 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/fs/hadoop/HadoopSecuredFileSystemTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/fs/hadoop/HadoopSecuredFileSystemTest.java
@@ -22,12 +22,18 @@ import org.apache.paimon.catalog.CatalogContext;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.options.Options;
 
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.RawLocalFileSystem;
+import org.apache.hadoop.security.UserGroupInformation;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import java.io.File;
+import java.io.IOException;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /** Test for {@link HadoopSecuredFileSystem}. */
 public class HadoopSecuredFileSystemTest {
@@ -78,6 +84,127 @@ public class HadoopSecuredFileSystemTest {
                 .isNotInstanceOf(HadoopSecuredFileSystem.class);
     }
 
+    @Test
+    public void testUnwrapAndCallAsLoginUser() throws Exception {
+        // tryAtomicOverwriteViaRename has to reach FileSystem's protected 
3-arg rename on the
+        // file system under the wrapper, and run the call as the login user.
+        HadoopSecuredFileSystem secured = securedFileSystem();
+
+        
assertThat(secured.unwrap()).isNotInstanceOf(HadoopSecuredFileSystem.class);
+        assertThat(secured.<String>callAsLoginUser(() -> 
"ran")).isEqualTo("ran");
+        assertThatThrownBy(
+                        () ->
+                                secured.callAsLoginUser(
+                                        () -> {
+                                            throw new IOException("rename 
failed");
+                                        }))
+                .isInstanceOf(IOException.class)
+                .hasMessage("rename failed");
+    }
+
+    @Test
+    public void testAtomicRenameRunsOnTheDelegateAsTheLoginUser() throws 
Exception {
+        File dir = new File(tmp.toFile(), "atomic");
+        assertThat(dir.mkdirs()).isTrue();
+        Path target = new Path(new File(dir, "LATEST").toURI());
+
+        AtomicRenameFileSystem delegate = new AtomicRenameFileSystem();
+        delegate.initialize(target.toUri(), new Configuration());
+        HadoopFileIO fileIO = new HadoopFileIO(target);
+        Options options = kerberosOptions();
+        fileIO.configure(CatalogContext.create(options));
+        fileIO.setFileSystem(
+                HadoopSecuredFileSystem.trySecureFileSystem(
+                        delegate, options, new Configuration()));
+
+        // Reflection only sees public methods, and the wrapper cannot 
override FileSystem's
+        // protected 3-arg rename, so this only works if the lookup goes to 
the delegate.
+        assertThat(fileIO.tryAtomicOverwriteViaRename(target, 
"content")).isTrue();
+        assertThat(delegate.atomicRenames).isEqualTo(1);
+        assertThat(delegate.renameUser)
+                .isEqualTo(UserGroupInformation.getLoginUser().getUserName());
+        assertThat(fileIO.readFileUtf8(target)).isEqualTo("content");
+    }
+
+    @Test
+    public void testAtomicRenameSurfacesDelegateIOExceptionThroughDoAs() 
throws Exception {
+        File dir = new File(tmp.toFile(), "atomic-failure");
+        assertThat(dir.mkdirs()).isTrue();
+        Path target = new Path(new File(dir, "LATEST").toURI());
+
+        FailingRenameFileSystem delegate = new FailingRenameFileSystem();
+        delegate.initialize(target.toUri(), new Configuration());
+        HadoopFileIO fileIO = new HadoopFileIO(target);
+        Options options = kerberosOptions();
+        fileIO.configure(CatalogContext.create(options));
+        fileIO.setFileSystem(
+                HadoopSecuredFileSystem.trySecureFileSystem(
+                        delegate, options, new Configuration()));
+
+        // Method.invoke wraps the delegate IOException in 
InvocationTargetException, which doAs
+        // would rewrap as UndeclaredThrowableException; without translation 
this surfaces as a
+        // RuntimeException and escapes HintFileUtils.commitHint's IOException 
retry loop.
+        assertThatThrownBy(() -> fileIO.tryAtomicOverwriteViaRename(target, 
"content"))
+                .isInstanceOf(IOException.class);
+    }
+
+    /** A local file system exposing {@link FileSystem}'s 3-arg rename as 
public. */
+    private static class AtomicRenameFileSystem extends RawLocalFileSystem {
+
+        private int atomicRenames;
+        private String renameUser;
+
+        @Override
+        public void rename(
+                org.apache.hadoop.fs.Path src,
+                org.apache.hadoop.fs.Path dst,
+                org.apache.hadoop.fs.Options.Rename... options)
+                throws IOException {
+            atomicRenames++;
+            renameUser = UserGroupInformation.getCurrentUser().getUserName();
+            if (!rename(src, dst)) {
+                throw new IOException("rename failed");
+            }
+        }
+    }
+
+    /** A local file system whose public 3-arg rename always fails with {@link 
IOException}. */
+    private static class FailingRenameFileSystem extends RawLocalFileSystem {
+
+        @Override
+        public void rename(
+                org.apache.hadoop.fs.Path src,
+                org.apache.hadoop.fs.Path dst,
+                org.apache.hadoop.fs.Options.Rename... options)
+                throws IOException {
+            throw new IOException("rename failed");
+        }
+    }
+
+    private Options kerberosOptions() throws IOException {
+        File keytabFile = new File(tmp.toFile(), "k.keytab");
+        if (!keytabFile.exists()) {
+            assertThat(keytabFile.createNewFile()).isTrue();
+        }
+        Options options = new Options();
+        options.set("security.kerberos.login.principal", "test-user");
+        options.set("security.kerberos.login.keytab", 
keytabFile.getAbsolutePath());
+        return options;
+    }
+
+    private HadoopSecuredFileSystem securedFileSystem() throws Exception {
+        File keytabFile = new File(tmp.toFile(), "k.keytab");
+        assertThat(keytabFile.createNewFile()).isTrue();
+        Options options = new Options();
+        options.set("security.kerberos.login.principal", "test-user");
+        options.set("security.kerberos.login.keytab", 
keytabFile.getAbsolutePath());
+
+        org.apache.hadoop.fs.FileSystem fs =
+                createFileIO(options).getFileSystem(new 
org.apache.hadoop.fs.Path("file:///tmp/t"));
+        assertThat(fs).isInstanceOf(HadoopSecuredFileSystem.class);
+        return (HadoopSecuredFileSystem) fs;
+    }
+
     private HadoopFileIO createFileIO(Options options) {
         HadoopFileIO fileIO = new HadoopFileIO(new Path("file:///tmp/test"));
         fileIO.configure(CatalogContext.create(options));

Reply via email to