This is an automated email from the ASF dual-hosted git repository.

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new 70a5a4dbd81c fix(fs): stop depending on the optional 
FileSystem#getScheme() (#19470)
70a5a4dbd81c is described below

commit 70a5a4dbd81c0b73b32db577aa16dbadcca8ace6
Author: Ranga Reddy <[email protected]>
AuthorDate: Tue Aug 4 00:17:36 2026 +0530

    fix(fs): stop depending on the optional FileSystem#getScheme() (#19470)
    
    * fix(fs): stop depending on the optional FileSystem#getScheme()
    
    FileSystem#getScheme() is optional in Hadoop: the base implementation throws
    UnsupportedOperationException, and proxy implementations such as Presto's
    PrestoS3FileSystem do not override it. Hudi called it unguarded on 
filesystems it
    did not implement, so opening a log file on such a filesystem failed with
    "Not implemented by the PrestoS3FileSystem FileSystem implementation" 
instead of
    reading anything (HUDI-4602).
    
    Adds HadoopFSUtils#getScheme(FileSystem), which returns fs.getScheme() and 
falls
    back to fs.getUri().getScheme() when it is unimplemented. getUri() is 
abstract, so
    every implementation supplies it, and its scheme is what getScheme() returns
    wherever both are present. This is the same conclusion as #793, which 
stopped
    HoodieWrapperFileSystem calling getScheme() on the filesystem it wraps.
    
    Routes the seven unguarded call sites through it: isGCSFileSystem and
    isCHDFileSystem (the reported read path), registerFileSystem,
    HoodieWrapperFileSystem#convertToHoodiePath, 
HoodieRetryWrapperFileSystem#getScheme,
    WriteMarkersFactory's HDFS gate, and HoodieHadoopStorage#getScheme, which 
is what
    the seven HoodieStorage#getScheme callers reach.
    
    isGCSFileSystem's comparison is also flipped to put the constant first, 
matching
    isCHDFileSystem, so a filesystem whose URI carries no scheme returns false 
rather
    than throwing NullPointerException.
    
    * test(fs): say which branch of the helper each assertion covers
    
    Review nit: the assertion messages did not make clear what had gone wrong. 
Each
    now names the filesystem and the branch of the helper it pins - 
LocalFileSystem
    overriding getScheme() so the helper returns what it reports, 
FilterFileSystem not
    overriding it so the helper falls back to getUri().getScheme().
    
    * fix(fs): fail loudly on an unresolvable scheme, and cover the sites this 
reroutes
    
    Review feedback, all of it well founded.
    
    The fallback no longer returns null. InLineFileSystem is the 
counter-example in this
    module: getScheme() is "inlinefs" while getUri() is URI.create("inlinefs"), 
which has no
    colon and so no scheme, so the two are not interchangeable and the javadoc 
claim that
    they agree was simply wrong. A null surfaced far from the cause as "does 
not support
    scheme null" or "Unsupported scheme :null" with the 
UnsupportedOperationException
    discarded; it now throws with that exception chained. HoodieException 
rather than
    HoodieIOException, since the latter only accepts an IOException cause.
    
    HoodieHadoopStorage memoizes the scheme. On a filesystem without 
getScheme() the
    fallback costs a thrown-and-caught exception, and this is called once per 
log block via
    StorageSchemes.isWriteTransactional and three times per immutable-file 
write via
    needCreateTempFile. A lazy field keeps all five constructors untouched.
    
    Test coverage for what this actually reroutes, none of which any test 
reached:
    
    - registerFileSystem, HoodieWrapperFileSystem#convertToHoodiePath (the 
write path) and
      HoodieHadoopStorage#getScheme, via a LocalFileSystem subclass whose 
getScheme() throws,
      registered as fs.file.impl so it is reached through FileSystem.get.
    - isGCSFileSystem and isCHDFileSystem, which become reachable for proxy 
filesystems for
      the first time here and select different stream wrappers: a scheme-less 
filesystem
      reporting gs:// now yields SchemeAwareFSDataInputStream and ofs:// yields
      BoundedFsDataInputStream.
    - the new unresolvable-scheme failure.
    
    TestFSUtilsWithRetryWrapperEnable#testGetSchema has been inert since 
HUDI-5286 added it:
    it asserted on HoodieWrapperFileSystem#getScheme, which is uri.getScheme() 
