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 7196f98d860 HDDS-15951. Reduce ofs getFileChecksum to a single OM RPC 
(#11380)
7196f98d860 is described below

commit 7196f98d8601827755bb338bd9aa8d24a7586e95
Author: Andrey Yarovoy <[email protected]>
AuthorDate: Thu Oct 8 12:54:44 2026 -0400

    HDDS-15951. Reduce ofs getFileChecksum to a single OM RPC (#11380)
    
    Generated-by: Claude Code (claude-opus)
---
 .../apache/hadoop/ozone/OzoneManagerVersion.java   |   5 +
 .../ozone/shell/keys/ChecksumKeyHandler.java       |   5 +-
 .../hadoop/ozone/client/OzoneClientUtils.java      |  23 +-
 .../client/checksum/BaseFileChecksumHelper.java    |  19 +-
 .../client/checksum/ChecksumHelperFactory.java     |  10 +-
 .../client/checksum/ECFileChecksumHelper.java      |   6 +-
 .../checksum/ReplicatedFileChecksumHelper.java     |   6 +-
 .../hadoop/ozone/client/TestOzoneClientUtils.java  |  12 +-
 .../client/checksum/FileChecksumBenchmark.java     |   9 +-
 .../client/checksum/TestFileChecksumHelper.java    |  26 +-
 .../ozone/AbstractRootedOzoneFileSystemTest.java   |  61 ++++
 .../ozone/TestOfsGetFileChecksumRpcBenchmark.java  | 314 +++++++++++++++++++++
 .../om/snapshot/TestOzoneManagerSnapshotAcl.java   |  30 +-
 .../java/org/apache/hadoop/ozone/om/OMMetrics.java |   5 +
 .../apache/hadoop/ozone/om/OmMetadataReader.java   |  12 +
 .../hadoop/ozone/om/TestOMMetadataReader.java      |  60 ++++
 .../fs/ozone/BasicOzoneClientAdapterImpl.java      |   6 +-
 .../ozone/BasicRootedOzoneClientAdapterImpl.java   |  46 ++-
 ...sicRootedOzoneClientAdapterGetFileChecksum.java | 199 +++++++++++++
 19 files changed, 775 insertions(+), 79 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneManagerVersion.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneManagerVersion.java
index 9bd041c615c..f33274dde83 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneManagerVersion.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneManagerVersion.java
@@ -67,6 +67,11 @@ public enum OzoneManagerVersion implements ComponentVersion {
           + "buckets server-side, so file system clients no longer need the "
           + "client-side InfoBucket layout check"),
 
+  LOOKUP_FILE_REJECTS_OBS(15,
+      "OzoneManager version that rejects lookupFile on OBJECT_STORE buckets "
+          + "server-side, so file system clients no longer need the "
+          + "client-side InfoBucket layout check for getFileChecksum"),
+
   FUTURE_VERSION(-1, "Used internally in the client when the server side is "
       + " newer and an unknown server version has arrived to the client.");
 
diff --git 
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/keys/ChecksumKeyHandler.java
 
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/keys/ChecksumKeyHandler.java
index c36c1af9910..cecbb8cc020 100644
--- 
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/keys/ChecksumKeyHandler.java
+++ 
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/keys/ChecksumKeyHandler.java
@@ -59,8 +59,9 @@ protected void execute(OzoneClient client, OzoneAddress 
address)
   protected FileChecksum getFileChecksum(OzoneVolume vol, OzoneBucket bucket,
       String keyName, long dataSize, ClientProtocol clientProxy)
       throws IOException {
-    return getFileChecksumWithCombineMode(vol, bucket, keyName, dataSize, mode,
-        clientProxy);
+    // LookupKey, not LookupFile: the CLI must keep working on OBJECT_STORE 
buckets.
+    return getFileChecksumWithCombineMode(vol.getName(), bucket.getName(), 
keyName, dataSize, mode,
+        clientProxy, false);
   }
 
   /**
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClientUtils.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClientUtils.java
index 0d5504bb167..47a13a04722 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClientUtils.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClientUtils.java
@@ -44,6 +44,7 @@
 import org.apache.hadoop.ozone.om.helpers.BucketLayout;
 import org.apache.hadoop.ozone.om.helpers.OmKeyArgs;
 import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -220,23 +221,31 @@ decision of finding the replication config( either from 
bucket defaults
         .parse(clientReplicationType, clientReplication, clientSideConfig);
   }
 
-  public static FileChecksum getFileChecksumWithCombineMode(OzoneVolume volume,
-      OzoneBucket bucket, String keyName, long length,
+  /**
+   * Computes the checksum of a key.
+   *
+   * @param useLookupFile when true the key is read with {@code lookupFile}, 
which rejects OBJECT_STORE buckets
+   *     server-side; when false {@code lookupKey} is used, which accepts any 
bucket layout
+   */
+  @SuppressWarnings("checkstyle:ParameterNumber")
+  public static FileChecksum getFileChecksumWithCombineMode(String volumeName,
+      String bucketName, String keyName, long length,
       OzoneClientConfig.ChecksumCombineMode combineMode,
-      ClientProtocol rpcClient) throws IOException {
+      ClientProtocol rpcClient, boolean useLookupFile) throws IOException {
     Preconditions.checkArgument(length >= 0);
 
     if (keyName.isEmpty()) {
       return null;
     }
-    OmKeyArgs keyArgs = new OmKeyArgs.Builder().setVolumeName(volume.getName())
-        .setBucketName(bucket.getName()).setKeyName(keyName)
+    OmKeyArgs keyArgs = new OmKeyArgs.Builder().setVolumeName(volumeName)
+        .setBucketName(bucketName).setKeyName(keyName)
         .setSortDatanodesInPipeline(true)
         .setLatestVersionLocation(true).build();
-    OmKeyInfo keyInfo = rpcClient.getOzoneManagerClient().lookupKey(keyArgs);
+    OzoneManagerProtocol omClient = rpcClient.getOzoneManagerClient();
+    OmKeyInfo keyInfo = useLookupFile ? omClient.lookupFile(keyArgs) : 
omClient.lookupKey(keyArgs);
     BaseFileChecksumHelper helper = ChecksumHelperFactory
         .getChecksumHelper(keyInfo.getReplicationConfig().getReplicationType(),
-            volume, bucket, keyName, length, combineMode, rpcClient, keyInfo);
+            volumeName, bucketName, keyName, length, combineMode, rpcClient, 
keyInfo);
     helper.compute();
     return helper.getFileChecksum();
   }
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/BaseFileChecksumHelper.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/BaseFileChecksumHelper.java
index da85d4d3e17..7c38b2b5049 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/BaseFileChecksumHelper.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/BaseFileChecksumHelper.java
@@ -30,8 +30,6 @@
 import org.apache.hadoop.hdds.scm.XceiverClientFactory;
 import org.apache.hadoop.io.DataOutputBuffer;
 import org.apache.hadoop.io.MD5Hash;
-import org.apache.hadoop.ozone.client.OzoneBucket;
-import org.apache.hadoop.ozone.client.OzoneVolume;
 import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
 import org.apache.hadoop.ozone.client.rpc.RpcClient;
 import org.apache.hadoop.ozone.om.helpers.OmKeyArgs;
@@ -50,8 +48,8 @@ public abstract class BaseFileChecksumHelper {
       LoggerFactory.getLogger(BaseFileChecksumHelper.class);
   private OmKeyInfo keyInfo;
 
-  private OzoneVolume volume;
-  private OzoneBucket bucket;
+  private String volumeName;
+  private String bucketName;
   private String keyName;
   private final long length;
 
@@ -68,12 +66,12 @@ public abstract class BaseFileChecksumHelper {
   private long crcPerBlock = 0;
 
   // initialization
-  public BaseFileChecksumHelper(OzoneVolume volume, OzoneBucket bucket,
+  public BaseFileChecksumHelper(String volumeName, String bucketName,
       String keyName, long length,
       OzoneClientConfig.ChecksumCombineMode checksumCombineMode,
       ClientProtocol rpcClient, OmKeyInfo keyInfo) throws IOException {
-    this.volume = volume;
-    this.bucket = bucket;
+    this.volumeName = volumeName;
+    this.bucketName = bucketName;
     this.keyName = keyName;
     this.length = length;
     this.combineMode = checksumCombineMode;
@@ -87,8 +85,7 @@ public BaseFileChecksumHelper(OzoneVolume volume, OzoneBucket 
bucket,
   }
 
   protected String getSrc() {
-    return "Volume: " + volume.getName() + " Bucket: " + bucket.getName() + " "
-        + keyName;
+    return "Volume: " + volumeName + " Bucket: " + bucketName + " " + keyName;
   }
 
   protected long getLength() {
@@ -268,8 +265,8 @@ private void fetchBlocks() throws IOException {
       OzoneManagerProtocol ozoneManagerClient =
           getRpcClient().getOzoneManagerClient();
       OmKeyArgs keyArgs =
-          new OmKeyArgs.Builder().setVolumeName(volume.getName())
-              .setBucketName(bucket.getName()).setKeyName(keyName)
+          new OmKeyArgs.Builder().setVolumeName(volumeName)
+              .setBucketName(bucketName).setKeyName(keyName)
               .setSortDatanodesInPipeline(true)
               .setLatestVersionLocation(true).build();
       keyInfo = ozoneManagerClient.lookupKey(keyArgs);
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ChecksumHelperFactory.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ChecksumHelperFactory.java
index 1c85c9f91d1..333231b27d2 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ChecksumHelperFactory.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ChecksumHelperFactory.java
@@ -20,8 +20,6 @@
 import java.io.IOException;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import org.apache.hadoop.hdds.scm.OzoneClientConfig;
-import org.apache.hadoop.ozone.client.OzoneBucket;
-import org.apache.hadoop.ozone.client.OzoneVolume;
 import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
 import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
 
@@ -35,16 +33,16 @@ private ChecksumHelperFactory() {
 
   @SuppressWarnings("checkstyle:ParameterNumber")
   public static BaseFileChecksumHelper getChecksumHelper(
-      HddsProtos.ReplicationType replicationType, OzoneVolume volume,
-      OzoneBucket bucket, String keyName, long length,
+      HddsProtos.ReplicationType replicationType, String volumeName,
+      String bucketName, String keyName, long length,
       OzoneClientConfig.ChecksumCombineMode combineMode,
       ClientProtocol rpcClient, OmKeyInfo keyInfo) throws IOException {
 
     if (replicationType == HddsProtos.ReplicationType.EC) {
-      return new ECFileChecksumHelper(volume, bucket, keyName, length,
+      return new ECFileChecksumHelper(volumeName, bucketName, keyName, length,
           combineMode, rpcClient, keyInfo);
     } else {
-      return new ReplicatedFileChecksumHelper(volume, bucket, keyName, length,
+      return new ReplicatedFileChecksumHelper(volumeName, bucketName, keyName, 
length,
           combineMode, rpcClient, keyInfo);
     }
   }
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ECFileChecksumHelper.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ECFileChecksumHelper.java
index faaa1e7725c..3d8f89614e9 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ECFileChecksumHelper.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ECFileChecksumHelper.java
@@ -37,8 +37,6 @@
 import org.apache.hadoop.hdds.scm.pipeline.PipelineID;
 import org.apache.hadoop.hdds.scm.storage.ContainerProtocolCalls;
 import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
-import org.apache.hadoop.ozone.client.OzoneBucket;
-import org.apache.hadoop.ozone.client.OzoneVolume;
 import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
 import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.OmKeyLocationInfo;
@@ -49,11 +47,11 @@
  */
 public class ECFileChecksumHelper extends BaseFileChecksumHelper {
 
-  public ECFileChecksumHelper(OzoneVolume volume, OzoneBucket bucket,
+  public ECFileChecksumHelper(String volumeName, String bucketName,
       String keyName, long length, OzoneClientConfig.ChecksumCombineMode
       checksumCombineMode, ClientProtocol rpcClient, OmKeyInfo keyInfo)
       throws IOException {
-    super(volume, bucket, keyName, length, checksumCombineMode, rpcClient,
+    super(volumeName, bucketName, keyName, length, checksumCombineMode, 
rpcClient,
         keyInfo);
   }
 
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ReplicatedFileChecksumHelper.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ReplicatedFileChecksumHelper.java
index 42a07690a3f..7dcc7fc94f6 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ReplicatedFileChecksumHelper.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/checksum/ReplicatedFileChecksumHelper.java
@@ -26,8 +26,6 @@
 import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
 import org.apache.hadoop.hdds.scm.storage.ContainerProtocolCalls;
 import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
-import org.apache.hadoop.ozone.client.OzoneBucket;
-import org.apache.hadoop.ozone.client.OzoneVolume;
 import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
 import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.OmKeyLocationInfo;
@@ -38,11 +36,11 @@
  */
 public class ReplicatedFileChecksumHelper extends BaseFileChecksumHelper {
 
-  public ReplicatedFileChecksumHelper(OzoneVolume volume, OzoneBucket bucket,
+  public ReplicatedFileChecksumHelper(String volumeName, String bucketName,
       String keyName, long length,
       OzoneClientConfig.ChecksumCombineMode checksumCombineMode,
       ClientProtocol rpcClient, OmKeyInfo keyInfo) throws IOException {
-    super(volume, bucket, keyName, length, checksumCombineMode, rpcClient,
+    super(volumeName, bucketName, keyName, length, checksumCombineMode, 
rpcClient,
         keyInfo);
   }
 
diff --git 
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestOzoneClientUtils.java
 
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestOzoneClientUtils.java
index c2154fe880b..b4923c536ad 100644
--- 
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestOzoneClientUtils.java
+++ 
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/TestOzoneClientUtils.java
@@ -48,26 +48,22 @@ public class TestOzoneClientUtils {
 
   @Test()
   public void testNegativeLength() throws IOException {
-    OzoneVolume volume = mock(OzoneVolume.class);
-    OzoneBucket bucket = mock(OzoneBucket.class);
     String keyName = "dummy";
     ClientProtocol clientProtocol = mock(ClientProtocol.class);
     assertThrows(IllegalArgumentException.class, () ->
-        OzoneClientUtils.getFileChecksumWithCombineMode(volume, bucket, 
keyName,
+        OzoneClientUtils.getFileChecksumWithCombineMode("vol", "bucket", 
keyName,
         -1, OzoneClientConfig.ChecksumCombineMode.MD5MD5CRC,
-        clientProtocol));
+        clientProtocol, false));
   }
 
   @Test
   public void testEmptyKeyName() throws IOException {
-    OzoneVolume volume = mock(OzoneVolume.class);
-    OzoneBucket bucket = mock(OzoneBucket.class);
     String keyName = "";
     ClientProtocol clientProtocol = mock(ClientProtocol.class);
     FileChecksum checksum =
-        OzoneClientUtils.getFileChecksumWithCombineMode(volume, bucket, 
keyName,
+        OzoneClientUtils.getFileChecksumWithCombineMode("vol", "bucket", 
keyName,
             1, OzoneClientConfig.ChecksumCombineMode.MD5MD5CRC,
-            clientProtocol);
+            clientProtocol, false);
 
     assertNull(checksum);
   }
diff --git 
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/checksum/FileChecksumBenchmark.java
 
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/checksum/FileChecksumBenchmark.java
index 6af4af3d839..1996a846173 100644
--- 
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/checksum/FileChecksumBenchmark.java
+++ 
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/checksum/FileChecksumBenchmark.java
@@ -49,8 +49,6 @@
 import org.apache.hadoop.hdds.scm.XceiverClientSpi;
 import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
 import org.apache.hadoop.hdds.scm.pipeline.PipelineID;
-import org.apache.hadoop.ozone.client.OzoneBucket;
-import org.apache.hadoop.ozone.client.OzoneVolume;
 import org.apache.hadoop.ozone.client.rpc.RpcClient;
 import org.apache.hadoop.ozone.om.helpers.OmKeyArgs;
 import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
@@ -323,11 +321,6 @@ private static BenchmarkResult measure(int latencyMs, 
ECReplicationConfig repCon
     when(mockRpcClient.getOzoneManagerClient()).thenReturn(mockOm);
     when(mockRpcClient.getXceiverClientManager()).thenReturn(xceiverFactory);
 
-    OzoneVolume mockVolume = mock(OzoneVolume.class);
-    when(mockVolume.getName()).thenReturn("vol");
-    OzoneBucket mockBucket = mock(OzoneBucket.class);
-    when(mockBucket.getName()).thenReturn("bucket");
-
     OzoneClientConfig.ChecksumCombineMode combineMode =
         OzoneClientConfig.ChecksumCombineMode.COMPOSITE_CRC;
 
@@ -344,7 +337,7 @@ private static BenchmarkResult measure(int latencyMs, 
ECReplicationConfig repCon
                 .setLatestVersionLocation(true)
                 .build());
         new ECFileChecksumHelper(
-            mockVolume, mockBucket, keyName, FILE_SIZE, combineMode,
+            "vol", "bucket", keyName, FILE_SIZE, combineMode,
             mockRpcClient, keyInfo)
             .compute();
       } catch (IOException e) {
diff --git 
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/checksum/TestFileChecksumHelper.java
 
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/checksum/TestFileChecksumHelper.java
index 42243ae2d38..fa02ebb91b6 100644
--- 
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/checksum/TestFileChecksumHelper.java
+++ 
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/checksum/TestFileChecksumHelper.java
@@ -141,13 +141,13 @@ private OmKeyInfo omKeyInfo(ReplicationType type, 
FileChecksum cachedChecksum, L
         .build();
   }
 
-  private BaseFileChecksumHelper checksumHelper(ReplicationType type, 
OzoneVolume mockVolume, OzoneBucket mockBucket,
+  private BaseFileChecksumHelper checksumHelper(ReplicationType type, String 
volumeName, String bucketName,
       int length, OzoneClientConfig.ChecksumCombineMode combineMode, RpcClient 
mockRpcClient, OmKeyInfo keyInfo)
       throws IOException {
     return type == ReplicationType.RATIS ? new ReplicatedFileChecksumHelper(
-        mockVolume, mockBucket, "dummy", length, combineMode, mockRpcClient, 
keyInfo)
+        volumeName, bucketName, "dummy", length, combineMode, mockRpcClient, 
keyInfo)
         : new ECFileChecksumHelper(
-        mockVolume, mockBucket, "dummy", length, combineMode, mockRpcClient, 
keyInfo);
+        volumeName, bucketName, "dummy", length, combineMode, mockRpcClient, 
keyInfo);
   }
 
   private Pipeline pipeline(ReplicationType type, List<DatanodeDetails> 
datanodeDetails) {
@@ -169,20 +169,15 @@ public void testEmptyBlock(ReplicationType helperType) 
throws IOException {
     // test the file checksum of a file with an empty block.
     RpcClient mockRpcClient = mock(RpcClient.class);
     OmKeyInfo omKeyInfo = omKeyInfo(helperType, noCachedChecksum, new 
ArrayList<>());
-    OzoneVolume mockVolume = mock(OzoneVolume.class);
-    OzoneBucket mockBucket = mock(OzoneBucket.class);
     OzoneClientConfig.ChecksumCombineMode combineMode =
         OzoneClientConfig.ChecksumCombineMode.MD5MD5CRC;
 
     OzoneManagerProtocol om = mock(OzoneManagerProtocol.class);
     when(mockRpcClient.getOzoneManagerClient()).thenReturn(om);
     when(om.lookupKey(any())).thenReturn(omKeyInfo);
-    when(mockVolume.getName()).thenReturn("vol1");
-    when(mockBucket.getName()).thenReturn("bucket1");
-
 
     BaseFileChecksumHelper helper =
-        checksumHelper(helperType, mockVolume, mockBucket, 10, combineMode, 
mockRpcClient, omKeyInfo);
+        checksumHelper(helperType, "vol1", "bucket1", 10, combineMode, 
mockRpcClient, omKeyInfo);
     helper.compute();
     FileChecksum fileChecksum = helper.getFileChecksum();
     assertInstanceOf(MD5MD5CRC32GzipFileChecksum.class, fileChecksum);
@@ -191,7 +186,7 @@ public void testEmptyBlock(ReplicationType helperType) 
throws IOException {
 
     // test negative length
     helper =
-        checksumHelper(helperType, mockVolume, mockBucket, -1, combineMode, 
mockRpcClient, omKeyInfo);
+        checksumHelper(helperType, "vol1", "bucket1", -1, combineMode, 
mockRpcClient, omKeyInfo);
     helper.compute();
     assertNull(helper.getKeyLocationInfoList());
   }
@@ -231,16 +226,11 @@ public XceiverClientReply sendCommandAsync(
     when(mockRpcClient.getOzoneManagerClient()).thenReturn(om);
     when(om.lookupKey(any())).thenReturn(omKeyInfo);
 
-    OzoneVolume mockVolume = mock(OzoneVolume.class);
-    when(mockVolume.getName()).thenReturn("vol1");
-    OzoneBucket mockBucket = mock(OzoneBucket.class);
-    when(mockBucket.getName()).thenReturn("bucket1");
-
     OzoneClientConfig.ChecksumCombineMode combineMode =
         OzoneClientConfig.ChecksumCombineMode.MD5MD5CRC;
 
     BaseFileChecksumHelper helper = checksumHelper(
-        helperType, mockVolume, mockBucket, 10, combineMode, mockRpcClient, 
omKeyInfo);
+        helperType, "vol1", "bucket1", 10, combineMode, mockRpcClient, 
omKeyInfo);
 
     helper.compute();
     FileChecksum fileChecksum = helper.getFileChecksum();
@@ -254,7 +244,7 @@ public XceiverClientReply sendCommandAsync(
         thenReturn(omKeyInfoWithChecksum);
 
     helper = checksumHelper(
-        helperType, mockVolume, mockBucket, 10, combineMode, mockRpcClient, 
omKeyInfo);
+        helperType, "vol1", "bucket1", 10, combineMode, mockRpcClient, 
omKeyInfo);
 
     helper.compute();
     fileChecksum = helper.getFileChecksum();
@@ -349,7 +339,7 @@ public void testPutKeyChecksum() throws IOException {
       OmKeyInfo keyInfo = rpcClient.getKeyInfo(
           volume.getName(), bucket.getName(), keyName, false);
       ReplicatedFileChecksumHelper helper = new ReplicatedFileChecksumHelper(
-          volume, bucket, keyName, 10, combineMode, rpcClient, keyInfo);
+          volume.getName(), bucket.getName(), keyName, 10, combineMode, 
rpcClient, keyInfo);
 
       helper.compute();
       FileChecksum fileChecksum = helper.getFileChecksum();
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 197d100d86c..0b31625e6f8 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
@@ -1881,6 +1881,67 @@ void testGetFileStatusRejectsObsBucket() throws 
Exception {
         "getFileStatus must not trigger InfoBucket");
   }
 
+  @Test
+  void testGetFileChecksumUsesSingleOmRpc() throws Exception {
+    Path filePath = new Path(bucketPath,
+        "checksum-rpc-" + RandomStringUtils.secure().nextAlphanumeric(5));
+    ContractTestUtils.createFile(fs, filePath, true, 
"data".getBytes(StandardCharsets.UTF_8));
+
+    OMMetrics metrics = getOMMetrics();
+    long bucketInfosBefore = metrics.getNumBucketInfos();
+    long volumeInfosBefore = metrics.getNumVolumeInfos();
+    long keyLookupsBefore = metrics.getNumKeyLookups();
+
+    assertNotNull(fs.getFileChecksum(filePath));
+
+    assertEquals(bucketInfosBefore, metrics.getNumBucketInfos(),
+        "getFileChecksum must not trigger InfoBucket");
+    assertEquals(volumeInfosBefore, metrics.getNumVolumeInfos(),
+        "getFileChecksum must not trigger InfoVolume");
+    assertEquals(keyLookupsBefore, metrics.getNumKeyLookups(),
+        "getFileChecksum must use LookupFile, not LookupKey");
+  }
+
+  @Test
+  void testGetFileChecksumRejectsObsBucket() throws Exception {
+    OzoneBucket obsBucket =
+        DataTestUtil.createVolumeAndBucket(client, BucketLayout.OBJECT_STORE);
+    String keyName = "obs-key-" + RandomStringUtils.secure().nextAlphabetic(5);
+    DataTestUtil.createKey(obsBucket, keyName,
+        "data".getBytes(StandardCharsets.UTF_8));
+    Path keyPath = new Path(new Path(
+        new Path(OZONE_URI_DELIMITER, obsBucket.getVolumeName()),
+        obsBucket.getName()), keyName);
+
+    OMMetrics metrics = getOMMetrics();
+    long bucketInfosBefore = metrics.getNumBucketInfos();
+
+    IllegalArgumentException exception = 
assertThrows(IllegalArgumentException.class,
+        () -> fs.getFileChecksum(keyPath));
+    assertThat(exception.getMessage()).contains(obsBucket.getName());
+    assertThat(exception.getMessage()).contains("OBJECT_STORE");
+    assertEquals(bucketInfosBefore, metrics.getNumBucketInfos(),
+        "getFileChecksum must not trigger InfoBucket");
+  }
+
+  @Test
+  void testGetFileChecksumOnDirectory() throws Exception {
+    Path dir = new Path(bucketPath,
+        "checksum-dir-" + RandomStringUtils.secure().nextAlphanumeric(5));
+    fs.mkdirs(dir);
+
+    assertThrows(FileNotFoundException.class, () -> fs.getFileChecksum(dir));
+  }
+
+  @Test
+  void testGetFileChecksumOnNonExistentFile() throws Exception {
+    Path missing = new Path(bucketPath,
+        "checksum-missing-" + RandomStringUtils.secure().nextAlphanumeric(5));
+
+    assertThrows(FileNotFoundException.class,
+        () -> fs.getFileChecksum(missing));
+  }
+
   @Test
   void testGetFileStatus() throws Exception {
     String volumeNameLocal = getRandomNonExistVolumeName();
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOfsGetFileChecksumRpcBenchmark.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOfsGetFileChecksumRpcBenchmark.java
new file mode 100644
index 00000000000..f515a5a7dad
--- /dev/null
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOfsGetFileChecksumRpcBenchmark.java
@@ -0,0 +1,314 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.contract.ContractTestUtils;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.utils.IOUtils;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.client.BucketArgs;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.apache.hadoop.ozone.om.OMMetrics;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Benchmark for the OFS getFileChecksum OM RPC count (HDDS-15951).
+ *
+ * <p>Adapted from the benchmark in PR #11369 so the same workload runs 
unchanged on the baseline
+ * ({@code master}: InfoVolume + InfoBucket + LookupKey) and on the 
server-side fix (a single LookupFile).
+ * The only changes are in the accounting: key reads are counted as LookupKey 
+ LookupFile, because the fix
+ * moves the read from one to the other, and the InfoBucket invariant no 
longer assumes at least one RPC per
+ * bucket, because the fix removes that RPC entirely instead of caching it.
+ *
+ * <p>{@link #NUM_BUCKETS} buckets are touched {@link #ACCESSES_PER_BUCKET} 
times each from a shuffled access
+ * sequence, which is the shape PR #11369's per-bucket layout cache was built 
for (90% of calls would hit it).
+ * Keeping that shape makes the three arms directly comparable. PR #11369 
drops InfoVolume unconditionally and
+ * caches InfoBucket, so it costs (2 - hitRatio) RPCs per call: 1.10 at this 
workload's 90% hit ratio, rising to
+ * 2.00 for a cold or scanning client. The server-side fix costs exactly 1.00 
regardless of hit ratio.
+ */
+@Tag("benchmark")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestOfsGetFileChecksumRpcBenchmark {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(TestOfsGetFileChecksumRpcBenchmark.class);
+
+  /** Distinct buckets in the working set. */
+  private static final int NUM_BUCKETS = 200;
+  /** getFileChecksum calls per bucket. */
+  private static final int ACCESSES_PER_BUCKET = 10;
+  private static final int TOTAL_CALLS = NUM_BUCKETS * ACCESSES_PER_BUCKET;
+  private static final long SHUFFLE_SEED = 20250831L;
+  /** Client threads issuing getFileChecksum in the concurrent comparison. */
+  private static final int CONCURRENT_THREADS = 10;
+  /** Size of each file, so every call computes a real checksum over one small 
block. */
+  private static final int FILE_SIZE = 4 * 1024;
+
+  private MiniOzoneCluster cluster;
+  private OzoneClient client;
+  private OzoneConfiguration conf;
+  private String rootPath;
+
+  private final List<Path> accessSequence = new ArrayList<>(TOTAL_CALLS);
+
+  @BeforeAll
+  void init() throws IOException, InterruptedException, TimeoutException {
+    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();
+    rootPath = String.format("%s://%s/", OzoneConsts.OZONE_OFS_URI_SCHEME, 
conf.get(OZONE_OM_ADDRESS_KEY));
+
+    ObjectStore objectStore = client.getObjectStore();
+    String volName = "benchvol";
+    objectStore.createVolume(volName);
+    OzoneVolume volume = objectStore.getVolume(volName);
+    BucketArgs fsoArgs = 
BucketArgs.newBuilder().setBucketLayout(BucketLayout.FILE_SYSTEM_OPTIMIZED).build();
+    byte[] data = ContractTestUtils.dataset(FILE_SIZE, 'a', 26);
+    try (FileSystem setupFs = FileSystem.newInstance(URI.create(rootPath), 
conf)) {
+      for (int i = 0; i < NUM_BUCKETS; i++) {
+        String buckName = "benchbucket-" + i;
+        volume.createBucket(buckName, fsoArgs);
+        Path file = new Path("/" + volName + "/" + buckName + "/file");
+        ContractTestUtils.createFile(setupFs, file, true, data);
+        for (int a = 0; a < ACCESSES_PER_BUCKET; a++) {
+          accessSequence.add(file);
+        }
+      }
+      assertThat(setupFs.getFileChecksum(accessSequence.get(0))).isNotNull();
+    }
+    Collections.shuffle(accessSequence, new Random(SHUFFLE_SEED));
+  }
+
+  @AfterAll
+  void shutdown() {
+    IOUtils.closeQuietly(client);
+    if (cluster != null) {
+      cluster.shutdown();
+    }
+  }
+
+  @Test
+  void benchmarkGetFileChecksum() throws Exception {
+    // Prime OM-side state and the JVM so the measured run pays no cold-start.
+    runWorkload(1);
+
+    Result single = runWorkload(1);
+    Result concurrent = runWorkload(CONCURRENT_THREADS);
+
+    assertWorkloadInvariants(single);
+    assertWorkloadInvariants(concurrent);
+
+    logResult("single-threaded", single);
+    logResult(CONCURRENT_THREADS + " concurrent threads", concurrent);
+  }
+
+  /**
+   * Invariants that hold on both the baseline and the fixed code, so the 
identical benchmark passes in either
+   * worktree: every getFileChecksum still issues exactly one key-read RPC, 
whether that is LookupKey (baseline)
+   * or LookupFile (fixed), and the volume and bucket RPCs stay within one per 
call.
+   */
+  private void assertWorkloadInvariants(Result r) {
+    assertThat(r.lookupKeyRpcs + r.lookupFileRpcs).isEqualTo(TOTAL_CALLS);
+    assertThat(r.bucketInfoRpcs).isBetween(0L, (long) TOTAL_CALLS);
+    assertThat(r.volumeInfoRpcs).isBetween(0L, (long) TOTAL_CALLS);
+  }
+
+  private void logResult(String label, Result r) {
+    long omRpcs = r.volumeInfoRpcs + r.bucketInfoRpcs + r.lookupKeyRpcs + 
r.lookupFileRpcs;
+    LOG.info(String.format("%n"
+            + "BENCHRESULT OFS getFileChecksum OM RPC benchmark (HDDS-15951) - 
%s%n"
+            + "  buckets=%d, accesses/bucket=%d, total getFileChecksum=%d, 
threads=%d, file size=%d bytes%n"
+            + "  
------------------------------------------------------------%n"
+            + "  InfoVolume RPCs             : %d%n"
+            + "  InfoBucket RPCs             : %d%n"
+            + "  LookupKey RPCs              : %d%n"
+            + "  LookupFile RPCs             : %d%n"
+            + "  total OM RPCs               : %d%n"
+            + "  OM RPCs per call            : %.2f%n"
+            + "  
------------------------------------------------------------%n"
+            + "  latency mean                : %.3f ms%n"
+            + "  latency p50                 : %.3f ms%n"
+            + "  latency p90                 : %.3f ms%n"
+            + "  latency p99                 : %.3f ms%n"
+            + "  latency max                 : %.3f ms%n"
+            + "  throughput                  : %.1f getFileChecksum/s%n",
+        label, NUM_BUCKETS, ACCESSES_PER_BUCKET, TOTAL_CALLS, r.threads, 
FILE_SIZE,
+        r.volumeInfoRpcs, r.bucketInfoRpcs, r.lookupKeyRpcs, r.lookupFileRpcs,
+        omRpcs, omRpcs / (double) TOTAL_CALLS,
+        millis(r.mean()), millis(r.percentile(0.50)),
+        millis(r.percentile(0.90)), millis(r.percentile(0.99)),
+        millis(r.max()), r.opsPerSecond()));
+  }
+
+  private static double millis(double nanos) {
+    return nanos / (double) TimeUnit.MILLISECONDS.toNanos(1);
+  }
+
+  private Result runWorkload(int threads) throws IOException, 
InterruptedException {
+    OzoneConfiguration runConf = new OzoneConfiguration(conf);
+    runConf.set(FS_DEFAULT_NAME_KEY, rootPath);
+
+    OMMetrics metrics = cluster.getOzoneManager().getMetrics();
+    long[] latencyNanos = new long[TOTAL_CALLS];
+    // A new FileSystem instance per run, so each run starts with no 
client-side state carried over.
+    try (FileSystem fs = FileSystem.newInstance(URI.create(rootPath), 
runConf)) {
+      long volumeInfosBefore = metrics.getNumVolumeInfos();
+      long bucketInfosBefore = metrics.getNumBucketInfos();
+      long keyLookupsBefore = metrics.getNumKeyLookups();
+      long lookupFilesBefore = metrics.getNumLookupFile();
+      long elapsedNanos = threads == 1
+          ? runSequential(fs, latencyNanos)
+          : runConcurrent(fs, threads, latencyNanos);
+      return new Result(threads,
+          metrics.getNumVolumeInfos() - volumeInfosBefore,
+          metrics.getNumBucketInfos() - bucketInfosBefore,
+          metrics.getNumKeyLookups() - keyLookupsBefore,
+          metrics.getNumLookupFile() - lookupFilesBefore,
+          elapsedNanos, latencyNanos);
+    }
+  }
+
+  private long runSequential(FileSystem fs, long[] latencyNanos) throws 
IOException {
+    long startNanos = System.nanoTime();
+    int i = 0;
+    for (Path path : accessSequence) {
+      long callStart = System.nanoTime();
+      fs.getFileChecksum(path);
+      latencyNanos[i++] = System.nanoTime() - callStart;
+    }
+    return System.nanoTime() - startNanos;
+  }
+
+  private long runConcurrent(FileSystem fs, int threads, long[] latencyNanos)
+      throws IOException, InterruptedException {
+    ExecutorService pool = Executors.newFixedThreadPool(threads);
+    CountDownLatch startGate = new CountDownLatch(1);
+    CountDownLatch doneGate = new CountDownLatch(threads);
+    AtomicReference<IOException> failure = new AtomicReference<>();
+    // Disjoint, contiguous slices of the shuffled sequence; each thread 
writes only its own latency indices, so no
+    // synchronization is needed per call.
+    int chunk = (TOTAL_CALLS + threads - 1) / threads;
+    for (int t = 0; t < threads; t++) {
+      final int from = t * chunk;
+      final int to = Math.min(TOTAL_CALLS, from + chunk);
+      pool.submit(() -> {
+        try {
+          startGate.await();
+          for (int i = from; i < to; i++) {
+            long callStart = System.nanoTime();
+            fs.getFileChecksum(accessSequence.get(i));
+            latencyNanos[i] = System.nanoTime() - callStart;
+          }
+        } catch (IOException e) {
+          failure.compareAndSet(null, e);
+        } catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+        } finally {
+          doneGate.countDown();
+        }
+      });
+    }
+    long startNanos = System.nanoTime();
+    startGate.countDown();
+    doneGate.await();
+    long elapsedNanos = System.nanoTime() - startNanos;
+    pool.shutdownNow();
+    if (failure.get() != null) {
+      throw failure.get();
+    }
+    return elapsedNanos;
+  }
+
+  private static final class Result {
+    private final int threads;
+    private final long volumeInfoRpcs;
+    private final long bucketInfoRpcs;
+    private final long lookupKeyRpcs;
+    private final long lookupFileRpcs;
+    private final long elapsedNanos;
+    private final long[] sortedLatencyNanos;
+
+    @SuppressWarnings("checkstyle:ParameterNumber")
+    Result(int threads, long volumeInfoRpcs, long bucketInfoRpcs, long 
lookupKeyRpcs, long lookupFileRpcs,
+        long elapsedNanos, long[] latencyNanos) {
+      this.threads = threads;
+      this.volumeInfoRpcs = volumeInfoRpcs;
+      this.bucketInfoRpcs = bucketInfoRpcs;
+      this.lookupKeyRpcs = lookupKeyRpcs;
+      this.lookupFileRpcs = lookupFileRpcs;
+      this.elapsedNanos = elapsedNanos;
+      this.sortedLatencyNanos = latencyNanos.clone();
+      Arrays.sort(this.sortedLatencyNanos);
+    }
+
+    double opsPerSecond() {
+      return TOTAL_CALLS / (elapsedNanos / (double) 
TimeUnit.SECONDS.toNanos(1));
+    }
+
+    double mean() {
+      long sum = 0;
+      for (long l : sortedLatencyNanos) {
+        sum += l;
+      }
+      return sum / (double) sortedLatencyNanos.length;
+    }
+
+    double percentile(double q) {
+      int idx = (int) Math.ceil(q * sortedLatencyNanos.length) - 1;
+      idx = Math.max(0, Math.min(sortedLatencyNanos.length - 1, idx));
+      return sortedLatencyNanos[idx];
+    }
+
+    double max() {
+      return sortedLatencyNanos[sortedLatencyNanos.length - 1];
+    }
+  }
+}
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOzoneManagerSnapshotAcl.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOzoneManagerSnapshotAcl.java
index 4f4508ebdc7..35de4fdd129 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOzoneManagerSnapshotAcl.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOzoneManagerSnapshotAcl.java
@@ -277,8 +277,12 @@ public void testLookupFileWithAllowedUser(BucketLayout 
bucketLayout)
     final OmKeyArgs snapshotKeyArgs = getOmKeyArgs(true);
 
     // WHEN-THEN
-    assertDoesNotThrow(
-        () -> ozoneManager.lookupFile(snapshotKeyArgs));
+    if (bucketLayout == BucketLayout.OBJECT_STORE) {
+      assertLookupFileNotSupported(snapshotKeyArgs);
+    } else {
+      assertDoesNotThrow(
+          () -> ozoneManager.lookupFile(snapshotKeyArgs));
+    }
   }
 
   @ParameterizedTest
@@ -292,6 +296,16 @@ public void testLookupFileWithNotAllowedUser(BucketLayout 
bucketLayout)
 
     // when reading from snapshot, read disallowed.
     UserGroupInformation.setLoginUser(UGI2);
+
+    if (bucketLayout == BucketLayout.OBJECT_STORE) {
+      // HDDS-15951: the layout check runs before the ACL check, so the
+      // rejection wins over PERMISSION_DENIED on both the snapshot and the
+      // active file system.
+      assertLookupFileNotSupported(snapshotKeyArgs);
+      assertLookupFileNotSupported(keyArgs);
+      return;
+    }
+
     final OMException ex = assertThrows(OMException.class,
         () -> ozoneManager.lookupFile(snapshotKeyArgs));
 
@@ -302,6 +316,18 @@ public void testLookupFileWithNotAllowedUser(BucketLayout 
bucketLayout)
     assertDoesNotThrow(() -> ozoneManager.lookupFile(keyArgs));
   }
 
+  /**
+   * HDDS-15951 moved the OBJECT_STORE layout check for LookupFile from the 
file
+   * system client into OM, so OBS buckets - which have no file system 
semantics
+   * - are now rejected server-side, snapshots included.
+   */
+  private static void assertLookupFileNotSupported(OmKeyArgs keyArgs) {
+    final OMException ex = assertThrows(OMException.class,
+        () -> ozoneManager.lookupFile(keyArgs));
+    assertEquals(OMException.ResultCodes.NOT_SUPPORTED_OPERATION,
+        ex.getResult());
+  }
+
   @ParameterizedTest
   @EnumSource(BucketLayout.class)
   public void testListKeysWithAllowedUser(BucketLayout bucketLayout)
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMetrics.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMetrics.java
index ff74e6b6e67..8ecb514a383 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMetrics.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMetrics.java
@@ -1217,6 +1217,11 @@ public long getNumKeyLookups() {
     return numKeyLookup.value();
   }
 
+  @VisibleForTesting
+  public long getNumLookupFile() {
+    return numLookupFile.value();
+  }
+
   @VisibleForTesting
   public long getNumKeyLookupFails() {
     return numKeyLookupFails.value();
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index e93219ae64a..4435cf78b82 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
@@ -361,6 +361,18 @@ public OmKeyInfo lookupFile(OmKeyArgs args) throws 
IOException {
     args = normalizeKeyArgs(bucket.update(args), bucket);
 
     try {
+      if (bucket.bucketLayout() != null) {
+        try {
+          OzoneFSUtils.validateBucketLayout(bucket.requestedBucket(),
+              bucket.bucketLayout());
+        } catch (IllegalArgumentException e) {
+          // Convert to an OMException so it is returned to the client as a
+          // normal (non-retryable) RPC response instead of escaping the read
+          // handler's IOException catch and triggering a client retry storm.
+          throw new OMException(e.getMessage(),
+              ResultCodes.NOT_SUPPORTED_OPERATION);
+        }
+      }
       if (isAclEnabled) {
         checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.READ,
             bucket, args.getKeyName());
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
index 5defb87760f..01a5bad1808 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
@@ -53,6 +53,7 @@
 import org.apache.hadoop.ozone.om.helpers.BucketLayout;
 import org.apache.hadoop.ozone.om.helpers.ListKeysResult;
 import org.apache.hadoop.ozone.om.helpers.OmKeyArgs;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.OzoneFileStatus;
 import org.apache.hadoop.ozone.om.protocolPB.grpc.GrpcClientConstants;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
@@ -840,4 +841,63 @@ public void getFileStatusAllowsLegacyLayout() throws 
Exception {
     assertEquals(expectedStatus, status);
     verify(keyManager).getFileStatus(any(OmKeyArgs.class), anyString());
   }
+
+  @Test
+  public void lookupFileRejectsObjectStoreLayout() throws Exception {
+    OzoneManager ozoneManager = mock(OzoneManager.class);
+    KeyManager keyManager = mock(KeyManager.class);
+    when(ozoneManager.getAclsEnabled()).thenReturn(false);
+    
when(ozoneManager.getBucketManager()).thenReturn(mock(BucketManager.class));
+    
when(ozoneManager.getVolumeManager()).thenReturn(mock(VolumeManager.class));
+    
when(ozoneManager.getPerfMetrics()).thenReturn(mock(OMPerformanceMetrics.class));
+    when(ozoneManager.resolveBucketLink(any(OmKeyArgs.class)))
+        .thenReturn(new ResolvedBucket("vol", "obs-bucket", "vol", 
"obs-bucket",
+            "owner", BucketLayout.OBJECT_STORE));
+
+    OmMetadataReader reader = new OmMetadataReader(keyManager,
+        mock(PrefixManager.class), ozoneManager, mock(org.slf4j.Logger.class),
+        mock(AuditLogger.class), mock(OmMetadataReaderMetrics.class), null);
+
+    OmKeyArgs keyArgs = new OmKeyArgs.Builder()
+        .setVolumeName("vol")
+        .setBucketName("obs-bucket")
+        .setKeyName("key1")
+        .build();
+
+    OMException exception = assertThrows(OMException.class,
+        () -> reader.lookupFile(keyArgs));
+    assertEquals(ResultCodes.NOT_SUPPORTED_OPERATION, exception.getResult());
+    assertTrue(exception.getMessage().contains("obs-bucket"));
+    assertTrue(exception.getMessage().contains("OBJECT_STORE"));
+    verify(keyManager, never()).lookupFile(any(), anyString());
+  }
+
+  @Test
+  public void lookupFileAllowsLegacyLayout() throws Exception {
+    OzoneManager ozoneManager = mock(OzoneManager.class);
+    KeyManager keyManager = mock(KeyManager.class);
+    when(ozoneManager.getAclsEnabled()).thenReturn(false);
+    
when(ozoneManager.getBucketManager()).thenReturn(mock(BucketManager.class));
+    
when(ozoneManager.getVolumeManager()).thenReturn(mock(VolumeManager.class));
+    
when(ozoneManager.getPerfMetrics()).thenReturn(mock(OMPerformanceMetrics.class));
+    when(ozoneManager.resolveBucketLink(any(OmKeyArgs.class)))
+        .thenReturn(new ResolvedBucket("vol", "legacy-bucket", "vol",
+            "legacy-bucket", "owner", BucketLayout.LEGACY));
+    OmKeyInfo expectedKeyInfo = mock(OmKeyInfo.class);
+    when(keyManager.lookupFile(any(OmKeyArgs.class), anyString()))
+        .thenReturn(expectedKeyInfo);
+
+    OmMetadataReader reader = new OmMetadataReader(keyManager,
+        mock(PrefixManager.class), ozoneManager, mock(org.slf4j.Logger.class),
+        mock(AuditLogger.class), mock(OmMetadataReaderMetrics.class), null);
+
+    OmKeyArgs keyArgs = new OmKeyArgs.Builder()
+        .setVolumeName("vol")
+        .setBucketName("legacy-bucket")
+        .setKeyName("key1")
+        .build();
+
+    assertEquals(expectedKeyInfo, reader.lookupFile(keyArgs));
+    verify(keyManager).lookupFile(any(OmKeyArgs.class), anyString());
+  }
 }
diff --git 
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicOzoneClientAdapterImpl.java
 
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicOzoneClientAdapterImpl.java
index 39cce33637f..b733b461d48 100644
--- 
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicOzoneClientAdapterImpl.java
+++ 
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicOzoneClientAdapterImpl.java
@@ -672,10 +672,12 @@ public FileChecksum getFileChecksum(String keyName, long 
length)
     if (combineMode == null) {
       return null;
     }
+    // The bucket layout is validated once in the constructor, so LookupKey is
+    // enough here; there is no InfoBucket RPC to save on this path.
     return OzoneClientUtils.getFileChecksumWithCombineMode(
-        volume, bucket, keyName,
+        volume.getName(), bucket.getName(), keyName,
         length, combineMode,
-        ozoneClient.getObjectStore().getClientProxy());
+        ozoneClient.getObjectStore().getClientProxy(), false);
   }
 
   @Override
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 9b1ed021267..c7cf1f729ac 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
@@ -1377,13 +1377,45 @@ public FileChecksum getFileChecksum(String keyName, 
long length)
       return null;
     }
     OFSPath ofsPath = new OFSPath(keyName, config);
-    OzoneVolume volume = objectStore.getVolume(ofsPath.getVolumeName());
-    OzoneBucket bucket = getBucket(ofsPath, false);
-    return OzoneClientUtils.getFileChecksumWithCombineMode(
-        volume, bucket, ofsPath.getKeyName(),
-        length, combineMode,
-        ozoneClient.getObjectStore().getClientProxy());
-
+    if (ofsPath.getBucketName().isEmpty()) {
+      // throw FileNotFoundException in this case to make Hadoop common happy
+      throw new FileNotFoundException(
+          "getFileChecksum: Invalid argument: given bucket string is empty.");
+    }
+    // OM LookupFile rejects OBJECT_STORE buckets (HDDS-15951), so the volume 
and
+    // bucket no longer have to be fetched up front. During a rolling upgrade 
a new
+    // client can talk to an older OM that lacks that check, so when the 
negotiated
+    // OM version predates LOOKUP_FILE_REJECTS_OBS, fall back to LookupKey and
+    // validate the bucket layout client-side as before.
+    boolean omRejectsObs = proxy.getOmVersion()
+        .compareTo(OzoneManagerVersion.LOOKUP_FILE_REJECTS_OBS) >= 0;
+    if (!omRejectsObs) {
+      // Called for its layout validation side effect; the bucket itself is 
not needed.
+      getBucket(ofsPath, false);
+    }
+    try {
+      return OzoneClientUtils.getFileChecksumWithCombineMode(
+          ofsPath.getVolumeName(), ofsPath.getBucketName(), 
ofsPath.getKeyName(),
+          length, combineMode,
+          ozoneClient.getObjectStore().getClientProxy(), omRejectsObs);
+    } catch (OMException e) {
+      if (e.getResult() == OMException.ResultCodes.NOT_SUPPORTED_OPERATION) {
+        // OM rejects LookupFile on an OBJECT_STORE bucket (no file system
+        // semantics). Surface it as IllegalArgumentException, matching the
+        // pre-HDDS-15951 client-side layout check.
+        throw new IllegalArgumentException(e.getMessage());
+      } else if (e.getResult() == OMException.ResultCodes.NOT_A_FILE
+          || e.getResult() == OMException.ResultCodes.FILE_NOT_FOUND
+          || e.getResult() == OMException.ResultCodes.KEY_NOT_FOUND
+          || e.getResult() == OMException.ResultCodes.BUCKET_NOT_FOUND) {
+        // A checksum only exists for an existing file, so report the missing
+        // path the same way HDFS and getFileStatusForKeyOrSnapshot do.
+        // LookupFile reports a directory as NOT_A_FILE and a missing key as
+        // FILE_NOT_FOUND; the LookupKey fallback reports KEY_NOT_FOUND.
+        throw new FileNotFoundException(e.getMessage());
+      }
+      throw e;
+    }
   }
 
   @Override
diff --git 
a/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicRootedOzoneClientAdapterGetFileChecksum.java
 
b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicRootedOzoneClientAdapterGetFileChecksum.java
new file mode 100644
index 00000000000..22d1918dd1c
--- /dev/null
+++ 
b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicRootedOzoneClientAdapterGetFileChecksum.java
@@ -0,0 +1,199 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+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.mock;
+import static org.mockito.Mockito.never;
+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 org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.scm.OzoneClientConfig;
+import org.apache.hadoop.ozone.OzoneManagerVersion;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.OmKeyArgs;
+import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link BasicRootedOzoneClientAdapterImpl#getFileChecksum},
+ * covering the single-RPC LookupFile path added by HDDS-15951, the LookupKey
+ * fallback used when the OM predates
+ * {@link OzoneManagerVersion#LOOKUP_FILE_REJECTS_OBS}, and the mapping of OM
+ * result codes onto file system exceptions. Uses a partial mock so no OM
+ * connection is required.
+ */
+public class TestBasicRootedOzoneClientAdapterGetFileChecksum {
+
+  private static final String KEY_PATH = "/vol/bucket/key";
+
+  private BasicRootedOzoneClientAdapterImpl adapter;
+  private ClientProtocol proxy;
+  private ObjectStore objectStore;
+  private OzoneManagerProtocol omClient;
+  private OzoneBucket bucket;
+
+  @BeforeEach
+  public void setUp() throws Exception {
+    adapter = mock(BasicRootedOzoneClientAdapterImpl.class, 
CALLS_REAL_METHODS);
+    proxy = mock(ClientProtocol.class);
+    objectStore = mock(ObjectStore.class);
+    omClient = mock(OzoneManagerProtocol.class);
+    bucket = mock(OzoneBucket.class);
+
+    when(proxy.getOzoneManagerClient()).thenReturn(omClient);
+    when(proxy.getBucketDetails(anyString(), anyString())).thenReturn(bucket);
+    when(bucket.getName()).thenReturn("bucket");
+    when(bucket.getBucketLayout())
+        .thenReturn(BucketLayout.FILE_SYSTEM_OPTIMIZED);
+
+    OzoneClient ozoneClient = mock(OzoneClient.class);
+    when(ozoneClient.getObjectStore()).thenReturn(objectStore);
+    when(objectStore.getClientProxy()).thenReturn(proxy);
+
+    OzoneClientConfig clientConfig = mock(OzoneClientConfig.class);
+    when(clientConfig.getChecksumCombineMode())
+        .thenReturn(OzoneClientConfig.ChecksumCombineMode.MD5MD5CRC);
+
+    setField("proxy", proxy);
+    setField("objectStore", objectStore);
+    setField("ozoneClient", ozoneClient);
+    setField("config", new OzoneConfiguration());
+    setField("clientConfig", clientConfig);
+
+    // Default to an OM that performs the server-side OBJECT_STORE rejection.
+    when(proxy.getOmVersion())
+        .thenReturn(OzoneManagerVersion.LOOKUP_FILE_REJECTS_OBS);
+  }
+
+  private void setField(String name, Object value) throws Exception {
+    Field field =
+        BasicRootedOzoneClientAdapterImpl.class.getDeclaredField(name);
+    field.setAccessible(true);
+    field.set(adapter, value);
+  }
+
+  @Test
+  public void lookupFileIsTheOnlyOmCall() throws IOException {
+    when(omClient.lookupFile(any(OmKeyArgs.class)))
+        .thenThrow(new OMException("missing",
+            OMException.ResultCodes.FILE_NOT_FOUND));
+
+    assertThrows(FileNotFoundException.class,
+        () -> adapter.getFileChecksum(KEY_PATH, 10));
+
+    verify(omClient).lookupFile(any(OmKeyArgs.class));
+    verify(omClient, never()).lookupKey(any(OmKeyArgs.class));
+    verify(proxy, never()).getBucketDetails(anyString(), anyString());
+    verify(objectStore, never()).getVolume(anyString());
+  }
+
+  @Test
+  public void notAFileMappedToFileNotFoundException() throws IOException {
+    when(omClient.lookupFile(any(OmKeyArgs.class)))
+        .thenThrow(new OMException("Can not write to directory: key",
+            OMException.ResultCodes.NOT_A_FILE));
+
+    assertThrows(FileNotFoundException.class,
+        () -> adapter.getFileChecksum(KEY_PATH, 10));
+  }
+
+  @Test
+  public void bucketNotFoundMappedToFileNotFoundException() throws IOException 
{
+    when(omClient.lookupFile(any(OmKeyArgs.class)))
+        .thenThrow(new OMException("no bucket",
+            OMException.ResultCodes.BUCKET_NOT_FOUND));
+
+    assertThrows(FileNotFoundException.class,
+        () -> adapter.getFileChecksum(KEY_PATH, 10));
+  }
+
+  @Test
+  public void obsRejectionMappedToIllegalArgumentException()
+      throws IOException {
+    when(omClient.lookupFile(any(OmKeyArgs.class)))
+        .thenThrow(new OMException("Bucket: bucket has layout: OBJECT_STORE",
+            OMException.ResultCodes.NOT_SUPPORTED_OPERATION));
+
+    IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+        () -> adapter.getFileChecksum(KEY_PATH, 10));
+    assertThat(ex.getMessage()).contains("OBJECT_STORE");
+  }
+
+  @Test
+  public void otherOMExceptionPropagates() throws IOException {
+    when(omClient.lookupFile(any(OmKeyArgs.class)))
+        .thenThrow(new OMException("boom",
+            OMException.ResultCodes.INTERNAL_ERROR));
+
+    assertThrows(OMException.class,
+        () -> adapter.getFileChecksum(KEY_PATH, 10));
+  }
+
+  @Test
+  public void olderOmFallsBackToClientSideBucketCheck() throws IOException {
+    // An OM older than LOOKUP_FILE_REJECTS_OBS has no server-side OBJECT_STORE
+    // rejection, so the adapter must keep the pre-HDDS-15951 path: fetch the
+    // bucket (which validates the layout client-side) and use LookupKey.
+    when(proxy.getOmVersion())
+        .thenReturn(OzoneManagerVersion.S3_BUCKET_TAGGING_API);
+    when(omClient.lookupKey(any(OmKeyArgs.class)))
+        .thenThrow(new OMException("missing",
+            OMException.ResultCodes.KEY_NOT_FOUND));
+
+    assertThrows(FileNotFoundException.class,
+        () -> adapter.getFileChecksum(KEY_PATH, 10));
+
+    verify(proxy).getBucketDetails(eq("vol"), eq("bucket"));
+    verify(omClient).lookupKey(any(OmKeyArgs.class));
+    verify(omClient, never()).lookupFile(any(OmKeyArgs.class));
+    // The bucket is not a link, so no InfoVolume is needed to resolve layout.
+    verify(objectStore, never()).getVolume(anyString());
+  }
+
+  @Test
+  public void olderOmRejectsObsBucketClientSide() throws IOException {
+    when(proxy.getOmVersion())
+        .thenReturn(OzoneManagerVersion.S3_BUCKET_TAGGING_API);
+    when(bucket.getBucketLayout()).thenReturn(BucketLayout.OBJECT_STORE);
+
+    IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+        () -> adapter.getFileChecksum(KEY_PATH, 10));
+    assertThat(ex.getMessage()).contains("OBJECT_STORE");
+
+    verify(proxy).getBucketDetails(eq("vol"), eq("bucket"));
+    verify(omClient, never()).lookupKey(any(OmKeyArgs.class));
+    verify(omClient, never()).lookupFile(any(OmKeyArgs.class));
+    verify(objectStore, never()).getVolume(anyString());
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to