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

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


The following commit(s) were added to refs/heads/master by this push:
     new 52764b6e004 [fix](fe) Expose Paimon FileIO access failures during 
catalog creation (#68766)
52764b6e004 is described below

commit 52764b6e004dd31bf51a89364bc46aa6a7e32712
Author: Socrates <[email protected]>
AuthorDate: Mon Oct 12 14:48:02 2026 +0800

    [fix](fe) Expose Paimon FileIO access failures during catalog creation 
(#68766)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    An OBS access timeout during Paimon catalog initialization can be
    reported as
    `Could not find a file io implementation for scheme 'obs'`. Paimon's
    `FileIO.get`
    stores the failed access probes as suppressed exceptions on an
    `UnsupportedSchemeException`, while Doris previously displayed only its
    message.
    
    Expose the underlying access failures in the catalog-creation error and
    use the
    first failed probe as the cause so root-cause consumers, including `SHOW
    CATALOGS`,
    can report the actual error. Retain the original aggregate as a
    suppressed
    exception, preserving every attempted FileIO failure in the diagnostic
    stack.
    Other catalog errors retain their existing wrapping.
    
    Add six offline tests covering the real Paimon/Hadoop fallback with a
    simulated
    OBS timeout, nested wrappers, multiple failures, errors without
    messages,
    missing filesystem implementations, and unrelated suppressed exceptions.
    
    ### Release note
    
    Paimon catalog initialization reports underlying filesystem access
    failures
    instead of only a misleading missing-FileIO message.
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [x] Unit Test
        - [ ] Manual test
        - [ ] No need to test or manual test.
    
    Offline Paimon connector unit tests, Checkstyle, and plugin packaging
    passed.
      No live OBS regression was run.
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. Catalog error messages and their cause chains expose FileIO
    access failures; storage access and retry behavior are unchanged.
    
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../doris/connector/paimon/PaimonConnector.java    |   2 +-
 .../connector/paimon/PaimonExceptionUtils.java     |  51 +++++++
 .../paimon/PaimonCatalogCreationExceptionTest.java | 151 +++++++++++++++++++++
 3 files changed, 203 insertions(+), 1 deletion(-)

diff --git 
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonConnector.java
 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonConnector.java
index 967196a550a..094883afb03 100644
--- 
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonConnector.java
+++ 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonConnector.java
@@ -566,7 +566,7 @@ public class PaimonConnector implements Connector {
                         metaCache.hasEnclosingWeightLimit());
             });
         } catch (Exception e) {
-            throw new RuntimeException(failureMessage + " (flavor=" + flavor + 
"): " + e.getMessage(), e);
+            throw PaimonExceptionUtils.catalogCreationFailure(failureMessage + 
" (flavor=" + flavor + ")", e);
         } finally {
             Thread.currentThread().setContextClassLoader(previous);
         }
diff --git 
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonExceptionUtils.java
 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonExceptionUtils.java
