You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by su...@apache.org on 2026/10/12 06:48:11 UTC
(doris) branch master updated: [fix](fe) Expose Paimon FileIO access failures during catalog creation (#68766)
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 <su...@selectdb.com>
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: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org