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]