This is an automated email from the ASF dual-hosted git repository.

ivandika3 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 ba05c2e3ad8 HDDS-16497. Support OM ThreadLocal context propagation for 
Ratis read (#11257)
ba05c2e3ad8 is described below

commit ba05c2e3ad8151251f61732a00f98bccefe33f3b
Author: Ivan Andika <[email protected]>
AuthorDate: Tue Sep 29 16:27:23 2026 +0800

    HDDS-16497. Support OM ThreadLocal context propagation for Ratis read 
(#11257)
---
 .../ozone/om/OzoneManagerHAFollowerReadTests.java  |   8 +-
 ...stOzoneManagerHAFollowerReadWithAllRunning.java |  43 ++++
 .../hadoop/ozone/om/execution/OMExecutionFlow.java |  20 +-
 .../hadoop/ozone/om/lock/OMLockDetailsUtil.java    |  48 +++++
 .../hadoop/ozone/om/lock/OzoneManagerLock.java     |   7 +-
 .../hadoop/ozone/om/ratis/OMThreadContext.java     | 198 ++++++++++++++++++
 .../ozone/om/ratis/OzoneManagerStateMachine.java   |  52 ++---
 .../hadoop/ozone/om/request/OMClientRequest.java   |  19 +-
 ...OzoneManagerProtocolServerSideTranslatorPB.java |  15 +-
 .../ozone/om/execution/TestOMExecutionFlow.java    | 165 +++++++++++++++
 .../ozone/om/lock/TestOMLockDetailsUtil.java       |  78 +++++++
 .../hadoop/ozone/om/ratis/TestOMThreadContext.java | 207 +++++++++++++++++++
 .../om/ratis/TestOzoneManagerStateMachine.java     | 224 +++++++++++++++++++++
 .../hadoop/ozone/om/response/TestOMResponse.java   |  51 ++++-
 14 files changed, 1078 insertions(+), 57 deletions(-)

diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/OzoneManagerHAFollowerReadTests.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/OzoneManagerHAFollowerReadTests.java
index 2db23fdbe7e..74a3959d9c9 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/OzoneManagerHAFollowerReadTests.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/OzoneManagerHAFollowerReadTests.java
@@ -17,6 +17,9 @@
 
 package org.apache.hadoop.ozone.om;
 
+import static 
org.apache.hadoop.hdds.security.SecurityConfig.OZONE_TEST_AUTHORIZATION_ENABLED;
+import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ACL_AUTHORIZER_CLASS;
+import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ACL_AUTHORIZER_CLASS_NATIVE;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.io.IOException;
@@ -32,7 +35,10 @@ public abstract class OzoneManagerHAFollowerReadTests 
extends AbstractOzoneManag
 
   @BeforeAll
   public static void init() throws Exception {
-    initCluster(true);
+    initCluster(true, conf -> {
+      conf.setBoolean(OZONE_TEST_AUTHORIZATION_ENABLED, true);
+      conf.set(OZONE_ACL_AUTHORIZER_CLASS, OZONE_ACL_AUTHORIZER_CLASS_NATIVE);
+    });
   }
 
   protected void listVolumes(boolean checkSuccess)
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerHAFollowerReadWithAllRunning.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerHAFollowerReadWithAllRunning.java
index 0e97b249420..25573262dd0 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerHAFollowerReadWithAllRunning.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerHAFollowerReadWithAllRunning.java
@@ -21,6 +21,7 @@
 import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_FOLLOWER_READ_DEFAULT_CONSISTENCY_KEY;
 import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_FOLLOWER_READ_ENABLED_KEY;
 import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_LEADER_READ_DEFAULT_CONSISTENCY_KEY;
+import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_READ_BLACKLIST_USERS;
 import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_TRANSPORT_CLASS;
 import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.DIRECTORY_NOT_FOUND;
 import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.FILE_ALREADY_EXISTS;
@@ -39,6 +40,7 @@
 
 import com.google.protobuf.ServiceException;
 import java.net.InetSocketAddress;
+import java.security.PrivilegedExceptionAction;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
@@ -78,6 +80,7 @@
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.VolumeInfo;
 import 
org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
+import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.ozone.test.tag.Flaky;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -281,6 +284,46 @@ void testLinearizableReadConsistency() throws Exception {
     }
   }
 
+  @Test
+  void testRemoteUserPreservedForLinearizableRead() throws Exception {
+    OzoneBucket bucket = setupBucket();
+    String key = createKey(bucket);
+    String deniedUserName = uniqueObjectName("denied-user");
+    UserGroupInformation deniedUser = 
UserGroupInformation.createRemoteUser(deniedUserName);
+    OzoneManager followerOM = getCluster().getOzoneManagersList().stream()
+        .filter(om -> !om.isLeaderReady())
+        .findFirst()
+        .orElseThrow(() -> new IllegalStateException("No follower OM 
available"));
+
+    for (OzoneManager om : getCluster().getOzoneManagersList()) {
+      
om.getReconfigurationHandler().reconfigureProperty(OZONE_READ_BLACKLIST_USERS, 
deniedUserName);
+    }
+
+    try {
+      long previousLinearizableRead = 
followerOM.getMetrics().getNumLinearizableRead();
+      OMException exception = 
deniedUser.doAs((PrivilegedExceptionAction<OMException>) () -> {
+        OzoneConfiguration clientConf = new OzoneConfiguration(getConf());
+        clientConf.setBoolean(OZONE_CLIENT_FOLLOWER_READ_ENABLED_KEY, true);
+        clientConf.set(OZONE_CLIENT_FOLLOWER_READ_DEFAULT_CONSISTENCY_KEY, 
"LINEARIZABLE_ALLOW_FOLLOWER");
+        clientConf.set(OZONE_OM_TRANSPORT_CLASS, 
Hadoop3OmTransportFactory.class.getName());
+        try (OzoneClient deniedClient = 
OzoneClientFactory.getRpcClient(getOmServiceId(), clientConf)) {
+          ObjectStore deniedObjectStore = deniedClient.getObjectStore();
+          changeFollowerReadInitialProxy(deniedObjectStore, 
Hadoop3OmTransportFactory.class,
+              getCluster().getOMLeader().getOMNodeId(), 
followerOM.getOMNodeId());
+          return assertThrows(OMException.class, () -> 
deniedObjectStore.getClientProxy().headObject(
+              bucket.getVolumeName(), bucket.getName(), key));
+        }
+      });
+
+      assertEquals(OMException.ResultCodes.PERMISSION_DENIED, 
exception.getResult());
+      
assertThat(followerOM.getMetrics().getNumLinearizableRead()).isGreaterThan(previousLinearizableRead);
+    } finally {
+      for (OzoneManager om : getCluster().getOzoneManagersList()) {
+        
om.getReconfigurationHandler().reconfigureProperty(OZONE_READ_BLACKLIST_USERS, 
"");
+      }
+    }
+  }
+
   @Test
   void testFileOperationsWithRecursive() throws Exception {
     OzoneBucket ozoneBucket = setupBucket();
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/execution/OMExecutionFlow.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/execution/OMExecutionFlow.java
index de45d7f5e46..63127d62c96 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/execution/OMExecutionFlow.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/execution/OMExecutionFlow.java
@@ -28,6 +28,7 @@
 import org.apache.hadoop.ozone.om.request.OMClientRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.hadoop.ozone.security.STSSecurityUtil;
 
 /**
  * entry for execution flow for write request.
@@ -73,7 +74,11 @@ private OMResponse submitExecutionToRatis(OMRequest request, 
boolean isWrite) th
         return OzoneManagerRatisUtils.createErrorResponse(request, ex);
       }
     } else {
-      requestToSubmit = request;
+      try {
+        requestToSubmit = captureReadContext(request);
+      } catch (IOException ex) {
+        return OzoneManagerRatisUtils.createErrorResponse(request, ex);
+      }
     }
 
     // 2. submit request to ratis
@@ -83,4 +88,17 @@ private OMResponse submitExecutionToRatis(OMRequest request, 
boolean isWrite) th
     }
     return response;
   }
+
+  /**
+   * Captures authenticated request context on the RPC thread before 
submitting a read request to Ratis.
+   */
+  private OMRequest captureReadContext(OMRequest request) throws IOException {
+    OMRequest.Builder requestBuilder = request.toBuilder()
+        .setUserInfo(OMClientRequest.getAuthenticatedUserInfo(request));
+    if (requestBuilder.hasS3Authentication()) {
+      requestBuilder.setS3Authentication(
+          
STSSecurityUtil.resolveS3Authentication(requestBuilder.getS3Authentication(), 
ozoneManager));
+    }
+    return requestBuilder.build();
+  }
 }
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/lock/OMLockDetailsUtil.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/lock/OMLockDetailsUtil.java
new file mode 100644
index 00000000000..7721152b566
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/lock/OMLockDetailsUtil.java
@@ -0,0 +1,48 @@
+/*
+ * 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.om.lock;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.ipc_.ProcessingDetails;
+import org.apache.hadoop.ipc_.ProcessingDetails.Timing;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMLockDetailsProto;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+
+/** Converts OM lock details between internal request processing and protocol 
responses. */
+public final class OMLockDetailsUtil {
+
+  private OMLockDetailsUtil() {
+  }
+
+  public static OMResponse addToResponse(OMResponse response, 
ProcessingDetails processingDetails) {
+    long waitNanos = processingDetails.get(Timing.LOCKWAIT, 
TimeUnit.NANOSECONDS);
+    long readNanos = processingDetails.get(Timing.LOCKSHARED, 
TimeUnit.NANOSECONDS);
+    long writeNanos = processingDetails.get(Timing.LOCKEXCLUSIVE, 
TimeUnit.NANOSECONDS);
+    if (waitNanos == 0 && readNanos == 0 && writeNanos == 0) {
+      return response;
+    }
+
+    OMLockDetailsProto.Builder lockDetails = response.hasOmLockDetails()
+        ? response.getOmLockDetails().toBuilder() : 
OMLockDetailsProto.newBuilder();
+    lockDetails.setWaitLockNanos(lockDetails.getWaitLockNanos() + waitNanos);
+    lockDetails.setReadLockNanos(lockDetails.getReadLockNanos() + readNanos);
+    lockDetails.setWriteLockNanos(lockDetails.getWriteLockNanos() + 
writeNanos);
+    return response.toBuilder().setOmLockDetails(lockDetails).build();
+  }
+
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/lock/OzoneManagerLock.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/lock/OzoneManagerLock.java
index 1847dd31b7c..3f875d586af 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/lock/OzoneManagerLock.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/lock/OzoneManagerLock.java
@@ -723,10 +723,9 @@ short getMask() {
   /**
    * Update the processing details.
    *
-   * If Server.getCurCall() is null, which means it's write operation on Ratis,
-   * then we need to update the omLockDetails.
-   * If not null, it's read operation, or write operation on non-Ratis cluster,
-   * we can update ThreadLocal variable directly.
+   * If Server.getCurCall() is null, the operation is detached from a Hadoop
+   * RPC call, such as a write operation on Ratis, so update omLockDetails for
+   * propagation to the original call. Otherwise, update the call directly.
    * @param type IPC Timing types
    * @param deltaNanos consumed time
    */
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OMThreadContext.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OMThreadContext.java
new file mode 100644
index 00000000000..b869916c2ad
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OMThreadContext.java
@@ -0,0 +1,198 @@
+/*
+ * 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.om.ratis;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+import org.apache.hadoop.ipc_.RPC;
+import org.apache.hadoop.ipc_.RpcConstants;
+import org.apache.hadoop.ipc_.Server;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.lock.OMLockDetailsUtil;
+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.S3Authentication;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UserInfo;
+import org.apache.hadoop.ozone.security.STSSecurityUtil;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Request-derived context that can be applied while OM executes a Ratis 
request.
+ *
+ * <p>Ratis may execute a read or write on the submitting thread or on a 
separate thread. Applying this context replaces
+ * request-scoped state on whichever thread performs the operation, and 
closing the returned {@link Scope} restores the
+ * state that was previously present on that thread.</p>
+ */
+public final class OMThreadContext {
+  private static final Logger LOG = 
LoggerFactory.getLogger(OMThreadContext.class);
+
+  private final UserInfo userInfo;
+  private final S3Authentication s3Authentication;
+  private final STSTokenIdentifier stsTokenIdentifier;
+
+  private OMThreadContext(UserInfo userInfo, S3Authentication s3Authentication,
+      STSTokenIdentifier stsTokenIdentifier) {
+    this.userInfo = userInfo;
+    this.s3Authentication = s3Authentication;
+    this.stsTokenIdentifier = stsTokenIdentifier;
+  }
+
+  /**
+   * Creates context for a Ratis read. Applying it creates a fresh synthetic 
Hadoop RPC call for the request.
+   */
+  public static OMThreadContext forRead(OMRequest request, OzoneManager 
ozoneManager) throws IOException {
+    Objects.requireNonNull(request, "request");
+    Objects.requireNonNull(ozoneManager, "ozoneManager");
+    UserInfo requestUserInfo = request.hasUserInfo() ? request.getUserInfo() : 
null;
+    return fromRequest(requestUserInfo, request, ozoneManager);
+  }
+
+  /**
+   * Creates context for a Ratis write. Writes do not install a synthetic 
Hadoop RPC call, preserving lock accounting
+   * through ResourceLockTracker.
+   */
+  public static OMThreadContext forWrite(OMRequest request, OzoneManager 
ozoneManager) throws IOException {
+    Objects.requireNonNull(request, "request");
+    Objects.requireNonNull(ozoneManager, "ozoneManager");
+    return fromRequest(null, request, ozoneManager);
+  }
+
+  private static OMThreadContext fromRequest(UserInfo userInfo, OMRequest 
request,
+      OzoneManager ozoneManager) throws IOException {
+    if (!ozoneManager.isSecurityEnabled() || !request.hasS3Authentication()) {
+      return new OMThreadContext(userInfo, null, null);
+    }
+
+    STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
+    S3Authentication s3Authentication = request.getS3Authentication();
+    return new OMThreadContext(userInfo, s3Authentication,
+        STSSecurityUtil.rehydrateStsTokenIdentifier(s3Authentication));
+  }
+
+  /**
+   * Replaces request-scoped state on the current thread until the returned 
scope is closed.
+   */
+  public Scope applyToCurrentThread() {
+    Server.Call currentCall = createCall(userInfo);
+    Scope scope = new Scope(Server.getCurCall().get(), 
OzoneManager.getS3Auth(),
+        OzoneManager.getStsTokenIdentifier(), currentCall);
+    install(currentCall, s3Authentication, stsTokenIdentifier);
+    return scope;
+  }
+
+  private static Server.Call createCall(UserInfo userInfo) {
+    if (userInfo == null) {
+      return null;
+    }
+
+    UserGroupInformation remoteUser = userInfo.hasUserName() && 
!userInfo.getUserName().isEmpty()
+        ? UserGroupInformation.createRemoteUser(userInfo.getUserName()) : null;
+    InetAddress remoteAddress = createRemoteAddress(userInfo);
+    if (remoteUser == null && remoteAddress == null) {
+      return null;
+    }
+
+    return new Server.Call(RpcConstants.INVALID_CALL_ID, 
RpcConstants.INVALID_RETRY_COUNT,
+        null, null, RPC.RpcKind.RPC_PROTOCOL_BUFFER, 
RpcConstants.DUMMY_CLIENT_ID) {
+      @Override
+      public UserGroupInformation getRemoteUser() {
+        return remoteUser;
+      }
+
+      @Override
+      public InetAddress getHostInetAddress() {
+        return remoteAddress;
+      }
+    };
+  }
+
+  private static InetAddress createRemoteAddress(UserInfo userInfo) {
+    if (!userInfo.hasRemoteAddress() || userInfo.getRemoteAddress().isEmpty()) 
{
+      return null;
+    }
+
+    try {
+      InetAddress address = InetAddress.getByName(userInfo.getRemoteAddress());
+      return userInfo.hasHostName() && !userInfo.getHostName().isEmpty()
+          ? InetAddress.getByAddress(userInfo.getHostName(), 
address.getAddress()) : address;
+    } catch (UnknownHostException ex) {
+      LOG.debug("Unable to restore remote address from Ratis request context: 
{}",
+          userInfo.getRemoteAddress(), ex);
+      return null;
+    }
+  }
+
+  private static void install(Server.Call call, S3Authentication 
s3Authentication,
+      STSTokenIdentifier stsTokenIdentifier) {
+    if (call == null) {
+      Server.getCurCall().remove();
+    } else {
+      Server.getCurCall().set(call);
+    }
+    OzoneManager.setS3Auth(s3Authentication);
+    OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
+  }
+
+  /**
+   * Applied OM request context. The scope must be closed on the thread where 
it was created.
+   */
+  public static final class Scope implements AutoCloseable {
+    private final Thread ownerThread;
+    private final Server.Call previousCall;
+    private final S3Authentication previousS3Authentication;
+    private final STSTokenIdentifier previousStsTokenIdentifier;
+    private final Server.Call currentCall;
+    private boolean closed;
+
+    private Scope(Server.Call previousCall, S3Authentication 
previousS3Authentication,
+        STSTokenIdentifier previousStsTokenIdentifier, Server.Call 
currentCall) {
+      this.ownerThread = Thread.currentThread();
+      this.previousCall = previousCall;
+      this.previousS3Authentication = previousS3Authentication;
+      this.previousStsTokenIdentifier = previousStsTokenIdentifier;
+      this.currentCall = currentCall;
+    }
+
+    /**
+     * Adds read lock timings from the synthetic Hadoop RPC call to the 
response.
+     */
+    public OMResponse addLockDetails(OMResponse response) {
+      return currentCall == null ? response
+          : OMLockDetailsUtil.addToResponse(response, 
currentCall.getProcessingDetails());
+    }
+
+    @Override
+    public void close() {
+      if (Thread.currentThread() != ownerThread) {
+        throw new IllegalStateException(
+            "OM thread context must be closed on the thread where it was 
applied");
+      }
+      if (closed) {
+        return;
+      }
+
+      install(previousCall, previousS3Authentication, 
previousStsTokenIdentifier);
+      closed = true;
+    }
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
index 8353df01fb1..5e4bb9ca360 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
@@ -56,8 +56,6 @@
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
 import org.apache.hadoop.ozone.protocolPB.OzoneManagerRequestHandler;
 import org.apache.hadoop.ozone.protocolPB.RequestHandler;
-import org.apache.hadoop.ozone.security.STSSecurityUtil;
-import org.apache.hadoop.ozone.security.STSTokenIdentifier;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.hadoop.util.Time;
 import org.apache.hadoop.util.concurrent.HadoopExecutors;
@@ -697,37 +695,15 @@ public void close() {
    */
   @VisibleForTesting
   OMResponse runCommand(OMRequest request, TermIndex termIndex) {
-    boolean isS3AuthThreadLocalSet = false;
-    boolean isStsThreadLocalSet = false;
-    try {
-      if (ozoneManager.isSecurityEnabled() && request.hasS3Authentication()) {
-        // STS token verification runs on the leader RPC path so we don't need 
to recheck here on the apply
-        // after the log is committed
-        STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
-
-        final OzoneManagerProtocolProtos.S3Authentication s3Auth = 
request.getS3Authentication();
-        // ThreadLocal carries S3 action for OmMetadataReader.
-        OzoneManager.setS3Auth(s3Auth);
-        isS3AuthThreadLocalSet = true;
-
-        // ThreadLocal carries session policy for OmMetadataReader
-        final STSTokenIdentifier rehydratedTokenIdentifier = 
STSSecurityUtil.rehydrateStsTokenIdentifier(s3Auth);
-        if (rehydratedTokenIdentifier != null) {
-          OzoneManager.setStsTokenIdentifier(rehydratedTokenIdentifier);
-          isStsThreadLocalSet = true;
-        }
-      }
+    try (OMThreadContext.Scope ignored =
+             OMThreadContext.forWrite(request, 
ozoneManager).applyToCurrentThread()) {
       ExecutionContext context = ExecutionContext.of(termIndex.getIndex(), 
termIndex);
       final OMClientResponse omClientResponse = handler.handleWriteRequest(
           request, context, ozoneManagerDoubleBuffer);
-      OMLockDetails omLockDetails = omClientResponse.getOmLockDetails();
-      OMResponse omResponse = omClientResponse.getOMResponse();
-      if (omLockDetails != null) {
-        return omResponse.toBuilder()
-            .setOmLockDetails(omLockDetails.toProtobufBuilder()).build();
-      } else {
-        return omResponse;
-      }
+      OMLockDetails lockDetails = omClientResponse.getOmLockDetails();
+      OMResponse response = omClientResponse.getOMResponse();
+      return lockDetails == null ? response
+          : 
response.toBuilder().setOmLockDetails(lockDetails.toProtobufBuilder()).build();
     } catch (IOException e) {
       LOG.warn("Failed to write, Exception occurred ", e);
       return createErrorResponse(request, e, termIndex);
@@ -735,13 +711,6 @@ OMResponse runCommand(OMRequest request, TermIndex 
termIndex) {
       // For any Runtime exceptions, terminate OM.
       String errorMessage = "Request " + request + " failed with exception";
       ExitUtils.terminate(1, errorMessage, e, LOG);
-    } finally {
-      if (isS3AuthThreadLocalSet) {
-        OzoneManager.setS3Auth(null);
-      }
-      if (isStsThreadLocalSet) {
-        OzoneManager.setStsTokenIdentifier(null);
-      }
     }
     return null;
   }
@@ -785,9 +754,12 @@ public void loadSnapshotInfoFromDB() throws IOException {
    * @param request OMRequest
    * @return response from OM
    */
-  private Message queryCommand(OMRequest request) {
-    OMResponse response = handler.handleReadRequest(request);
-    return OMRatisHelper.convertResponseToMessage(response);
+  private Message queryCommand(OMRequest request) throws IOException {
+    try (OMThreadContext.Scope context =
+             OMThreadContext.forRead(request, 
ozoneManager).applyToCurrentThread()) {
+      OMResponse response = handler.handleReadRequest(request);
+      return 
OMRatisHelper.convertResponseToMessage(context.addLockDetails(response));
+    }
   }
 
   private static <T> CompletableFuture<T> completeExceptionally(Exception e) {
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
index 62013f7c071..071a722bc42 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
@@ -183,6 +183,22 @@ public OMRequest getOmRequest() {
    * @return User Info.
    */
   public OzoneManagerProtocolProtos.UserInfo getUserInfo() throws IOException {
+    return getUserInfo(omRequest, true);
+  }
+
+  /**
+   * Get authenticated user information for a read request submitted to Ratis.
+   * Client-supplied user information is not trusted on this path.
+   * @param omRequest OM request
+   * @return User Info.
+   */
+  public static OzoneManagerProtocolProtos.UserInfo getAuthenticatedUserInfo(
+      OMRequest omRequest) throws IOException {
+    return getUserInfo(omRequest, false);
+  }
+
+  private static OzoneManagerProtocolProtos.UserInfo getUserInfo(
+      OMRequest omRequest, boolean allowClientUserInfo) throws IOException {
     UserGroupInformation user = ProtobufRpcEngine.Server.getRemoteUser();
     InetAddress remoteAddress = ProtobufRpcEngine.Server.getRemoteIp();
     OzoneManagerProtocolProtos.UserInfo.Builder userInfo =
@@ -224,7 +240,8 @@ public OzoneManagerProtocolProtos.UserInfo getUserInfo() 
throws IOException {
     // client-supplied user name when no identity was established above:
     // it is unauthenticated data and must never override the identity
     // derived from S3 authentication or from the RPC user.
-    if (user == null && !userInfo.hasUserName() && omRequest.hasUserInfo()) {
+    if (allowClientUserInfo && user == null && !userInfo.hasUserName() &&
+        omRequest.hasUserInfo()) {
       userInfo.setUserName(omRequest.getUserInfo().getUserName());
     }
 
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
index b5d46fcdb17..12c2069f720 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
@@ -139,16 +139,15 @@ public OMResponse submitRequest(RpcController controller,
   public OMResponse processRequest(OMRequest request) throws ServiceException {
     OMResponse response = internalProcessRequest(request);
     if (response.hasOmLockDetails()) {
-      OzoneManagerProtocolProtos.OMLockDetailsProto omLockDetailsProto =
-          response.getOmLockDetails();
       Server.Call call = Server.getCurCall().get();
       if (call != null) {
-        call.getProcessingDetails().add(Timing.LOCKWAIT,
-            omLockDetailsProto.getWaitLockNanos(), TimeUnit.NANOSECONDS);
-        call.getProcessingDetails().add(Timing.LOCKSHARED,
-            omLockDetailsProto.getReadLockNanos(), TimeUnit.NANOSECONDS);
-        call.getProcessingDetails().add(Timing.LOCKEXCLUSIVE,
-            omLockDetailsProto.getWriteLockNanos(), TimeUnit.NANOSECONDS);
+        OzoneManagerProtocolProtos.OMLockDetailsProto lockDetails = 
response.getOmLockDetails();
+        call.getProcessingDetails().add(
+            Timing.LOCKWAIT, lockDetails.getWaitLockNanos(), 
TimeUnit.NANOSECONDS);
+        call.getProcessingDetails().add(
+            Timing.LOCKSHARED, lockDetails.getReadLockNanos(), 
TimeUnit.NANOSECONDS);
+        call.getProcessingDetails().add(
+            Timing.LOCKEXCLUSIVE, lockDetails.getWriteLockNanos(), 
TimeUnit.NANOSECONDS);
       }
     }
     return response;
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/execution/TestOMExecutionFlow.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/execution/TestOMExecutionFlow.java
new file mode 100644
index 00000000000..f327cdc7fe9
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/execution/TestOMExecutionFlow.java
@@ -0,0 +1,165 @@
+/*
+ * 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.om.execution;
+
+import static 
org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod.KERBEROS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.InetAddress;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.ipc_.RPC;
+import org.apache.hadoop.ipc_.Server;
+import org.apache.hadoop.ozone.om.OzoneAclUtils;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.ratis.OzoneManagerRatisServer;
+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.S3Authentication;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UserInfo;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
+import org.apache.hadoop.security.Credentials;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.token.Token;
+import org.apache.hadoop.security.token.TokenIdentifier;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class TestOMExecutionFlow {
+
+  @AfterEach
+  void tearDown() {
+    Server.getCurCall().remove();
+    OzoneManager.setS3Auth(null);
+    OzoneManager.setStsTokenIdentifier(null);
+  }
+
+  @Test
+  void readRequestUsesAuthenticatedCallerContext() throws Exception {
+    OzoneManager ozoneManager = mock(OzoneManager.class);
+    OzoneManagerRatisServer ratisServer = mock(OzoneManagerRatisServer.class);
+    AtomicReference<OMRequest> submittedRequest = new AtomicReference<>();
+    when(ozoneManager.getOmRatisServer()).thenReturn(ratisServer);
+    when(ratisServer.submitRequest(any(OMRequest.class), 
eq(false))).thenAnswer(invocation -> {
+      submittedRequest.set(invocation.getArgument(0));
+      return successResponse();
+    });
+
+    UserGroupInformation realUser = 
UserGroupInformation.createRemoteUser("kerberos-user");
+    realUser.setAuthenticationMethod(KERBEROS);
+    UserGroupInformation remoteUser = 
UserGroupInformation.createProxyUser("authenticated-user", realUser);
+    Credentials credentials = new Credentials();
+    credentials.addSecretKey(new Text("credential-key"), new byte[] {1, 2, 3});
+    remoteUser.addCredentials(credentials);
+    remoteUser.addToken(new Token<TokenIdentifier>(
+        new byte[] {4}, new byte[] {5}, new Text("token-kind"), new 
Text("token-service")));
+    InetAddress remoteAddress = InetAddress.getByAddress("client.example.com", 
new byte[] {10, 20, 30, 40});
+    Server.getCurCall().set(createCall(remoteUser, remoteAddress));
+
+    OMRequest request = OMRequest.newBuilder()
+        .setCmdType(Type.ServiceList)
+        .setClientId("client-id")
+        .setUserInfo(UserInfo.newBuilder()
+            .setUserName("forged-user")
+            .setHostName("forged-host")
+            .setRemoteAddress("192.0.2.1"))
+        .build();
+
+    new OMExecutionFlow(ozoneManager).submit(request, false);
+
+    UserInfo userInfo = submittedRequest.get().getUserInfo();
+    assertEquals("authenticated-user", userInfo.getUserName());
+    assertEquals("client.example.com", userInfo.getHostName());
+    assertEquals("10.20.30.40", userInfo.getRemoteAddress());
+  }
+
+  @Test
+  void readRequestCarriesResolvedStsContext() throws Exception {
+    OzoneManager ozoneManager = mock(OzoneManager.class);
+    OzoneManagerRatisServer ratisServer = mock(OzoneManagerRatisServer.class);
+    AtomicReference<OMRequest> submittedRequest = new AtomicReference<>();
+    when(ozoneManager.getOmRatisServer()).thenReturn(ratisServer);
+    when(ozoneManager.isSecurityEnabled()).thenReturn(true);
+    when(ratisServer.submitRequest(any(OMRequest.class), 
eq(false))).thenAnswer(invocation -> {
+      submittedRequest.set(invocation.getArgument(0));
+      return successResponse();
+    });
+
+    S3Authentication s3Authentication = S3Authentication.newBuilder()
+        .setAccessId("temp-access-id")
+        .setSessionToken("session-token")
+        .setS3Action("GetObject")
+        .build();
+    STSTokenIdentifier stsToken = mock(STSTokenIdentifier.class);
+    UUID secretKeyId = UUID.randomUUID();
+    when(stsToken.getSessionPolicy()).thenReturn("session-policy");
+    when(stsToken.getRoleArn()).thenReturn("role-arn");
+    when(stsToken.getOriginalAccessKeyId()).thenReturn("original-access-id");
+    when(stsToken.getTempAccessKeyId()).thenReturn("temp-access-id");
+    when(stsToken.getSecretKeyId()).thenReturn(secretKeyId);
+    OzoneManager.setS3Auth(s3Authentication);
+    OzoneManager.setStsTokenIdentifier(stsToken);
+
+    OMRequest request = OMRequest.newBuilder()
+        .setCmdType(Type.ServiceList)
+        .setClientId("client-id")
+        .setS3Authentication(s3Authentication)
+        .setUserInfo(UserInfo.newBuilder().setUserName("forged-user"))
+        .build();
+
+    new OMExecutionFlow(ozoneManager).submit(request, false);
+
+    OMRequest submitted = submittedRequest.get();
+    assertEquals(OzoneAclUtils.accessIdToUserPrincipal("original-access-id"),
+        submitted.getUserInfo().getUserName());
+    assertEquals("session-policy", 
submitted.getS3Authentication().getResolvedStsSessionPolicy());
+    assertEquals("role-arn", 
submitted.getS3Authentication().getResolvedStsRoleArn());
+    assertEquals("original-access-id", 
submitted.getS3Authentication().getResolvedStsOriginalAccessKeyId());
+    assertEquals("temp-access-id", 
submitted.getS3Authentication().getResolvedStsTempAccessKeyId());
+    assertEquals(secretKeyId.toString(), 
submitted.getS3Authentication().getResolvedStsSecretKeyId());
+  }
+
+  private static Server.Call createCall(UserGroupInformation user, InetAddress 
remoteAddress) {
+    return new Server.Call(1, 0, null, null, RPC.RpcKind.RPC_PROTOCOL_BUFFER, 
new byte[0]) {
+      @Override
+      public UserGroupInformation getRemoteUser() {
+        return user;
+      }
+
+      @Override
+      public InetAddress getHostInetAddress() {
+        return remoteAddress;
+      }
+    };
+  }
+
+  private static OMResponse successResponse() {
+    return OMResponse.newBuilder()
+        .setCmdType(Type.ServiceList)
+        .setStatus(Status.OK)
+        .setSuccess(true)
+        .build();
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/lock/TestOMLockDetailsUtil.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/lock/TestOMLockDetailsUtil.java
new file mode 100644
index 00000000000..82d5548d556
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/lock/TestOMLockDetailsUtil.java
@@ -0,0 +1,78 @@
+/*
+ * 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.om.lock;
+
+import static org.apache.hadoop.ipc_.ProcessingDetails.Timing.LOCKEXCLUSIVE;
+import static org.apache.hadoop.ipc_.ProcessingDetails.Timing.LOCKSHARED;
+import static org.apache.hadoop.ipc_.ProcessingDetails.Timing.LOCKWAIT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.ipc_.RPC;
+import org.apache.hadoop.ipc_.RpcConstants;
+import org.apache.hadoop.ipc_.Server;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMLockDetailsProto;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import org.junit.jupiter.api.Test;
+
+class TestOMLockDetailsUtil {
+
+  @Test
+  void testAddProcessingDetailsToResponse() {
+    Server.Call call = newCall();
+    call.getProcessingDetails().add(LOCKWAIT, 11, TimeUnit.NANOSECONDS);
+    call.getProcessingDetails().add(LOCKSHARED, 22, TimeUnit.NANOSECONDS);
+    call.getProcessingDetails().add(LOCKEXCLUSIVE, 33, TimeUnit.NANOSECONDS);
+    OMResponse response = newResponseBuilder()
+        .setOmLockDetails(OMLockDetailsProto.newBuilder()
+            .setIsLockAcquired(true)
+            .setWaitLockNanos(1)
+            .setReadLockNanos(2)
+            .setWriteLockNanos(3))
+        .build();
+
+    OMResponse result = OMLockDetailsUtil.addToResponse(response, 
call.getProcessingDetails());
+
+    assertTrue(result.getOmLockDetails().getIsLockAcquired());
+    assertEquals(12, result.getOmLockDetails().getWaitLockNanos());
+    assertEquals(24, result.getOmLockDetails().getReadLockNanos());
+    assertEquals(36, result.getOmLockDetails().getWriteLockNanos());
+  }
+
+  @Test
+  void testEmptyProcessingDetailsDoesNotChangeResponse() {
+    OMResponse response = OMResponse.getDefaultInstance();
+
+    assertSame(response, OMLockDetailsUtil.addToResponse(response, 
newCall().getProcessingDetails()));
+  }
+
+  private static Server.Call newCall() {
+    return new Server.Call(RpcConstants.INVALID_CALL_ID, 
RpcConstants.INVALID_RETRY_COUNT,
+        null, null, RPC.RpcKind.RPC_PROTOCOL_BUFFER, 
RpcConstants.DUMMY_CLIENT_ID);
+  }
+
+  private static OMResponse.Builder newResponseBuilder() {
+    return OMResponse.newBuilder()
+        .setCmdType(Type.ServiceList)
+        .setStatus(Status.OK);
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOMThreadContext.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOMThreadContext.java
new file mode 100644
index 00000000000..bd7c8e8c7ed
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOMThreadContext.java
@@ -0,0 +1,207 @@
+/*
+ * 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.om.ratis;
+
+import static org.apache.hadoop.ipc_.ProcessingDetails.Timing.LOCKSHARED;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.InetAddress;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.ipc_.RPC;
+import org.apache.hadoop.ipc_.RpcConstants;
+import org.apache.hadoop.ipc_.Server;
+import org.apache.hadoop.ozone.om.OzoneManager;
+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.S3Authentication;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UserInfo;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class TestOMThreadContext {
+
+  @AfterEach
+  void tearDown() {
+    Server.getCurCall().remove();
+    OzoneManager.setS3Auth(null);
+    OzoneManager.setStsTokenIdentifier(null);
+  }
+
+  @Test
+  void readContextAppliesRequestAndRestoresPreviousContext() throws Exception {
+    OzoneManager ozoneManager = secureOzoneManager();
+    S3Authentication requestS3Auth = 
S3Authentication.newBuilder().setAccessId("request-access-id").build();
+    OMRequest request = requestWithContext("request-user", requestS3Auth);
+    OMThreadContext context = OMThreadContext.forRead(request, ozoneManager);
+
+    Server.Call previousCall = createCall("previous-user");
+    S3Authentication previousS3Auth = 
S3Authentication.newBuilder().setAccessId("previous-access-id").build();
+    STSTokenIdentifier previousStsToken = mock(STSTokenIdentifier.class);
+    Server.getCurCall().set(previousCall);
+    OzoneManager.setS3Auth(previousS3Auth);
+    OzoneManager.setStsTokenIdentifier(previousStsToken);
+
+    try (OMThreadContext.Scope ignored = context.applyToCurrentThread()) {
+      assertEquals("request-user", Server.getRemoteUser().getUserName());
+      assertEquals("127.0.0.1", Server.getRemoteIp().getHostAddress());
+      assertEquals(requestS3Auth, OzoneManager.getS3Auth());
+      assertNull(OzoneManager.getStsTokenIdentifier());
+    }
+
+    assertSame(previousCall, Server.getCurCall().get());
+    assertSame(previousS3Auth, OzoneManager.getS3Auth());
+    assertSame(previousStsToken, OzoneManager.getStsTokenIdentifier());
+  }
+
+  @Test
+  void writeContextAppliesRequestAndRestoresPreviousContext() throws Exception 
{
+    OzoneManager ozoneManager = secureOzoneManager();
+    S3Authentication requestS3Auth = 
S3Authentication.newBuilder().setAccessId("request-access-id").build();
+    OMThreadContext context = 
OMThreadContext.forWrite(requestWithContext("request-user", requestS3Auth), 
ozoneManager);
+
+    Server.Call previousCall = createCall("previous-user");
+    S3Authentication previousS3Auth = 
S3Authentication.newBuilder().setAccessId("previous-access-id").build();
+    STSTokenIdentifier previousStsToken = mock(STSTokenIdentifier.class);
+    Server.getCurCall().set(previousCall);
+    OzoneManager.setS3Auth(previousS3Auth);
+    OzoneManager.setStsTokenIdentifier(previousStsToken);
+
+    try (OMThreadContext.Scope ignored = context.applyToCurrentThread()) {
+      assertNull(Server.getCurCall().get());
+      assertEquals(requestS3Auth, OzoneManager.getS3Auth());
+      assertNull(OzoneManager.getStsTokenIdentifier());
+    }
+
+    assertSame(previousCall, Server.getCurCall().get());
+    assertSame(previousS3Auth, OzoneManager.getS3Auth());
+    assertSame(previousStsToken, OzoneManager.getStsTokenIdentifier());
+  }
+
+  @Test
+  void eachReadScopeUsesFreshCallAndLockDetails() throws Exception {
+    OMThreadContext context = OMThreadContext.forRead(
+        requestWithContext("request-user", null), secureOzoneManager());
+    OMResponse response = OMResponse.newBuilder()
+        .setCmdType(Type.ServiceList)
+        .setStatus(Status.OK)
+        .setSuccess(true)
+        .build();
+
+    Server.Call firstCall;
+    try (OMThreadContext.Scope scope = context.applyToCurrentThread()) {
+      firstCall = Server.getCurCall().get();
+      firstCall.getProcessingDetails().add(LOCKSHARED, 11, 
TimeUnit.NANOSECONDS);
+      assertEquals(11, 
scope.addLockDetails(response).getOmLockDetails().getReadLockNanos());
+    }
+
+    try (OMThreadContext.Scope scope = context.applyToCurrentThread()) {
+      assertNotSame(firstCall, Server.getCurCall().get());
+      assertSame(response, scope.addLockDetails(response));
+    }
+  }
+
+  @Test
+  void closeIsIdempotent() throws Exception {
+    Server.Call previousCall = createCall("previous-user");
+    Server.getCurCall().set(previousCall);
+    OMThreadContext.Scope scope = OMThreadContext.forRead(
+        requestWithContext("request-user", null), 
secureOzoneManager()).applyToCurrentThread();
+
+    scope.close();
+    scope.close();
+
+    assertSame(previousCall, Server.getCurCall().get());
+  }
+
+  @Test
+  void closeRejectsDifferentThread() throws Exception {
+    Server.Call previousCall = createCall("previous-user");
+    Server.getCurCall().set(previousCall);
+    OMThreadContext.Scope scope = OMThreadContext.forRead(
+        requestWithContext("request-user", null), 
secureOzoneManager()).applyToCurrentThread();
+
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    try {
+      Future<Void> close = executor.submit(() -> {
+        scope.close();
+        return null;
+      });
+      ExecutionException exception = assertThrows(ExecutionException.class, 
close::get);
+      assertInstanceOf(IllegalStateException.class, exception.getCause());
+      assertEquals("OM thread context must be closed on the thread where it 
was applied",
+          exception.getCause().getMessage());
+    } finally {
+      executor.shutdownNow();
+      scope.close();
+    }
+
+    assertSame(previousCall, Server.getCurCall().get());
+  }
+
+  private static OzoneManager secureOzoneManager() {
+    OzoneManager ozoneManager = mock(OzoneManager.class);
+    when(ozoneManager.isSecurityEnabled()).thenReturn(true);
+    return ozoneManager;
+  }
+
+  private static OMRequest requestWithContext(String userName, 
S3Authentication s3Authentication) {
+    OMRequest.Builder builder = OMRequest.newBuilder()
+        .setCmdType(Type.ServiceList)
+        .setClientId("client-id")
+        .setUserInfo(UserInfo.newBuilder()
+            .setUserName(userName)
+            .setHostName("localhost")
+            .setRemoteAddress("127.0.0.1"));
+    if (s3Authentication != null) {
+      builder.setS3Authentication(s3Authentication);
+    }
+    return builder.build();
+  }
+
+  private static Server.Call createCall(String userName) throws Exception {
+    UserGroupInformation user = 
UserGroupInformation.createRemoteUser(userName);
+    InetAddress address = InetAddress.getByAddress("localhost", new byte[] 
{127, 0, 0, 1});
+    return new Server.Call(RpcConstants.INVALID_CALL_ID, 
RpcConstants.INVALID_RETRY_COUNT,
+        null, null, RPC.RpcKind.RPC_PROTOCOL_BUFFER, 
RpcConstants.DUMMY_CLIENT_ID) {
+      @Override
+      public UserGroupInformation getRemoteUser() {
+        return user;
+      }
+
+      @Override
+      public InetAddress getHostInetAddress() {
+        return address;
+      }
+    };
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerStateMachine.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerStateMachine.java
index 40975d5af61..c5640b92c01 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerStateMachine.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerStateMachine.java
@@ -17,6 +17,8 @@
 
 package org.apache.hadoop.ozone.om.ratis;
 
+import static 
org.apache.hadoop.ozone.om.lock.OzoneManagerLock.LeveledResource.VOLUME_LOCK;
+import static 
org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod.SIMPLE;
 import static org.apache.ozone.test.GenericTestUtils.waitFor;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -38,9 +40,12 @@
 import static org.mockito.Mockito.when;
 
 import java.io.IOException;
+import java.net.InetAddress;
 import java.nio.file.Path;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
@@ -49,9 +54,14 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.LockSupport;
+import java.util.stream.Collectors;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.utils.TransactionInfo;
 import org.apache.hadoop.hdds.utils.db.DBStore;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.ipc_.RPC;
+import org.apache.hadoop.ipc_.Server;
 import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.audit.AuditEventStatus;
 import org.apache.hadoop.ozone.audit.AuditMessage;
@@ -68,6 +78,7 @@
 import org.apache.hadoop.ozone.om.ha.OMServiceManager;
 import org.apache.hadoop.ozone.om.helpers.OMRatisHelper;
 import org.apache.hadoop.ozone.om.lock.OMLockDetails;
+import org.apache.hadoop.ozone.om.lock.OzoneManagerLock;
 import org.apache.hadoop.ozone.om.ratis_snapshot.OmRatisSnapshotProvider;
 import org.apache.hadoop.ozone.om.response.DummyOMClientResponse;
 import org.apache.hadoop.ozone.om.response.OMClientResponse;
@@ -79,10 +90,12 @@
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.PrepareRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.PrepareRequestArgs;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.PrepareStatusResponse.PrepareStatus;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UserInfo;
 import org.apache.hadoop.ozone.protocolPB.RequestHandler;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.ratis.proto.RaftProtos;
 import org.apache.ratis.protocol.ClientId;
@@ -134,9 +147,23 @@ public void setup() {
   @AfterEach
   public void tearDown() {
     sm.stop();
+    Server.getCurCall().remove();
+    OzoneManager.setS3Auth(null);
     OzoneManager.setStsTokenIdentifier(null);
   }
 
+  @Test
+  public void testOzoneManagerThreadLocalsAreReviewedForRatisPropagation() {
+    List<String> threadLocalFields = 
Arrays.stream(OzoneManager.class.getDeclaredFields())
+        .filter(field -> ThreadLocal.class.isAssignableFrom(field.getType()))
+        .map(field -> field.getName())
+        .sorted()
+        .collect(Collectors.toList());
+
+    assertEquals(Arrays.asList("S3_AUTH", "STS_TOKEN"), threadLocalFields,
+        "Update OMThreadContext when adding request-scoped ThreadLocal fields 
to OzoneManager");
+  }
+
   // --- startTransaction tests ---
 
   @Test
@@ -455,6 +482,40 @@ public void testRunCommandSetsAndClearsStsThreadLocal() 
throws Exception {
     assertNull(OzoneManager.getStsTokenIdentifier(), "Expected STS ThreadLocal 
to be cleared after runCommand");
   }
 
+  @Test
+  public void testRunCommandRestoresPreviousRequestContext() throws Exception {
+    OMRequest request = sampleWriteRequest();
+    TermIndex ti = TermIndex.valueOf(1, 5);
+    OMResponse expectedResponse = OMResponse.newBuilder()
+        .setCmdType(Type.CreateKey)
+        .setStatus(Status.OK)
+        .setSuccess(true)
+        .build();
+    OMClientResponse clientResponse = mock(OMClientResponse.class);
+    when(clientResponse.getOMResponse()).thenReturn(expectedResponse);
+    when(clientResponse.getOmLockDetails()).thenReturn(null);
+    when(handler.handleWriteRequest(eq(request), any(), 
eq(doubleBuffer))).thenAnswer(invocation -> {
+      assertNull(Server.getCurCall().get());
+      assertNull(OzoneManager.getS3Auth());
+      assertNull(OzoneManager.getStsTokenIdentifier());
+      return clientResponse;
+    });
+
+    Server.Call previousCall = createCall("previous-user", 
"previous.example.com", new byte[] {10, 0, 0, 1});
+    S3Authentication previousS3Auth = 
S3Authentication.newBuilder().setAccessId("previous-access-id").build();
+    STSTokenIdentifier previousStsToken = mock(STSTokenIdentifier.class);
+    Server.getCurCall().set(previousCall);
+    OzoneManager.setS3Auth(previousS3Auth);
+    OzoneManager.setStsTokenIdentifier(previousStsToken);
+
+    OMResponse result = sm.runCommand(request, ti);
+
+    assertTrue(result.getSuccess());
+    assertSame(previousCall, Server.getCurCall().get());
+    assertSame(previousS3Auth, OzoneManager.getS3Auth());
+    assertSame(previousStsToken, OzoneManager.getStsTokenIdentifier());
+  }
+
   @Test
   public void testRunCommandSetsStsThreadLocalWithLegacyResolvedFields() 
throws Exception {
     when(om.isSecurityEnabled()).thenReturn(true);
@@ -635,6 +696,153 @@ public void testQueryIOException() {
     assertInstanceOf(IOException.class, ex.getCause());
   }
 
+  @Test
+  public void testQueryInstallsAndRestoresCallerContext() throws Exception {
+    UserInfo userInfo = UserInfo.newBuilder()
+        .setUserName("ratis-user")
+        .setHostName("client.example.com")
+        .setRemoteAddress("10.20.30.40")
+        .build();
+    OMRequest request = sampleReadRequest().toBuilder()
+        .setUserInfo(userInfo)
+        .build();
+    OMResponse expectedResponse = OMResponse.newBuilder()
+        .setCmdType(Type.ServiceList)
+        .setStatus(Status.OK)
+        .setSuccess(true)
+        .build();
+    AtomicReference<UserGroupInformation> observedUser = new 
AtomicReference<>();
+    AtomicReference<InetAddress> observedAddress = new AtomicReference<>();
+    
when(handler.handleReadRequest(any(OMRequest.class))).thenAnswer(invocation -> {
+      observedUser.set(Server.getRemoteUser());
+      observedAddress.set(Server.getRemoteIp());
+      return expectedResponse;
+    });
+
+    Server.Call previousCall = createCall("previous-user", 
"previous.example.com", new byte[] {10, 0, 0, 1});
+    Server.getCurCall().set(previousCall);
+
+    
sm.query(Message.valueOf(OMRatisHelper.convertRequestToByteString(request))).get();
+
+    assertSame(previousCall, Server.getCurCall().get());
+    assertEquals("ratis-user", observedUser.get().getUserName());
+    assertEquals(SIMPLE, observedUser.get().getAuthenticationMethod());
+    assertNull(observedUser.get().getRealUser());
+    assertTrue(observedUser.get().getTokens().isEmpty());
+    assertNull(observedUser.get().getCredentials().getSecretKey(new 
Text("credential-key")));
+    assertEquals("10.20.30.40", observedAddress.get().getHostAddress());
+    assertEquals("client.example.com", observedAddress.get().getHostName());
+  }
+
+  @Test
+  public void testQueryReturnsResourceLockDetails() throws Exception {
+    OMRequest request = sampleReadRequest().toBuilder()
+        .setUserInfo(UserInfo.newBuilder().setUserName("ratis-user"))
+        .build();
+    OMResponse expectedResponse = OMResponse.newBuilder()
+        .setCmdType(Type.ServiceList)
+        .setStatus(Status.OK)
+        .setSuccess(true)
+        .build();
+    OzoneManagerLock lock = new OzoneManagerLock(new OzoneConfiguration());
+    
when(handler.handleReadRequest(any(OMRequest.class))).thenAnswer(invocation -> {
+      lock.acquireReadLock(VOLUME_LOCK, "volume");
+      try {
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
+        return expectedResponse;
+      } finally {
+        lock.releaseReadLock(VOLUME_LOCK, "volume");
+      }
+    });
+
+    OMResponse response;
+    try {
+      Message message = sm.query(Message.valueOf(
+          OMRatisHelper.convertRequestToByteString(request))).get();
+      response = 
OMRatisHelper.convertByteStringToOMResponse(message.getContent());
+    } finally {
+      lock.cleanup();
+    }
+
+    assertTrue(response.hasOmLockDetails());
+    assertTrue(response.getOmLockDetails().getWaitLockNanos() > 0);
+    assertTrue(response.getOmLockDetails().getReadLockNanos() > 0);
+    assertEquals(0, response.getOmLockDetails().getWriteLockNanos());
+  }
+
+  @Test
+  public void testQueryInstallsAndRestoresS3Context() throws Exception {
+    when(om.isSecurityEnabled()).thenReturn(true);
+    S3Authentication requestS3Auth = S3Authentication.newBuilder()
+        .setAccessId("temp-access-id")
+        .setSessionToken("session-token")
+        .setS3Action("GetObject")
+        .setResolvedStsSessionPolicy("session-policy")
+        .setResolvedStsRoleArn("role-arn")
+        .setResolvedStsOriginalAccessKeyId("original-access-id")
+        .setResolvedStsTempAccessKeyId("temp-access-id")
+        .setResolvedStsSecretKeyId(UUID.randomUUID().toString())
+        .build();
+    OMRequest request = sampleReadRequest().toBuilder()
+        .setS3Authentication(requestS3Auth)
+        .build();
+    OMResponse expectedResponse = OMResponse.newBuilder()
+        .setCmdType(Type.ServiceList)
+        .setStatus(Status.OK)
+        .setSuccess(true)
+        .build();
+    AtomicReference<S3Authentication> observedS3Auth = new AtomicReference<>();
+    AtomicReference<STSTokenIdentifier> observedStsToken = new 
AtomicReference<>();
+    
when(handler.handleReadRequest(any(OMRequest.class))).thenAnswer(invocation -> {
+      observedS3Auth.set(OzoneManager.getS3Auth());
+      observedStsToken.set(OzoneManager.getStsTokenIdentifier());
+      return expectedResponse;
+    });
+
+    S3Authentication previousS3Auth = 
S3Authentication.newBuilder().setAccessId("previous-access-id").build();
+    STSTokenIdentifier previousStsToken = mock(STSTokenIdentifier.class);
+    OzoneManager.setS3Auth(previousS3Auth);
+    OzoneManager.setStsTokenIdentifier(previousStsToken);
+
+    
sm.query(Message.valueOf(OMRatisHelper.convertRequestToByteString(request))).get();
+
+    assertSame(previousS3Auth, OzoneManager.getS3Auth());
+    assertSame(previousStsToken, OzoneManager.getStsTokenIdentifier());
+    assertEquals(requestS3Auth, observedS3Auth.get());
+    assertEquals("session-policy", observedStsToken.get().getSessionPolicy());
+    assertEquals("role-arn", observedStsToken.get().getRoleArn());
+    assertEquals("original-access-id", 
observedStsToken.get().getOriginalAccessKeyId());
+    assertEquals("temp-access-id", 
observedStsToken.get().getTempAccessKeyId());
+  }
+
+  @Test
+  public void testQueryRestoresContextAfterFailure() throws Exception {
+    when(om.isSecurityEnabled()).thenReturn(true);
+    S3Authentication requestS3Auth = S3Authentication.newBuilder()
+        .setAccessId("request-access-id")
+        .build();
+    OMRequest request = sampleReadRequest().toBuilder()
+        .setUserInfo(UserInfo.newBuilder().setUserName("request-user"))
+        .setS3Authentication(requestS3Auth)
+        .build();
+    when(handler.handleReadRequest(any(OMRequest.class)))
+        .thenThrow(new IllegalStateException("read failed"));
+
+    Server.Call previousCall = createCall("previous-user", 
"previous.example.com", new byte[] {10, 0, 0, 1});
+    S3Authentication previousS3Auth = 
S3Authentication.newBuilder().setAccessId("previous-access-id").build();
+    STSTokenIdentifier previousStsToken = mock(STSTokenIdentifier.class);
+    Server.getCurCall().set(previousCall);
+    OzoneManager.setS3Auth(previousS3Auth);
+    OzoneManager.setStsTokenIdentifier(previousStsToken);
+
+    assertThrows(IllegalStateException.class,
+        () -> 
sm.query(Message.valueOf(OMRatisHelper.convertRequestToByteString(request))));
+
+    assertSame(previousCall, Server.getCurCall().get());
+    assertSame(previousS3Auth, OzoneManager.getS3Auth());
+    assertSame(previousStsToken, OzoneManager.getStsTokenIdentifier());
+  }
+
   // --- notifyTermIndexUpdated tests ---
 
   @Test
@@ -1317,6 +1525,22 @@ private OMRequest sampleReadRequest() {
         .build();
   }
 
+  private static Server.Call createCall(String userName, String hostName, 
byte[] address) throws IOException {
+    UserGroupInformation user = 
UserGroupInformation.createRemoteUser(userName);
+    InetAddress remoteAddress = InetAddress.getByAddress(hostName, address);
+    return new Server.Call(1, 0, null, null, RPC.RpcKind.RPC_PROTOCOL_BUFFER, 
new byte[0]) {
+      @Override
+      public UserGroupInformation getRemoteUser() {
+        return user;
+      }
+
+      @Override
+      public InetAddress getHostInetAddress() {
+        return remoteAddress;
+      }
+    };
+  }
+
   private OzoneManagerProtocolProtos.S3Authentication stsS3Authentication() {
     return OzoneManagerProtocolProtos.S3Authentication.newBuilder()
         .setAccessId("accessId")
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/TestOMResponse.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/TestOMResponse.java
index bf7defdb4c1..7d0a0d0dc6d 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/TestOMResponse.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/TestOMResponse.java
@@ -18,21 +18,30 @@
 package org.apache.hadoop.ozone.om.response;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import org.apache.hadoop.hdds.utils.ProtocolMessageMetrics;
+import org.apache.hadoop.ipc_.ProcessingDetails.Timing;
+import org.apache.hadoop.ipc_.RPC;
+import org.apache.hadoop.ipc_.Server;
 import org.apache.hadoop.ozone.om.OMConfigKeys;
 import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OMMetrics;
 import org.apache.hadoop.ozone.om.OmConfig;
 import org.apache.hadoop.ozone.om.OmMetadataManagerImpl;
 import org.apache.hadoop.ozone.om.OzoneManager;
 import org.apache.hadoop.ozone.om.execution.OMExecutionFlow;
 import org.apache.hadoop.ozone.om.ratis.OzoneManagerRatisServer;
+import 
org.apache.hadoop.ozone.om.ratis.OzoneManagerRatisServer.RaftServerStatus;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
 import 
org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
 import org.apache.ozone.test.GenericTestUtils;
@@ -49,6 +58,8 @@ public class TestOMResponse {
   private Path folder;
 
   private OzoneManagerProtocolServerSideTranslatorPB translator;
+  private OzoneManager ozoneManager;
+  private OzoneManagerRatisServer ratisServer;
   private GenericTestUtils.LogCapturer logCapturer;
   private static final long IPC_MAX_LEN = 1024 * 1024; // 1MB
   private static final long WARN_THRESHOLD = IPC_MAX_LEN / 2; // 512KB
@@ -61,7 +72,7 @@ public void setup() throws Exception {
     configuration.setLong(
         "ipc.maximum.response.length", IPC_MAX_LEN);
 
-    OzoneManager ozoneManager = mock(OzoneManager.class);
+    ozoneManager = mock(OzoneManager.class);
     OMMetadataManager omMetadataManager = new OmMetadataManagerImpl(
         configuration, ozoneManager);
     when(ozoneManager.getMetadataManager()).thenReturn(omMetadataManager);
@@ -73,7 +84,7 @@ public void setup() throws Exception {
     OMExecutionFlow omExecutionFlow = new OMExecutionFlow(ozoneManager);
     when(ozoneManager.getOmExecutionFlow()).thenReturn(omExecutionFlow);
 
-    OzoneManagerRatisServer ratisServer = mock(OzoneManagerRatisServer.class);
+    ratisServer = mock(OzoneManagerRatisServer.class);
     when(ratisServer.checkRetryCache()).thenReturn(null);
 
     ProtocolMessageMetrics<OzoneManagerProtocolProtos.Type> 
protocolMessageMetrics =
@@ -146,4 +157,40 @@ public void testSmallResponseNoLogging() {
     long responseSize = smallResponse.getSerializedSize();
     assertThat(responseSize).isLessThan(WARN_THRESHOLD);
   }
+
+  @Test
+  public void testRatisReadLockDetailsAddedToCurrentCall() throws Exception {
+    OMExecutionFlow executionFlow = mock(OMExecutionFlow.class);
+    when(ozoneManager.getOmExecutionFlow()).thenReturn(executionFlow);
+    when(ozoneManager.getMetrics()).thenReturn(mock(OMMetrics.class));
+    
when(ratisServer.getLeaderStatus()).thenReturn(RaftServerStatus.LEADER_AND_READY);
+    when(ratisServer.isLinearizableRead()).thenReturn(true);
+
+    OMRequest request = OMRequest.newBuilder()
+        .setCmdType(OzoneManagerProtocolProtos.Type.ServiceList)
+        .setClientId("test-client")
+        .build();
+    OMResponse response = OMResponse.newBuilder()
+        .setCmdType(request.getCmdType())
+        .setStatus(OzoneManagerProtocolProtos.Status.OK)
+        .setSuccess(true)
+        
.setOmLockDetails(OzoneManagerProtocolProtos.OMLockDetailsProto.newBuilder()
+            .setWaitLockNanos(11)
+            .setReadLockNanos(22)
+            .setWriteLockNanos(33))
+        .build();
+    when(executionFlow.submit(any(OMRequest.class), 
eq(false))).thenReturn(response);
+
+    Server.Call call = new Server.Call(1, 0, null, null, 
RPC.RpcKind.RPC_PROTOCOL_BUFFER, new byte[0]);
+    Server.getCurCall().set(call);
+    try {
+      translator.processRequest(request);
+
+      assertThat(call.getProcessingDetails().get(Timing.LOCKWAIT, 
TimeUnit.NANOSECONDS)).isEqualTo(11);
+      assertThat(call.getProcessingDetails().get(Timing.LOCKSHARED, 
TimeUnit.NANOSECONDS)).isEqualTo(22);
+      assertThat(call.getProcessingDetails().get(Timing.LOCKEXCLUSIVE, 
TimeUnit.NANOSECONDS)).isEqualTo(33);
+    } finally {
+      Server.getCurCall().remove();
+    }
+  }
 }


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

Reply via email to