This is an automated email from the ASF dual-hosted git repository.
Gabriel39 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 cce55108667 [fix](fe) Bundle Azure io_uring transport dependencies
(#69420)
cce55108667 is described below
commit cce551086679dbdc2c2b244e16643dadab986cd5
Author: Gabriel <[email protected]>
AuthorDate: Thu Oct 22 21:04:37 2026 +0800
[fix](fe) Bundle Azure io_uring transport dependencies (#69420)
### What problem does this PR solve?
Two Azure regression suites fail because the FE Azure filesystem plugin
mixes its own Netty core classes with io_uring transport classes loaded
from the FE host. When Reactor Netty creates the HTTP client's native
event loop, the JVM rejects the incompatible class identities with an
`IoHandlerFactory` loader-constraint violation.
This is a Doris plugin dependency-packaging problem. Both failures share
the same cause; their test assertions and Azure credentials do not need
to change.
### Failing regression cases
Both suites are under
`regression-test/suites/external_table_p2/refactor_catalog_param/`:
| Suite | Source file | Observed failure |
| --- | --- | --- |
| `azure_non_catalog_all_test` | `azure_non_catalog_all_test.groovy` |
FE-side Azure file access fails while the Azure SDK creates its HTTP
event loop. This prevents the suite from completing its Azure
file-access checks. |
| `refactor_storage_backup_restore_azure` |
`backup_restore_azure.groovy` | Azure repository creation encounters the
same class-loading failure. The suite fails before exercising backup and
restore. |
The common failure path is:
```text
Azure SDK HTTP request
-> NettyAsyncHttpClient.sendSync
-> DefaultLoopResources.cacheNativeClientLoops / cacheNativeServerLoops
-> DefaultLoopIOUring.newEventLoopGroup
-> MultiThreadIoEventLoopGroup / IoUringIoHandler
-> LinkageError: loader constraint violation involving
io.netty.channel.IoHandlerFactory
```
The exception occurs during local HTTP transport initialization, before
the affected HTTP request can reach Azure. It is not an Azure
authentication rejection or a regression-test expected-result mismatch.
### Root cause and trigger conditions
The inspected failing artifact is Doris commit `6ca5ef36d8`, with Netty
`4.2.17.Final`, Reactor Netty `1.3.7`, and Azure Core HTTP Netty
`1.16.4`.
1. The Azure plugin bundles Reactor Netty and Netty core, but omits
`netty-transport-classes-io_uring` and the corresponding native
libraries.
2. The FE host does contain the io_uring artifacts. Doris loads
plugin-private classes child-first, then falls back to the parent when a
class is absent from the plugin.
3. Reactor's native transport detection can therefore discover the
host's io_uring implementation. When io_uring is available and selected,
it combines that implementation with the plugin's own Netty event-loop
classes.
4. Java identifies a type by both its name and its defining class
loader. The two copies of `IoHandlerFactory` are incompatible even
though the host and plugin use the same Netty version.
The original directory-loaded artifact produced this class ownership in
the local reproduction:
| Class | Defining loader |
| --- | --- |
| `reactor.netty.resources.DefaultLoopIOUring` | Plugin
`ChildFirstClassLoader` |
| `io.netty.channel.MultiThreadIoEventLoopGroup` | Plugin
`ChildFirstClassLoader` |
| `io.netty.channel.IoHandlerFactory` | Plugin `ChildFirstClassLoader` |
| `io.netty.channel.uring.IoUringIoHandler` | Host `AppClassLoader` |
Directly exercising `DefaultLoopIOUring.newEventLoopGroup` reproduced
the same category of `IoHandlerFactory` linkage failure without
accessing Azure. The exact error wording depends on which loader
resolves the interface first.
### Impact scope
- **Confirmed affected:** FE Azure file access and Azure repository
creation in the two suites above.
- **Potentially affected through the same client:** FE operations that
use `AzureObjStorage` and its Azure SDK HTTP client, including object
listing, metadata lookup, reads, writes, copies, and deletes. These
operations share the transport initialization path; they have not all
been independently reproduced.
- **Environment-dependent trigger:** the directory-loaded Azure plugin
has the incomplete dependency set, the host supplies io_uring, and
Reactor selects that native transport. Environments that use another
transport may not exhibit this failure, which explains why an ordinary
flat-classpath unit test can pass.
- **Boundary:** the evidence establishes an FE Azure plugin failure. It
does not establish the same defect in BE-native Azure clients or other
filesystem providers. This PR changes only Azure dependency packaging
and its test support, not global class-loading policy, SQL semantics, or
credentials.
- **Version boundary:** the affected artifact above is confirmed. The
first introducing commit and the oldest affected release have not been
established; this PR does not claim that every Azure deployment or
release is affected.
### Fix
Bundle these dependencies in the Azure plugin, using the existing Netty
BOM for their versions:
- `netty-transport-classes-io_uring`
- `netty-transport-native-io_uring:linux-x86_64`
- `netty-transport-native-io_uring:linux-aarch_64`
This keeps the io_uring implementation, its Netty interfaces, and native
resources inside the plugin's loader. Bundling both native classifiers
also avoids relying on the host for native resources. No
single-interface parent-first exception or process-wide Netty loading
change is introduced.
Add three isolated-loader tests using the Maven **runtime** dependency
set, excluding dependencies supplied only by the test/provided
classpath:
1. Verify io_uring and its Netty interfaces are defined by the plugin
loader and method signatures resolve to the same interface type.
2. Verify both supported Linux native resources are available in the
plugin and resource lookup does not mix host resources into the result.
3. Initialize host io_uring first, then create a plugin event loop,
execute a task, and await shutdown. Dispose plugin-local Reactor
schedulers as well so they do not retain the test loader.
Regenerate the test runtime-classpath file on partial Maven cache
restores, since tests may execute after cached generation steps and the
file contains checkout-specific dependency paths.
### Validation and remaining coverage
- Reproduced the original directory-plugin class-loader conflict before
the fix without an Azure connection.
- The new tests fail before the dependency fix because io_uring
classes/native resources are absent. After the fix, all **94 Azure
module unit tests pass**, including the three new tests.
- Inspected the actual assembled plugin zip and confirmed it contains
all three dependencies. Loaded that bundle through the real directory
plugin manager: the relevant Netty classes all come from the plugin
loader, and an event-loop task executes successfully.
- FE build/package, final FE Checkstyle, Maven cache-configuration
validation, and `git diff --check` pass. The FE-only local build
bypassed an unrelated preflight check for missing BE native archives;
that temporary build-script adjustment was restored and is not included
in this PR.
- **Local limitation:** the kernel disables io_uring ring creation.
Class-identity and native-resource checks run unconditionally, while
successful event-loop execution exercises automatic fallback on this
host. An io_uring-enabled execution and complete credential-backed runs
of the two original Azure suites have not been validated locally and
require CI coverage.
### Release note
Fix FE Azure file access and repository creation failures caused by
mixed host/plugin Netty classes when io_uring is selected.
### Check List (For Author)
- Test:
- [x] Unit test
- [x] Manual test
- Behavior changed:
- [x] Fix dependency packaging; no SQL or configuration changes.
- Does this need documentation?
- [x] No.
---
fe/.mvn/maven-build-cache-config.xml | 7 ++
fe/fe-filesystem/fe-filesystem-azure/pom.xml | 49 +++++++++
.../azure/AzureNettyClassLoaderTest.java | 111 +++++++++++++++++++++
3 files changed, 167 insertions(+)
diff --git a/fe/.mvn/maven-build-cache-config.xml
b/fe/.mvn/maven-build-cache-config.xml
index 3643d9a32b1..905cc232355 100644
--- a/fe/.mvn/maven-build-cache-config.xml
+++ b/fe/.mvn/maven-build-cache-config.xml
@@ -70,6 +70,13 @@ under the License.
<execId>validate</execId>
</execIds>
</execution>
+ <!-- Partial cache restores may still run plugin isolation
tests. Regenerate their
+ runtime classpath with this checkout's paths instead of
caching the local file. -->
+ <execution artifactId="maven-dependency-plugin">
+ <execIds>
+ <execId>plugin-runtime-classpath</execId>
+ </execIds>
+ </execution>
<!-- The wipe of target/lib that be-java-extensions/pom.xml
binds right before the
copy-dependencies forced below. The two run as a pair or
not at all: a copy
without the wipe deploys the union of every dependency
set the directory has
diff --git a/fe/fe-filesystem/fe-filesystem-azure/pom.xml
b/fe/fe-filesystem/fe-filesystem-azure/pom.xml
index cb76d8c2f09..159e0bb7328 100644
--- a/fe/fe-filesystem/fe-filesystem-azure/pom.xml
+++ b/fe/fe-filesystem/fe-filesystem-azure/pom.xml
@@ -100,11 +100,36 @@ under the License.
<artifactId>azure-identity</artifactId>
</dependency>
+ <!-- Reactor probes optional transports. Keep io_uring in the plugin
so its Netty types and
+ native resources cannot fall back to the host's different
ClassLoader. -->
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-transport-classes-io_uring</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-transport-native-io_uring</artifactId>
+ <classifier>linux-x86_64</classifier>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-transport-native-io_uring</artifactId>
+ <classifier>linux-aarch_64</classifier>
+ <scope>runtime</scope>
+ </dependency>
+
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.doris</groupId>
+ <artifactId>fe-extension-loader</artifactId>
+ <version>${revision}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
@@ -125,6 +150,30 @@ under the License.
<build>
<finalName>doris-fe-filesystem-azure</finalName>
<plugins>
+ <plugin>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>plugin-runtime-classpath</id>
+ <phase>generate-test-resources</phase>
+ <goals>
+ <goal>build-classpath</goal>
+ </goals>
+ <configuration>
+ <includeScope>runtime</includeScope>
+
<outputFile>${project.build.directory}/plugin-runtime-classpath.txt</outputFile>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+
<plugin.runtime.classpath>${project.build.directory}/plugin-runtime-classpath.txt</plugin.runtime.classpath>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
diff --git
a/fe/fe-filesystem/fe-filesystem-azure/src/test/java/org/apache/doris/filesystem/azure/AzureNettyClassLoaderTest.java
b/fe/fe-filesystem/fe-filesystem-azure/src/test/java/org/apache/doris/filesystem/azure/AzureNettyClassLoaderTest.java
new file mode 100644
index 00000000000..bc47093d35b
--- /dev/null
+++
b/fe/fe-filesystem/fe-filesystem-azure/src/test/java/org/apache/doris/filesystem/azure/AzureNettyClassLoaderTest.java
@@ -0,0 +1,111 @@
+// 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.filesystem.azure;
+
+import org.apache.doris.extension.loader.ChildFirstClassLoader;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+class AzureNettyClassLoaderTest {
+ private static final String IO_HANDLER_FACTORY =
"io.netty.channel.IoHandlerFactory";
+ private static final String URING_HANDLER =
"io.netty.channel.uring.IoUringIoHandler";
+
+ private ChildFirstClassLoader pluginLoader() throws Exception {
+ // Surefire also exposes provided/test dependencies, which can hide a
missing plugin runtime dependency.
+ String classpath =
Files.readString(Path.of(System.getProperty("plugin.runtime.classpath"))).trim();
+ List<URL> urls = new ArrayList<>();
+ for (String entry :
classpath.split(Pattern.quote(File.pathSeparator))) {
+ urls.add(Path.of(entry).toUri().toURL());
+ }
+ return new ChildFirstClassLoader(urls.toArray(new URL[0]),
getClass().getClassLoader(),
+ ChildFirstClassLoader.DEFAULT_PARENT_FIRST_PACKAGES);
+ }
+
+ @Test
+ void ioUringTypesStayInThePluginLoader() throws Exception {
+ try (ChildFirstClassLoader loader = pluginLoader()) {
+ Class<?> factory = Class.forName(IO_HANDLER_FACTORY, false,
loader);
+ Class<?> handler = Class.forName(URING_HANDLER, false, loader);
+ Assertions.assertSame(loader, factory.getClassLoader());
+ Assertions.assertSame(loader, handler.getClassLoader());
+ Assertions.assertSame(loader,
Class.forName("io.netty.channel.MultiThreadIoEventLoopGroup", false, loader)
+ .getClassLoader());
+ // Resolving the signature must use the plugin's interface even
when the host also carries Netty.
+ Assertions.assertSame(factory,
handler.getMethod("newFactory").getReturnType());
+ }
+ }
+
+ @Test
+ void nativeLibrariesAreAvailableWithoutTheHost() throws Exception {
+ try (ChildFirstClassLoader loader = pluginLoader()) {
+ for (String architecture : List.of("x86_64", "aarch_64")) {
+ String name =
"META-INF/native/libnetty_transport_native_io_uring42_" + architecture + ".so";
+ URL resource = loader.findResource(name);
+ Assertions.assertNotNull(resource, "Missing plugin native
library: " + name);
+ Assertions.assertEquals(List.of(resource),
Collections.list(loader.getResources(name)));
+ }
+ }
+ }
+
+ @Test
+ void nativeSelectionWorksAfterHostInitialization() throws Exception {
+ // Initialize the host first to cover native libraries already loaded
by a different ClassLoader.
+
Class.forName("io.netty.channel.uring.IoUring").getMethod("isAvailable").invoke(null);
+ try (ChildFirstClassLoader loader = pluginLoader()) {
+ ClassLoader previous =
Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(loader);
+ try {
+ Class<?> loops =
Class.forName("reactor.netty.resources.LoopResources", true, loader);
+ Object resources = loops.getMethod("create", String.class,
int.class, boolean.class)
+ .invoke(null, "azure-plugin-test", 1, true);
+ try {
+ Object group = loops.getMethod("onClient",
boolean.class).invoke(resources, true);
+ Assertions.assertSame(loader,
group.getClass().getClassLoader());
+ Assertions.assertEquals("executed", ((ExecutorService)
group)
+ .submit(() -> "executed").get(5,
TimeUnit.SECONDS));
+ } finally {
+ try {
+ Object completion = loops.getMethod("disposeLater",
Duration.class, Duration.class)
+ .invoke(resources, Duration.ZERO,
Duration.ofSeconds(5));
+ Class.forName("reactor.core.publisher.Mono", true,
loader)
+ .getMethod("block",
Duration.class).invoke(completion, Duration.ofSeconds(10));
+ } finally {
+ // The disposal timeout initializes plugin-local
schedulers that otherwise retain the loader.
+ Class.forName("reactor.core.scheduler.Schedulers",
true, loader)
+ .getMethod("shutdownNow").invoke(null);
+ }
+ }
+ } finally {
+ Thread.currentThread().setContextClassLoader(previous);
+ }
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]