and never
    dispatches into the retry wrapper, and FakeRemoteFileSystem overrode 
getScheme() to
    delegate to a real LocalFileSystem so it could not throw. Dropping that 
override gives
    the fake the PrestoS3FileSystem shape and the assertion now targets the 
retry wrapper,
    so it guards both HUDI-5286 and this change. Verified: it fails with the 
pre-PR helper.
    
    Also drops the try/catch in convertToHoodiePath that only rethrew 
HoodieIOException
    unchanged, dead since ef70de2bba7b, and the duplicated fixture and 
redundant nested
    close in TestHadoopFSUtils.
---
 .../hudi/table/marker/WriteMarkersFactory.java     |   2 +-
 .../org/apache/hudi/hadoop/fs/HadoopFSUtils.java   |  42 +++++-
 .../hadoop/fs/HoodieRetryWrapperFileSystem.java    |   2 +-
 .../hudi/hadoop/fs/HoodieWrapperFileSystem.java    |   8 +-
 .../hudi/storage/hadoop/HoodieHadoopStorage.java   |  14 +-
 .../fs/TestFSUtilsWithRetryWrapperEnable.java      |  16 +-
 .../apache/hudi/hadoop/fs/TestHadoopFSUtils.java   | 163 +++++++++++++++++++++
 7 files changed, 226 insertions(+), 21 deletions(-)

diff --git 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/WriteMarkersFactory.java
 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/WriteMarkersFactory.java