new file mode 100644
index 00000000000..93d4fdc5071
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonExceptionUtils.java
@@ -0,0 +1,51 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.paimon;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.paimon.fs.UnsupportedSchemeException;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+final class PaimonExceptionUtils {
+
+    private PaimonExceptionUtils() {
+    }
+
+    static RuntimeException catalogCreationFailure(String prefix, Exception 
failure) {
+        for (Throwable cause : ExceptionUtils.getThrowableList(failure)) {
+            if (cause instanceof UnsupportedSchemeException && 
cause.getSuppressed().length > 0) {
+                // FileIO.get reports failed access probes as an unsupported 
scheme, even when the
+                // implementation exists. The actual IO failures are attached 
as suppressed exceptions.
+                Throwable[] accessFailures = cause.getSuppressed();
+                String details = Arrays.stream(accessFailures)
+                        .map(ExceptionUtils::getRootCauseMessage)
+                        .collect(Collectors.joining("; "));
+                RuntimeException wrapped = new RuntimeException(prefix + ": 
Failed to initialize Paimon FileIO. "
+                        + "Access failures: " + details + ". FileIO 
resolution: " + cause.getMessage(),
+                        accessFailures[0]);
+                // Root-cause consumers (including SHOW CATALOGS) must reach 
an actual access failure.
+                // Retain the original aggregate and all of its failed 
attempts in the diagnostic stack.
+                wrapped.addSuppressed(failure);
+                return wrapped;
+            }
+        }
+        return new RuntimeException(prefix + ": " + failure.getMessage(), 
failure);
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonCatalogCreationExceptionTest.java
 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonCatalogCreationExceptionTest.java
new file mode 100644
index 00000000000..3f1a208275b
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonCatalogCreationExceptionTest.java
@@ -0,0 +1,151 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.paimon;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.RawLocalFileSystem;
+import org.apache.hadoop.fs.UnsupportedFileSystemException;
+import org.apache.paimon.fs.UnsupportedSchemeException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.Map;
+
+class PaimonCatalogCreationExceptionTest {
+
+    private static final String PREFIX = "Failed to create Paimon catalog with 
filesystem metastore "
+            + "(flavor=filesystem)";
+    private static final String RESOLUTION_ERROR = "Could not find a file io 
implementation for scheme 'obs' "
+            + "in the classpath. Hadoop FileSystem also cannot access this 
path 'obs://bucket/warehouse'.";
+
+    @Test
+    void obsAccessTimeoutIsVisibleThroughRealFileIoFallback() throws Exception 
{
+        Map<String, String> properties = Map.of(
+                "paimon.catalog.type", "filesystem",
+                "warehouse", "obs://bucket/warehouse",
+                "fs.obs.impl", FailingObsFileSystem.class.getName(),
+                "fs.obs.impl.disable.cache", "true");
+        ClassLoader previous = Thread.currentThread().getContextClassLoader();
+        try (PaimonConnector connector = new PaimonConnector(properties, new 
RecordingConnectorContext())) {
+            RuntimeException failure = 
Assertions.assertThrows(RuntimeException.class,
+                    () -> connector.getMetadata(null));
+
+            Assertions.assertTrue(failure.getMessage().startsWith(PREFIX), 
failure.getMessage());
+            
Assertions.assertTrue(failure.getMessage().contains("SocketTimeoutException: 
OBS connect timed out"),
+                    failure.getMessage());
+            
Assertions.assertTrue(failure.getMessage().contains("obs://bucket/warehouse"), 
failure.getMessage());
+            Assertions.assertEquals("SocketTimeoutException: OBS connect timed 
out",
+                    ExceptionUtils.getRootCauseMessage(failure));
+            Assertions.assertEquals(1, failure.getSuppressed().length);
+            Assertions.assertInstanceOf(UnsupportedSchemeException.class,
+                    ExceptionUtils.getRootCause(failure.getSuppressed()[0]));
+            Assertions.assertSame(previous, 
Thread.currentThread().getContextClassLoader());
+        }
+    }
+
+    @Test
+    void nestedFileIoFailureExposesAllAccessAttemptsAndRetainsOriginalStack() {
+        SocketTimeoutException timeout = new SocketTimeoutException("OBS 
connect timed out");
+        IOException access = new IOException("Failed to access 
obs://bucket/warehouse", timeout);
+        UnknownHostException unknownHost = new 
UnknownHostException("obs.example.invalid");
+        UnsupportedSchemeException resolution = new 
UnsupportedSchemeException(RESOLUTION_ERROR);
+        resolution.addSuppressed(access);
+        resolution.addSuppressed(unknownHost);
+        RuntimeException original = new RuntimeException("Catalog factory 
failed", resolution);
+
+        RuntimeException failure = 
PaimonExceptionUtils.catalogCreationFailure(PREFIX, original);
+
+        
Assertions.assertTrue(failure.getMessage().contains("SocketTimeoutException: 
OBS connect timed out"));
+        
Assertions.assertTrue(failure.getMessage().contains("UnknownHostException: 
obs.example.invalid"));
+        Assertions.assertTrue(failure.getMessage().contains(RESOLUTION_ERROR));
+        Assertions.assertSame(access, failure.getCause());
+        Assertions.assertSame(original, failure.getSuppressed()[0]);
+        Assertions.assertEquals("SocketTimeoutException: OBS connect timed 
out",
+                ExceptionUtils.getRootCauseMessage(failure));
+        StringWriter stack = new StringWriter();
+        failure.printStackTrace(new PrintWriter(stack));
+        Assertions.assertTrue(stack.toString().contains("Catalog factory 
failed"));
+        Assertions.assertTrue(stack.toString().contains("UnknownHostException: 
obs.example.invalid"));
+    }
+
+    @Test
+    void accessFailureWithoutMessageStillShowsItsType() {
+        UnsupportedSchemeException resolution = new 
UnsupportedSchemeException(RESOLUTION_ERROR);
+        resolution.addSuppressed(new IOException(new 
SocketTimeoutException()));
+
+        RuntimeException failure = 
PaimonExceptionUtils.catalogCreationFailure(PREFIX, resolution);
+
+        
Assertions.assertTrue(failure.getMessage().contains("SocketTimeoutException"), 
failure.getMessage());
+    }
+
+    @Test
+    void unsupportedSchemeWithoutAccessFailuresPreservesOriginalError() {
+        UnsupportedSchemeException original = new 
UnsupportedSchemeException(RESOLUTION_ERROR);
+
+        RuntimeException failure = 
PaimonExceptionUtils.catalogCreationFailure(PREFIX, original);
+
+        Assertions.assertEquals(PREFIX + ": " + RESOLUTION_ERROR, 
failure.getMessage());
+        Assertions.assertSame(original, failure.getCause());
+        Assertions.assertEquals(0, failure.getSuppressed().length);
+    }
+
+    @Test
+    void missingHadoopImplementationStillReportsUnsupportedFileSystem() {
+        UnsupportedFileSystemException missing = new 
UnsupportedFileSystemException("No FileSystem for scheme obs");
+        UnsupportedSchemeException resolution = new 
UnsupportedSchemeException(RESOLUTION_ERROR);
+        resolution.addSuppressed(missing);
+
+        RuntimeException failure = 
PaimonExceptionUtils.catalogCreationFailure(PREFIX, resolution);
+
+        Assertions.assertTrue(failure.getMessage().contains(
+                "UnsupportedFileSystemException: No FileSystem for scheme 
obs"));
+        Assertions.assertSame(missing, failure.getCause());
+        Assertions.assertSame(resolution, failure.getSuppressed()[0]);
+    }
+
+    @Test
+    void unrelatedSuppressedExceptionDoesNotReplaceCatalogFailure() {
+        IllegalArgumentException original = new 
IllegalArgumentException("Invalid catalog option");
+        original.addSuppressed(new IOException("close failed"));
+
+        RuntimeException failure = 
PaimonExceptionUtils.catalogCreationFailure(PREFIX, original);
+
+        Assertions.assertEquals(PREFIX + ": Invalid catalog option", 
failure.getMessage());
+        Assertions.assertSame(original, failure.getCause());
+    }
+
+    /** Exercises Paimon's Hadoop fallback without connecting to an OBS 
service. */
+    public static class FailingObsFileSystem extends RawLocalFileSystem {
+        @Override
+        public URI getUri() {
+            return URI.create("obs://bucket");
+        }
+
+        @Override
+        public boolean exists(Path path) throws IOException {
+            throw new IOException("Failed to access " + path, new 
SocketTimeoutException("OBS connect timed out"));
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to