diff --git a/CONFIGURATION.md b/CONFIGURATION.md
index 8a7cd52c..60123315 100644
--- a/CONFIGURATION.md
+++ b/CONFIGURATION.md
@@ -49,7 +49,6 @@ These parameters fine-tune the low-level data streaming behavior. They allow you
| `analytics-core.adaptive-read.sequential-read-threshold` | Threshold for number of sequential reads to switch to sequential mode. | `3` |
| `analytics-core.random-read.min-request-size` | Minimum request size for random reads. If the requested read size is smaller, it reads up to this size. | `131072` (128 KB) |
-
### Telemetry and Monitoring
These settings enable the emission of deep internal metrics—such as cache hit rates, operational durations, and throughput—to local logging consoles or distributed OpenTelemetry backends like Google Cloud Monitoring.
diff --git a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/FlatNamespaceStrategyImpl.java b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/FlatNamespaceStrategyImpl.java
new file mode 100644
index 00000000..fe5e203c
--- /dev/null
+++ b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/FlatNamespaceStrategyImpl.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed 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 com.google.cloud.gcs.analyticscore.client;
+
+final class FlatNamespaceStrategyImpl implements NamespaceStrategy {
+ private final GcsClient gcsClient;
+
+ FlatNamespaceStrategyImpl(GcsClient gcsClient) {
+ this.gcsClient = gcsClient;
+ }
+}
diff --git a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsClient.java b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsClient.java
index 47d4d0ab..1b20984e 100644
--- a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsClient.java
+++ b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsClient.java
@@ -43,6 +43,8 @@ WritableByteChannel createWriteChannel(GcsItemId itemId, GcsWriteOptions options
/** Fetches object metadata. */
GcsItemInfo getGcsItemInfo(GcsItemId itemId) throws IOException;
+ boolean isHnsBucket(String bucketName) throws IOException;
+
/** Close the client. */
void close();
}
diff --git a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsClientImpl.java b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsClientImpl.java
index b9ee619c..07e7461f 100644
--- a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsClientImpl.java
+++ b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsClientImpl.java
@@ -171,6 +171,11 @@ BucketProperties getBucketProperties(String bucketName) throws IOException {
}
}
+ @Override
+ public boolean isHnsBucket(String bucketName) throws IOException {
+ return getBucketProperties(bucketName).isHnsEnabled();
+ }
+
@Override
public void close() {
try {
diff --git a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemImpl.java b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemImpl.java
index 322e68d1..dc127eb2 100644
--- a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemImpl.java
+++ b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemImpl.java
@@ -34,6 +34,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.channels.WritableByteChannel;
import java.util.Collections;
@@ -51,6 +52,9 @@ public class GcsFileSystemImpl implements GcsFileSystem {
private final Telemetry telemetry;
private final AnalyticsCacheManager cacheManager;
+ private final FlatNamespaceStrategyImpl flatStrategy;
+ private final HierarchicalNamespaceStrategyImpl hnsStrategy;
+
public GcsFileSystemImpl(GcsFileSystemOptions fileSystemOptions) {
this.fileSystemOptions = fileSystemOptions;
this.executorServiceSupplier = initializeExecutionServiceSupplier();
@@ -64,6 +68,8 @@ public GcsFileSystemImpl(GcsFileSystemOptions fileSystemOptions) {
recorder ->
new GcsClientImpl(
fileSystemOptions.getGcsClientOptions(), executorServiceSupplier, telemetry));
+ this.flatStrategy = new FlatNamespaceStrategyImpl(this.gcsClient);
+ this.hnsStrategy = new HierarchicalNamespaceStrategyImpl(this.gcsClient);
}
public GcsFileSystemImpl(Credentials credentials, GcsFileSystemOptions fileSystemOptions) {
@@ -82,6 +88,8 @@ public GcsFileSystemImpl(Credentials credentials, GcsFileSystemOptions fileSyste
fileSystemOptions.getGcsClientOptions(),
executorServiceSupplier,
telemetry));
+ this.flatStrategy = new FlatNamespaceStrategyImpl(this.gcsClient);
+ this.hnsStrategy = new HierarchicalNamespaceStrategyImpl(this.gcsClient);
}
@VisibleForTesting
@@ -104,6 +112,32 @@ public GcsFileSystemImpl(Credentials credentials, GcsFileSystemOptions fileSyste
this.executorServiceSupplier = initializeExecutionServiceSupplier();
this.telemetry = telemetry;
this.cacheManager = cacheManager;
+ this.flatStrategy = new FlatNamespaceStrategyImpl(this.gcsClient);
+ this.hnsStrategy = new HierarchicalNamespaceStrategyImpl(this.gcsClient);
+ }
+
+ @VisibleForTesting
+ NamespaceStrategy resolveStrategy(String bucketName) throws IOException {
+ checkNotNull(bucketName, "bucketName cannot be null");
+ if (!fileSystemOptions.isHnsApiEnabled()) {
+ return flatStrategy;
+ }
+
+ BucketProperties properties =
+ cacheManager.getBucketProperties(
+ bucketName,
+ name -> {
+ try {
+ return BucketProperties.create(gcsClient.isHnsBucket(name));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+
+ if (properties.isHnsEnabled()) {
+ return hnsStrategy;
+ }
+ return flatStrategy;
}
@Override
@@ -163,6 +197,16 @@ public AnalyticsCacheManager getCacheManager() {
return cacheManager;
}
+ @VisibleForTesting
+ FlatNamespaceStrategyImpl getFlatStrategy() {
+ return flatStrategy;
+ }
+
+ @VisibleForTesting
+ HierarchicalNamespaceStrategyImpl getHnsStrategy() {
+ return hnsStrategy;
+ }
+
@Override
public void close() {
ExecutorService executorService = executorServiceSupplier.get();
diff --git a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemOptions.java b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemOptions.java
index 95ab244c..3ba3a043 100644
--- a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemOptions.java
+++ b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemOptions.java
@@ -25,6 +25,7 @@ public abstract class GcsFileSystemOptions {
private static final String READ_THREAD_COUNT_KEY = "analytics-core.read.thread.count";
private static final String CLIENT_TYPE_KEY = "client.type";
+ private static final String HNS_API_ENABLED_KEY = "analytics-core.hierarchical.namespace.enable";
/** Cloud Storage client to use. */
public enum ClientType {
@@ -43,12 +44,15 @@ public enum ClientType {
public abstract TelemetryOptions getAnalyticsCoreTelemetryOptions();
+ public abstract boolean isHnsApiEnabled();
+
public abstract Builder toBuilder();
public static Builder builder() {
return new AutoValue_GcsFileSystemOptions.Builder()
.setReadThreadCount(16)
.setClientType(ClientType.HTTP_CLIENT)
+ .setHnsApiEnabled(true)
.setGcsClientOptions(GcsClientOptions.builder().build())
.setGcsCacheOptions(GcsCacheOptions.builder().build())
.setAnalyticsCoreTelemetryOptions(TelemetryOptions.builder().build());
@@ -65,6 +69,11 @@ public static GcsFileSystemOptions createFromOptions(
optionsBuilder.setClientType(
ClientType.valueOf(analyticsCoreOptions.get(prefix + CLIENT_TYPE_KEY)));
}
+ if (analyticsCoreOptions.containsKey(prefix + HNS_API_ENABLED_KEY)) {
+ optionsBuilder.setHnsApiEnabled(
+ Boolean.parseBoolean(analyticsCoreOptions.get(prefix + HNS_API_ENABLED_KEY)));
+ }
+
optionsBuilder.setGcsClientOptions(
GcsClientOptions.createFromOptions(analyticsCoreOptions, prefix));
optionsBuilder.setGcsCacheOptions(
@@ -84,6 +93,8 @@ public abstract static class Builder {
public abstract Builder setReadThreadCount(int readThreadCount);
+ public abstract Builder setHnsApiEnabled(boolean isHnsApiEnabled);
+
public abstract Builder setGcsClientOptions(GcsClientOptions gcsClientOptions);
/** Sets the configuration options for the GCS caching layer. */
diff --git a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/HierarchicalNamespaceStrategyImpl.java b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/HierarchicalNamespaceStrategyImpl.java
new file mode 100644
index 00000000..b05e8652
--- /dev/null
+++ b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/HierarchicalNamespaceStrategyImpl.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed 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 com.google.cloud.gcs.analyticscore.client;
+
+final class HierarchicalNamespaceStrategyImpl implements NamespaceStrategy {
+ private final GcsClient gcsClient;
+
+ HierarchicalNamespaceStrategyImpl(GcsClient gcsClient) {
+ this.gcsClient = gcsClient;
+ }
+}
diff --git a/client/src/main/java/com/google/cloud/gcs/analyticscore/client/NamespaceStrategy.java b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/NamespaceStrategy.java
new file mode 100644
index 00000000..9f896b7f
--- /dev/null
+++ b/client/src/main/java/com/google/cloud/gcs/analyticscore/client/NamespaceStrategy.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed 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 com.google.cloud.gcs.analyticscore.client;
+
+/**
+ * Strategy interface for handling directory operations across different namespace models (Flat vs.
+ * HNS).
+ *
+ *
Methods for directory operations will be added in follow-up PRs. These methods will include:
+ *
+ *
+ * - {@code GcsItemInfo getFileInfo(GcsItemId id, PathType pathType) throws IOException;}
+ *
- {@code void createDirectory(GcsItemId id) throws IOException;}
+ *
- {@code boolean isDirectoryEmpty(GcsItemId id) throws IOException;}
+ *
- {@code void renameDirectory(GcsItemId src, GcsItemId dst) throws IOException;}
+ *
- {@code java.util.List listObjectInfo(GcsItemId id) throws IOException;}
+ *
- {@code java.util.List listRecursive(GcsItemId id) throws IOException;}
+ *
+ */
+interface NamespaceStrategy {}
diff --git a/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsClientImplTest.java b/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsClientImplTest.java
index fc0598d9..0a229db7 100644
--- a/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsClientImplTest.java
+++ b/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsClientImplTest.java
@@ -83,8 +83,10 @@ class GcsClientImplTest {
private static final String TEST_OBJECT_ID = "test-object-id";
private static final String TEST_WRITE_OBJECT = "test-write-object";
private static final String TEST_NULL_OPTIONS_OBJECT = "test-null-options";
- private static final String TEST_NON_EXISTENT_OBJECT = "non-existent";
- private static final String NON_EXISTENT_BUCKET = "non-existent-bucket";
+ private static final String TEST_NON_EXISTENT_OBJECT = "non-existent-object";
+ private static final String TEST_HNS_BUCKET = "hns-bucket";
+ private static final String TEST_FLAT_BUCKET = "flat-bucket";
+ private static final String TEST_NON_EXISTENT_BUCKET = "non-existent-bucket";
private static final String TEST_OBJECT_NAME = "test-object-name";
private static final String BLOB_WRITE_SESSION_CONFIG_FIELD = "blobWriteSessionConfig";
private static final int MB = 1024 * 1024;
@@ -310,39 +312,61 @@ void getBucketProperties_nullBucketName_throwsNullPointerException() {
}
@Test
- void getBucketProperties_hnsEnabled_returnsTrue() throws IOException {
+ void getBucketProperties_hnsBucket_returnsTrue() throws IOException {
Storage mockStorage = mock(Storage.class);
GcsClientImpl localGcsClient = createClientWithMockStorage(mockStorage);
Bucket mockBucket = mockBucketWithHns(true);
- doReturn(mockBucket).when(mockStorage).get(eq("hns-bucket"), any(BucketGetOption.class));
+ doReturn(mockBucket).when(mockStorage).get(eq(TEST_HNS_BUCKET), any(BucketGetOption.class));
- BucketProperties bucketProperties = localGcsClient.getBucketProperties("hns-bucket");
+ BucketProperties bucketProperties = localGcsClient.getBucketProperties(TEST_HNS_BUCKET);
assertThat(bucketProperties.isHnsEnabled()).isTrue();
}
@Test
- void getBucketProperties_hnsDisabled_returnsFalse() throws IOException {
+ void isHnsBucket_hnsBucket_returnsTrue() throws IOException {
+ Storage mockStorage = mock(Storage.class);
+ GcsClientImpl localGcsClient = createClientWithMockStorage(mockStorage);
+ Bucket mockBucket = mockBucketWithHns(true);
+ doReturn(mockBucket).when(mockStorage).get(eq(TEST_HNS_BUCKET), any(BucketGetOption.class));
+
+ boolean isHns = localGcsClient.isHnsBucket(TEST_HNS_BUCKET);
+
+ assertThat(isHns).isTrue();
+ }
+
+ @Test
+ void getBucketProperties_flatBucket_returnsFalse() throws IOException {
Storage mockStorage = mock(Storage.class);
GcsClientImpl localGcsClient = createClientWithMockStorage(mockStorage);
Bucket mockBucket = mockBucketWithHns(false);
- doReturn(mockBucket).when(mockStorage).get(eq("flat-bucket"), any(BucketGetOption.class));
+ doReturn(mockBucket).when(mockStorage).get(eq(TEST_FLAT_BUCKET), any(BucketGetOption.class));
- BucketProperties bucketProperties = localGcsClient.getBucketProperties("flat-bucket");
+ BucketProperties bucketProperties = localGcsClient.getBucketProperties(TEST_FLAT_BUCKET);
assertThat(bucketProperties.isHnsEnabled()).isFalse();
}
@Test
- void getBucketProperties_hnsNull_returnsFalse() throws IOException {
+ void isHnsBucket_flatBucket_returnsFalse() throws IOException {
+ Storage mockStorage = mock(Storage.class);
+ GcsClientImpl localGcsClient = createClientWithMockStorage(mockStorage);
+ Bucket mockBucket = mockBucketWithHns(false);
+ doReturn(mockBucket).when(mockStorage).get(eq(TEST_FLAT_BUCKET), any(BucketGetOption.class));
+
+ boolean isHns = localGcsClient.isHnsBucket(TEST_FLAT_BUCKET);
+
+ assertThat(isHns).isFalse();
+ }
+
+ @Test
+ void getBucketProperties_missingHnsProperty_returnsFalse() throws IOException {
Storage mockStorage = mock(Storage.class);
GcsClientImpl localGcsClient = createClientWithMockStorage(mockStorage);
Bucket mockBucket = mockBucketWithHns(null);
- doReturn(mockBucket)
- .when(mockStorage)
- .get(eq("flat-bucket-null-hns"), any(BucketGetOption.class));
+ doReturn(mockBucket).when(mockStorage).get(eq(TEST_BUCKET), any(BucketGetOption.class));
- BucketProperties bucketProperties = localGcsClient.getBucketProperties("flat-bucket-null-hns");
+ BucketProperties bucketProperties = localGcsClient.getBucketProperties(TEST_BUCKET);
assertThat(bucketProperties.isHnsEnabled()).isFalse();
}
@@ -351,9 +375,9 @@ void getBucketProperties_hnsNull_returnsFalse() throws IOException {
void getBucketProperties_bucketNotFound_returnsDisabledHns() throws Exception {
Storage mockStorage = mock(Storage.class);
GcsClientImpl localGcsClient = createClientWithMockStorage(mockStorage);
- doReturn(null).when(mockStorage).get(eq(NON_EXISTENT_BUCKET), any(BucketGetOption.class));
+ doReturn(null).when(mockStorage).get(eq(TEST_NON_EXISTENT_BUCKET), any(BucketGetOption.class));
- BucketProperties properties = localGcsClient.getBucketProperties(NON_EXISTENT_BUCKET);
+ BucketProperties properties = localGcsClient.getBucketProperties(TEST_NON_EXISTENT_BUCKET);
assertThat(properties.isHnsEnabled()).isFalse();
}
@@ -505,9 +529,12 @@ void create_whenBucketOrObjectNotFound_throwsFileNotFoundException() throws Exce
Storage mockStorage = mock(Storage.class);
GcsClientImpl clientWithMock = createClientWithMockStorage(mockStorage);
GcsItemId itemId =
- GcsItemId.builder().setBucketName(NON_EXISTENT_BUCKET).setObjectName(TEST_OBJECT).build();
+ GcsItemId.builder()
+ .setBucketName(TEST_NON_EXISTENT_BUCKET)
+ .setObjectName(TEST_NON_EXISTENT_OBJECT)
+ .build();
BlobInfo blobInfo =
- BlobInfo.newBuilder(BlobId.of(NON_EXISTENT_BUCKET, TEST_OBJECT))
+ BlobInfo.newBuilder(BlobId.of(TEST_NON_EXISTENT_BUCKET, TEST_NON_EXISTENT_OBJECT))
.setContentType("application/octet-stream")
.build();
StorageException e404 = new StorageException(404, "Not Found");
diff --git a/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemImplTest.java b/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemImplTest.java
index 3897c3e9..e333d77a 100644
--- a/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemImplTest.java
+++ b/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemImplTest.java
@@ -35,6 +35,7 @@
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.WritableByteChannel;
@@ -66,6 +67,7 @@ class GcsFileSystemImplTest {
GcsFileSystemOptions.builder().setGcsClientOptions(TEST_GCS_CLIENT_OPTIONS).build();
@Mock private GcsClient mockClient;
+
private GcsFileSystem gcsFileSystem;
@BeforeEach
@@ -88,6 +90,10 @@ void constructor_withCredentials_createsClientWithProvidedCredentials() {
assertThat(gcsClientImpl.storage.getOptions().getCredentials())
.isEqualTo(NoCredentials.getInstance());
+ assertThat(gcsFileSystem.getTelemetry()).isNotNull();
+ assertThat(gcsFileSystem.getCacheManager()).isNotNull();
+ assertThat(gcsFileSystem.getFlatStrategy()).isNotNull();
+ assertThat(gcsFileSystem.getHnsStrategy()).isNotNull();
}
}
@@ -104,12 +110,17 @@ void constructor_withFileSystemOptions_createsClientWithDefaultCredentials() {
assertThat(gcsFileSystem.getFileSystemOptions()).isSameInstanceAs(fileSystemOptions);
assertThat(gcsClient).isNotNull();
assertThat(gcsClient.storage.getOptions().getProjectId()).isEqualTo("test-project-default");
+ assertThat(gcsFileSystem.getTelemetry()).isNotNull();
+ assertThat(gcsFileSystem.getCacheManager()).isNotNull();
+ assertThat(gcsFileSystem.getFlatStrategy()).isNotNull();
+ assertThat(gcsFileSystem.getHnsStrategy()).isNotNull();
}
}
@Test
- void constructor_withValidOptions_passesExecutorToClient() {
+ void constructor_withValidOptions_passesMemorizedExecutorServiceAndTelemetryToGcsClient() {
final AtomicReference> capturedSupplier = new AtomicReference<>();
+ final AtomicReference capturedTelemetry = new AtomicReference<>();
try (MockedConstruction mockGcsClientConstruction =
Mockito.mockConstruction(
GcsClientImpl.class,
@@ -118,6 +129,9 @@ void constructor_withValidOptions_passesExecutorToClient() {
Supplier supplier =
(Supplier) context.arguments().get(1);
capturedSupplier.set(supplier);
+
+ Telemetry telemetry = (Telemetry) context.arguments().get(2);
+ capturedTelemetry.set(telemetry);
})) {
try (GcsFileSystemImpl fs = new GcsFileSystemImpl(TEST_GCS_FILESYSTEM_OPTIONS)) {
@@ -128,6 +142,10 @@ void constructor_withValidOptions_passesExecutorToClient() {
assertThat(capturedSupplier.get()).isNotNull();
assertThat(capturedSupplier.get().get()).isNotNull();
assertThat(executorService1).isEqualTo(executorService2);
+ assertThat(capturedTelemetry.get()).isNotNull();
+ assertThat(capturedTelemetry.get()).isSameInstanceAs(fs.getTelemetry());
+ assertThat(fs.getFlatStrategy()).isNotNull();
+ assertThat(fs.getHnsStrategy()).isNotNull();
}
}
}
@@ -574,6 +592,74 @@ void create_nullWriteOptions_delegatesToClientWithNullOptions() throws IOExcepti
assertThat(resultChannel).isSameInstanceAs(mockChannel);
}
+ @Test
+ void resolveStrategy_hnsFlagEnabledAndHnsBucket_returnsHnsStrategy() throws IOException {
+ GcsFileSystemOptions options =
+ GcsFileSystemOptions.builder()
+ .setGcsClientOptions(TEST_GCS_CLIENT_OPTIONS)
+ .setHnsApiEnabled(true)
+ .build();
+ when(mockClient.isHnsBucket(TEST_BUCKET)).thenReturn(true);
+
+ try (GcsFileSystemImpl gcsFileSystem = new GcsFileSystemImpl(mockClient, options)) {
+ NamespaceStrategy strategy = gcsFileSystem.resolveStrategy(TEST_BUCKET);
+
+ assertThat(strategy).isInstanceOf(HierarchicalNamespaceStrategyImpl.class);
+ }
+ }
+
+ @Test
+ void resolveStrategy_hnsFlagDisabled_returnsFlatStrategy() throws IOException {
+ GcsFileSystemOptions options =
+ GcsFileSystemOptions.builder()
+ .setGcsClientOptions(TEST_GCS_CLIENT_OPTIONS)
+ .setHnsApiEnabled(false)
+ .build();
+
+ try (GcsFileSystemImpl gcsFileSystem = new GcsFileSystemImpl(mockClient, options)) {
+ NamespaceStrategy strategy = gcsFileSystem.resolveStrategy(TEST_BUCKET);
+
+ assertThat(strategy).isInstanceOf(FlatNamespaceStrategyImpl.class);
+ verify(mockClient, never()).isHnsBucket(anyString());
+ }
+ }
+
+ @Test
+ void resolveStrategy_hnsFlagEnabledAndFlatBucket_returnsFlatStrategy() throws IOException {
+ GcsFileSystemOptions options =
+ GcsFileSystemOptions.builder()
+ .setGcsClientOptions(TEST_GCS_CLIENT_OPTIONS)
+ .setHnsApiEnabled(true)
+ .build();
+ when(mockClient.isHnsBucket(TEST_BUCKET)).thenReturn(false);
+
+ try (GcsFileSystemImpl gcsFileSystem = new GcsFileSystemImpl(mockClient, options)) {
+ NamespaceStrategy strategy = gcsFileSystem.resolveStrategy(TEST_BUCKET);
+
+ assertThat(strategy).isInstanceOf(FlatNamespaceStrategyImpl.class);
+ }
+ }
+
+ @Test
+ void resolveStrategy_isHnsBucketThrowsIoException_throwsUncheckedIOException()
+ throws IOException {
+ GcsFileSystemOptions options =
+ GcsFileSystemOptions.builder()
+ .setGcsClientOptions(TEST_GCS_CLIENT_OPTIONS)
+ .setHnsApiEnabled(true)
+ .build();
+ when(mockClient.isHnsBucket(TEST_BUCKET)).thenThrow(new IOException("test exception"));
+
+ try (GcsFileSystemImpl gcsFileSystem = new GcsFileSystemImpl(mockClient, options)) {
+ UncheckedIOException exception =
+ assertThrows(
+ UncheckedIOException.class, () -> gcsFileSystem.resolveStrategy(TEST_BUCKET));
+
+ assertThat(exception).hasCauseThat().isInstanceOf(IOException.class);
+ assertThat(exception).hasCauseThat().hasMessageThat().isEqualTo("test exception");
+ }
+ }
+
@SuppressWarnings("unchecked")
private List getRegisteredTelemetryListeners(Telemetry telemetry) {
try {
diff --git a/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemOptionsTest.java b/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemOptionsTest.java
index 0b4a3ff0..c919b3fa 100644
--- a/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemOptionsTest.java
+++ b/client/src/test/java/com/google/cloud/gcs/analyticscore/client/GcsFileSystemOptionsTest.java
@@ -32,13 +32,15 @@ void createFromOptions_withValidProperties_shouldCreateCorrectOptions() {
ImmutableMap.of(
"fs.gs.project-id", "test-project",
"fs.gs.client.type", "GRPC_CLIENT",
- "fs.gs.analytics-core.read.thread.count", "32");
+ "fs.gs.analytics-core.read.thread.count", "32",
+ "fs.gs.analytics-core.hierarchical.namespace.enable", "true");
GcsFileSystemOptions options = GcsFileSystemOptions.createFromOptions(properties, "fs.gs.");
assertThat(options.getGcsClientOptions().getProjectId().get()).isEqualTo("test-project");
assertThat(options.getClientType()).isEqualTo(GcsFileSystemOptions.ClientType.GRPC_CLIENT);
assertThat(options.getReadThreadCount()).isEqualTo(32);
+ assertThat(options.isHnsApiEnabled()).isTrue();
}
@Test
@@ -62,4 +64,22 @@ void createFromOptions_cacheProperties_createsCorrectOptions() {
assertThat(cacheOptions.isSmallObjectCacheEnabled()).isTrue();
assertThat(cacheOptions.getSmallObjectCacheMaxSizeBytes()).isEqualTo(200 * MB);
}
+
+ @Test
+ void createFromOptions_withDefaultProperties_shouldCreateCorrectOptions() {
+ ImmutableMap properties = ImmutableMap.of();
+
+ GcsFileSystemOptions options = GcsFileSystemOptions.createFromOptions(properties, "fs.gs.");
+
+ assertThat(options.getGcsClientOptions().getProjectId().isEmpty()).isTrue();
+ assertThat(options.getClientType()).isEqualTo(GcsFileSystemOptions.ClientType.HTTP_CLIENT);
+ assertThat(options.getReadThreadCount()).isEqualTo(16);
+ assertThat(options.isHnsApiEnabled()).isTrue();
+
+ GcsCacheOptions cacheOptions = options.getGcsCacheOptions();
+ assertThat(cacheOptions.isFooterCacheEnabled()).isFalse();
+ assertThat(cacheOptions.getFooterCacheMaxSizeBytes()).isEqualTo(100 * MB);
+ assertThat(cacheOptions.isSmallObjectCacheEnabled()).isFalse();
+ assertThat(cacheOptions.getSmallObjectCacheMaxSizeBytes()).isEqualTo(200 * MB);
+ }
}
diff --git a/test-lib/src/main/java/com/google/cloud/gcs/analyticscore/client/FakeGcsClientImpl.java b/test-lib/src/main/java/com/google/cloud/gcs/analyticscore/client/FakeGcsClientImpl.java
index 81c17521..69efabf2 100644
--- a/test-lib/src/main/java/com/google/cloud/gcs/analyticscore/client/FakeGcsClientImpl.java
+++ b/test-lib/src/main/java/com/google/cloud/gcs/analyticscore/client/FakeGcsClientImpl.java
@@ -51,6 +51,12 @@ protected Storage createStorage(Optional credentials) {
return storage;
}
+ @Override
+ BucketProperties getBucketProperties(String bucketName) {
+ // FakeStorageRpc does not support bucket operations
+ return BucketProperties.create(false);
+ }
+
@Override
public VectoredSeekableByteChannel openReadChannel(
GcsItemInfo itemInfo, GcsReadOptions readOptions) throws IOException {