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]

Reply via email to