OIiveirra commented on code in PR #69527:
URL: https://github.com/apache/doris/pull/69527#discussion_r4369659017
##########
fe/be-java-extensions/hadoop-deps/src/main/java/org/apache/hadoop/fs/FileSystem.java:
##########
@@ -3622,13 +3633,628 @@ public static Class<? extends FileSystem>
getFileSystemClass(String scheme,
* @return the initialized filesystem.
* @throws IOException problems loading or initializing the FileSystem
*/
+ // DORIS-PATCH: TVF policy is checked against the frozen configuration used
by the consumer.
+ public static int getDorisTvfGuardVersion() {
+ return 1;
+ }
+
+ public static final class TvfGuard {
+ public static final String POLICY_KEY = "fs.doris.tvf.remote.paths";
+ public static final String NATIVE_POLICY = "native";
+ public static final String JAVA_READ_POLICY = "java-read";
+ public static final String ROOT_KEY = "fs.doris.tvf.root";
+ public static final String PATH_KEY = "fs.doris.tvf.path";
+ public static final String CONTEXT_KEY = "fs.doris.tvf.context";
+ private static final ThreadLocal<Context> LAST_NATIVE_CONTEXT = new
ThreadLocal<>();
+
+ private TvfGuard() {
+ }
+
+ public static final class Rule {
+ private final String filesystem;
+ private final String path;
+ private final boolean descendants;
+
+ public Rule(String filesystem, String path, boolean descendants) {
+ this.filesystem = filesystem;
+ this.path = path;
+ this.descendants = descendants;
+ }
+ }
+
+ public static String encode(List<Rule> rules) {
+ if (FileSystem.getDorisTvfGuardVersion() != 1) {
+ throw new IllegalStateException("TVF Hadoop runtime policy version
mismatch");
+ }
+ try {
+ java.io.ByteArrayOutputStream bytes = new
java.io.ByteArrayOutputStream();
+ java.io.DataOutputStream out = new java.io.DataOutputStream(bytes);
+ out.writeInt(1);
+ out.writeInt(rules.size());
+ for (Rule rule : rules) {
+ out.writeUTF(rule.filesystem);
+ out.writeUTF(rule.path);
+ out.writeBoolean(rule.descendants);
+ }
+ out.flush();
+ return
java.util.Base64.getEncoder().encodeToString(bytes.toByteArray());
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Cannot encode TVF Hadoop route
policy", e);
+ }
+ }
+
+ private static IOException rejected() {
+ return new IOException("Hadoop TVF requires a proven remote filesystem
route. "
+ + "Use local() with LOCAL TVF privileges and secure-path
restrictions.");
+ }
+
+ private static Configuration freeze(Configuration conf) throws IOException
{
+ // Service providers (notably HDFS) register their default/site
resources on first load.
+ // Load them before copying raw values, as an ordinary FileSystem lookup
does.
+ if (!FILE_SYSTEMS_LOADED) {
+ loadFileSystems();
+ }
+ if (conf instanceof FrozenConfiguration) {
+ return conf;
+ }
+ return new FrozenConfiguration(conf);
+ }
+
+ private static Configuration prepare(Configuration conf) throws
IOException {
+ String policy = conf.getRaw(POLICY_KEY);
+ if (policy == null || policy.isEmpty()) {
+ return conf;
+ }
+ Configuration frozen = freeze(conf);
+ Context context = ((FrozenConfiguration) frozen).context;
+ if (NATIVE_POLICY.equals(policy)) {
+ context.checkNative(frozen.getRaw(ROOT_KEY), frozen.getRaw(PATH_KEY));
+ } else if (JAVA_READ_POLICY.equals(policy)) {
+ context.checkJava(new Path(frozen.getRaw(PATH_KEY)));
+ } else {
+ context.checkRules(policy);
+ }
+ return frozen;
+ }
+
+ public static void check(URI ignored, Configuration conf) throws
IOException {
+ prepare(conf);
+ }
+
+ public static void clearNativeContext() {
+ LAST_NATIVE_CONTEXT.remove();
+ }
+
+ public static Context takeNativeContext() {
+ Context result = LAST_NATIVE_CONTEXT.get();
+ LAST_NATIVE_CONTEXT.remove();
+ return result;
+ }
+
+ public static Configuration prepareJavaRead(Configuration conf, Path path,
String marker) throws IOException {
+ if (marker == null || marker.isEmpty()) {
+ return conf;
+ }
+ if (!NATIVE_POLICY.equals(marker)) {
+ throw rejected();
+ }
+ Configuration request = new Configuration(conf);
+ request.set(POLICY_KEY, JAVA_READ_POLICY);
+ request.set(ROOT_KEY, path.toUri().toString());
+ request.set(PATH_KEY, path.toString());
+ return prepare(request);
+ }
+
+ public static void checkJavaRead(FileSystem fs, Path path, String marker)
throws IOException {
+ if (marker == null || marker.isEmpty()) {
+ return;
+ }
+ if (!NATIVE_POLICY.equals(marker)) {
+ throw rejected();
+ }
+ // The marker is request-owned, never inferred from an unmarked cached
filesystem.
+ Configuration conf = fs.getConf();
+ if (JAVA_READ_POLICY.equals(conf.getRaw(POLICY_KEY))) {
+ // S3A derives a Configuration for bucket options. Its copy preserves
our internal
+ // marker and raw snapshot values, but not the private Configuration
subclass.
+ ((FrozenConfiguration) freeze(conf)).context.checkJava(path);
+ } else {
+ throw rejected();
+ }
+ }
+
+ private static final class FrozenConfiguration extends Configuration {
+ private final Context context;
+
+ private FrozenConfiguration(Configuration source) throws IOException {
+ super(false);
+ setClassLoader(source.getClassLoader());
+ java.util.SortedMap<String, String> values = new java.util.TreeMap<>();
+ for (Map.Entry<String, String> entry : source) {
+ String key = entry.getKey();
+ String value = source.getRaw(key);
+ set(key, value);
+ if (!PATH_KEY.equals(key) && !ROOT_KEY.equals(key) &&
!CONTEXT_KEY.equals(key)) {
+ values.put(key, value);
+ }
+ }
+ try {
+ java.security.MessageDigest digest =
java.security.MessageDigest.getInstance("SHA-256");
+ java.io.DataOutputStream output = new java.io.DataOutputStream(
+ new java.security.DigestOutputStream(new java.io.OutputStream() {
+ @Override
+ public void write(int value) {
+ }
+
+ @Override
+ public void write(byte[] values, int offset, int length) {
+ }
+ }, digest));
+ for (Map.Entry<String, String> entry : values.entrySet()) {
+ for (String value : new String[] {entry.getKey(),
entry.getValue()}) {
+ // Preserve raw Java code units, including unpaired surrogates.
UTF-8 replacement
+ // would give a literal '?' and an unpaired surrogate the same
cache profile.
+ output.writeInt(value.length());
+ output.writeChars(value);
+ }
+ }
+ output.flush();
+ StringBuilder identity = new StringBuilder();
+ for (byte value : digest.digest()) {
+ identity.append(Character.forDigit((value & 255) >>> 4, 16));
+ identity.append(Character.forDigit(value & 15, 16));
+ }
+ set(CONTEXT_KEY, identity.toString());
+ } catch (java.security.NoSuchAlgorithmException e) {
+ throw new IOException("SHA-256 is required for TVF Hadoop cache
isolation");
+ }
+ context = new Context(this);
+ }
+ }
+
+ public static final class Context {
+ private final Configuration conf;
+ private final Map<String, String> routingKeys = new HashMap<>();
+
+ private Context(Configuration conf) {
+ this.conf = conf;
+ for (Map.Entry<String, String> entry : conf) {
+ String key = entry.getKey();
+ if ("fs.defaultFS".equals(key) || "fs.default.name".equals(key)
+ || key.startsWith("fs.viewfs.") || (key.startsWith("fs.") &&
key.endsWith(".impl"))) {
+ routingKeys.put(key, entry.getValue());
+ }
+ }
+ }
+
+ private EffectiveRoute route(boolean descendants) {
+ return new EffectiveRoute(routingKeys, descendants, conf);
+ }
+
+ public String profile() {
+ return conf.getRaw(CONTEXT_KEY);
+ }
+
+ public boolean isDirectRemote() throws IOException {
+ String root = conf.getRaw(ROOT_KEY);
+ if (root == null) {
+ throw rejected();
+ }
+ try {
+ URI uri = nativeUri(root);
+ return route(false).provider(uri.getScheme()) ==
Route.Provider.REMOTE;
+ } catch (RuntimeException | URISyntaxException e) {
+ throw rejected();
+ }
+ }
+
+ private static URI nativeUri(String root) throws URISyntaxException {
+ root = root.trim();
+ if (root.isEmpty() || "default".equals(root)) {
+ throw new URISyntaxException("", "Unproven native filesystem");
+ }
+ return new URI(root.contains("://") ? root : "hdfs://" + root);
+ }
+
+ public void checkNative(String root, String path) throws IOException {
+ if (root == null || path == null || path.isEmpty()) {
+ throw rejected();
+ }
+ try {
+ URI uri = nativeUri(root);
+ String actual = new Path(path).toUri().getPath();
+ route(false).checkRoute(uri, actual, new HashSet<>());
+ } catch (RuntimeException | URISyntaxException e) {
+ throw rejected();
+ }
+ }
+
+ private void checkJava(Path path) throws IOException {
+ try {
+ URI uri = path.toUri();
+ route(false).checkRoute(uri, uri.getPath(), new HashSet<>());
+ } catch (RuntimeException | URISyntaxException e) {
+ throw rejected();
+ }
+ }
+
+ private void checkRules(String policy) throws IOException {
+ try {
+ byte[] bytes = java.util.Base64.getDecoder().decode(policy);
+ java.io.DataInputStream in = new java.io.DataInputStream(new
java.io.ByteArrayInputStream(bytes));
+ int version = in.readInt();
+ int count = in.readInt();
+ if (version != 1 || count <= 0 || count > in.available() / 5) {
+ throw rejected();
+ }
+ for (int i = 0; i < count; i++) {
+ URI filesystem = new URI(in.readUTF());
+ String path = in.readUTF();
+ boolean descendants = in.readBoolean();
+ route(descendants).checkRoute(filesystem, path, new HashSet<>());
+ }
+ if (in.available() != 0) {
+ throw rejected();
+ }
+ } catch (RuntimeException | URISyntaxException e) {
+ throw rejected();
+ }
+ }
+ }
+
+ private static final class EffectiveRoute extends Route {
+ private final Configuration conf;
+
+ private EffectiveRoute(Map<String, String> properties, boolean
descendants, Configuration conf) {
+ super(properties, descendants);
+ this.conf = conf;
+ }
+
+ @Override
+ protected String property(String key) {
+ return conf.get(key);
+ }
+
+ @Override
+ protected Provider provider(String scheme) {
+ if ("file".equalsIgnoreCase(scheme)) {
+ return Provider.LOCAL;
+ }
+ try {
+ String implementation = FileSystem.getFileSystemClass(scheme,
conf).getName();
+ if (Arrays.asList("io.juicefs.JuiceFileSystem",
"org.apache.hadoop.fs.ozone.OzoneFileSystem",
+ "org.apache.hadoop.fs.ozone.RootedOzoneFileSystem",
"org.apache.hadoop.fs.cosn.CosNFileSystem",
+
"com.qcloud.chdfs.fs.CHDFSHadoopFileSystemAdapter").contains(implementation)) {
+ return Provider.REMOTE;
+ }
+ return implementationProvider(implementation);
+ } catch (IOException | RuntimeException e) {
+ return Provider.UNKNOWN;
+ }
+ }
+ }
+
+ public static class Route {
+ protected enum Provider {
+ REMOTE,
+ LOCAL,
+ MOUNT_TABLE,
+ DYNAMIC,
+ UNKNOWN
+ }
+
+ protected static final Set<String> REMOTE_SCHEMES = new
HashSet<>(Arrays.asList(
+ "hdfs", "s3a", "s3n", "oss", "ofs", "jfs", "abfs", "abfss", "wasb",
"wasbs", "gs", "cosn"));
+ protected static final Set<String> REMOTE_IMPLEMENTATIONS = new
HashSet<>(Arrays.asList(
+ "org.apache.hadoop.hdfs.DistributedFileSystem",
+ "org.apache.hadoop.fs.s3a.S3AFileSystem",
+ "org.apache.hadoop.fs.azurebfs.AzureBlobFileSystem",
+ "org.apache.hadoop.fs.azurebfs.SecureAzureBlobFileSystem",
+ "org.apache.hadoop.fs.azure.NativeAzureFileSystem",
+ "org.apache.hadoop.fs.aliyun.oss.AliyunOSSFileSystem",
+ "com.aliyun.jindodata.oss.JindoOssFileSystem",
+ "org.apache.hadoop.fs.obs.OBSFileSystem",
+ "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem"));
+
+ private final boolean descendants;
+ private final Map<String, String> properties;
+
+ public Route(Map<String, String> properties, boolean descendants) {
+ // Both callers supply a statement-local snapshot that is never
mutated afterward.
+ this.properties = properties;
+ this.descendants = descendants;
+ }
+
+ private List<URI> defaults() throws URISyntaxException {
+ List<URI> defaults = new ArrayList<>();
+ for (String key : Arrays.asList("fs.defaultFS", "fs.default.name")) {
+ String value = property(key);
+ if (value != null) {
+ value = selector(value).trim();
+ // FileSystem.fixName treats slash-free host[:port] strings as
HDFS defaults.
+ defaults.add(new URI("local".equals(value) ? "file:///"
+ : (value.indexOf('/') < 0 ? "hdfs://" + value : value)));
+ }
+ }
+ if (defaults.isEmpty()) {
+ defaults.add(new URI("file:///"));
+ }
+ return defaults;
+ }
+
+ public void checkRoute(URI filesystem, String path, Set<String>
activeTables) throws URISyntaxException {
+ if (filesystem.getScheme() == null) {
+ for (URI defaultFs : defaults()) {
+ checkViewFsEnabled(defaultFs, path, activeTables);
+ }
+ return;
+ }
+ if (filesystem.getAuthority() == null) {
+ boolean completed = false;
+ for (URI defaultFs : defaults()) {
+ if (filesystem.getScheme().equals(defaultFs.getScheme()) &&
defaultFs.getAuthority() != null) {
+ checkViewFsEnabled(defaultFs, path, activeTables);
+ completed = true;
+ }
+ }
+ if (completed) {
+ return;
+ }
+ }
+ checkViewFsEnabled(filesystem, path, activeTables);
+ }
+
+ protected Provider provider(String scheme) {
+ if ("file".equalsIgnoreCase(scheme)) {
+ return Provider.LOCAL;
+ }
+ String implementation = property("fs." + scheme + ".impl");
+ if (implementation != null) {
+ implementation = selector(implementation).trim();
+ return implementationProvider(implementation);
Review Comment:
Fixed in 43ccc016ef318ddd47909ea8f6feebd0da30b891. The raw and effective
route classifiers now share the same exact known-provider set, including
JuiceFS and the other providers previously accepted only at runtime. Explicit
JuiceFS TVF construction is covered with LOCAL both enabled and disabled; local
provider overrides remain rejected. No arbitrary subclass or unknown
implementation is admitted.
Validation: Final standard JDK17 FE: 107 selected tests passed with fresh
XML, zero failures/errors/skips and frozen source. Final standard Clang21/ASAN
BE: 28 selected tests passed. Each release also passed 16 Lance and 19 Paimon
build-control tests; the new SDK unit/C API tests, matching Rust std/ABI checks
and private standard build passed. Independent Standards/Spec rechecks found no
remaining confirmed issue in this follow-up.
##########
build.sh:
##########
@@ -474,6 +474,49 @@ if [[ ! -f
"${DORIS_THIRDPARTY}/installed/lib/${LAST_THIRDPARTY_LIB}" ||
fi
fi
+# Lance TVF storage policy needs both sides of the metadata-only SDK ABI.
+# Check the selected third-party prefix only; a system Lance install cannot
satisfy it.
+lance_c_is_compatible() {
+ local install_dir="$1"
+ [[ ! -e "${install_dir}/lib64/.lance-installing" &&
+ -s "${install_dir}/include/lance/lance.h" &&
+ -s "${install_dir}/lib64/liblance_c.a" ]] || return 1
+ grep -Eq
'^#[[:space:]]*define[[:space:]]+LANCE_STORAGE_BASES_JSON_VERSION[[:space:]]+1([[:space:]]|$)'
\
+ "${install_dir}/include/lance/lance.h" || return 1
+ # GNU nm and Darwin nm spell C exports differently. Read the whole stream:
+ # early awk exit would turn a large archive's producer SIGPIPE into
failure.
+ "${NM:-nm}" -g "${install_dir}/lib64/liblance_c.a" 2>/dev/null | awk '
+ NF >= 2 && $(NF - 1) ~ /^[TW]$/ && $NF ~
/^_?lance_dataset_storage_bases_json$/ { found = 1 }
+ END { exit !found }
+ '
+}
+
+lance_c_source_matches() {
+ local source_dir="$1"
+ [[ -f "${source_dir}/build-thirdparty.sh" && -f
"${source_dir}/download-thirdparty.sh" &&
+ -f "${source_dir}/vars.sh" && -f
"${source_dir}/patches/lance-c-storage-bases.patch" ]] || return 1
+ grep -Fq 'install_lance_c() {' "${source_dir}/build-thirdparty.sh" ||
return 1
+ grep -Fq 'lance-c-storage-bases.patch'
"${source_dir}/download-thirdparty.sh" || return 1
+ cmp -s "${DORIS_HOME}/thirdparty/patches/lance-c-storage-bases.patch" \
+ "${source_dir}/patches/lance-c-storage-bases.patch" || return 1
+ cmp -s <(grep -E '^LANCE_C_(DOWNLOAD|NAME|SOURCE)='
"${DORIS_HOME}/thirdparty/vars.sh") \
+ <(grep -E '^LANCE_C_(DOWNLOAD|NAME|SOURCE)=' "${source_dir}/vars.sh")
+}
+
+if [[ "${BUILD_BE}" -eq 1 ]] && ! lance_c_is_compatible
"${DORIS_THIRDPARTY}/installed"; then
+ if ! lance_c_source_matches "${DORIS_THIRDPARTY}"; then
+ echo "Lance SDK header/archive pair lacks
LANCE_STORAGE_BASES_JSON_VERSION=1 or its exported API, or is incomplete." >&2
+ echo "Refresh the compilation image's include/lance and
lib64/liblance_c.a together, or set DORIS_THIRDPARTY to matching complete
third-party sources." >&2
+ exit 1
+ fi
+ echo "Rebuilding only lance_c for the TVF storage-base metadata ABI ..."
+ bash "${DORIS_THIRDPARTY}/build-thirdparty.sh" -j "${PARALLEL}" lance_c
Review Comment:
Fixed in 43ccc016ef318ddd47909ea8f6feebd0da30b891. A pair-level
.lance-paimon-repairing marker is created before invoking the two
selected-toolchain builds and removed only after both outputs validate. A
failure after Lance publishes but before Paimon's installer leaves the marker,
so the next build takes the narrow paired repair even when Lance already
reports compatible. Direct CMake also rejects incomplete individual/pair
markers. Tests reproduce that window, verify successful retry, incomplete
Paimon output, installed-only failure and unrelated-prefix preservation.
Validation: Final standard JDK17 FE: 107 selected tests passed with fresh
XML, zero failures/errors/skips and frozen source. Final standard Clang21/ASAN
BE: 28 selected tests passed. Each release also passed 16 Lance and 19 Paimon
build-control tests; the new SDK unit/C API tests, matching Rust std/ABI checks
and private standard build passed. Independent Standards/Spec rechecks found no
remaining confirmed issue in this follow-up.
--
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]