This is an automated email from the ASF dual-hosted git repository.
mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new ee0a57070b [#8953]Improve(gvfs-java): refactor gvfs cache (#8954)
ee0a57070b is described below
commit ee0a57070b86a872350be290005841ba898dc850
Author: Junda Yang <[email protected]>
AuthorDate: Wed Oct 29 01:10:02 2025 -0700
[#8953]Improve(gvfs-java): refactor gvfs cache (#8954)
### What changes were proposed in this pull request?
Refactored the FileSystem caching strategy in BaseGVFSOperations to
cache by storage backend instead of by fileset.
Main Changes:
- New cache key: Changed from Pair<NameIdentifier, String> (fileset
identifier + location name) to FileSystemCacheKey(scheme, authority,
UserGroupInformation)
- New FileSystemCacheKey inner class: Static inner class with proper
equals()/hashCode() implementation based on filesystem scheme,
authority, and user
- Restructured caching logic: Moved FileSystem caching from
getActualFileSystemByLocationName() to getActualFileSystemByPath(),
separating fileset metadata resolution from filesystem instantiation
### Why are the changes needed?
More granular caching at the actual filesystem level rather than virtual
fileset level. Now getActualFileSystemByPath() load FS from cache.
Fix: #8953
### Does this PR introduce _any_ user-facing change?
No. This is an internal caching optimization. The external API and
behavior remain unchanged.
### How was this patch tested?
Unit tests
---
.../filesystem/hadoop/BaseGVFSOperations.java | 177 ++++++++++++++-------
.../filesystem/hadoop/TestFileSystemCacheKey.java | 116 ++++++++++++++
.../gravitino/filesystem/hadoop/TestGvfsBase.java | 91 +++++++++--
3 files changed, 314 insertions(+), 70 deletions(-)
diff --git
a/clients/filesystem-hadoop3/src/main/java/org/apache/gravitino/filesystem/hadoop/BaseGVFSOperations.java
b/clients/filesystem-hadoop3/src/main/java/org/apache/gravitino/filesystem/hadoop/BaseGVFSOperations.java
index 920ed90128..e61b6abc0f 100644
---
a/clients/filesystem-hadoop3/src/main/java/org/apache/gravitino/filesystem/hadoop/BaseGVFSOperations.java
+++
b/clients/filesystem-hadoop3/src/main/java/org/apache/gravitino/filesystem/hadoop/BaseGVFSOperations.java
@@ -44,6 +44,7 @@ import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
@@ -54,7 +55,6 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
-import org.apache.commons.lang3.tuple.Pair;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.audit.CallerContext;
@@ -85,6 +85,7 @@ import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.security.Credentials;
+import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.util.Progressable;
import org.slf4j.Logger;
@@ -125,9 +126,7 @@ public abstract class BaseGVFSOperations implements
Closeable {
private final Configuration conf;
- // Fileset nameIdentifier-locationName Pair and its corresponding FileSystem
cache, the name
- // identifier has four levels, the first level is metalake name.
- private final Cache<Pair<NameIdentifier, String>, FileSystem>
internalFileSystemCache;
+ private final Cache<FileSystemCacheKey, FileSystem> fileSystemCache;
private final Map<String, FileSystemProvider> fileSystemProvidersMap;
@@ -140,6 +139,66 @@ public abstract class BaseGVFSOperations implements
Closeable {
private final boolean enableCredentialVending;
private final boolean autoCreateLocation;
+ /** A key class for caching FileSystem instances based on scheme, authority,
and configuration. */
+ public static class FileSystemCacheKey {
+ private final String scheme;
+ private final String authority;
+ private final UserGroupInformation ugi;
+
+ /**
+ * Constructor for FileSystemCacheKey.
+ *
+ * @param scheme the scheme of the filesystem
+ * @param authority the authority of the filesystem
+ * @param ugi the user group information
+ */
+ FileSystemCacheKey(String scheme, String authority, UserGroupInformation
ugi) {
+ this.scheme = scheme;
+ this.authority = authority;
+ this.ugi = ugi;
+ }
+
+ /**
+ * Get the scheme of the filesystem.
+ *
+ * @return the scheme
+ */
+ public String scheme() {
+ return scheme;
+ }
+
+ /**
+ * Get the authority of the filesystem.
+ *
+ * @return the authority
+ */
+ public String authority() {
+ return authority;
+ }
+
+ /**
+ * Get the UserGroupInformation
+ *
+ * @return the UserGroupInformation
+ */
+ public UserGroupInformation ugi() {
+ return ugi;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof FileSystemCacheKey)) return false;
+ FileSystemCacheKey that = (FileSystemCacheKey) o;
+ return Objects.equals(scheme, that.scheme)
+ && Objects.equals(authority, that.authority)
+ && Objects.equals(ugi, that.ugi);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(scheme, authority, ugi);
+ }
+ }
/**
* Constructs a new {@link BaseGVFSOperations} with the given {@link
Configuration}.
@@ -159,7 +218,7 @@ public abstract class BaseGVFSOperations implements
Closeable {
FS_GRAVITINO_FILESET_METADATA_CACHE_ENABLE,
FS_GRAVITINO_FILESET_METADATA_CACHE_ENABLE_DEFAULT);
- this.internalFileSystemCache = newFileSystemCache(configuration);
+ this.fileSystemCache = newFileSystemCache(configuration);
this.fileSystemProvidersMap =
ImmutableMap.copyOf(getFileSystemProviders());
@@ -211,14 +270,14 @@ public abstract class BaseGVFSOperations implements
Closeable {
@Override
public void close() throws IOException {
// close all actual FileSystems
- for (FileSystem fileSystem : internalFileSystemCache.asMap().values()) {
+ for (FileSystem fileSystem : fileSystemCache.asMap().values()) {
try {
fileSystem.close();
} catch (IOException e) {
// ignore
}
}
- internalFileSystemCache.invalidateAll();
+ fileSystemCache.invalidateAll();
try {
if (filesetMetadataCache != null && filesetMetadataCache.isPresent()) {
@@ -378,7 +437,7 @@ public abstract class BaseGVFSOperations implements
Closeable {
*/
protected Token<?>[] addDelegationTokensForAllFS(String renewer, Credentials
credentials) {
List<Token<?>> tokenList = Lists.newArrayList();
- for (FileSystem fileSystem : internalFileSystemCache.asMap().values()) {
+ for (FileSystem fileSystem : fileSystemCache.asMap().values()) {
try {
tokenList.addAll(Arrays.asList(fileSystem.addDelegationTokens(renewer,
credentials)));
} catch (IOException e) {
@@ -603,8 +662,8 @@ public abstract class BaseGVFSOperations implements
Closeable {
}
@VisibleForTesting
- Cache<Pair<NameIdentifier, String>, FileSystem> internalFileSystemCache() {
- return internalFileSystemCache;
+ Cache<FileSystemCacheKey, FileSystem> internalFileSystemCache() {
+ return fileSystemCache;
}
/**
@@ -655,40 +714,25 @@ public abstract class BaseGVFSOperations implements
Closeable {
NameIdentifier catalogIdent =
NameIdentifier.of(filesetIdent.namespace().level(0),
filesetIdent.namespace().level(1));
try {
- return internalFileSystemCache.get(
- Pair.of(filesetIdent, locationName),
- cacheKey -> {
- try {
- Fileset fileset = getFileset(cacheKey.getLeft());
- String targetLocationName =
- cacheKey.getRight() == null
- ?
fileset.properties().get(PROPERTY_DEFAULT_LOCATION_NAME)
- : cacheKey.getRight();
-
- Preconditions.checkArgument(
- fileset.storageLocations().containsKey(targetLocationName),
- "Location name: %s is not found in fileset: %s.",
- targetLocationName,
- cacheKey.getLeft());
-
- Path targetLocation = new
Path(fileset.storageLocations().get(targetLocationName));
- Map<String, String> allProperties =
- getAllProperties(
- cacheKey.getLeft(), targetLocation.toUri().getScheme(),
targetLocationName);
-
- FileSystem actualFileSystem =
- getActualFileSystemByPath(targetLocation, allProperties);
- createFilesetLocationIfNeed(cacheKey.getLeft(),
actualFileSystem, targetLocation);
- return actualFileSystem;
- } catch (IOException ioe) {
- throw new GravitinoRuntimeException(
- ioe,
- "Exception occurs when create new FileSystem for fileset:
%s, location: %s, msg: %s",
- cacheKey.getLeft(),
- cacheKey.getRight(),
- ioe.getMessage());
- }
- });
+ Fileset fileset = getFileset(filesetIdent);
+ String targetLocationName =
+ locationName == null
+ ? fileset.properties().get(PROPERTY_DEFAULT_LOCATION_NAME)
+ : locationName;
+
+ Preconditions.checkArgument(
+ fileset.storageLocations().containsKey(targetLocationName),
+ "Location name: %s is not found in fileset: %s.",
+ targetLocationName,
+ filesetIdent);
+
+ Path targetLocation = new
Path(fileset.storageLocations().get(targetLocationName));
+ Map<String, String> allProperties =
+ getAllProperties(filesetIdent, targetLocation.toUri().getScheme(),
targetLocationName);
+
+ FileSystem actualFileSystem = getActualFileSystemByPath(targetLocation,
allProperties);
+ createFilesetLocationIfNeed(filesetIdent, actualFileSystem,
targetLocation);
+ return actualFileSystem;
} catch (RuntimeException e) {
Throwable cause = e.getCause();
if (cause instanceof NoSuchCatalogException || cause instanceof
CatalogNotInUseException) {
@@ -718,21 +762,43 @@ public abstract class BaseGVFSOperations implements
Closeable {
}
}
- private FileSystem getActualFileSystemByPath(
- Path actualFilePath, Map<String, String> allProperties) throws
IOException {
+ /**
+ * Get the actual file system by the given actual file path and properties.
+ *
+ * @param actualFilePath the actual file path.
+ * @param allProperties the properties.
+ * @return the actual file system.
+ */
+ protected FileSystem getActualFileSystemByPath(
+ Path actualFilePath, Map<String, String> allProperties) {
URI uri = actualFilePath.toUri();
String scheme = uri.getScheme();
Preconditions.checkArgument(
StringUtils.isNotBlank(scheme), "Scheme of the actual file location
cannot be null.");
- FileSystemProvider provider = getFileSystemProviderByScheme(scheme);
-
- // Reset the FileSystem service loader to make sure the FileSystem will
reload the
- // service file systems, this is a temporary solution to fix the issue
- // https://github.com/apache/gravitino/issues/5609
- resetFileSystemServiceLoader(scheme);
-
- return provider.getFileSystem(actualFilePath, allProperties);
+ UserGroupInformation ugi;
+ try {
+ ugi = UserGroupInformation.getCurrentUser();
+ } catch (IOException e) {
+ throw new GravitinoRuntimeException(
+ e, "Cannot get current user for path: %s", actualFilePath);
+ }
+ return fileSystemCache.get(
+ new FileSystemCacheKey(scheme, uri.getAuthority(), ugi),
+ cacheKey -> {
+ FileSystemProvider provider = getFileSystemProviderByScheme(scheme);
+
+ // Reset the FileSystem service loader to make sure the FileSystem
will reload the
+ // service file systems, this is a temporary solution to fix the
issue
+ // https://github.com/apache/gravitino/issues/5609
+ resetFileSystemServiceLoader(scheme);
+ try {
+ return provider.getFileSystem(actualFilePath, allProperties);
+ } catch (IOException e) {
+ throw new GravitinoRuntimeException(
+ e, "Cannot get FileSystem for path: %s", actualFilePath);
+ }
+ });
}
private void resetFileSystemServiceLoader(String fsScheme) {
@@ -753,8 +819,7 @@ public abstract class BaseGVFSOperations implements
Closeable {
}
}
- private Cache<Pair<NameIdentifier, String>, FileSystem> newFileSystemCache(
- Configuration configuration) {
+ private Cache<FileSystemCacheKey, FileSystem>
newFileSystemCache(Configuration configuration) {
int maxCapacity =
configuration.getInt(
GravitinoVirtualFileSystemConfiguration.FS_GRAVITINO_FILESET_CACHE_MAX_CAPACITY_KEY,
diff --git
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestFileSystemCacheKey.java
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestFileSystemCacheKey.java
new file mode 100644
index 0000000000..3837fd92d3
--- /dev/null
+++
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestFileSystemCacheKey.java
@@ -0,0 +1,116 @@
+/*
+ * 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.gravitino.filesystem.hadoop;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.io.IOException;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link BaseGVFSOperations.FileSystemCacheKey}. */
+public class TestFileSystemCacheKey {
+
+ @Test
+ public void testEqualityWithSameValues() throws IOException {
+ UserGroupInformation ugi1 = UserGroupInformation.getCurrentUser();
+ UserGroupInformation ugi2 = UserGroupInformation.getCurrentUser();
+
+ BaseGVFSOperations.FileSystemCacheKey key1 =
+ new BaseGVFSOperations.FileSystemCacheKey("hdfs", "namenode:8020",
ugi1);
+ BaseGVFSOperations.FileSystemCacheKey key2 =
+ new BaseGVFSOperations.FileSystemCacheKey("hdfs", "namenode:8020",
ugi2);
+
+ assertEquals(key1, key2);
+ assertEquals(key1.hashCode(), key2.hashCode());
+ }
+
+ @Test
+ public void testInequalityWithDifferentScheme() throws IOException {
+ UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
+
+ BaseGVFSOperations.FileSystemCacheKey key1 =
+ new BaseGVFSOperations.FileSystemCacheKey("hdfs", "namenode:8020",
ugi);
+ BaseGVFSOperations.FileSystemCacheKey key2 =
+ new BaseGVFSOperations.FileSystemCacheKey("s3a", "namenode:8020", ugi);
+
+ assertNotEquals(key1, key2);
+ }
+
+ @Test
+ public void testInequalityWithDifferentAuthority() throws IOException {
+ UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
+
+ BaseGVFSOperations.FileSystemCacheKey key1 =
+ new BaseGVFSOperations.FileSystemCacheKey("hdfs", "namenode1:8020",
ugi);
+ BaseGVFSOperations.FileSystemCacheKey key2 =
+ new BaseGVFSOperations.FileSystemCacheKey("hdfs", "namenode2:8020",
ugi);
+
+ assertNotEquals(key1, key2);
+ }
+
+ @Test
+ public void testInequalityWithNullAuthority() throws IOException {
+ UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
+
+ BaseGVFSOperations.FileSystemCacheKey key1 =
+ new BaseGVFSOperations.FileSystemCacheKey("file", null, ugi);
+ BaseGVFSOperations.FileSystemCacheKey key2 =
+ new BaseGVFSOperations.FileSystemCacheKey("file", "localhost", ugi);
+
+ assertNotEquals(key1, key2);
+ }
+
+ @Test
+ public void testEqualityWithBothNullAuthority() throws IOException {
+ UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
+
+ BaseGVFSOperations.FileSystemCacheKey key1 =
+ new BaseGVFSOperations.FileSystemCacheKey("file", null, ugi);
+ BaseGVFSOperations.FileSystemCacheKey key2 =
+ new BaseGVFSOperations.FileSystemCacheKey("file", null, ugi);
+
+ assertEquals(key1, key2);
+ assertEquals(key1.hashCode(), key2.hashCode());
+ }
+
+ @Test
+ public void testGetters() throws IOException {
+ UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
+
+ BaseGVFSOperations.FileSystemCacheKey key =
+ new BaseGVFSOperations.FileSystemCacheKey("hdfs", "namenode:8020",
ugi);
+
+ assertEquals("hdfs", key.scheme());
+ assertEquals("namenode:8020", key.authority());
+ assertEquals(ugi, key.ugi());
+ }
+
+ @Test
+ public void testNotEqualsWithDifferentType() throws IOException {
+ UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
+
+ BaseGVFSOperations.FileSystemCacheKey key =
+ new BaseGVFSOperations.FileSystemCacheKey("hdfs", "namenode:8020",
ugi);
+
+ assertNotEquals(key, "not a FileSystemCacheKey");
+ assertNotEquals(key, null);
+ }
+}
diff --git
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestGvfsBase.java
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestGvfsBase.java
index b3ae42dba4..53a04ddc65 100644
---
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestGvfsBase.java
+++
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/TestGvfsBase.java
@@ -33,7 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -46,6 +46,7 @@ import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.github.benmanes.caffeine.cache.Cache;
import com.google.common.collect.ImmutableMap;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -60,9 +61,7 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.concurrent.TimeUnit;
-import org.apache.commons.lang3.tuple.Pair;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Version;
import org.apache.gravitino.dto.AuditDTO;
@@ -343,15 +342,19 @@ public class TestGvfsBase extends GravitinoMockServerBase
{
buildMockResourceForCredential(filesetName, localPath.toString());
FileSystemTestUtils.mkdirs(managedFilesetPath, gravitinoFileSystem);
- FileSystem proxyLocalFs =
- Objects.requireNonNull(
- ((GravitinoVirtualFileSystem) gravitinoFileSystem)
- .getOperations()
- .internalFileSystemCache()
- .getIfPresent(
- Pair.of(
- NameIdentifier.of(metalakeName, catalogName,
schemaName, "testFSCache"),
- null)));
+
+ // Verify the internal cache contains a FileSystem for the local scheme
+ Cache<BaseGVFSOperations.FileSystemCacheKey, FileSystem> cache =
+ ((GravitinoVirtualFileSystem) gravitinoFileSystem)
+ .getOperations()
+ .internalFileSystemCache();
+
+ // The cache should have one entry for the local filesystem
+ assertEquals(1, cache.asMap().size());
+
+ // Get the cached filesystem (should be a local filesystem)
+ FileSystem proxyLocalFs = cache.asMap().values().iterator().next();
+ assertNotNull(proxyLocalFs);
String anotherFilesetName = "test_new_fs";
Path diffLocalPath =
@@ -401,11 +404,71 @@ public class TestGvfsBase extends GravitinoMockServerBase
{
.asMap()
.size()));
- assertNull(
+ // Verify the cache is empty after eviction
+ assertTrue(
((GravitinoVirtualFileSystem) fs)
.getOperations()
.internalFileSystemCache()
- .getIfPresent(Pair.of(NameIdentifier.of("file"),
LOCATION_NAME_UNKNOWN)));
+ .asMap()
+ .isEmpty());
+ }
+ }
+
+ @Test
+ public void testCacheReuseAcrossMultipleFilesets() throws IOException {
+ // Create two different filesets that point to the same local directory
+ String fileset1Name = "fileset_cache_reuse_1";
+ String fileset2Name = "fileset_cache_reuse_2";
+
+ // Both filesets point to the same underlying local path
+ Path localPath =
+ FileSystemTestUtils.createLocalDirPrefix(catalogName, schemaName,
"shared_cache");
+
+ Path filesetPath1 =
+ FileSystemTestUtils.createFilesetPath(catalogName, schemaName,
fileset1Name, true);
+ Path filesetPath2 =
+ FileSystemTestUtils.createFilesetPath(catalogName, schemaName,
fileset2Name, true);
+
+ String locationPath1 =
+ String.format(
+ "/api/metalakes/%s/catalogs/%s/schemas/%s/filesets/%s/location",
+ metalakeName, catalogName, schemaName, fileset1Name);
+ String locationPath2 =
+ String.format(
+ "/api/metalakes/%s/catalogs/%s/schemas/%s/filesets/%s/location",
+ metalakeName, catalogName, schemaName, fileset2Name);
+
+ try (FileSystem fs = filesetPath1.getFileSystem(conf)) {
+ // Mock server responses for both filesets pointing to the same location
+ FileLocationResponse fileLocationResponse = new
FileLocationResponse(localPath.toString());
+ Map<String, String> queryParams = new HashMap<>();
+ queryParams.put("sub_path", "");
+
+ buildMockResource(Method.GET, locationPath1, queryParams, null,
fileLocationResponse, SC_OK);
+ buildMockResource(Method.GET, locationPath2, queryParams, null,
fileLocationResponse, SC_OK);
+ buildMockResourceForCredential(fileset1Name, localPath.toString());
+ buildMockResourceForCredential(fileset2Name, localPath.toString());
+
+ // Access first fileset
+ FileSystemTestUtils.mkdirs(filesetPath1, fs);
+
+ Cache<BaseGVFSOperations.FileSystemCacheKey, FileSystem> cache =
+ ((GravitinoVirtualFileSystem)
fs).getOperations().internalFileSystemCache();
+
+ // Should have one cached filesystem for the local scheme
+ assertEquals(1, cache.asMap().size());
+ FileSystem cachedFs1 = cache.asMap().values().iterator().next();
+ assertNotNull(cachedFs1);
+
+ // Access second fileset pointing to the same location
+ FileSystemTestUtils.mkdirs(filesetPath2, fs);
+
+ // Should still have only one cached filesystem (reused)
+ assertEquals(1, cache.asMap().size());
+ FileSystem cachedFs2 = cache.asMap().values().iterator().next();
+
+ // Both should be the same instance since they share
scheme/authority/user
+ assertSame(cachedFs1, cachedFs2, "FileSystem instances should be reused
for same storage");
}
}