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

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


The following commit(s) were added to refs/heads/master by this push:
     new f5767d61c5 [filesystem][s3] Cache FileSystem by scheme and authority 
in HadoopCompliantFileIO (#10182)
f5767d61c5 is described below

commit f5767d61c5d781ca054ab891ea1e6fb82198e195
Author: Eunbin Son <[email protected]>
AuthorDate: Sat Oct 3 18:11:44 2026 +0900

    [filesystem][s3] Cache FileSystem by scheme and authority in 
HadoopCompliantFileIO (#10182)
---
 .../apache/paimon/s3/HadoopCompliantFileIO.java    | 18 +++--
 .../apache/paimon/s3/S3FileIOSchemeCacheTest.java  | 92 ++++++++++++++++++++++
 2 files changed, 102 insertions(+), 8 deletions(-)

diff --git 
a/paimon-filesystems/paimon-s3-impl/src/main/java/org/apache/paimon/s3/HadoopCompliantFileIO.java
 
b/paimon-filesystems/paimon-s3-impl/src/main/java/org/apache/paimon/s3/HadoopCompliantFileIO.java
index 5965d7e670..298a088c0e 100644
--- 
a/paimon-filesystems/paimon-s3-impl/src/main/java/org/apache/paimon/s3/HadoopCompliantFileIO.java
+++ 
b/paimon-filesystems/paimon-s3-impl/src/main/java/org/apache/paimon/s3/HadoopCompliantFileIO.java
@@ -24,6 +24,7 @@ import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.PositionOutputStream;
 import org.apache.paimon.fs.RemoteIterator;
 import org.apache.paimon.fs.SeekableInputStream;
+import org.apache.paimon.utils.Pair;
 
 import org.apache.hadoop.fs.FSDataInputStream;
 import org.apache.hadoop.fs.FSDataOutputStream;
@@ -31,6 +32,7 @@ import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.io.IOUtils;
 
 import java.io.IOException;
+import java.net.URI;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -43,7 +45,7 @@ public abstract class HadoopCompliantFileIO implements FileIO 
{
 
     private static final long serialVersionUID = 1L;
 
-    protected transient volatile Map<String, FileSystem> fsMap;
+    protected transient volatile Map<Pair<String, String>, FileSystem> fsMap;
 
     @Override
     public SeekableInputStream newInputStream(Path path) throws IOException {
@@ -137,16 +139,16 @@ public abstract class HadoopCompliantFileIO implements 
FileIO {
             }
         }
 
-        Map<String, FileSystem> map = fsMap;
+        Map<Pair<String, String>, FileSystem> map = fsMap;
 
-        String authority = path.toUri().getAuthority();
-        if (authority == null) {
-            authority = "DEFAULT";
-        }
-        FileSystem fs = map.get(authority);
+        URI uri = path.toUri();
+        String scheme = uri.getScheme();
+        String authority = uri.getAuthority();
+        Pair<String, String> key = Pair.of(scheme, authority);
+        FileSystem fs = map.get(key);
         if (fs == null) {
             fs = createFileSystem(path);
-            map.put(authority, fs);
+            map.put(key, fs);
         }
         return fs;
     }
diff --git 
a/paimon-filesystems/paimon-s3-impl/src/test/java/org/apache/paimon/s3/S3FileIOSchemeCacheTest.java
 
b/paimon-filesystems/paimon-s3-impl/src/test/java/org/apache/paimon/s3/S3FileIOSchemeCacheTest.java
new file mode 100644
index 0000000000..1aebd61a79
--- /dev/null
+++ 
b/paimon-filesystems/paimon-s3-impl/src/test/java/org/apache/paimon/s3/S3FileIOSchemeCacheTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.paimon.s3;
+
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.options.Options;
+
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that one {@link S3FileIO} instance serves both {@code s3://} and 
{@code s3a://} paths of
+ * the same bucket. No S3 backend is contacted: the bucket probe is disabled 
and the endpoint is
+ * unroutable, and only {@link FileSystem#makeQualified} is called on the 
returned file systems.
+ */
+class S3FileIOSchemeCacheTest {
+
+    private static final String BUCKET = "paimon-scheme-cache-bucket";
+
+    @Test
+    void testS3ThenS3aOnSameBucket() throws Exception {
+        assertSchemesAreServedSeparately("s3", "s3a");
+    }
+
+    @Test
+    void testS3aThenS3OnSameBucket() throws Exception {
+        assertSchemesAreServedSeparately("s3a", "s3");
+    }
+
+    @Test
+    void testSameSchemeAndBucketReusesFileSystem() throws Exception {
+        S3FileIO fileIO = newFileIO();
+        for (String scheme : new String[] {"s3", "s3a"}) {
+            FileSystem first = fileIO.getFileSystem(path(scheme, 
"warehouse/a"));
+            FileSystem second = fileIO.getFileSystem(path(scheme, 
"warehouse/b"));
+            assertThat(second).isSameAs(first);
+        }
+    }
+
+    private static void assertSchemesAreServedSeparately(String firstScheme, 
String secondScheme)
+            throws Exception {
+        S3FileIO fileIO = newFileIO();
+        Path firstPath = path(firstScheme, "warehouse/db/t");
+        Path secondPath = path(secondScheme, "hive/t/part-0.parquet");
+
+        FileSystem firstFs = fileIO.getFileSystem(firstPath);
+        FileSystem secondFs = fileIO.getFileSystem(secondPath);
+
+        // makeQualified runs FileSystem#checkPath, which rejects a path of 
another scheme.
+        assertThat(firstFs.makeQualified(firstPath)).isEqualTo(firstPath);
+        assertThat(secondFs.makeQualified(secondPath)).isEqualTo(secondPath);
+        assertThat(firstFs.getUri().getScheme()).isEqualTo(firstScheme);
+        assertThat(secondFs.getUri().getScheme()).isEqualTo(secondScheme);
+        assertThat(secondFs).isNotSameAs(firstFs);
+    }
+
+    private static S3FileIO newFileIO() {
+        Options options = new Options();
+        // Never reach the network: no bucket probe, dummy credentials and an 
unroutable endpoint.
+        options.set("s3.bucket.probe", "0");
+        options.set("s3.endpoint", "http://localhost:1";);
+        options.set("s3.endpoint.region", "us-east-1");
+        options.set("s3.access-key", "dummy-access-key");
+        options.set("s3.secret-key", "dummy-secret-key");
+        S3FileIO fileIO = new S3FileIO();
+        fileIO.configure(CatalogContext.create(options));
+        return fileIO;
+    }
+
+    private static Path path(String scheme, String key) {
+        return new Path(scheme + "://" + BUCKET + "/" + key);
+    }
+}

Reply via email to