This is an automated email from the ASF dual-hosted git repository.
jojochuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new d2700a4b01b HDDS-15678. OFS isDirectory/isFile should not trigger
pipeline refresh or return block locations (#10741)
d2700a4b01b is described below
commit d2700a4b01b21eb04f86d0dfcb160eac09e3be57
Author: Andrey Yarovoy <[email protected]>
AuthorDate: Wed Jul 15 20:36:48 2026 -0400
HDDS-15678. OFS isDirectory/isFile should not trigger pipeline refresh or
return block locations (#10741)
---
.../apache/hadoop/ozone/client/OzoneBucket.java | 16 ++
.../ozone/client/protocol/ClientProtocol.java | 22 ++-
.../apache/hadoop/ozone/client/rpc/RpcClient.java | 3 +-
.../hadoop/ozone/client/TestOzoneBucket.java | 72 +++++++++
.../client/TestRpcClientGetFileStatusHeadOp.java | 93 +++++++++++
...OzoneManagerProtocolClientSideTranslatorPB.java | 1 +
.../ozone/AbstractRootedOzoneFileSystemTest.java | 24 +++
.../fs/ozone/TestOFSIsDirectoryBenchmark.java | 153 ++++++++++++++++++
.../protocolPB/OzoneManagerRequestHandler.java | 13 +-
.../protocolPB/TestOzoneManagerRequestHandler.java | 83 ++++++++++
.../ozone/BasicRootedOzoneClientAdapterImpl.java | 14 +-
.../fs/ozone/BasicRootedOzoneFileSystem.java | 33 +++-
.../apache/hadoop/fs/ozone/OzoneClientAdapter.java | 11 ++
.../TestBasicRootedOzoneClientAdapterHeadOp.java | 176 +++++++++++++++++++++
.../fs/ozone/TestRootedOzoneFileSystemHeadOp.java | 174 ++++++++++++++++++++
.../hadoop/ozone/client/ClientProtocolStub.java | 3 +-
pom.xml | 4 +-
17 files changed, 881 insertions(+), 14 deletions(-)
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
index 3c8714d8e39..ff2bd13dfe1 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
@@ -1014,6 +1014,22 @@ public OzoneFileStatus getFileStatus(String keyName)
throws IOException {
return proxy.getOzoneFileStatus(volumeName, name, keyName);
}
+ /**
+ * OzoneFS api to get file status for an entry.
+ *
+ * @param keyName Key name
+ * @param headOp when true, request a metadata-only (type) check so the OM
+ * skips the pipeline refresh and datanode sorting.
+ * @throws OMException if file does not exist
+ * if bucket does not exist
+ * @throws IOException if there is error in the db
+ * invalid arguments
+ */
+ public OzoneFileStatus getFileStatus(String keyName, boolean headOp)
+ throws IOException {
+ return proxy.getOzoneFileStatus(volumeName, name, keyName, headOp);
+ }
+
/**
* Ozone FS api to create a directory. Parent directories if do not exist
* are created for the input directory.
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index 518a9b772da..d39c46041cf 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -988,8 +988,28 @@ TenantUserList listUsersInTenant(String tenantId, String
prefix)
* @throws IOException if there is error in the db
* invalid arguments
*/
+ default OzoneFileStatus getOzoneFileStatus(String volumeName,
+ String bucketName, String keyName) throws IOException {
+ return getOzoneFileStatus(volumeName, bucketName, keyName, false);
+ }
+
+ /**
+ * Get the Ozone File Status for a particular Ozone key.
+ *
+ * @param volumeName volume name.
+ * @param bucketName bucket name.
+ * @param keyName key name.
+ * @param headOp when true, this is a metadata-only (type) check: the OM
+ * skips the pipeline refresh (SCM round-trip) and datanode
+ * sorting since block locations are not needed.
+ * @return OzoneFileStatus for the key.
+ * @throws OMException if file does not exist
+ * if bucket does not exist
+ * @throws IOException if there is error in the db
+ * invalid arguments
+ */
OzoneFileStatus getOzoneFileStatus(String volumeName, String bucketName,
- String keyName) throws IOException;
+ String keyName, boolean headOp) throws IOException;
/**
* Creates directory with keyName as the absolute path for the directory.
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 57d358e393e..f635c5cba73 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -2274,13 +2274,14 @@ public OzoneMultipartUploadList
listMultipartUploads(String volumeName,
@Override
public OzoneFileStatus getOzoneFileStatus(String volumeName,
- String bucketName, String keyName) throws IOException {
+ String bucketName, String keyName, boolean headOp) throws IOException {
OmKeyArgs keyArgs = new OmKeyArgs.Builder()
.setVolumeName(volumeName)
.setBucketName(bucketName)
.setKeyName(keyName)
.setSortDatanodesInPipeline(topologyAwareReadEnabled)
.setLatestVersionLocation(getLatestVersionLocation)
+ .setHeadOp(headOp)
.build();
return ozoneManagerClient.getFileStatus(keyArgs);
}
diff --git
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestOzoneBucket.java
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestOzoneBucket.java
new file mode 100644
index 00000000000..fd2c52faa63
--- /dev/null
+++
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestOzoneBucket.java
@@ -0,0 +1,72 @@
+/*
+ * 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.hadoop.ozone.client;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
+import org.apache.hadoop.ozone.om.helpers.OzoneFileStatus;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link OzoneBucket}.
+ */
+public class TestOzoneBucket {
+
+ /**
+ * getFileStatus(key) must be a full status request (headOp=false), while the
+ * headOp overload must forward the flag so the OM can skip the pipeline
+ * refresh for type-only checks (HDDS-15678).
+ */
+ @Test
+ public void getFileStatusPropagatesHeadOp() throws IOException {
+ ClientProtocol proxy = mock(ClientProtocol.class);
+ OzoneBucket bucket = OzoneBucket.newBuilder(new OzoneConfiguration(),
proxy)
+ .setVolumeName("vol")
+ .setName("bucket")
+ .build();
+
+ bucket.getFileStatus("key");
+ verify(proxy).getOzoneFileStatus("vol", "bucket", "key");
+
+ bucket.getFileStatus("key", true);
+ verify(proxy).getOzoneFileStatus("vol", "bucket", "key", true);
+ }
+
+ /**
+ * The 3-arg convenience method has a default that delegates to the
+ * headOp-aware overload with headOp=false, so implementations only need to
+ * provide the headOp-aware method and can never silently ignore the flag.
+ */
+ @Test
+ public void clientProtocol3argDefaultDelegates() throws IOException {
+ ClientProtocol proxy = mock(ClientProtocol.class, CALLS_REAL_METHODS);
+ OzoneFileStatus status = mock(OzoneFileStatus.class);
+ doReturn(status).when(proxy)
+ .getOzoneFileStatus("vol", "bucket", "key", false);
+
+ assertSame(status, proxy.getOzoneFileStatus("vol", "bucket", "key"));
+ verify(proxy).getOzoneFileStatus("vol", "bucket", "key", false);
+ }
+}
diff --git
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestRpcClientGetFileStatusHeadOp.java
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestRpcClientGetFileStatusHeadOp.java
new file mode 100644
index 00000000000..50c216f81ac
--- /dev/null
+++
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestRpcClientGetFileStatusHeadOp.java
@@ -0,0 +1,93 @@
+/*
+ * 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.hadoop.ozone.client;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import jakarta.annotation.Nonnull;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hadoop.hdds.conf.InMemoryConfigurationForTesting;
+import org.apache.hadoop.hdds.scm.OzoneClientConfig;
+import org.apache.hadoop.hdds.scm.XceiverClientFactory;
+import org.apache.hadoop.ozone.client.rpc.RpcClient;
+import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
+import org.apache.hadoop.ozone.om.protocolPB.OmTransport;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.KeyArgs;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that {@link RpcClient#getOzoneFileStatus} propagates the headOp
flag
+ * all the way into the wire {@code KeyArgs}, so the OM can skip the pipeline
+ * refresh for OFS type checks (HDDS-15678).
+ */
+public class TestRpcClientGetFileStatusHeadOp {
+
+ private final AtomicReference<KeyArgs> captured = new AtomicReference<>();
+
+ private RpcClient newClient() throws IOException {
+ InMemoryConfigurationForTesting conf = new
InMemoryConfigurationForTesting();
+ conf.setFromObject(conf.getObject(OzoneClientConfig.class));
+ return new RpcClient(conf, null) {
+ @Override
+ protected OmTransport createOmTransport(String omServiceId) {
+ return new MockOmTransport() {
+ @Override
+ public OMResponse submitRequest(OMRequest payload) throws
IOException {
+ if (payload.getCmdType() == Type.GetFileStatus) {
+ captured.set(payload.getGetFileStatusRequest().getKeyArgs());
+ // Request captured; short-circuit before building a response.
+ throw new IOException("captured");
+ }
+ return super.submitRequest(payload);
+ }
+ };
+ }
+
+ @Nonnull
+ @Override
+ protected XceiverClientFactory createXceiverClientFactory(
+ ServiceInfoEx serviceInfo) {
+ return new MockXceiverClientFactory();
+ }
+ };
+ }
+
+ @Test
+ public void headOpFlagReachesWireKeyArgs() throws IOException {
+ RpcClient client = newClient();
+ try {
+ assertThrows(IOException.class,
+ () -> client.getOzoneFileStatus("vol", "bucket", "key", true));
+ assertTrue(captured.get().getHeadOp(),
+ "headOp=true must be sent in the GetFileStatus KeyArgs");
+
+ assertThrows(IOException.class,
+ () -> client.getOzoneFileStatus("vol", "bucket", "key"));
+ assertFalse(captured.get().getHeadOp(),
+ "default getOzoneFileStatus must not set headOp");
+ } finally {
+ client.close();
+ }
+ }
+}
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index 9de50cd0d35..fe38d251c5b 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -2209,6 +2209,7 @@ public OzoneFileStatus getFileStatus(OmKeyArgs args)
throws IOException {
.setKeyName(args.getKeyName())
.setSortDatanodes(args.getSortDatanodes())
.setLatestVersionLocation(args.getLatestVersionLocation())
+ .setHeadOp(args.isHeadOp())
.build();
GetFileStatusRequest req =
GetFileStatusRequest.newBuilder()
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractRootedOzoneFileSystemTest.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractRootedOzoneFileSystemTest.java
index aa6431fc56c..e479cbb661e 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractRootedOzoneFileSystemTest.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/AbstractRootedOzoneFileSystemTest.java
@@ -551,6 +551,30 @@ void testGetFileStatusRoot() throws Exception {
assertEquals(FsPermission.getDirDefault(), fileStatus.getPermission());
}
+ /**
+ * OFS: isFile/isDirectory are metadata-only (headOp) checks. They must
report
+ * the correct entry type for files, directories and non-existent paths
+ * (HDDS-15678).
+ */
+ @Test
+ void testIsFileAndIsDirectory() throws Exception {
+ Path dir = new Path(bucketPath, "isdir-dir");
+ fs.mkdirs(dir);
+ Path file = new Path(dir, "isdir-file");
+ ContractTestUtils.touch(fs, file);
+
+ assertTrue(fs.isDirectory(dir));
+ assertFalse(fs.isFile(dir));
+ assertTrue(fs.isFile(file));
+ assertFalse(fs.isDirectory(file));
+
+ Path missing = new Path(dir, "does-not-exist");
+ assertFalse(fs.isDirectory(missing));
+ assertFalse(fs.isFile(missing));
+
+ fs.delete(dir, true);
+ }
+
/**
* Test listStatus operation in a bucket.
*/
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOFSIsDirectoryBenchmark.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOFSIsDirectoryBenchmark.java
new file mode 100644
index 00000000000..b34eea8e694
--- /dev/null
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOFSIsDirectoryBenchmark.java
@@ -0,0 +1,153 @@
+/*
+ * 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.hadoop.fs.ozone;
+
+import static
org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.fs.FSDataOutputStream;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * On-demand benchmark (not part of {@code mvn test}) for HDDS-15678.
+ *
+ * <p>Measures OFS {@link FileSystem#isFile}/{@link FileSystem#isDirectory}
+ * (the metadata-only head-op path added by this change) against a full
+ * {@link FileSystem#getFileStatus} on the same <b>file</b> path. For a file,
+ * the full path makes the OM contact SCM to refresh pipeline/block locations;
+ * the head-op path skips that round-trip. The A/B in a single run isolates the
+ * eliminated SCM refresh (FULL = pre-fix behaviour, HEAD = this fix).
+ *
+ * <p>To instead run a classic before/after across two builds, revert the
+ * {@code isDirectory}/{@code isFile} overrides in
+ * {@link BasicRootedOzoneFileSystem} and compare the HEAD numbers.
+ *
+ * <p>The {@code benchmark} tag is excluded from {@code mvn test} and CI by
+ * default, so it must be re-enabled explicitly to run on demand:
+ *
+ * <pre>
+ * mvn -pl hadoop-ozone/integration-test test \
+ * -Dtest=TestOFSIsDirectoryBenchmark -Dgroups=benchmark \
+ * -Dexcluded-test-groups= -DskipShade -DskipRecon \
+ * -Dsurefire.failIfNoSpecifiedTests=false
+ * </pre>
+ */
+@Tag("benchmark")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestOFSIsDirectoryBenchmark {
+
+ private static final int WARMUP = 2_000;
+ private static final int ITERATIONS = 20_000;
+
+ private MiniOzoneCluster cluster;
+ private OzoneClient client;
+ private FileSystem fs;
+ private Path filePath;
+
+ @BeforeAll
+ void init() throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(OMConfigKeys.OZONE_DEFAULT_BUCKET_LAYOUT,
+ BucketLayout.FILE_SYSTEM_OPTIMIZED.name());
+ cluster = MiniOzoneCluster.newBuilder(conf).setNumDatanodes(3).build();
+ cluster.waitForClusterToBeReady();
+ client = cluster.newClient();
+
+ ObjectStore store = client.getObjectStore();
+ store.createVolume("vol");
+ store.getVolume("vol").createBucket("bucket");
+
+ String rootPath = String.format("%s://%s/",
+ OzoneConsts.OZONE_OFS_URI_SCHEME, conf.get(OZONE_OM_ADDRESS_KEY));
+ conf.set(FS_DEFAULT_NAME_KEY, rootPath);
+ fs = FileSystem.get(conf);
+
+ filePath = new Path("/vol/bucket/file");
+ try (FSDataOutputStream out = fs.create(filePath, true)) {
+ out.write(new byte[4096]);
+ }
+ }
+
+ @AfterAll
+ void cleanup() throws IOException {
+ if (fs != null) {
+ fs.close();
+ }
+ if (client != null) {
+ client.close();
+ }
+ if (cluster != null) {
+ cluster.shutdown();
+ }
+ }
+
+ @FunctionalInterface
+ private interface Op {
+ void run() throws IOException;
+ }
+
+ private long timeNanos(Op op) throws IOException {
+ long start = System.nanoTime();
+ for (int i = 0; i < ITERATIONS; i++) {
+ op.run();
+ }
+ return System.nanoTime() - start;
+ }
+
+ @Test
+ @Timeout(value = 600, unit = TimeUnit.SECONDS)
+ @SuppressWarnings("deprecation") // FileSystem.isFile is the API under test
+ void benchmarkHeadOpVsFullStatus() throws IOException {
+ // Warm up both paths.
+ for (int i = 0; i < WARMUP; i++) {
+ fs.isFile(filePath);
+ fs.getFileStatus(filePath);
+ }
+
+ long headNanos = timeNanos(() -> fs.isFile(filePath));
+ long fullNanos = timeNanos(() -> fs.getFileStatus(filePath));
+
+ double headOps = ITERATIONS * 1_000_000_000.0 / headNanos;
+ double fullOps = ITERATIONS * 1_000_000_000.0 / fullNanos;
+
+ System.out.println();
+ System.out.println("=== HDDS-15678 OFS type-check benchmark ===");
+ System.out.printf("iterations=%d on a 1-block file%n", ITERATIONS);
+ System.out.printf("FULL getFileStatus (pre-fix): %,10.0f ops/s %6.1f
us/op%n",
+ fullOps, fullNanos / 1000.0 / ITERATIONS);
+ System.out.printf("HEAD isFile (this fix ): %,10.0f ops/s %6.1f
us/op%n",
+ headOps, headNanos / 1000.0 / ITERATIONS);
+ System.out.printf("speedup (head/full): %.2fx%n", headOps / fullOps);
+ }
+}
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
index 760dd90e9ce..7359065986a 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
@@ -1077,10 +1077,21 @@ private GetFileStatusResponse getOzoneFileStatus(
.setVolumeName(keyArgs.getVolumeName())
.setBucketName(keyArgs.getBucketName())
.setKeyName(keyArgs.getKeyName())
+ .setHeadOp(keyArgs.getHeadOp())
.build();
GetFileStatusResponse.Builder rb = GetFileStatusResponse.newBuilder();
- rb.setStatus(impl.getFileStatus(omKeyArgs).getProtobuf(clientVersion));
+ OzoneFileStatusProto status =
+ impl.getFileStatus(omKeyArgs).getProtobuf(clientVersion);
+ if (keyArgs.getHeadOp() && status.hasKeyInfo()) {
+ // A head op only needs the entry type. The block locations are not
+ // refreshed for a head op (they carry no pipeline) and the caller does
+ // not use them, so drop them to keep the response small (HDDS-15678).
+ status = status.toBuilder()
+ .setKeyInfo(status.getKeyInfo().toBuilder().clearKeyLocationList())
+ .build();
+ }
+ rb.setStatus(status);
return rb.build();
}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/protocolPB/TestOzoneManagerRequestHandler.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/protocolPB/TestOzoneManagerRequestHandler.java
index 5e796ad0dbc..35ee959236d 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/protocolPB/TestOzoneManagerRequestHandler.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/protocolPB/TestOzoneManagerRequestHandler.java
@@ -149,6 +149,89 @@ private void
mockOmRequest(OzoneManagerProtocolProtos.OMRequest request,
}
}
+ /**
+ * getFileStatus must forward the headOp flag from the request KeyArgs into
+ * the OmKeyArgs handed to the OM, and must drop the (unrefreshed) block
+ * locations from a head-op response so it stays small (HDDS-15678).
+ */
+ @Test
+ public void getFileStatusForwardsHeadOpAndStripsLocations() throws
IOException {
+ for (boolean headOp : new boolean[] {true, false}) {
+ OzoneManagerRequestHandler requestHandler = getRequestHandler(10);
+ OzoneManager ozoneManager = requestHandler.getOzoneManager();
+
+ OzoneFileStatus status = Mockito.mock(OzoneFileStatus.class);
+ OzoneManagerProtocolProtos.OzoneFileStatusProto proto =
+ OzoneManagerProtocolProtos.OzoneFileStatusProto.newBuilder()
+ .setKeyInfo(OzoneManagerProtocolProtos.KeyInfo.newBuilder()
+ .setVolumeName("volume").setBucketName("bucket")
+ .setKeyName("key").setDataSize(0)
+ .setType(HddsProtos.ReplicationType.RATIS)
+ .setCreationTime(0).setModificationTime(0)
+ .addKeyLocationList(
+ OzoneManagerProtocolProtos.KeyLocationList.newBuilder()
+ .setVersion(0).build())
+ .build())
+ .build();
+ Mockito.when(status.getProtobuf(Mockito.anyInt())).thenReturn(proto);
+ ArgumentCaptor<OmKeyArgs> captor =
ArgumentCaptor.forClass(OmKeyArgs.class);
+
Mockito.when(ozoneManager.getFileStatus(captor.capture())).thenReturn(status);
+
+ OzoneManagerProtocolProtos.OMRequest request =
+ Mockito.mock(OzoneManagerProtocolProtos.OMRequest.class);
+ Mockito.when(request.getTraceID()).thenReturn("traceId");
+ Mockito.when(request.getCmdType())
+ .thenReturn(OzoneManagerProtocolProtos.Type.GetFileStatus);
+ Mockito.when(request.getGetFileStatusRequest()).thenReturn(
+ OzoneManagerProtocolProtos.GetFileStatusRequest.newBuilder()
+ .setKeyArgs(OzoneManagerProtocolProtos.KeyArgs.newBuilder()
+ .setVolumeName("volume").setBucketName("bucket")
+ .setKeyName("key").setHeadOp(headOp).build())
+ .build());
+
+ OzoneManagerProtocolProtos.OMResponse response =
+ requestHandler.handleReadRequest(request);
+
+ Assertions.assertEquals(headOp, captor.getValue().isHeadOp());
+ int locations = response.getGetFileStatusResponse().getStatus()
+ .getKeyInfo().getKeyLocationListCount();
+ // headOp -> block locations stripped; otherwise retained.
+ Assertions.assertEquals(headOp ? 0 : 1, locations);
+ }
+ }
+
+ /**
+ * A head-op status with no keyInfo (defensive) must be returned unchanged.
+ */
+ @Test
+ public void getFileStatusHeadOpWithoutKeyInfoIsNoop() throws IOException {
+ OzoneManagerRequestHandler requestHandler = getRequestHandler(10);
+ OzoneManager ozoneManager = requestHandler.getOzoneManager();
+
+ OzoneFileStatus status = Mockito.mock(OzoneFileStatus.class);
+ Mockito.when(status.getProtobuf(Mockito.anyInt())).thenReturn(
+ OzoneManagerProtocolProtos.OzoneFileStatusProto.newBuilder()
+ .setIsDirectory(true).build());
+ Mockito.when(ozoneManager.getFileStatus(Mockito.any())).thenReturn(status);
+
+ OzoneManagerProtocolProtos.OMRequest request =
+ Mockito.mock(OzoneManagerProtocolProtos.OMRequest.class);
+ Mockito.when(request.getTraceID()).thenReturn("traceId");
+ Mockito.when(request.getCmdType())
+ .thenReturn(OzoneManagerProtocolProtos.Type.GetFileStatus);
+ Mockito.when(request.getGetFileStatusRequest()).thenReturn(
+ OzoneManagerProtocolProtos.GetFileStatusRequest.newBuilder()
+ .setKeyArgs(OzoneManagerProtocolProtos.KeyArgs.newBuilder()
+ .setVolumeName("volume").setBucketName("bucket")
+ .setKeyName("key").setHeadOp(true).build())
+ .build());
+
+ OzoneManagerProtocolProtos.OMResponse response =
+ requestHandler.handleReadRequest(request);
+ Assertions.assertFalse(
+ response.getGetFileStatusResponse().getStatus().hasKeyInfo());
+ }
+
@ParameterizedTest
@ValueSource(ints = {0, 9, 10, 11, 50})
public void testListKeysResponseSize(int resultSize) throws IOException {
diff --git
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneClientAdapterImpl.java
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneClientAdapterImpl.java
index 0b11ab95de5..50842949af2 100644
---
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneClientAdapterImpl.java
+++
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneClientAdapterImpl.java
@@ -667,6 +667,12 @@ boolean deleteObjects(OzoneBucket bucket, List<String>
keyNameList) {
@Override
public FileStatusAdapter getFileStatus(String path, URI uri,
Path qualifiedPath, String userName) throws IOException {
+ return getFileStatus(path, uri, qualifiedPath, userName, false);
+ }
+
+ @Override
+ public FileStatusAdapter getFileStatus(String path, URI uri,
+ Path qualifiedPath, String userName, boolean headOp) throws IOException {
incrementCounter(Statistic.OBJECTS_QUERY, 1);
OFSPath ofsPath = new OFSPath(path, config);
if (ofsPath.isRoot()) {
@@ -676,7 +682,7 @@ public FileStatusAdapter getFileStatus(String path, URI uri,
return getFileStatusAdapterForVolume(volume, uri);
} else {
return getFileStatusForKeyOrSnapshot(
- ofsPath, uri, qualifiedPath, userName);
+ ofsPath, uri, qualifiedPath, userName, headOp);
}
}
@@ -686,8 +692,8 @@ public FileStatusAdapter getFileStatus(String path, URI uri,
* Throws exception in case of failure.
*/
private FileStatusAdapter getFileStatusForKeyOrSnapshot(
- OFSPath ofsPath, URI uri, Path qualifiedPath, String userName)
- throws IOException {
+ OFSPath ofsPath, URI uri, Path qualifiedPath, String userName,
+ boolean headOp) throws IOException {
String key = ofsPath.getKeyName();
try {
OzoneBucket bucket = getBucket(ofsPath, false);
@@ -696,7 +702,7 @@ private FileStatusAdapter getFileStatusForKeyOrSnapshot(
return getFileStatusAdapterWithSnapshotIndicator(
volume, bucket, uri);
} else {
- OzoneFileStatus status = bucket.getFileStatus(key);
+ OzoneFileStatus status = bucket.getFileStatus(key, headOp);
return toFileStatusAdapter(status, userName, uri, qualifiedPath,
ofsPath.getNonKeyPath());
}
diff --git
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneFileSystem.java
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneFileSystem.java
index b61dca450cb..6be8ebbddb6 100644
---
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneFileSystem.java
+++
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneFileSystem.java
@@ -1070,6 +1070,17 @@ public FileStatus getFileStatus(Path f) throws
IOException {
}
public FileStatusAdapter getFileStatusAdapter(Path f) throws IOException {
+ return getFileStatusAdapter(f, false);
+ }
+
+ /**
+ * @param headOp when true, requests a metadata-only (type) check so the OM
+ * skips the pipeline refresh (SCM round-trip) and datanode
+ * sorting. Used by {@link #isDirectory(Path)}/{@link
#isFile(Path)},
+ * which only need the entry type.
+ */
+ public FileStatusAdapter getFileStatusAdapter(Path f, boolean headOp)
+ throws IOException {
incrementCounter(Statistic.INVOCATION_GET_FILE_STATUS, 1);
statistics.incrementReadOps(1);
LOG.trace("getFileStatus() path:{}", f);
@@ -1081,8 +1092,8 @@ public FileStatusAdapter getFileStatusAdapter(Path f)
throws IOException {
}
FileStatusAdapter fileStatus = null;
try {
- fileStatus =
- adapter.getFileStatus(key, uri, qualifiedPath, getUsername());
+ fileStatus =
+ adapter.getFileStatus(key, uri, qualifiedPath, getUsername(), headOp);
} catch (IOException e) {
if (e instanceof OMException) {
OMException ex = (OMException) e;
@@ -1163,14 +1174,28 @@ public FileStatus[] globStatus(Path pathPattern,
PathFilter filter)
@SuppressWarnings("deprecation")
public boolean isDirectory(Path f) throws IOException {
incrementCounter(Statistic.INVOCATION_IS_DIRECTORY);
- return super.isDirectory(f);
+ try {
+ // headOp: only the entry type is needed, so skip the pipeline refresh.
+ // Read the type straight off the adapter to avoid the extra work of
+ // building a Hadoop FileStatus.
+ return getFileStatusAdapter(f, true).isDir();
+ } catch (FileNotFoundException e) {
+ return false;
+ }
}
@Override
@SuppressWarnings("deprecation")
public boolean isFile(Path f) throws IOException {
incrementCounter(Statistic.INVOCATION_IS_FILE);
- return super.isFile(f);
+ try {
+ // headOp: only the entry type is needed, so skip the pipeline refresh.
+ // Read the type straight off the adapter to avoid the extra work of
+ // building a Hadoop FileStatus.
+ return getFileStatusAdapter(f, true).isFile();
+ } catch (FileNotFoundException e) {
+ return false;
+ }
}
@Override
diff --git
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneClientAdapter.java
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneClientAdapter.java
index b4ec884fccb..57003f1b455 100644
---
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneClientAdapter.java
+++
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneClientAdapter.java
@@ -89,6 +89,17 @@ Token<OzoneTokenIdentifier> getDelegationToken(String
renewer)
FileStatusAdapter getFileStatus(String key, URI uri,
Path qualifiedPath, String userName) throws IOException;
+ /**
+ * @param headOp when true, request a metadata-only (type) check so the OM
+ * skips the pipeline refresh (SCM round-trip) and datanode
+ * sorting. Implementations that cannot honor it fall back to a
+ * full status.
+ */
+ default FileStatusAdapter getFileStatus(String key, URI uri,
+ Path qualifiedPath, String userName, boolean headOp) throws IOException {
+ return getFileStatus(key, uri, qualifiedPath, userName);
+ }
+
boolean isFSOptimizedBucket();
FileChecksum getFileChecksum(String keyName, long length) throws IOException;
diff --git
a/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicRootedOzoneClientAdapterHeadOp.java
b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicRootedOzoneClientAdapterHeadOp.java
new file mode 100644
index 00000000000..8f67adef134
--- /dev/null
+++
b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicRootedOzoneClientAdapterHeadOp.java
@@ -0,0 +1,176 @@
+/*
+ * 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.hadoop.fs.ozone;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.URI;
+import java.time.Instant;
+import java.util.Collections;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.ozone.OFSPath;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.OzoneFileStatus;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Unit tests for headOp propagation through
+ * {@link BasicRootedOzoneClientAdapterImpl#getFileStatus} (HDDS-15678). Uses a
+ * partial mock so no OM connection is required.
+ */
+public class TestBasicRootedOzoneClientAdapterHeadOp {
+
+ private static final URI URI_OFS = URI.create("ofs://om/");
+ private static final Path WORKING_DIR = new Path("/");
+
+ private BasicRootedOzoneClientAdapterImpl adapter;
+ private OzoneBucket bucket;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ adapter = mock(BasicRootedOzoneClientAdapterImpl.class,
CALLS_REAL_METHODS);
+ bucket = mock(OzoneBucket.class);
+ doReturn(bucket).when(adapter).getBucket(any(OFSPath.class), eq(false));
+
+ // Inject a mock object store so the volume/snapshot dispatch branches can
+ // run without a live OM connection.
+ OzoneVolume volume = mock(OzoneVolume.class);
+ when(volume.getName()).thenReturn("vol");
+ when(volume.getOwner()).thenReturn("user");
+ when(volume.getCreationTime()).thenReturn(Instant.EPOCH);
+ ObjectStore objectStore = mock(ObjectStore.class);
+ when(objectStore.getVolume(anyString())).thenReturn(volume);
+ Field field =
+
BasicRootedOzoneClientAdapterImpl.class.getDeclaredField("objectStore");
+ field.setAccessible(true);
+ field.set(adapter, objectStore);
+ }
+
+ private static OzoneFileStatus fileStatus(boolean isDir) {
+ OmKeyInfo keyInfo = new OmKeyInfo.Builder()
+ .setVolumeName("vol")
+ .setBucketName("bucket")
+ .setKeyName("key")
+ .setReplicationConfig(RatisReplicationConfig.getInstance(
+ HddsProtos.ReplicationFactor.THREE))
+ .setOmKeyLocationInfos(Collections.emptyList())
+ .setDataSize(0)
+ .setCreationTime(0)
+ .setModificationTime(0)
+ .setAcls(Collections.emptyList())
+ .build();
+ return new OzoneFileStatus(keyInfo, 512, isDir);
+ }
+
+ @Test
+ public void keyPathThreadsHeadOp() throws IOException {
+ when(bucket.getFileStatus(anyString(), anyBoolean()))
+ .thenReturn(fileStatus(false));
+
+ assertFalse(adapter.getFileStatus("/vol/bucket/key", URI_OFS, WORKING_DIR,
+ "user", true).isDir());
+
+ ArgumentCaptor<Boolean> headOp = ArgumentCaptor.forClass(Boolean.class);
+ verify(bucket).getFileStatus(anyString(), headOp.capture());
+ assertTrue(headOp.getValue());
+ }
+
+ @Test
+ public void fourArgOverloadDoesNotUseHeadOp() throws IOException {
+ when(bucket.getFileStatus(anyString(), anyBoolean()))
+ .thenReturn(fileStatus(true));
+
+ assertTrue(adapter.getFileStatus("/vol/bucket/key", URI_OFS, WORKING_DIR,
+ "user").isDir());
+ verify(bucket).getFileStatus(anyString(), eq(false));
+ }
+
+ @Test
+ public void rootPathReturnsDirectory() throws IOException {
+ assertTrue(adapter.getFileStatus("/", URI_OFS, WORKING_DIR, "user", true)
+ .isDir());
+ }
+
+ @Test
+ public void fileNotFoundMappedToFileNotFoundException() throws IOException {
+ when(bucket.getFileStatus(anyString(), anyBoolean()))
+ .thenThrow(new OMException("missing",
+ OMException.ResultCodes.FILE_NOT_FOUND));
+ assertThrows(FileNotFoundException.class,
+ () -> adapter.getFileStatus("/vol/bucket/key", URI_OFS, WORKING_DIR,
+ "user", true));
+ }
+
+ @Test
+ public void otherOMExceptionPropagates() throws IOException {
+ when(bucket.getFileStatus(anyString(), anyBoolean()))
+ .thenThrow(new OMException("boom",
+ OMException.ResultCodes.INTERNAL_ERROR));
+ assertThrows(OMException.class,
+ () -> adapter.getFileStatus("/vol/bucket/key", URI_OFS, WORKING_DIR,
+ "user", true));
+ }
+
+ @Test
+ public void bucketNotFoundMappedToFileNotFoundException() throws IOException
{
+ when(bucket.getFileStatus(anyString(), anyBoolean()))
+ .thenThrow(new OMException("no bucket",
+ OMException.ResultCodes.BUCKET_NOT_FOUND));
+ assertThrows(FileNotFoundException.class,
+ () -> adapter.getFileStatus("/vol/bucket/key", URI_OFS, WORKING_DIR,
+ "user", true));
+ }
+
+ @Test
+ public void volumePathReturnsDirectory() throws IOException {
+ assertTrue(adapter.getFileStatus("/vol", URI_OFS, WORKING_DIR, "user",
true)
+ .isDir());
+ }
+
+ @Test
+ public void snapshotIndicatorPathReturnsDirectory() throws IOException {
+ when(bucket.getVolumeName()).thenReturn("vol");
+ when(bucket.getName()).thenReturn("bucket");
+ when(bucket.getCreationTime()).thenReturn(Instant.EPOCH);
+ // keyName == ".snapshot" is the snapshot indicator path.
+ assertTrue(adapter.getFileStatus("/vol/bucket/.snapshot", URI_OFS,
+ WORKING_DIR, "user", true).isDir());
+ }
+}
diff --git
a/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestRootedOzoneFileSystemHeadOp.java
b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestRootedOzoneFileSystemHeadOp.java
new file mode 100644
index 00000000000..ab844b4350a
--- /dev/null
+++
b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestRootedOzoneFileSystemHeadOp.java
@@ -0,0 +1,174 @@
+/*
+ * 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.hadoop.fs.ozone;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.URI;
+import org.apache.hadoop.fs.BlockLocation;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Unit tests for the head-op (metadata-only) type checks on OFS
+ * ({@link BasicRootedOzoneFileSystem#isDirectory}/{@link
+ * BasicRootedOzoneFileSystem#isFile}) added in HDDS-15678. Uses a mock adapter
+ * so no cluster is required.
+ */
+public class TestRootedOzoneFileSystemHeadOp {
+
+ private BasicRootedOzoneClientAdapterImpl adapter;
+ private BasicRootedOzoneFileSystem fs;
+
+ /** Test FS that injects a mock adapter instead of connecting to OM. */
+ private final class MockAdapterFs extends BasicRootedOzoneFileSystem {
+ @Override
+ protected OzoneClientAdapter createAdapter(ConfigurationSource conf,
+ String omHost, int omPort) {
+ return adapter;
+ }
+ }
+
+ @BeforeEach
+ public void setUp() throws IOException {
+ adapter = mock(BasicRootedOzoneClientAdapterImpl.class);
+ fs = new MockAdapterFs();
+ fs.initialize(URI.create("ofs://om/"), new OzoneConfiguration());
+ }
+
+ private static FileStatusAdapter status(Path path, boolean isDir) {
+ return new FileStatusAdapter(0L, 0L, path, isDir, (short) 3, 0L, 0L, 0L,
+ (short) 0, "user", "group", null, new BlockLocation[0], false, false);
+ }
+
+ private void stubStatus(boolean isDir) throws IOException {
+ when(adapter.getFileStatus(anyString(), any(URI.class), any(Path.class),
+ anyString(), anyBoolean()))
+ .thenAnswer(inv -> status(inv.getArgument(2), isDir));
+ }
+
+ private void stubThrow(IOException e) throws IOException {
+ when(adapter.getFileStatus(anyString(), any(URI.class), any(Path.class),
+ anyString(), anyBoolean())).thenThrow(e);
+ }
+
+ @Test
+ public void isDirectoryUsesHeadOp() throws IOException {
+ stubStatus(true);
+ Path dir = new Path("/vol/bucket/dir");
+
+ assertTrue(fs.isDirectory(dir));
+ assertFalse(fs.isFile(dir));
+
+ ArgumentCaptor<Boolean> headOp = ArgumentCaptor.forClass(Boolean.class);
+ verify(adapter, org.mockito.Mockito.atLeastOnce()).getFileStatus(
+ anyString(), any(URI.class), any(Path.class), anyString(),
+ headOp.capture());
+ for (Boolean v : headOp.getAllValues()) {
+ assertTrue(v, "isDirectory/isFile must request headOp");
+ }
+ }
+
+ @Test
+ public void isFileUsesHeadOp() throws IOException {
+ stubStatus(false);
+ Path file = new Path("/vol/bucket/file");
+
+ assertTrue(fs.isFile(file));
+ assertFalse(fs.isDirectory(file));
+ }
+
+ @Test
+ public void fullGetFileStatusDoesNotUseHeadOp() throws IOException {
+ stubStatus(false);
+ fs.getFileStatus(new Path("/vol/bucket/file"));
+ verify(adapter).getFileStatus(anyString(), any(URI.class), any(Path.class),
+ anyString(), eq(false));
+ }
+
+ @Test
+ public void missingPathReturnsFalse() throws IOException {
+ // Each *_NOT_FOUND result is mapped to FileNotFoundException and
swallowed.
+ for (OMException.ResultCodes code : new OMException.ResultCodes[] {
+ OMException.ResultCodes.KEY_NOT_FOUND,
+ OMException.ResultCodes.BUCKET_NOT_FOUND,
+ OMException.ResultCodes.VOLUME_NOT_FOUND}) {
+ stubThrow(new OMException("not found", code));
+ Path missing = new Path("/vol/bucket/missing");
+ assertFalse(fs.isDirectory(missing));
+ assertFalse(fs.isFile(missing));
+ }
+ }
+
+ @Test
+ public void nonExistenceOMExceptionPropagates() throws IOException {
+ stubThrow(new OMException("denied",
+ OMException.ResultCodes.PERMISSION_DENIED));
+ assertThrows(OMException.class,
+ () -> fs.isDirectory(new Path("/vol/bucket/x")));
+ }
+
+ @Test
+ public void plainIOExceptionPropagates() throws IOException {
+ stubThrow(new IOException("io"));
+ assertThrows(IOException.class,
+ () -> fs.isFile(new Path("/vol/bucket/x")));
+ }
+
+ @Test
+ public void distCpNonePathReturnsFalse() throws IOException {
+ // Key "NONE" is rejected before any RPC.
+ assertFalse(fs.isDirectory(new Path("/NONE")));
+ }
+
+ /**
+ * The OzoneClientAdapter headOp overload has a default that delegates to the
+ * 4-arg method (used by the non-rooted o3fs adapter, which keeps full
status).
+ */
+ @Test
+ public void adapterHeadOpDefaultDelegates() throws IOException {
+ OzoneClientAdapter mockAdapter =
+ mock(OzoneClientAdapter.class, CALLS_REAL_METHODS);
+ URI uri = URI.create("ofs://om/");
+ Path path = new Path("/vol/bucket/file");
+ FileStatusAdapter expected = status(path, false);
+ doReturn(expected).when(mockAdapter).getFileStatus("k", uri, path, "user");
+
+ assertSame(expected,
+ mockAdapter.getFileStatus("k", uri, path, "user", true));
+ verify(mockAdapter).getFileStatus("k", uri, path, "user");
+ }
+}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index 8cc421c5c6e..0f92024723a 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -578,7 +578,8 @@ public String getCanonicalServiceName() {
@Override
public OzoneFileStatus getOzoneFileStatus(String volumeName,
- String bucketName, String keyName)
+ String bucketName, String keyName,
+ boolean headOp)
throws IOException {
return null;
}
diff --git a/pom.xml b/pom.xml
index b63164afa94..07756ec33d1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -78,7 +78,7 @@
<enforced.maven.version>[3.6.3,)</enforced.maven.version>
<errorprone-annotations.version>2.29.2</errorprone-annotations.version>
<!-- test groups excluded by default (without any manual profile
activation) -->
- <excluded-test-groups>unhealthy</excluded-test-groups>
+ <excluded-test-groups>unhealthy | benchmark</excluded-test-groups>
<exec-maven-plugin.version>3.6.3</exec-maven-plugin.version>
<failIfNoTests>false</failIfNoTests>
<frontend-maven-plugin.version>1.15.4</frontend-maven-plugin.version>
@@ -216,7 +216,7 @@
<!-- number of threads/forks to use when running tests in parallel, see
parallel-tests profile -->
<testsThreadCount>4</testsThreadCount>
<!-- test groups excluded in CI (except in dedicated profiles for flaky)
-->
- <unstable-test-groups>flaky | slow | unhealthy</unstable-test-groups>
+ <unstable-test-groups>flaky | slow | unhealthy |
benchmark</unstable-test-groups>
<vault.driver.version>5.1.0</vault.driver.version>
<weld-servlet.version>3.1.9.Final</weld-servlet.version>
<woodstox.version>5.4.0</woodstox.version>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]