index 2765ffdd6286..8191e6d04cba 100644
--- 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/WriteMarkersFactory.java
+++ 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/WriteMarkersFactory.java
@@ -53,7 +53,7 @@ public class WriteMarkersFactory {
         }
         String basePath = table.getMetaClient().getBasePath().toString();
         if (StorageSchemes.HDFS.getScheme().equals(
-            HadoopFSUtils.getFs(basePath, table.getContext().getStorageConf(), 
true).getScheme())) {
+            HadoopFSUtils.getScheme(HadoopFSUtils.getFs(basePath, 
table.getContext().getStorageConf(), true)))) {
           log.warn("Timeline-server-based markers are not supported for HDFS: "
               + "base path {}.  Falling back to direct markers.", basePath);
           return getDirectWriteMarkers(table, instantTime);
diff --git 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HadoopFSUtils.java 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HadoopFSUtils.java
index e16765780314..8c0293dbe795 100644
--- 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HadoopFSUtils.java
+++ 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HadoopFSUtils.java
@@ -26,6 +26,7 @@ import org.apache.hudi.common.engine.HoodieEngineContext;
 import org.apache.hudi.common.fs.FSUtils;
 import org.apache.hudi.common.util.collection.ImmutablePair;
 import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieIOException;
 import org.apache.hudi.storage.StorageConfiguration;
 import org.apache.hudi.storage.StoragePath;
@@ -277,7 +278,7 @@ public class HadoopFSUtils {
    * @return true if the inputstream or the wrapped one is of type 
GoogleHadoopFSInputStream
    */
   public static boolean isGCSFileSystem(FileSystem fs) {
-    return fs.getScheme().equals(StorageSchemes.GCS.getScheme());
+    return StorageSchemes.GCS.getScheme().equals(getScheme(fs));
   }
 
   /**
@@ -285,7 +286,42 @@ public class HadoopFSUtils {
    * Wrapped by {@code BoundedFsDataInputStream}, to check whether the desired 
offset is out of the file size in advance.
    */
   public static boolean isCHDFileSystem(FileSystem fs) {
-    return StorageSchemes.CHDFS.getScheme().equals(fs.getScheme());
+    return StorageSchemes.CHDFS.getScheme().equals(getScheme(fs));
+  }
+
+  /**
+   * Resolves the scheme of {@code fs} without depending on {@link 
FileSystem#getScheme()}.
+   *
+   * <p>{@code getScheme()} is optional in Hadoop: {@link FileSystem}'s own 
implementation throws
+   * {@link UnsupportedOperationException}, and proxy implementations such as 
Presto's
+   * {@code PrestoS3FileSystem} do not override it, so calling it unguarded 
turns an unrelated read into
+   * "Not implemented by the PrestoS3FileSystem FileSystem implementation" 
(HUDI-4602).
+   * {@link FileSystem#getUri()} is abstract, so every implementation supplies 
one to fall back on.
+   *
+   * <p>The two are not interchangeable, which is why {@code getScheme()} is 
tried first:
+   * {@code InLineFileSystem} returns {@code "inlinefs"} from {@code 
getScheme()} while its
+   * {@code getUri()} is {@code URI.create("inlinefs")}, which has no colon 
and so carries no scheme at all.
+   * A URI with no scheme is therefore a resolution failure rather than a 
value to pass on - returning null
+   * would surface much later as {@code does not support scheme null} or 
{@code Unsupported scheme :null},
+   * with the original {@code UnsupportedOperationException} discarded.
+   *
+   * @param fs instance of {@link FileSystem} in use.
+   * @return the scheme of {@code fs}, never null.
+   * @throws HoodieException if {@code getScheme()} is unimplemented and the 
URI carries no scheme.
+   */
+  public static String getScheme(FileSystem fs) {
+    try {
+      return fs.getScheme();
+    } catch (UnsupportedOperationException e) {
+      String scheme = fs.getUri().getScheme();
+      if (scheme == null) {
+        // HoodieException rather than HoodieIOException: the latter only 
accepts an IOException cause, and
+        // discarding the UnsupportedOperationException is the thing being 
fixed here.
+        throw new HoodieException("Cannot resolve the scheme of " + 
fs.getClass().getName()
+            + ": getScheme() is unimplemented and its URI " + fs.getUri() + " 
carries no scheme", e);
+      }
+      return scheme;
+    }
   }
 
   private static StorageConfiguration<Configuration> 
getStorageConf(Configuration conf, boolean copy) {
@@ -294,7 +330,7 @@ public class HadoopFSUtils {
 
   public static Configuration registerFileSystem(StoragePath file, 
Configuration conf) {
     Configuration returnConf = new Configuration(conf);
-    String scheme = HadoopFSUtils.getFs(file.toString(), conf).getScheme();
+    String scheme = getScheme(HadoopFSUtils.getFs(file.toString(), conf));
     returnConf.set("fs." + HoodieWrapperFileSystem.getHoodieScheme(scheme) + 
".impl",
         HoodieWrapperFileSystem.class.getName());
     return returnConf;
diff --git 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HoodieRetryWrapperFileSystem.java
 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HoodieRetryWrapperFileSystem.java
index c9d8fff3fbbd..d7c1ca5f72fc 100644
--- 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HoodieRetryWrapperFileSystem.java
+++ 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HoodieRetryWrapperFileSystem.java
@@ -277,7 +277,7 @@ public class HoodieRetryWrapperFileSystem extends 
FileSystem {
 
   @Override
   public String getScheme() {
-    return fileSystem.getScheme();
+    return HadoopFSUtils.getScheme(fileSystem);
   }
 
   @Override
diff --git 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HoodieWrapperFileSystem.java
 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HoodieWrapperFileSystem.java
index 24674ee725a9..f8d731fd0ce2 100644
--- 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HoodieWrapperFileSystem.java
+++ 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HoodieWrapperFileSystem.java
@@ -159,12 +159,8 @@ public class HoodieWrapperFileSystem extends FileSystem {
   }
 
   public static Path convertToHoodiePath(StoragePath file, Configuration conf) 
{
-    try {
-      String scheme = HadoopFSUtils.getFs(file.toString(), conf).getScheme();
-      return convertPathWithScheme(convertToHadoopPath(file), 
getHoodieScheme(scheme));
-    } catch (HoodieIOException e) {
-      throw e;
-    }
+    String scheme = 
HadoopFSUtils.getScheme(HadoopFSUtils.getFs(file.toString(), conf));
+    return convertPathWithScheme(convertToHadoopPath(file), 
getHoodieScheme(scheme));
   }
 
   public static Path convertPathWithScheme(Path oldPath, String newScheme) {
diff --git 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/storage/hadoop/HoodieHadoopStorage.java
 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/storage/hadoop/HoodieHadoopStorage.java
index 87a9ad1019f6..067f77d491bd 100644
--- 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/storage/hadoop/HoodieHadoopStorage.java
+++ 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/storage/hadoop/HoodieHadoopStorage.java
@@ -20,6 +20,7 @@
 package org.apache.hudi.storage.hadoop;
 
 import org.apache.hudi.common.fs.ConsistencyGuard;
+import org.apache.hudi.common.util.Lazy;
 import org.apache.hudi.exception.HoodieIOException;
 import org.apache.hudi.hadoop.fs.HadoopFSUtils;
 import org.apache.hudi.hadoop.fs.HoodieRetryWrapperFileSystem;
@@ -58,6 +59,13 @@ import static org.apache.hudi.hadoop.fs.HadoopFSUtils.getFs;
  */
 public class HoodieHadoopStorage extends HoodieStorage {
   private final FileSystem fs;
+  /**
+   * Resolved once. On a filesystem that does not implement {@code 
getScheme()} the fallback in
+   * {@link HadoopFSUtils#getScheme} costs a thrown-and-caught exception, and 
this is called once per log
+   * block via {@code StorageSchemes.isWriteTransactional} and three times per 
immutable-file write via
+   * {@code needCreateTempFile}. {@code fs} is final, so the answer cannot 
change.
+   */
+  private final Lazy<String> scheme = Lazy.lazily(this::resolveScheme);
 
   public HoodieHadoopStorage(StoragePath path, StorageConfiguration<?> conf) {
     super(conf);
@@ -111,7 +119,11 @@ public class HoodieHadoopStorage extends HoodieStorage {
 
   @Override
   public String getScheme() {
-    return fs.getScheme();
+    return scheme.get();
+  }
+
+  private String resolveScheme() {
+    return HadoopFSUtils.getScheme(fs);
   }
 
   @Override
diff --git 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/fs/TestFSUtilsWithRetryWrapperEnable.java
 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/fs/TestFSUtilsWithRetryWrapperEnable.java
index bb0b3608c7fc..aaaf749e85d4 100644
--- 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/fs/TestFSUtilsWithRetryWrapperEnable.java
+++ 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/fs/TestFSUtilsWithRetryWrapperEnable.java
@@ -45,7 +45,6 @@ import java.net.URI;
 import java.util.Arrays;
 import java.util.List;
 
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -107,9 +106,13 @@ public class TestFSUtilsWithRetryWrapperEnable extends 
TestFSUtils {
     FileSystem fileSystem =
         new HoodieRetryWrapperFileSystem(fakeFs, maxRetryIntervalMs, 
maxRetryNumbers,
             initialRetryIntervalMs, "");
-    HoodieWrapperFileSystem fs =
-        new HoodieWrapperFileSystem(fileSystem, new NoOpConsistencyGuard());
-    assertDoesNotThrow(fs::getScheme, "Method #getSchema does not implement 
correctly");
+    // FakeRemoteFileSystem deliberately does not override getScheme(), so 
FileSystem's own implementation
+    // throws - the PrestoS3FileSystem shape (HUDI-4602). Assert on the retry 
wrapper itself: asserting on
+    // HoodieWrapperFileSystem instead would only exercise its own 
uri.getScheme() and never reach here,
+    // which is why this guard was inert from the day HUDI-5286 added it.
+    assertThrows(UnsupportedOperationException.class, fakeFs::getScheme);
+    assertEquals("file", ((HoodieRetryWrapperFileSystem) 
fileSystem).getScheme(),
+        "the retry wrapper should resolve the scheme of a filesystem that does 
not implement getScheme()");
   }
 
   @Test
@@ -254,11 +257,6 @@ public class TestFSUtilsWithRetryWrapperEnable extends 
TestFSUtils {
       return fs.getConf();
     }
 
-    @Override
-    public String getScheme() {
-      return fs.getScheme();
-    }
-
     @Override
     public short getDefaultReplication(Path path) {
       return defaultReplication;
diff --git 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
index 7768ff4feae7..da4e9a7500f4 100644
--- 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
+++ 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
@@ -19,25 +19,188 @@
 
 package org.apache.hudi.hadoop.fs;
 
+import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.storage.StoragePathInfo;
+import org.apache.hudi.storage.hadoop.HoodieHadoopStorage;
 
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FSDataInputStream;
 import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.FilterFileSystem;
+import org.apache.hadoop.fs.LocalFileSystem;
 import org.apache.hadoop.fs.Path;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.ValueSource;
 
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+
 import static 
org.apache.hudi.hadoop.fs.HadoopFSUtils.convertToHadoopFileStatus;
 import static org.apache.hudi.hadoop.fs.HadoopFSUtils.convertToHadoopPath;
 import static org.apache.hudi.hadoop.fs.HadoopFSUtils.convertToStoragePath;
 import static org.apache.hudi.hadoop.fs.HadoopFSUtils.convertToStoragePathInfo;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Tests {@link HadoopFSUtils}
  */
 public class TestHadoopFSUtils {
+  /**
+   * HUDI-4602: {@link FileSystem#getScheme()} is optional in Hadoop -- the 
base implementation throws
+   * {@link UnsupportedOperationException} -- and proxy implementations such 
as Presto's
+   * {@code PrestoS3FileSystem} do not override it. Opening a log file went 
straight through
+   * {@code isGCSFileSystem}, so a MOR {@code _rt} query on Presto failed with
+   * "Not implemented by the PrestoS3FileSystem FileSystem implementation" 
rather than reading anything.
+   *
+   * <p>{@link FilterFileSystem} has the same shape: it leaves {@code 
getScheme()} to the throwing base
+   * implementation while overriding {@code getUri()}.
+   */
+  @Test
+  public void testGetFSDataInputStreamWhenGetSchemeIsUnimplemented(@TempDir 
java.nio.file.Path tempDir) throws IOException {
+    java.nio.file.Path file = tempDir.resolve("log.file");
+    byte[] contents = new byte[] {1, 2, 3, 4};
+    Files.write(file, contents);
+    // newInstanceLocal rather than getLocal, so closing this does not evict a 
cached FileSystem that
+    // other tests in the same JVM share.
+    try (FileSystem fs = newFsWithoutGetScheme(FileSystem.newInstanceLocal(new 
Configuration()))) {
+      try (FSDataInputStream stream =
+               HadoopFSUtils.getFSDataInputStream(fs, new 
StoragePath(file.toUri()), 1024, true)) {
+        byte[] read = new byte[contents.length];
+        stream.readFully(read);
+        assertArrayEquals(contents, read, "The read path should not depend on 
the optional getScheme()");
+      }
+    }
+  }
+
+  @Test
+  public void testGetSchemeFallsBackToTheUriWhenUnimplemented() throws 
IOException {
+    try (FileSystem localFs = FileSystem.newInstanceLocal(new 
Configuration())) {
+      assertEquals("file", HadoopFSUtils.getScheme(localFs),
+          "LocalFileSystem overrides getScheme(), so the helper should return 
what it reports "
+              + "rather than falling back to getUri()");
+
+      // FilterFileSystem#close closes the delegate, so the wrapper is not 
given its own block: it owns
+      // nothing, and closing it here would close localFs a second time.
+      FileSystem noScheme = newFsWithoutGetScheme(localFs);
+      assertEquals("file", HadoopFSUtils.getScheme(noScheme),
+          "FilterFileSystem does not override getScheme(), so the helper 
should fall back to "
+              + "getUri().getScheme()");
+    }
+  }
+
+  /**
+   * A URI with no scheme cannot stand in for an unimplemented {@code 
getScheme()}. {@code InLineFileSystem}
+   * is the case in this module: {@code getScheme()} returns "inlinefs" while 
{@code getUri()} is
+   * {@code URI.create("inlinefs")}, which has no colon and so no scheme. 
Returning null there would surface
+   * far away as "does not support scheme null" with the original failure 
discarded, so it must fail here.
+   */
+  @Test
+  public void testGetSchemeFailsLoudlyWhenNeitherSourceHasOne() throws 
IOException {
+    try (FileSystem localFs = FileSystem.newInstanceLocal(new 
Configuration())) {
+      FileSystem schemeless = new NoSchemeFileSystem(localFs, 
URI.create("inlinefs"));
+
+      HoodieException thrown =
+          assertThrows(HoodieException.class, () -> 
HadoopFSUtils.getScheme(schemeless));
+      assertTrue(thrown.getMessage().contains("carries no scheme"),
+          () -> "the failure should say the URI carries no scheme, but was: " 
+ thrown.getMessage());
+      assertInstanceOf(UnsupportedOperationException.class, thrown.getCause(),
+          "the original getScheme() failure must be chained rather than 
discarded");
+    }
+  }
+
+  /**
+   * The three call sites this rerouted that no test in the repo reached: 
{@code registerFileSystem},
+   * {@code HoodieWrapperFileSystem#convertToHoodiePath} - which is on the 
write path, via
+   * {@code HoodieBaseParquetWriter} and friends - and {@code 
HoodieHadoopStorage#getScheme}. All three threw
+   * {@link UnsupportedOperationException} on a filesystem without {@code 
getScheme()} before this change.
+   */
+  @Test
+  public void testCallSitesWorkOnAFileSystemWithoutGetScheme(@TempDir 
java.nio.file.Path tempDir) {
+    Configuration conf = new Configuration();
+    conf.setClass("fs.file.impl", NoSchemeLocalFileSystem.class, 
FileSystem.class);
+    StoragePath path = new StoragePath(tempDir.toUri());
+
+    assertDoesNotThrow(() -> HadoopFSUtils.registerFileSystem(path, conf),
+        "registerFileSystem resolves the scheme to build the fs.<scheme>.impl 
key");
+    assertDoesNotThrow(() -> HoodieWrapperFileSystem.convertToHoodiePath(path, 
conf),
+        "convertToHoodiePath is on the write path and resolves the scheme to 
rewrite it");
+    assertEquals("file", new HoodieHadoopStorage(path, 
HadoopFSUtils.getStorageConf(conf)).getScheme(),
+        "HoodieHadoopStorage#getScheme is what HoodieStorage callers reach");
+  }
+
+  /**
+   * {@code isGCSFileSystem} and {@code isCHDFileSystem} become reachable for 
a filesystem without
+   * {@code getScheme()} for the first time with this change, and they select 
different stream wrappers.
+   * Neither predicate had a test before.
+   */
+  @ParameterizedTest
+  @CsvSource({
+      "gs://bucket, org.apache.hudi.hadoop.fs.SchemeAwareFSDataInputStream",
+      "ofs://cluster, org.apache.hudi.hadoop.fs.BoundedFsDataInputStream"
+  })
+  public void testSchemeSpecificStreamIsSelectedWithoutGetScheme(String uri, 
String expectedStream,
+                                                                 @TempDir 
java.nio.file.Path tempDir) throws IOException {
+    java.nio.file.Path file = tempDir.resolve("log.file");
+    Files.write(file, new byte[] {1, 2, 3, 4});
+    try (FileSystem localFs = FileSystem.newInstanceLocal(new 
Configuration())) {
+      // Reports a gs:// or ofs:// URI while leaving getScheme() to the 
throwing base implementation.
+      FileSystem fs = new NoSchemeFileSystem(localFs, URI.create(uri));
+      assertThrows(UnsupportedOperationException.class, fs::getScheme);
+
+      try (FSDataInputStream stream =
+               HadoopFSUtils.getFSDataInputStream(fs, new 
StoragePath(file.toUri()), 1024, true)) {
+        assertEquals(expectedStream, stream.getClass().getName(),
+            "the scheme-specific wrapper should be selected from the 
fallback-resolved scheme");
+      }
+    }
+  }
+
+  /** A FileSystem with the reported shape: {@code getUri()} works, {@code 
getScheme()} throws. */
+  private static FileSystem newFsWithoutGetScheme(FileSystem delegate) {
+    FileSystem fs = new FilterFileSystem(delegate);
+    // The premise of every assertion below: this is the call the read path 
used to make unguarded.
+    assertThrows(UnsupportedOperationException.class, fs::getScheme);
+    return fs;
+  }
+
+  /** Same shape, but reporting a URI of our choosing so scheme-specific 
branches can be reached. */
+  private static class NoSchemeFileSystem extends FilterFileSystem {
+    private final URI uri;
+
+    NoSchemeFileSystem(FileSystem delegate, URI uri) {
+      super(delegate);
+      this.uri = uri;
+    }
+
+    @Override
+    public URI getUri() {
+      return uri;
+    }
+  }
+
+  /**
+   * A {@link LocalFileSystem} that does not implement {@code getScheme()}, so 
it can be registered as
+   * {@code fs.file.impl} and reached through the normal {@code 
FileSystem.get} path.
+   */
+  public static class NoSchemeLocalFileSystem extends LocalFileSystem {
+    @Override
+    public String getScheme() {
+      throw new UnsupportedOperationException(
+          "Not implemented by the NoSchemeLocalFileSystem FileSystem 
implementation");
+    }
+  }
+
   @ParameterizedTest
   @ValueSource(strings = {
       "/a/b/c",

Reply via email to