github-actions[bot] commented on code in PR #67770:
URL: https://github.com/apache/doris/pull/67770#discussion_r3978332587
##########
build.sh:
##########
@@ -1181,6 +1185,29 @@ if [[ "${BUILD_FE}" -eq 1 ]]; then
mkdir -p "${DORIS_OUTPUT}/fe/plugins/hadoop_conf/"
mkdir -p "${DORIS_OUTPUT}/fe/plugins/java_extensions/"
+ # Shared library bundles: libraries that every plugin needing them must
load from ONE place,
+ # because their classes inherit across jars and their static state
(FileSystem.CACHE, the UGI
+ # login, the FileSystem service registry) is what makes two plugins agree
about one cluster.
+ # SharedLibraryLayer turns this directory into the parent classloader of
every filesystem and
+ # connector plugin. Deliberately at plugins/ level and not under
plugins/filesystem or
+ # plugins/connector: DirectoryPluginRuntimeManager treats every direct
subdirectory of those as
+ # a plugin, and would report this one as a plugin that failed to load.
+ # NOTE: plugins/shared/ is NOT added to the FE CLASSPATH - it is loaded by
FE, not by the JVM
+ # launcher. See bin/start_fe.sh.
+ SHARED_LIB_DIR="${DORIS_OUTPUT}/fe/plugins/shared"
+ mkdir -p "${SHARED_LIB_DIR}"
+
HADOOP_RUNTIME_ZIP="${DORIS_HOME}/fe/fe-hadoop-runtime/target/doris-fe-hadoop-runtime.zip"
+ if [[ -f "${HADOOP_RUNTIME_ZIP}" ]]; then
+ # Same rule as the plugin directories: unzip -o overwrites but never
removes, so a version
+ # bump would leave both copies of every versioned jar here and the
layer would bind whichever
+ # the sorted URL order reached first. Clear what the zip owns and
unpack fresh.
+ rm -rf "${SHARED_LIB_DIR}/hadoop/lib"
+ rm -f "${SHARED_LIB_DIR}/hadoop"/*.jar
+ mkdir -p "${SHARED_LIB_DIR}/hadoop"
+ unzip -q -o "${HADOOP_RUNTIME_ZIP}" -d "${SHARED_LIB_DIR}/hadoop/"
Review Comment:
[P2] Avoid shipping the shared bundle before it has a consumer
On this head, `SharedLibraryLayer.resolve()` has no production call site.
`ConnectorPluginManager` and `FileSystemPluginManager` still pass their
application classloader directly to `DirectoryPluginRuntimeManager`, and `Env`
scans only `plugins/connector` and `plugins/filesystem`. This line therefore
adds a 29-jar bundle to every FE output alongside the unchanged kernel/plugin
copies, but no runtime path can use it. Please either wire the shared parent in
this change, with a production-topology test, or defer building and unpacking
the bundle until the consumer lands.
##########
fe/fe-core/pom.xml:
##########
@@ -743,6 +770,22 @@ under the License.
<artifactId>hadoop-aws</artifactId>
</dependency>
+ <!-- Compiled against directly, but reaching fe-core only as a hadoop
transitive:
+ metrics-core (com.codahale.metrics) via hadoop-auth, carrying the
whole metric layer -
+ MetricRepo, the MetricVisitors, HistogramMetric, CloudMetrics,
SqlBlockRule - and
+ bcprov-jdk18on (org.bouncycastle.util) via hadoop-common, used by
TableScanParams.
+ Declared here so that neither is a passenger of a dependency that
says nothing about
+ them, and so that dropping hadoop from the kernel does not
silently take them along.
+ Both are already on this classpath at these versions; no jar is
added to fe/lib. -->
+ <dependency>
+ <groupId>io.dropwizard.metrics</groupId>
+ <artifactId>metrics-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk18on</artifactId>
Review Comment:
[P1] Account for this dependency in the required license check
The exact-head `dependency-review` job fails on this newly direct
`org.bouncycastle:bcprov-jdk18on` dependency as `LicenseRef-bad-non-standard`.
Doris already records Bouncy Castle under MIT in
`dist/licenses/LICENSE.bouncycastle.txt` and `dist/NOTICE-dist.txt`, but
`.github/workflows/third_party_review.yml` has no package-specific exception
for this coordinate. Please add the narrow exception, or otherwise make the
approved license metadata visible, so the required gate can pass.
##########
fe/fe-extension-loader/src/main/java/org/apache/doris/extension/loader/SharedLibraryLayer.java:
##########
@@ -0,0 +1,157 @@
+// 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.extension.loader;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * A library layer shared by every plugin classloader of every family.
+ *
+ * <p>Some libraries cannot be bundled per plugin. A library whose classes
inherit from one another
+ * across jars has to be loaded once or the JVM refuses the link; one that
holds process-wide static
+ * state (a client cache, a login context, a first-caller-wins registry) has
to be loaded once or two
+ * plugins silently stop sharing it; one with a JNI native image can only be
bound to a single
+ * classloader per process. Hadoop is all three at once. Giving each plugin
its own copy is therefore
+ * not "the same thing, duplicated" - it changes behavior.
+ *
+ * <p>So such libraries are installed once, under a shared root, and this
class turns that root into
+ * a single classloader that is then the PARENT of every plugin classloader:
+ *
+ * <pre>
+ * app classloader (fe/lib)
+ * └── shared library layer <- plugins/shared/<bundle>/*.jar
+ lib/*.jar
+ * ├── filesystem plugin (child-first)
+ * ├── connector plugin (child-first)
+ * └── ...
+ * </pre>
+ *
+ * <p>The layer is itself child-first with only the mandatory parent-first
prefixes, so a bundle uses
+ * its own dependencies where it has them and falls back to fe/lib otherwise;
logging stays the
+ * kernel's, which keeps the bundle's output in fe.log. Plugins reach the
bundle by the ordinary
+ * child-first fallback: a plugin that carries its own copy of the library
keeps using it and is
+ * simply not sharing, which is the pre-existing behavior for a third-party
plugin.
+ *
+ * <h2>Layout</h2>
+ *
+ * <p>{@code <root>/<bundle>/*.jar} then {@code <root>/<bundle>/lib/*.jar},
bundles in name order -
+ * the same convention {@link DirectoryPluginRuntimeManager} uses for a plugin
directory. Root jars
+ * before {@code lib/} is what lets a bundle ship a patched copy of a class
that also exists in one
+ * of its dependency jars: put the patch in the bundle root and the stock jar
under {@code lib/}.
+ *
+ * <h2>Absence is normal</h2>
+ *
+ * <p>A root that does not exist, or holds no jar, yields {@code parent}
unchanged, so a deployment
+ * that installs no bundle keeps exactly the classloader graph it had before
this class existed. An
+ * unreadable root is not that case and is raised, because degrading it to
"nothing installed" turns
+ * a permissions mistake into a missing-class failure much later and somewhere
else.
+ *
+ * <h2>One instance per root</h2>
+ *
+ * <p>Memoized on the resolved root, for the whole process. This is the point
of the class rather
+ * than an optimization: two callers that got two layers over the same jars
would get two copies of
+ * every class in them, which is the situation the layer exists to prevent.
The first caller's parent
+ * is the one that ends up in the graph - there is one FE app classloader, so
every caller passes the
+ * same one. The layer is never closed; it lives as long as the process, like
the app classloader.
+ */
+public final class SharedLibraryLayer {
+
+ private static final ConcurrentMap<Path, ClassLoader> LAYERS = new
ConcurrentHashMap<>();
+
+ private SharedLibraryLayer() {
+ }
+
+ /**
+ * Returns the classloader for the shared bundles under {@code root}, or
{@code parent} itself
+ * when there is nothing to install there.
+ *
+ * @param root the shared library root; null is treated as "not configured"
+ * @param parent the classloader the layer delegates to, normally the FE
app classloader
+ * @throws UncheckedIOException if the root exists but cannot be read
+ */
+ public static ClassLoader resolve(Path root, ClassLoader parent) {
+ Objects.requireNonNull(parent, "parent");
+ if (root == null) {
+ return parent;
+ }
+ Path resolved = root.toAbsolutePath().normalize();
Review Comment:
[P2] Canonicalize the physical root before memoizing
`toAbsolutePath().normalize()` removes lexical `..` components but does not
resolve symlinks. If one physical bundle root is reached through both a release
symlink and its real path, the unequal keys each create a child-first loader
over the same jars, producing duplicate Hadoop class identity and static state
-- the exact condition this layer is meant to prevent. Canonicalize an existing
root with `toRealPath()` before `computeIfAbsent`, preserving the documented
I/O failure behavior, and add a symlink-alias identity test.
##########
fe/fe-extension-loader/src/main/java/org/apache/doris/extension/loader/DirectoryPluginRuntimeManager.java:
##########
@@ -523,6 +534,38 @@ private String discoverSingleFactoryClassName(Class<F>
factoryType, ClassLoader
return classNames.get(0);
}
+ /**
+ * Turns a load failure caused by an absent class into an actionable
sentence, or "" for any other
+ * failure. A plugin misses a class either because it does not bundle it
or because it expected to
+ * inherit it from the layer its classloader delegates to, and the message
must not leave the reader
+ * guessing which - a shared library bundle that was never installed looks
exactly like a broken
+ * plugin jar otherwise.
+ */
+ private static String missingClassAdvice(Throwable failure) {
+ String missing = missingClassName(failure);
+ if (missing == null) {
+ return "";
+ }
+ return ". The class " + missing + " is in neither this plugin's own
jars nor its parent"
+ + " classloader; if it is meant to come from a shared library
bundle, check that the"
+ + " bundle is installed under the FE shared library root";
+ }
+
+ /** The absent class named by a NoClassDefFoundError /
ClassNotFoundException anywhere in the chain. */
+ private static String missingClassName(Throwable failure) {
+ for (Throwable t = failure; t != null; t = t.getCause() == t ? null :
t.getCause()) {
+ if (t instanceof NoClassDefFoundError || t instanceof
ClassNotFoundException) {
Review Comment:
[P2] Do not report initializer failures as a missing class named `Could`
After a class initializer has failed, a later JVM linkage attempt reports
`NoClassDefFoundError: Could not initialize class X`. This branch accepts that
error and line 562 returns its first whitespace token, so the new advice says
that a class named `Could` is absent and recommends checking the shared bundle
even though the class was found. Recognize the initialization-failure form
before extracting a binary name, inspect the cause chain only for a genuine
missing class, and add a two-attempt initializer test.
##########
fe/fe-extension-loader/src/main/java/org/apache/doris/extension/loader/SharedLibraryLayer.java:
##########
@@ -0,0 +1,157 @@
+// 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.extension.loader;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * A library layer shared by every plugin classloader of every family.
+ *
+ * <p>Some libraries cannot be bundled per plugin. A library whose classes
inherit from one another
+ * across jars has to be loaded once or the JVM refuses the link; one that
holds process-wide static
+ * state (a client cache, a login context, a first-caller-wins registry) has
to be loaded once or two
+ * plugins silently stop sharing it; one with a JNI native image can only be
bound to a single
+ * classloader per process. Hadoop is all three at once. Giving each plugin
its own copy is therefore
+ * not "the same thing, duplicated" - it changes behavior.
+ *
+ * <p>So such libraries are installed once, under a shared root, and this
class turns that root into
+ * a single classloader that is then the PARENT of every plugin classloader:
+ *
+ * <pre>
+ * app classloader (fe/lib)
+ * └── shared library layer <- plugins/shared/<bundle>/*.jar
+ lib/*.jar
+ * ├── filesystem plugin (child-first)
+ * ├── connector plugin (child-first)
+ * └── ...
+ * </pre>
+ *
+ * <p>The layer is itself child-first with only the mandatory parent-first
prefixes, so a bundle uses
+ * its own dependencies where it has them and falls back to fe/lib otherwise;
logging stays the
+ * kernel's, which keeps the bundle's output in fe.log. Plugins reach the
bundle by the ordinary
+ * child-first fallback: a plugin that carries its own copy of the library
keeps using it and is
+ * simply not sharing, which is the pre-existing behavior for a third-party
plugin.
+ *
+ * <h2>Layout</h2>
+ *
+ * <p>{@code <root>/<bundle>/*.jar} then {@code <root>/<bundle>/lib/*.jar},
bundles in name order -
+ * the same convention {@link DirectoryPluginRuntimeManager} uses for a plugin
directory. Root jars
+ * before {@code lib/} is what lets a bundle ship a patched copy of a class
that also exists in one
+ * of its dependency jars: put the patch in the bundle root and the stock jar
under {@code lib/}.
+ *
+ * <h2>Absence is normal</h2>
+ *
+ * <p>A root that does not exist, or holds no jar, yields {@code parent}
unchanged, so a deployment
+ * that installs no bundle keeps exactly the classloader graph it had before
this class existed. An
+ * unreadable root is not that case and is raised, because degrading it to
"nothing installed" turns
+ * a permissions mistake into a missing-class failure much later and somewhere
else.
+ *
+ * <h2>One instance per root</h2>
+ *
+ * <p>Memoized on the resolved root, for the whole process. This is the point
of the class rather
+ * than an optimization: two callers that got two layers over the same jars
would get two copies of
+ * every class in them, which is the situation the layer exists to prevent.
The first caller's parent
+ * is the one that ends up in the graph - there is one FE app classloader, so
every caller passes the
+ * same one. The layer is never closed; it lives as long as the process, like
the app classloader.
+ */
+public final class SharedLibraryLayer {
+
+ private static final ConcurrentMap<Path, ClassLoader> LAYERS = new
ConcurrentHashMap<>();
+
+ private SharedLibraryLayer() {
+ }
+
+ /**
+ * Returns the classloader for the shared bundles under {@code root}, or
{@code parent} itself
+ * when there is nothing to install there.
+ *
+ * @param root the shared library root; null is treated as "not configured"
+ * @param parent the classloader the layer delegates to, normally the FE
app classloader
+ * @throws UncheckedIOException if the root exists but cannot be read
+ */
+ public static ClassLoader resolve(Path root, ClassLoader parent) {
+ Objects.requireNonNull(parent, "parent");
+ if (root == null) {
+ return parent;
+ }
+ Path resolved = root.toAbsolutePath().normalize();
+ return LAYERS.computeIfAbsent(resolved, dir -> build(dir, parent));
+ }
+
+ private static ClassLoader build(Path root, ClassLoader parent) {
+ if (!Files.isDirectory(root)) {
Review Comment:
[P2] Preserve attribute I/O failures instead of caching an empty layer
`Files.isDirectory` and `Files.isRegularFile` return `false` when attributes
cannot be read. A root that can be listed but whose children cannot be searched
can therefore have every bundle entry silently filtered out; `build()` returns
`parent`, and `computeIfAbsent` caches that false no-op for the process
lifetime even after permissions are repaired. This contradicts the documented
fail-loud behavior for an unreadable root. Use attribute reads that preserve
`IOException` for the root, bundle, `lib`, and jar probes, wrap failures as
documented, and add a permission-failure/non-memoization test.
##########
fe/fe-core/src/test/java/org/apache/doris/common/FeCoreHasNoHadoopClassesTest.java:
##########
@@ -0,0 +1,127 @@
+// 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.common;
+
+import org.apache.doris.catalog.Env;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * The FE kernel must carry no Hadoop.
+ *
+ * <p>Hadoop belongs to the filesystem and connector plugins, which load it
through their own
+ * classloaders; fe-core itself must not reference a single {@code
org.apache.hadoop} type. Removing
+ * the jars from {@code fe/lib} is only half of that — the other half is that
no fe-core class names
+ * one, because javac needs nothing but the class on the compile classpath and
a reference that
+ * compiles today keeps compiling after the jar leaves, failing instead at
runtime with
+ * {@code NoClassDefFoundError} on whichever code path first reaches it.
+ *
+ * <p>This scans fe-core's own compiled output rather than its source: a
constant-pool entry is the
+ * form a dependency actually takes, so it catches the reference no import
reveals — a supertype
+ * inherited from another module, a type that only appears in a method
descriptor, a synthetic
+ * bridge. The needles are in JVM internal form ({@code org/apache/hadoop/}),
which is how class
+ * references and field/method descriptors are spelled; the dotted spelling is
deliberately NOT
+ * matched, because {@link org.apache.doris.connector.ConnectorPluginManager}
and
+ * {@link org.apache.doris.fs.FileSystemPluginManager} legitimately hold
{@code "org.apache.hadoop."}
+ * as a parent-first classloader-policy string.
+ *
+ * <p>{@code org/apache/doris/kerberos/} is a needle too. That module splits
into a Hadoop half
+ * (UGI logins, {@code AuthenticationConfig(Configuration)}) and a Hadoop-free
remainder, and its
+ * Hadoop half would sit in {@code fe/lib} as classes that can never link
there. fe-core takes the
+ * one interface it needs, {@code ExecutionAuthenticator}, from fe-foundation
instead, and
+ * {@code fe-core/pom.xml} no longer depends on fe-kerberos at all.
+ */
+public class FeCoreHasNoHadoopClassesTest {
+
+ /** JVM internal form: how class references and descriptors are spelled in
the constant pool. */
+ private static final List<String> FORBIDDEN = Arrays.asList(
Review Comment:
[P2] Cover reflective dotted names in the Hadoop-free guard
This raw-byte scan rejects only JVM-internal slash names. A reflective
target such as `Class.forName("org.apache.hadoop.conf.Configuration")` or
`loadClass("org.apache.doris.kerberos.AuthenticationConfig")` is stored as a
dotted `CONSTANT_String`, passes this test, and can then fail at runtime after
those kernel jars disappear. fe-core already has reflective loading paths.
Please reject the dotted prefixes too, narrowly allowlist the two legitimate
parent-first policy owners, and add a reflective negative-control fixture.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]