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

devmadhuu pushed a commit to branch HDDS-11233
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-11233 by this push:
     new 1ebaf1383cc HDDS-16665. Create key should support StoragePolicy 
(#11377)
1ebaf1383cc is described below

commit 1ebaf1383cc4252311f9c676816c29536db0a7f8
Author: Devesh Kumar Singh <[email protected]>
AuthorDate: Wed Oct 14 09:36:22 2026 +0530

    HDDS-16665. Create key should support StoragePolicy (#11377)
---
 .../hadoop/hdds/scm/storage/BlockOutputStream.java |  17 +-
 .../hdds/scm/storage/ECBlockOutputStream.java      |   5 +-
 .../hdds/scm/storage/RatisBlockOutputStream.java   |   7 +-
 .../storage/TestBlockOutputStreamCorrectness.java  |   4 +-
 .../hadoop/hdds/client/OzoneStoragePolicy.java     |   2 +
 .../hadoop/hdds/client/StorageTypeUtils.java       |   2 +-
 .../apache/hadoop/ozone/client/OzoneBucket.java    |  25 ++
 .../org/apache/hadoop/ozone/client/OzoneKey.java   |  39 +-
 .../hadoop/ozone/client/OzoneKeyDetails.java       |  19 +-
 .../ozone/client/io/BlockOutputStreamEntry.java    |  13 +-
 .../client/io/BlockOutputStreamEntryPool.java      |  20 +
 .../ozone/client/protocol/ClientProtocol.java      |  25 ++
 .../apache/hadoop/ozone/client/rpc/RpcClient.java  |  17 +-
 .../apache/hadoop/ozone/om/helpers/OmKeyArgs.java  |  21 +
 .../apache/hadoop/ozone/om/helpers/OmKeyInfo.java  |  25 ++
 ...OzoneManagerProtocolClientSideTranslatorPB.java |  10 +
 .../ozone/client/rpc/TestOzoneStoragePolicy.java   | 446 +++++++++++++++++++++
 .../src/main/proto/OmClientProtocol.proto          |   9 +
 .../ozone/om/request/file/OMFileCreateRequest.java |   9 +-
 .../om/request/key/OMAllocateBlockRequest.java     |   5 +-
 .../ozone/om/request/key/OMKeyCreateRequest.java   |  11 +-
 .../om/request/key/OMKeyCreateRequestWithFSO.java  |  12 +
 .../hadoop/ozone/om/request/key/OMKeyRequest.java  |  44 +-
 .../om/request/key/TestOMAllocateBlockRequest.java |  10 +-
 24 files changed, 770 insertions(+), 27 deletions(-)

diff --git 
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockOutputStream.java
 
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockOutputStream.java
index 98855d28ac0..3e0624e4050 100644
--- 
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockOutputStream.java
+++ 
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockOutputStream.java
@@ -100,6 +100,9 @@ public class BlockOutputStream extends OutputStream {
   private AtomicReference<BlockID> blockID;
   // planned block full size
   private long blockSize;
+  // Storage type the block should be written to, from the key's storage 
policy.
+  // Null leaves the datanode free to pick any volume.
+  private final StorageType storageType;
   private AtomicBoolean eofSent = new AtomicBoolean(false);
   private final AtomicReference<ChunkInfo> previousChunkInfo
       = new AtomicReference<>();
@@ -179,19 +182,23 @@ public BlockOutputStream(
       OzoneClientConfig config,
       Token<? extends TokenIdentifier> token,
       ContainerClientMetrics clientMetrics, StreamBufferArgs streamBufferArgs,
-      Supplier<ExecutorService> blockOutputStreamResourceProvider
+      Supplier<ExecutorService> blockOutputStreamResourceProvider,
+      StorageType storageType
   ) throws IOException {
     this.xceiverClientFactory = xceiverClientManager;
     this.config = config;
     this.blockID = new AtomicReference<>(blockID);
     this.blockSize = blockSize;
+    this.storageType = storageType;
     replicationIndex = pipeline.getReplicaIndex(pipeline.getClosestNode());
     KeyValue keyValue =
         KeyValue.newBuilder().setKey("TYPE").setValue("KEY").build();
 
+    // Send the storage type SCM chose for this block so the datanode places 
it on a volume of
+    // that type. Null leaves the datanode free to pick any volume.
     this.containerBlockData = BlockData.newBuilder()
         .setBlockID(blockID.getDatanodeBlockIDProtobufBuilder(replicationIndex,
-            StorageTypeUtils.getID(StorageType.DISK)))
+            storageType == null ? null : StorageTypeUtils.getID(storageType)))
         .addMetadata(keyValue);
     this.pipeline = pipeline;
     // tell DataNode I will send incremental chunk list
@@ -959,12 +966,10 @@ private CompletableFuture<PutBlockResult> 
writeChunkToContainer(
         byteBufferList = null;
       }
 
-      // TODO: Pass the requested storage type from allocation/storage policy
-      // once that context is wired through BlockOutputStream. Null preserves
-      // the current any-volume behavior.
       asyncReply = writeChunkAsync(xceiverClient, chunkInfo,
           blockID.get(), data, tokenString, replicationIndex, blockData, close,
-          null, containerAutoCreate());
+          storageType == null ? null : 
StorageTypeUtils.getStorageTypeProto(storageType),
+          containerAutoCreate());
       CompletableFuture<ContainerCommandResponseProto>
           respFuture = asyncReply.getResponse();
       validateFuture = respFuture.thenApplyAsync(e -> {
diff --git 
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ECBlockOutputStream.java
 
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ECBlockOutputStream.java
index 5f33e02f4d5..7b55a4be312 100644
--- 
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ECBlockOutputStream.java
+++ 
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ECBlockOutputStream.java
@@ -101,8 +101,11 @@ public ECBlockOutputStream(
       Supplier<ExecutorService> executorServiceSupplier,
       boolean containerAutoCreate
   ) throws IOException {
+    // EC keys do not carry a per-block storage type yet; null leaves the 
datanode
+    // free to pick any volume. EC storage policy support is tracked 
separately.
     super(blockID, -1, xceiverClientManager,
-        pipeline, bufferPool, config, token, clientMetrics, streamBufferArgs, 
executorServiceSupplier);
+        pipeline, bufferPool, config, token, clientMetrics, streamBufferArgs, 
executorServiceSupplier,
+        null);
     // In EC stream, there will be only one node in pipeline.
     this.datanodeDetails = pipeline.getClosestNode();
     this.containerAutoCreate = containerAutoCreate;
diff --git 
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/RatisBlockOutputStream.java
 
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/RatisBlockOutputStream.java
index 684707908fb..d2b0eff67d1 100644
--- 
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/RatisBlockOutputStream.java
+++ 
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/RatisBlockOutputStream.java
@@ -26,6 +26,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Supplier;
+import org.apache.hadoop.fs.StorageType;
 import org.apache.hadoop.fs.Syncable;
 import org.apache.hadoop.hdds.client.BlockID;
 import org.apache.hadoop.hdds.scm.ContainerClientMetrics;
@@ -78,10 +79,12 @@ public RatisBlockOutputStream(
       OzoneClientConfig config,
       Token<? extends TokenIdentifier> token,
       ContainerClientMetrics clientMetrics, StreamBufferArgs streamBufferArgs,
-      Supplier<ExecutorService> blockOutputStreamResourceProvider
+      Supplier<ExecutorService> blockOutputStreamResourceProvider,
+      StorageType storageType
   ) throws IOException {
     super(blockID, blockSize, xceiverClientManager, pipeline,
-        bufferPool, config, token, clientMetrics, streamBufferArgs, 
blockOutputStreamResourceProvider);
+        bufferPool, config, token, clientMetrics, streamBufferArgs, 
blockOutputStreamResourceProvider,
+        storageType);
     this.commitWatcher = new CommitWatcher(bufferPool, getXceiverClient());
   }
 
diff --git 
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockOutputStreamCorrectness.java
 
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockOutputStreamCorrectness.java
index 81deaaf36bb..460c6519668 100644
--- 
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockOutputStreamCorrectness.java
+++ 
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockOutputStreamCorrectness.java
@@ -32,6 +32,7 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.commons.lang3.RandomUtils;
+import org.apache.hadoop.fs.StorageType;
 import org.apache.hadoop.hdds.client.BlockID;
 import org.apache.hadoop.hdds.client.ECReplicationConfig;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
@@ -221,7 +222,8 @@ private BlockOutputStream 
createBlockOutputStream(BufferPool bufferPool)
         null,
         ContainerClientMetrics.acquire(),
         streamBufferArgs,
-        () -> newFixedThreadPool(10));
+        () -> newFixedThreadPool(10),
+        StorageType.DISK);
   }
 
   private ECBlockOutputStream createECBlockOutputStream(OzoneClientConfig 
clientConfig,
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/OzoneStoragePolicy.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/OzoneStoragePolicy.java
index cdfbf2953db..b27297fa6ea 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/OzoneStoragePolicy.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/OzoneStoragePolicy.java
@@ -18,6 +18,7 @@
 package org.apache.hadoop.hdds.client;
 
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos.StoragePolicyProto;
+import org.apache.ratis.util.Preconditions;
 
 /**
  * Enum defining different storage policies using StorageTier.
@@ -117,6 +118,7 @@ public static OzoneStoragePolicy getDefaultPolicy() {
   }
 
   public static void setDefaultPolicy(OzoneStoragePolicy storagePolicy) {
+    Preconditions.assertNotNull(storagePolicy, "storagePolicy == null");
     defaultPolicy = storagePolicy;
   }
 
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/StorageTypeUtils.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/StorageTypeUtils.java
index 0b56ccd55a2..55cf6ec74d5 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/StorageTypeUtils.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/client/StorageTypeUtils.java
@@ -108,7 +108,7 @@ public static int getIDFromProtobuf(@Nonnull 
HddsProtos.StorageTypeProto proto)
    * Returns integer representation of FileSystem StorageType.
    * @return storageType int ID value
    */
-  public static int getID(StorageType storageType) throws 
IllegalArgumentException {
+  public static int getID(@Nonnull StorageType storageType) throws 
IllegalArgumentException {
     return getStorageTypeProto(storageType).getNumber();
   }
 }
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
index 930d3d9db6c..e539247b486 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneBucket.java
@@ -578,6 +578,31 @@ public OzoneOutputStream createKey(String key, long size,
         .createKey(volumeName, name, key, size, replicationConfig, 
keyMetadata, tags, derivedKeyPiggyBacking);
   }
 
+  /**
+   * Creates a new key in this bucket and places it according to the given 
storage
+   * policy.
+   *
+   * @param key               Name of the key to be created.
+   * @param size              Size of the data the key will hold.
+   * @param replicationConfig Replication configuration.
+   * @param keyMetadata       Custom key metadata.
+   * @param tags              Tags used for S3 object tags.
+   * @param keyStoragePolicy  Storage policy for the key, or null to inherit 
this
+   *                          bucket's policy.
+   * @return OzoneOutputStream to which the data has to be written.
+   * @throws IOException
+   */
+  @SuppressWarnings("checkstyle:parameternumber")
+  public OzoneOutputStream createKey(String key, long size,
+      ReplicationConfig replicationConfig,
+      Map<String, String> keyMetadata,
+      Map<String, String> tags,
+      StoragePolicy keyStoragePolicy)
+      throws IOException {
+    return proxy
+        .createKey(volumeName, name, key, size, replicationConfig, 
keyMetadata, tags, keyStoragePolicy);
+  }
+
   /**
    * This API allows to atomically update an existing key. The key read before 
invoking this API
    * should remain unchanged for this key to be written. This is controlled by 
the generation
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneKey.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneKey.java
index 7cd0bc42c59..2a5df6a6c42 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneKey.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneKey.java
@@ -23,6 +23,7 @@
 import java.util.Map;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
 import org.apache.hadoop.hdds.client.ReplicationType;
+import org.apache.hadoop.hdds.client.StoragePolicy;
 import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
 
 /**
@@ -70,11 +71,26 @@ public class OzoneKey {
    */
   private final boolean isFile;
 
+  /**
+   * Storage policy the key was created with. Null for keys written before 
storage
+   * policy support.
+   */
+  private final StoragePolicy storagePolicy;
+
   @SuppressWarnings("parameternumber")
   public OzoneKey(String volumeName, String bucketName,
       String keyName, long size, long creationTime,
       long modificationTime, ReplicationConfig replicationConfig,
       boolean isFile, String owner) {
+    this(volumeName, bucketName, keyName, size, creationTime, modificationTime,
+        replicationConfig, isFile, owner, null);
+  }
+
+  @SuppressWarnings("parameternumber")
+  public OzoneKey(String volumeName, String bucketName,
+      String keyName, long size, long creationTime,
+      long modificationTime, ReplicationConfig replicationConfig,
+      boolean isFile, String owner, StoragePolicy storagePolicy) {
     this.volumeName = volumeName;
     this.bucketName = bucketName;
     this.name = keyName;
@@ -84,6 +100,7 @@ public OzoneKey(String volumeName, String bucketName,
     this.replicationConfig = replicationConfig;
     this.isFile = isFile;
     this.owner = owner;
+    this.storagePolicy = storagePolicy;
   }
 
   @SuppressWarnings("parameternumber")
@@ -92,8 +109,18 @@ public OzoneKey(String volumeName, String bucketName,
                   long modificationTime, ReplicationConfig replicationConfig,
                   Map<String, String> metadata, boolean isFile, String owner,
                   Map<String, String> tags) {
+    this(volumeName, bucketName, keyName, size, creationTime, modificationTime,
+        replicationConfig, metadata, isFile, owner, tags, null);
+  }
+
+  @SuppressWarnings("parameternumber")
+  public OzoneKey(String volumeName, String bucketName,
+                  String keyName, long size, long creationTime,
+                  long modificationTime, ReplicationConfig replicationConfig,
+                  Map<String, String> metadata, boolean isFile, String owner,
+                  Map<String, String> tags, StoragePolicy storagePolicy) {
     this(volumeName, bucketName, keyName, size, creationTime,
-        modificationTime, replicationConfig, isFile, owner);
+        modificationTime, replicationConfig, isFile, owner, storagePolicy);
     this.metadata.putAll(metadata);
     this.tags.putAll(tags);
   }
@@ -213,6 +240,14 @@ public boolean isFile() {
     return isFile;
   }
 
+  /**
+   * Returns the storage policy the key was created with, or null when the key
+   * predates storage policy support.
+   */
+  public StoragePolicy getStoragePolicy() {
+    return storagePolicy;
+  }
+
   /**
    * Constructs OzoneKey from OmKeyInfo.
    *
@@ -222,7 +257,7 @@ public static OzoneKey fromKeyInfo(OmKeyInfo keyInfo) {
         keyInfo.getKeyName(), keyInfo.getDataSize(), keyInfo.getCreationTime(),
         keyInfo.getModificationTime(), keyInfo.getReplicationConfig(),
         keyInfo.getMetadata(), keyInfo.isFile(), keyInfo.getOwnerName(),
-        keyInfo.getTags());
+        keyInfo.getTags(), keyInfo.getStoragePolicy());
   }
 
 }
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneKeyDetails.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneKeyDetails.java
index 7514f2a7cd7..52976b795f7 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneKeyDetails.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneKeyDetails.java
@@ -23,6 +23,7 @@
 import java.util.Map;
 import org.apache.hadoop.fs.FileEncryptionInfo;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.StoragePolicy;
 import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.client.io.OzoneInputStream;
 import org.apache.ratis.util.function.CheckedSupplier;
@@ -59,8 +60,24 @@ public OzoneKeyDetails(String volumeName, String bucketName, 
String keyName,
       FileEncryptionInfo feInfo,
       CheckedSupplier<OzoneInputStream, IOException> contentSupplier,
       boolean isFile, String owner, Map<String, String> tags, Long generation) 
{
+    this(volumeName, bucketName, keyName, size, creationTime, modificationTime,
+        ozoneKeyLocations, replicationConfig, metadata, feInfo, 
contentSupplier,
+        isFile, owner, tags, generation, null);
+  }
+
+  @SuppressWarnings("parameternumber")
+  public OzoneKeyDetails(String volumeName, String bucketName, String keyName,
+      long size, long creationTime, long modificationTime,
+      List<OzoneKeyLocation> ozoneKeyLocations,
+      ReplicationConfig replicationConfig,
+      Map<String, String> metadata,
+      FileEncryptionInfo feInfo,
+      CheckedSupplier<OzoneInputStream, IOException> contentSupplier,
+      boolean isFile, String owner, Map<String, String> tags, Long generation,
+      StoragePolicy storagePolicy) {
     super(volumeName, bucketName, keyName, size, creationTime,
-        modificationTime, replicationConfig, metadata, isFile, owner, tags);
+        modificationTime, replicationConfig, metadata, isFile, owner, tags,
+        storagePolicy);
     this.ozoneKeyLocations = ozoneKeyLocations;
     this.feInfo = feInfo;
     this.contentSupplier = contentSupplier;
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/BlockOutputStreamEntry.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/BlockOutputStreamEntry.java
index 5fc0e92fb72..da62af663d1 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/BlockOutputStreamEntry.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/BlockOutputStreamEntry.java
@@ -26,6 +26,7 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.Condition;
 import java.util.function.Supplier;
+import org.apache.hadoop.fs.StorageType;
 import org.apache.hadoop.fs.Syncable;
 import org.apache.hadoop.hdds.client.BlockID;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
@@ -70,6 +71,9 @@ public class BlockOutputStreamEntry extends OutputStream {
   private final ContainerClientMetrics clientMetrics;
   private final StreamBufferArgs streamBufferArgs;
   private final Supplier<ExecutorService> executorServiceSupplier;
+  // Storage type the block should be written to, derived from the key's 
storage
+  // tier. Null lets the datanode pick any volume.
+  private final StorageType storageType;
 
   /**
    * An indicator that this BlockOutputStream is created to handoff writes 
from another faulty BlockOutputStream.
@@ -97,6 +101,7 @@ public class BlockOutputStreamEntry extends OutputStream {
     this.streamBufferArgs = b.streamBufferArgs;
     this.executorServiceSupplier = b.executorServiceSupplier;
     this.isHandlingRetry = b.forRetry;
+    this.storageType = b.storageType;
   }
 
   @Override
@@ -155,7 +160,7 @@ void waitForAllPendingFlushes() throws IOException {
   void createOutputStream() throws IOException {
     outputStream = new RatisBlockOutputStream(blockID, length, 
xceiverClientManager,
         pipeline, bufferPool, config, token, clientMetrics, streamBufferArgs,
-        executorServiceSupplier);
+        executorServiceSupplier, storageType);
   }
 
   ContainerClientMetrics getClientMetrics() {
@@ -415,6 +420,7 @@ public static class Builder {
     private StreamBufferArgs streamBufferArgs;
     private Supplier<ExecutorService> executorServiceSupplier;
     private boolean forRetry;
+    private StorageType storageType;
 
     public Pipeline getPipeline() {
       return pipeline;
@@ -485,6 +491,11 @@ public Builder setForRetry(boolean forRetry) {
       return this;
     }
 
+    public Builder setStorageType(StorageType storageType) {
+      this.storageType = storageType;
+      return this;
+    }
+
     public BlockOutputStreamEntry build() {
       return new BlockOutputStreamEntry(this);
     }
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/BlockOutputStreamEntryPool.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/BlockOutputStreamEntryPool.java
index 0abfc001ee0..24ae5f57179 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/BlockOutputStreamEntryPool.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/BlockOutputStreamEntryPool.java
@@ -30,7 +30,9 @@
 import java.util.Objects;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Supplier;
+import org.apache.hadoop.fs.StorageType;
 import org.apache.hadoop.hdds.client.ContainerBlockID;
+import org.apache.hadoop.hdds.client.StorageTier;
 import org.apache.hadoop.hdds.scm.ByteStringConversion;
 import org.apache.hadoop.hdds.scm.ContainerClientMetrics;
 import org.apache.hadoop.hdds.scm.OzoneClientConfig;
@@ -102,6 +104,10 @@ public BlockOutputStreamEntryPool(KeyOutputStream.Builder 
b) {
         .setBucketName(info.getBucketName()).setKeyName(info.getKeyName())
         .setReplicationConfig(b.getReplicationConfig())
         .setDataSize(info.getDataSize())
+        // Carry the key's storage policy into later allocateBlock calls, so 
blocks allocated
+        // after the pre-allocated ones run out land on the same tier. Null 
when the key did not
+        // ask for a policy, which leaves OM to resolve it from the bucket as 
it did on create.
+        .setStoragePolicy(info.getStoragePolicy())
         .setIsMultipartKey(b.isMultipartKey())
         .setMultipartUploadID(b.getMultipartUploadID())
         .setMultipartUploadPartNumber(b.getMultipartNumber());
@@ -169,9 +175,23 @@ BlockOutputStreamEntry createStreamEntry(OmKeyLocationInfo 
subKeyInfo, boolean f
             .setStreamBufferArgs(streamBufferArgs)
             .setExecutorServiceSupplier(executorServiceSupplier)
             .setForRetry(forRetry)
+            .setStorageType(getStorageType(subKeyInfo))
             .build();
   }
 
+  /**
+   * Resolves the storage type the block should land on from the storage tier 
SCM
+   * assigned to it. Returns null when the tier is absent or spans more than 
one
+   * storage type, which leaves the datanode free to pick any volume.
+   */
+  private static StorageType getStorageType(OmKeyLocationInfo subKeyInfo) {
+    StorageTier storageTier = subKeyInfo.getStorageTier();
+    if (storageTier == null || !storageTier.isUniform()) {
+      return null;
+    }
+    return storageTier.getUniformStorageType();
+  }
+
   private synchronized void addKeyLocationInfo(OmKeyLocationInfo subKeyInfo, 
boolean forRetry) {
     Objects.requireNonNull(subKeyInfo.getPipeline(), "subKeyInfo.getPipeline() 
== null");
     streamEntries.add(createStreamEntry(subKeyInfo, forRetry));
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index 3ee60408bd3..4bb1a9a31ae 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -18,6 +18,7 @@
 package org.apache.hadoop.ozone.client.protocol;
 
 import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
 import java.io.IOException;
 import java.net.URI;
 import java.util.List;
@@ -479,6 +480,30 @@ OzoneOutputStream createKey(String volumeName, String 
bucketName,
       Map<String, String> metadata, Map<String, String> tags)
       throws IOException;
 
+  /**
+   * Writes a key in an existing bucket, placing it according to the given 
storage
+   * policy.
+   * @param volumeName Name of the Volume
+   * @param bucketName Name of the Bucket
+   * @param keyName Name of the Key
+   * @param size Size of the data
+   * @param metadata Custom key value metadata
+   * @param tags Tags used for S3 object tags
+   * @param storagePolicy storage policy for the key, or null to inherit the
+   *                      bucket's policy
+   * @return {@link OzoneOutputStream}
+   *
+   */
+  @SuppressWarnings("checkstyle:parameternumber")
+  default OzoneOutputStream createKey(String volumeName, String bucketName,
+      String keyName, long size, ReplicationConfig replicationConfig,
+      Map<String, String> metadata, Map<String, String> tags,
+      @Nullable StoragePolicy storagePolicy)
+      throws IOException {
+    return createKey(volumeName, bucketName, keyName, size, replicationConfig,
+        metadata, tags);
+  }
+
   /**
    * Writes a key in an existing bucket.
    * @param volumeName Name of the Volume
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 220476d9e6d..36edc0dd0d1 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -1449,6 +1449,21 @@ public OzoneOutputStream createKey(
     return openOutputStream(builder.build(), size);
   }
 
+  @Override
+  @SuppressWarnings("checkstyle:parameternumber")
+  public OzoneOutputStream createKey(
+      String volumeName, String bucketName, String keyName, long size,
+      ReplicationConfig replicationConfig,
+      Map<String, String> metadata, Map<String, String> tags,
+      StoragePolicy storagePolicy) throws IOException {
+    String ownerName = getRealUserInfo().getShortUserName();
+    OmKeyArgs.Builder builder = createWriteKeyArgsBuilder(volumeName,
+        bucketName, keyName, size, replicationConfig, metadata, tags);
+    builder.setOwnerName(ownerName);
+    builder.setStoragePolicy(storagePolicy);
+    return openOutputStream(builder.build(), size);
+  }
+
   @Override
   public OzoneOutputStream rewriteKey(String volumeName, String bucketName, 
String keyName,
       long size, long existingKeyGeneration, ReplicationConfig 
replicationConfig,
@@ -1940,7 +1955,7 @@ private OzoneKeyDetails getOzoneKeyDetails(OmKeyInfo 
keyInfo) {
         keyInfo.getFileEncryptionInfo(),
         () -> getInputStreamWithRetryFunction(keyInfo), keyInfo.isFile(),
         keyInfo.getOwnerName(), keyInfo.getTags(),
-        keyInfo.getGeneration()
+        keyInfo.getGeneration(), keyInfo.getStoragePolicy()
     );
   }
 
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
index 03ff4bd8f61..c99fc5f69c0 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
@@ -21,11 +21,14 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import org.apache.hadoop.hdds.client.OzoneStoragePolicy;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.StoragePolicy;
 import org.apache.hadoop.ozone.OzoneAcl;
 import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.audit.Auditable;
@@ -66,6 +69,8 @@ public final class OmKeyArgs extends WithMetadata implements 
Auditable {
   private final String listPrefix;
   private final String expectedETag;
   private final boolean derivedKeyPiggyBacking;
+  // When null, OM resolves the policy from the bucket, then the cluster 
default.
+  private final StoragePolicy storagePolicy;
 
   private OmKeyArgs(Builder b) {
     super(b);
@@ -90,6 +95,7 @@ private OmKeyArgs(Builder b) {
     this.listPrefix = b.listPrefix;
     this.expectedETag = b.expectedETag;
     this.derivedKeyPiggyBacking = b.derivedKeyPiggyBacking;
+    this.storagePolicy = b.storagePolicy;
   }
 
   public boolean getIsMultipartKey() {
@@ -188,6 +194,11 @@ public boolean isDerivedKeyPiggyBacking() {
     return derivedKeyPiggyBacking;
   }
 
+  @Nullable
+  public StoragePolicy getStoragePolicy() {
+    return storagePolicy;
+  }
+
   @Override
   public Map<String, String> toAuditMap() {
     Map<String, String> auditMap = new LinkedHashMap<>();
@@ -236,6 +247,9 @@ public KeyArgs toProtobuf() {
     if (expectedETag != null) {
       builder.setExpectedETag(expectedETag);
     }
+    if (storagePolicy != null) {
+      builder.setStoragePolicy(OzoneStoragePolicy.toProto(storagePolicy));
+    }
     return builder.build();
   }
 
@@ -264,6 +278,7 @@ public static class Builder extends WithMetadata.Builder {
     private String listPrefix = null;
     private String expectedETag;
     private boolean derivedKeyPiggyBacking;
+    private StoragePolicy storagePolicy;
 
     public Builder() {
       this(AclListBuilder.empty());
@@ -311,6 +326,7 @@ public Builder(OmKeyArgs obj) {
       this.expectedDataGeneration = obj.expectedDataGeneration;
       this.expectedETag = obj.expectedETag;
       this.derivedKeyPiggyBacking = obj.derivedKeyPiggyBacking;
+      this.storagePolicy = obj.storagePolicy;
       this.tags = MapBuilder.of(obj.tags);
       this.acls = AclListBuilder.of(obj.acls);
       this.listPrefix = obj.listPrefix;
@@ -462,6 +478,11 @@ public Builder setDerivedKeyPiggyBacking(boolean 
derivedKeyPiggyBacking) {
       return this;
     }
 
+    public Builder setStoragePolicy(StoragePolicy storagePolicy) {
+      this.storagePolicy = storagePolicy;
+      return this;
+    }
+
     public OmKeyArgs build() {
       return new OmKeyArgs(this);
     }
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyInfo.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyInfo.java
index c2dc49a2d1e..adba2860ef0 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyInfo.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyInfo.java
@@ -33,8 +33,10 @@
 import org.apache.hadoop.fs.FileEncryptionInfo;
 import org.apache.hadoop.hdds.client.ContainerBlockID;
 import org.apache.hadoop.hdds.client.ECReplicationConfig;
+import org.apache.hadoop.hdds.client.OzoneStoragePolicy;
 import org.apache.hadoop.hdds.client.RatisReplicationConfig;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.StoragePolicy;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor;
 import org.apache.hadoop.hdds.utils.db.Codec;
 import org.apache.hadoop.hdds.utils.db.CopyObject;
@@ -125,6 +127,10 @@ public final class OmKeyInfo extends WithParentObjectId
   // been modified.
   private final Long expectedDataGeneration;
 
+  // Storage policy the key was created with. Null for keys written before
+  // storage policy support; those fall back to the bucket policy on read.
+  private final StoragePolicy storagePolicy;
+
   private OmKeyInfo(Builder b) {
     super(b);
     this.volumeName = b.volumeName;
@@ -143,6 +149,7 @@ private OmKeyInfo(Builder b) {
     this.ownerName = b.ownerName;
     this.tags = b.tags.build();
     this.expectedDataGeneration = b.expectedDataGeneration;
+    this.storagePolicy = b.storagePolicy;
   }
 
   /**
@@ -221,6 +228,11 @@ public Long getExpectedDataGeneration() {
     return expectedDataGeneration;
   }
 
+  @Nullable
+  public StoragePolicy getStoragePolicy() {
+    return storagePolicy;
+  }
+
   public String getOwnerName() {
     return ownerName;
   }
@@ -526,6 +538,7 @@ public static class Builder extends 
WithParentObjectId.Builder<OmKeyInfo> {
     private boolean isFile;
     private final MapBuilder<String, String> tags;
     private Long expectedDataGeneration = null;
+    private StoragePolicy storagePolicy;
 
     public Builder() {
       this.acls = AclListBuilder.empty();
@@ -548,6 +561,7 @@ public Builder(OmKeyInfo obj) {
       this.fileChecksum = obj.fileChecksum;
       this.isFile = obj.isFile;
       this.expectedDataGeneration = obj.expectedDataGeneration;
+      this.storagePolicy = obj.storagePolicy;
       this.tags = MapBuilder.of(obj.tags);
       obj.keyLocationVersions.forEach(keyLocationVersion ->
           this.omKeyLocationInfoGroups.add(
@@ -719,6 +733,11 @@ public Builder setExpectedDataGeneration(Long 
existingGeneration) {
       return this;
     }
 
+    public Builder setStoragePolicy(StoragePolicy storagePolicy) {
+      this.storagePolicy = storagePolicy;
+      return this;
+    }
+
     @Override
     protected void validate() {
       super.validate();
@@ -870,6 +889,9 @@ private KeyInfo getProtobuf(boolean ignorePipeline, String 
fullKeyName,
     if (ownerName != null) {
       kb.setOwnerName(ownerName);
     }
+    if (storagePolicy != null) {
+      kb.setStoragePolicy(OzoneStoragePolicy.toProto(storagePolicy));
+    }
     return kb.build();
   }
 
@@ -924,6 +946,9 @@ public static Builder builderFromProtobuf(KeyInfo keyInfo) {
     if (keyInfo.hasOwnerName()) {
       builder.setOwnerName(keyInfo.getOwnerName());
     }
+    if (keyInfo.hasStoragePolicy()) {
+      
builder.setStoragePolicy(OzoneStoragePolicy.fromProto(keyInfo.getStoragePolicy()));
+    }
     return builder;
   }
 
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index caa6acc5ce2..8b686be238d 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -46,6 +46,7 @@
 import org.apache.hadoop.fs.SafeModeAction;
 import org.apache.hadoop.hdds.annotation.InterfaceAudience;
 import org.apache.hadoop.hdds.client.ECReplicationConfig;
+import org.apache.hadoop.hdds.client.OzoneStoragePolicy;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
 import 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.TransferLeadershipRequestProto;
 import 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.UpgradeFinalizationStatus;
@@ -789,6 +790,9 @@ public OpenKeySession openKey(OmKeyArgs args) throws 
IOException {
     if (args.getExpectedETag() != null) {
       keyArgs.setExpectedETag(args.getExpectedETag());
     }
+    if (args.getStoragePolicy() != null) {
+      
keyArgs.setStoragePolicy(OzoneStoragePolicy.toProto(args.getStoragePolicy()));
+    }
 
     req.setKeyArgs(keyArgs.build());
     req.setDerivedKeyPiggyBacking(args.isDerivedKeyPiggyBacking());
@@ -837,6 +841,12 @@ public OmKeyLocationInfo allocateBlock(OmKeyArgs args, 
long clientId,
       keyArgs.setType(args.getReplicationConfig().getReplicationType());
     }
 
+    // Send the key's storage policy so OM allocates on the same tier as the 
key's earlier
+    // blocks. Absent when the key inherited its policy, which OM then 
resolves from the bucket.
+    if (args.getStoragePolicy() != null) {
+      
keyArgs.setStoragePolicy(OzoneStoragePolicy.toProto(args.getStoragePolicy()));
+    }
+
     req.setKeyArgs(keyArgs);
     req.setClientID(clientId);
     req.setExcludeList(excludeList.getProtoBuf());
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneStoragePolicy.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneStoragePolicy.java
new file mode 100644
index 00000000000..4e4981acd31
--- /dev/null
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneStoragePolicy.java
@@ -0,0 +1,446 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.client.rpc;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_RATIS_PIPELINE_LIMIT;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SCM_BLOCK_SIZE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import org.apache.hadoop.fs.StorageType;
+import org.apache.hadoop.hdds.client.DefaultReplicationConfig;
+import org.apache.hadoop.hdds.client.OzoneStoragePolicy;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
+import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.StorageTier;
+import org.apache.hadoop.hdds.client.StorageTypeUtils;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.conf.StorageUnit;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.XceiverClientGrpc;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import org.apache.hadoop.hdds.scm.container.ContainerManager;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.scm.pipeline.PipelineManager;
+import org.apache.hadoop.hdds.scm.storage.ContainerProtocolCalls;
+import org.apache.hadoop.hdds.utils.IOUtils;
+import org.apache.hadoop.ozone.HddsDatanodeService;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.client.BucketArgs;
+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.OzoneClientFactory;
+import org.apache.hadoop.ozone.client.OzoneKeyDetails;
+import org.apache.hadoop.ozone.client.io.OzoneOutputStream;
+import org.apache.hadoop.ozone.container.common.impl.ContainerData;
+import org.apache.hadoop.ozone.container.common.interfaces.Container;
+import org.apache.hadoop.ozone.om.OzoneManager;
+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.helpers.OmKeyLocationInfo;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that a key honours its storage policy end to end: the policy is
+ * persisted on the key, SCM allocates the container on the policy's creation
+ * tier, and the block physically lands on a datanode volume of that tier.
+ *
+ * Each test builds its own cluster because the datanode volume topology 
differs
+ * per case.
+ */
+public class TestOzoneStoragePolicy {
+
+  private static final ReplicationConfig RATIS_THREE =
+      RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.THREE);
+
+  private MiniOzoneCluster cluster;
+  private OzoneClient ozClient;
+  private ObjectStore store;
+  private OzoneManager ozoneManager;
+  private ContainerManager containerManager;
+  private PipelineManager pipelineManager;
+  private OzoneConfiguration conf;
+
+  /**
+   * Starts a cluster whose datanodes each expose one volume per storage type, 
so
+   * every storage policy has a matching volume to land on.
+   */
+  private void startCluster(List<List<StorageType>> storageTypeList)
+      throws Exception {
+    conf = new OzoneConfiguration();
+    conf.setInt(OZONE_SCM_RATIS_PIPELINE_LIMIT, 10);
+    conf.setStorageSize(OZONE_SCM_BLOCK_SIZE, 4, StorageUnit.MB);
+    cluster = MiniOzoneCluster.newBuilder(conf)
+        .setNumDatanodes(storageTypeList.size())
+        .setNumDataVolumes(storageTypeList.get(0).size())
+        .setDatanodeStorageType(storageTypeList)
+        .build();
+    cluster.waitForClusterToBeReady();
+    cluster.waitTobeOutOfSafeMode();
+
+    ozClient = OzoneClientFactory.getRpcClient(conf);
+    store = ozClient.getObjectStore();
+    ozoneManager = cluster.getOzoneManager();
+    containerManager = 
cluster.getStorageContainerManager().getContainerManager();
+    pipelineManager = 
cluster.getStorageContainerManager().getPipelineManager();
+  }
+
+  private static List<List<StorageType>> allTierTopology(int datanodes) {
+    List<StorageType> volumes =
+        Arrays.asList(StorageType.DISK, StorageType.SSD, StorageType.ARCHIVE);
+    return Collections.nCopies(datanodes, volumes);
+  }
+
+  @AfterEach
+  public void shutdownCluster() {
+    IOUtils.closeQuietly(ozClient);
+    if (cluster != null) {
+      cluster.shutdown();
+    }
+  }
+
+  /**
+   * A key created with an explicit storage policy records that policy and 
lands
+   * on the policy's creation tier, in SCM and on the datanodes.
+   */
+  @Test
+  public void testKeyStoragePolicyIsHonoured() throws Exception {
+    startCluster(allTierTopology(RATIS_THREE.getRequiredNodes()));
+
+    for (OzoneStoragePolicy storagePolicy : OzoneStoragePolicy.values()) {
+      OzoneBucket bucket = createBucket(null, BucketLayout.OBJECT_STORE);
+      OmKeyInfo keyInfo = createKeyAndLookup(bucket, storagePolicy);
+
+      assertKeyInfo(keyInfo, storagePolicy);
+      assertScmContainerAndPipeline(keyInfo, storagePolicy.getCreationTier());
+      assertDatanodeContainerAndBlock(keyInfo, 
storagePolicy.getCreationTier());
+    }
+  }
+
+  /**
+   * A key created without a storage policy inherits the bucket's policy, and 
an
+   * explicit key level policy overrides the bucket's.
+   */
+  @Test
+  public void testKeyInheritsAndOverridesBucketStoragePolicy() throws 
Exception {
+    startCluster(allTierTopology(RATIS_THREE.getRequiredNodes()));
+
+    OzoneStoragePolicy bucketPolicy = OzoneStoragePolicy.COLD;
+    OzoneStoragePolicy keyPolicy = OzoneStoragePolicy.HOT;
+
+    OzoneBucket bucket = createBucket(bucketPolicy, BucketLayout.OBJECT_STORE);
+    assertEquals(bucketPolicy, bucket.getStoragePolicy());
+
+    // No key level policy: the bucket's policy applies.
+    OmKeyInfo inherited = createKeyAndLookup(bucket, null);
+    assertKeyInfo(inherited, bucketPolicy);
+    assertScmContainerAndPipeline(inherited, bucketPolicy.getCreationTier());
+    assertDatanodeContainerAndBlock(inherited, bucketPolicy.getCreationTier());
+
+    // Explicit key level policy wins over the bucket's.
+    OmKeyInfo overridden = createKeyAndLookup(bucket, keyPolicy);
+    assertKeyInfo(overridden, keyPolicy);
+    assertScmContainerAndPipeline(overridden, keyPolicy.getCreationTier());
+    assertDatanodeContainerAndBlock(overridden, keyPolicy.getCreationTier());
+  }
+
+  /**
+   * With neither a key nor a bucket policy, the key falls back to the cluster
+   * default rather than being left without one.
+   */
+  @Test
+  public void testKeyWithoutAnyPolicyUsesClusterDefault() throws Exception {
+    startCluster(allTierTopology(RATIS_THREE.getRequiredNodes()));
+
+    OzoneBucket bucket = createBucket(null, BucketLayout.OBJECT_STORE);
+    OmKeyInfo keyInfo = createKeyAndLookup(bucket, null);
+
+    assertKeyInfo(keyInfo, OzoneStoragePolicy.getDefaultPolicy());
+    assertScmContainerAndPipeline(keyInfo,
+        OzoneStoragePolicy.getDefaultPolicy().getCreationTier());
+  }
+
+  /**
+   * The storage policy survives the round trip to the client on read.
+   */
+  @Test
+  public void testStoragePolicyVisibleToClient() throws Exception {
+    startCluster(allTierTopology(RATIS_THREE.getRequiredNodes()));
+
+    OzoneBucket bucket = createBucket(null, BucketLayout.OBJECT_STORE);
+    String keyName = UUID.randomUUID().toString();
+    writeKey(bucket, keyName, OzoneStoragePolicy.COLD);
+
+    OzoneKeyDetails details = bucket.getKey(keyName);
+    assertEquals(OzoneStoragePolicy.COLD, details.getStoragePolicy());
+  }
+
+  /**
+   * A key written through an FSO bucket carries its storage policy too, since 
FSO
+   * creates keys through a separate OM request path.
+   */
+  @Test
+  public void testStoragePolicyOnFsoBucket() throws Exception {
+    startCluster(allTierTopology(RATIS_THREE.getRequiredNodes()));
+
+    OzoneBucket bucket =
+        createBucket(null, BucketLayout.FILE_SYSTEM_OPTIMIZED);
+    OmKeyInfo keyInfo = createKeyAndLookup(bucket, OzoneStoragePolicy.HOT);
+
+    assertKeyInfo(keyInfo, OzoneStoragePolicy.HOT);
+    assertScmContainerAndPipeline(keyInfo,
+        OzoneStoragePolicy.HOT.getCreationTier());
+    assertDatanodeContainerAndBlock(keyInfo,
+        OzoneStoragePolicy.HOT.getCreationTier());
+  }
+
+  /**
+   * An empty key skips block allocation but must still record its policy,
+   * otherwise a later rewrite would lose it.
+   */
+  @Test
+  public void testEmptyKeyRecordsStoragePolicy() throws Exception {
+    startCluster(allTierTopology(RATIS_THREE.getRequiredNodes()));
+
+    OzoneBucket bucket = createBucket(null, BucketLayout.OBJECT_STORE);
+    String keyName = UUID.randomUUID().toString();
+    try (OzoneOutputStream out = bucket.createKey(keyName, 0, RATIS_THREE,
+        Collections.emptyMap(), Collections.emptyMap(),
+        OzoneStoragePolicy.COLD)) {
+      // Intentionally write nothing.
+      assertNotNull(out);
+    }
+
+    OmKeyInfo keyInfo = lookupKey(bucket, keyName);
+    assertEquals(OzoneStoragePolicy.COLD, keyInfo.getStoragePolicy());
+    assertTrue(keyInfo.getLatestVersionLocations()
+        .getBlocksLatestVersionOnly().isEmpty());
+  }
+
+  /**
+   * Blocks allocated after the pre-allocated ones run out must stay on the 
key's tier.
+   *
+   * <p>Creating the key with size 0 pre-allocates nothing, so every block 
comes from a separate
+   * allocateBlock call. The key asks for COLD while its bucket is WARM, so if 
the policy is not
+   * carried into those calls, OM falls back to the bucket and the later 
blocks land on DISK
+   * instead of ARCHIVE.
+   */
+  @Test
+  public void testStoragePolicyHonouredOnSubsequentBlockAllocation()
+      throws Exception {
+    startCluster(allTierTopology(RATIS_THREE.getRequiredNodes()));
+
+    OzoneBucket bucket =
+        createBucket(OzoneStoragePolicy.WARM, BucketLayout.OBJECT_STORE);
+    String keyName = UUID.randomUUID().toString();
+
+    // Size 0 means OM pre-allocates no blocks, so the first write already 
needs an
+    // allocateBlock round trip. Write enough to span more than one block.
+    byte[] data = new byte[(int) (2 * ozoneManager.getScmBlockSize() + 1024)];
+    Arrays.fill(data, (byte) 'p');
+    try (OzoneOutputStream out = bucket.createKey(keyName, 0, RATIS_THREE,
+        Collections.emptyMap(), Collections.emptyMap(),
+        OzoneStoragePolicy.COLD)) {
+      out.write(data);
+    }
+
+    OmKeyInfo keyInfo = lookupKey(bucket, keyName);
+    assertEquals(OzoneStoragePolicy.COLD, keyInfo.getStoragePolicy());
+
+    List<OmKeyLocationInfo> blocks =
+        keyInfo.getLatestVersionLocations().getBlocksLatestVersionOnly();
+    assertThat(blocks.size())
+        .withFailMessage("Expected more than one block so a later allocation 
is exercised, "
+            + "but the key has %d", blocks.size())
+        .isGreaterThan(1);
+
+    // Every block, not just the first, must sit on the tier the key asked for.
+    assertAllBlocksOnTier(keyInfo, OzoneStoragePolicy.COLD.getCreationTier());
+  }
+
+  /**
+   * Overwriting a key with a different storage policy must update the 
recorded policy, not just
+   * the tier of the new blocks.
+   *
+   * <p>The overwrite takes the existing-key branch of {@code 
prepareFileInfo}, which starts from
+   * a copy of the old key. If that branch does not apply the new policy, the 
blocks land on the new
+   * tier while {@code getStoragePolicy()} keeps reporting the old one.
+   */
+  @Test
+  public void testOverwriteWithDifferentStoragePolicyUpdatesKey()
+      throws Exception {
+    startCluster(allTierTopology(RATIS_THREE.getRequiredNodes()));
+
+    OzoneBucket bucket = createBucket(null, BucketLayout.OBJECT_STORE);
+    String keyName = UUID.randomUUID().toString();
+
+    writeKey(bucket, keyName, OzoneStoragePolicy.HOT);
+    OmKeyInfo original = lookupKey(bucket, keyName);
+    assertEquals(OzoneStoragePolicy.HOT, original.getStoragePolicy());
+    assertAllBlocksOnTier(original, OzoneStoragePolicy.HOT.getCreationTier());
+
+    // Overwrite the same key name with a different policy.
+    writeKey(bucket, keyName, OzoneStoragePolicy.COLD);
+    OmKeyInfo overwritten = lookupKey(bucket, keyName);
+
+    // The recorded policy and the tier the data actually sits on must agree.
+    assertEquals(OzoneStoragePolicy.COLD, overwritten.getStoragePolicy());
+    assertAllBlocksOnTier(overwritten, 
OzoneStoragePolicy.COLD.getCreationTier());
+  }
+
+  /**
+   * Checks every block of a key landed on a container of the expected tier. 
The
+   * {@code assertDatanodeContainerAndBlock} helper only inspects the first 
block, which cannot
+   * catch a tier that changes partway through a key.
+   */
+  private void assertAllBlocksOnTier(OmKeyInfo keyInfo, StorageTier 
expectedTier)
+      throws IOException {
+    StorageType expectedStorageType = expectedTier.getUniformStorageType();
+    List<OmKeyLocationInfo> blocks =
+        keyInfo.getLatestVersionLocations().getBlocksLatestVersionOnly();
+    for (int i = 0; i < blocks.size(); i++) {
+      OmKeyLocationInfo block = blocks.get(i);
+      ContainerInfo containerInfo = containerManager.getContainer(
+          ContainerID.valueOf(block.getContainerID()));
+      assertEquals(expectedStorageType,
+          containerInfo.getStorageTier().getUniformStorageType(),
+          "block " + i + " of " + blocks.size() + " landed on the wrong tier");
+    }
+  }
+
+  private OzoneBucket createBucket(OzoneStoragePolicy storagePolicy,
+      BucketLayout bucketLayout) throws IOException {
+    String volumeName = UUID.randomUUID().toString();
+    String bucketName = UUID.randomUUID().toString();
+    store.createVolume(volumeName);
+    BucketArgs.Builder builder = BucketArgs.newBuilder()
+        .setBucketLayout(bucketLayout)
+        .setDefaultReplicationConfig(new 
DefaultReplicationConfig(RATIS_THREE));
+    if (storagePolicy != null) {
+      builder.setStoragePolicy(storagePolicy);
+    }
+    store.getVolume(volumeName).createBucket(bucketName, builder.build());
+    return store.getVolume(volumeName).getBucket(bucketName);
+  }
+
+  private void writeKey(OzoneBucket bucket, String keyName,
+      OzoneStoragePolicy storagePolicy) throws IOException {
+    byte[] data = UUID.randomUUID().toString().getBytes(UTF_8);
+    try (OzoneOutputStream out = bucket.createKey(keyName, data.length,
+        RATIS_THREE, Collections.emptyMap(), Collections.emptyMap(),
+        storagePolicy)) {
+      out.write(data);
+    }
+  }
+
+  private OmKeyInfo createKeyAndLookup(OzoneBucket bucket,
+      OzoneStoragePolicy storagePolicy) throws IOException {
+    String keyName = UUID.randomUUID().toString();
+    writeKey(bucket, keyName, storagePolicy);
+    return lookupKey(bucket, keyName);
+  }
+
+  private OmKeyInfo lookupKey(OzoneBucket bucket, String keyName)
+      throws IOException {
+    OmKeyArgs keyArgs = new OmKeyArgs.Builder()
+        .setVolumeName(bucket.getVolumeName())
+        .setBucketName(bucket.getName())
+        .setKeyName(keyName)
+        .build();
+    return ozoneManager.lookupKey(keyArgs);
+  }
+
+  private void assertKeyInfo(OmKeyInfo keyInfo,
+      OzoneStoragePolicy expectedPolicy) {
+    assertEquals(1, keyInfo.getKeyLocationVersions().size());
+    assertNotNull(keyInfo.getLatestVersionLocations()
+        .getBlocksLatestVersionOnly());
+    assertEquals(expectedPolicy, keyInfo.getStoragePolicy());
+    assertEquals(RATIS_THREE, keyInfo.getReplicationConfig());
+  }
+
+  private void assertScmContainerAndPipeline(OmKeyInfo keyInfo,
+      StorageTier expectedTier) throws IOException {
+    List<OmKeyLocationInfo> blocks =
+        keyInfo.getLatestVersionLocations().getBlocksLatestVersionOnly();
+    if (blocks.isEmpty()) {
+      return;
+    }
+    ContainerInfo containerInfo = containerManager.getContainer(
+        ContainerID.valueOf(blocks.get(0).getContainerID()));
+    Pipeline pipeline =
+        pipelineManager.getPipeline(containerInfo.getPipelineID());
+
+    assertEquals(RATIS_THREE, pipeline.getReplicationConfig());
+    assertEquals(expectedTier, containerInfo.getStorageTier());
+    assertEquals(expectedTier, pipeline.getSupportedStorageTier());
+    assertEquals(RATIS_THREE.getRequiredNodes(), pipeline.getNodeSet().size());
+  }
+
+  /**
+   * Checks the block actually landed on a volume of the expected tier, and 
that
+   * the storage type travelled to the datanode on the block id.
+   */
+  private void assertDatanodeContainerAndBlock(OmKeyInfo keyInfo,
+      StorageTier expectedTier) throws IOException {
+    StorageType expectedStorageType = expectedTier.getUniformStorageType();
+    OmKeyLocationInfo block =
+        
keyInfo.getLatestVersionLocations().getBlocksLatestVersionOnly().get(0);
+    ContainerInfo containerInfo = containerManager.getContainer(
+        ContainerID.valueOf(block.getContainerID()));
+    Pipeline pipeline =
+        pipelineManager.getPipeline(containerInfo.getPipelineID());
+
+    int replicasChecked = 0;
+    try (XceiverClientGrpc client = new XceiverClientGrpc(pipeline, conf)) {
+      for (HddsDatanodeService datanode : cluster.getHddsDatanodes()) {
+        Container<?> container = datanode.getDatanodeStateMachine()
+            .getContainer().getContainerSet()
+            .getContainer(block.getContainerID());
+        if (container == null) {
+          continue;
+        }
+        ContainerData containerData = container.getContainerData();
+        assertEquals(expectedStorageType, containerData.getStorageType());
+        assertEquals(expectedStorageType,
+            containerData.getVolume().getStorageType());
+        
assertTrue(pipeline.getNodeSet().contains(datanode.getDatanodeDetails()));
+
+        ContainerProtos.BlockData blockData = ContainerProtocolCalls.getBlock(
+            client, block.getBlockID(), null, 
client.getPipeline()).getBlockData();
+        assertEquals(expectedStorageType, StorageTypeUtils
+            .getStorageTypeFromID(blockData.getBlockID().getStorageTypeID()));
+        replicasChecked++;
+      }
+    }
+    assertEquals(RATIS_THREE.getRequiredNodes(), replicasChecked);
+  }
+}
diff --git 
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto 
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index d3d31205571..6ea52d9204e 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -1164,6 +1164,11 @@ message KeyArgs {
     // the given ETag for the operation to succeed. This is used for
     // S3 conditional writes with the If-Match header.
     optional string expectedETag = 24;
+
+    // storagePolicy, when set, overrides the bucket's storage policy for this
+    // key. When unset, OM falls back to the bucket policy, then to the cluster
+    // default, so older clients keep their current behaviour.
+    optional hadoop.hdds.StoragePolicyProto storagePolicy = 25;
 }
 
 message KeyLocation {
@@ -1259,6 +1264,10 @@ message KeyInfo {
   // This allows a key to be created an committed atomically if the original 
has not
   // been modified.
     optional uint64 expectedDataGeneration = 22;
+
+    // Storage policy this key was created with. Absent for keys written before
+    // storage policy support, which are read back using the bucket policy.
+    optional hadoop.hdds.StoragePolicyProto storagePolicy = 23;
 }
 
 // KeyInfoProtoLight is a lightweight subset of KeyInfo message containing
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/file/OMFileCreateRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/file/OMFileCreateRequest.java
index 5b91f66cf3f..52c526a131f 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/file/OMFileCreateRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/file/OMFileCreateRequest.java
@@ -31,7 +31,9 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
+import org.apache.hadoop.hdds.client.OzoneStoragePolicy;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.StoragePolicy;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import org.apache.hadoop.hdds.scm.container.common.helpers.ExcludeList;
 import org.apache.hadoop.hdds.utils.UniqueId;
@@ -127,12 +129,15 @@ public OMRequest preExecute(OzoneManager ozoneManager) 
throws IOException {
     // File system client does not know the final file size in advance but use 
0 as
     // the placeholder for the data size. Therefore, we should at least 
allocate a
     // single block and we cannot simply skip the allocate block call
+    final StoragePolicy storagePolicy = getStoragePolicy(bucketInfo, keyArgs);
     final List< OmKeyLocationInfo > omKeyLocationInfoList = allocateBlock(
-        repConfig, new ExcludeList(), requestedSize, 
keyArgs.getSortDatanodes(), userInfo, ozoneManager);
+        repConfig, new ExcludeList(), requestedSize, 
keyArgs.getSortDatanodes(), userInfo, ozoneManager,
+        storagePolicy, getAllowFallbackStoragePolicy(bucketInfo));
 
     KeyArgs.Builder newKeyArgs = keyArgs.toBuilder()
         .setModificationTime(Time.now()).setType(type).setFactor(factor)
-        .setDataSize(requestedSize);
+        .setDataSize(requestedSize)
+        .setStoragePolicy(OzoneStoragePolicy.toProto(storagePolicy));
 
     newKeyArgs.addAllKeyLocations(omKeyLocationInfoList.stream()
         .map(info -> info.getProtobuf(getOmRequest().getVersion()))
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMAllocateBlockRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMAllocateBlockRequest.java
index 6f25c9da497..a829c4262a8 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMAllocateBlockRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMAllocateBlockRequest.java
@@ -108,8 +108,11 @@ public OMRequest preExecute(OzoneManager ozoneManager) 
throws IOException {
     // To allocate atleast one block passing requested size and scmBlockSize
     // as same value. When allocating block requested size is same as
     // scmBlockSize.
+    final OmBucketInfo bucketInfo = ozoneManager
+        .getBucketInfo(keyArgs.getVolumeName(), keyArgs.getBucketName());
     final List<OmKeyLocationInfo> omKeyLocationInfoList = 
allocateBlock(repConfig, excludeList,
-        ozoneManager.getScmBlockSize(), keyArgs.getSortDatanodes(), userInfo, 
ozoneManager);
+        ozoneManager.getScmBlockSize(), keyArgs.getSortDatanodes(), userInfo, 
ozoneManager,
+        getStoragePolicy(bucketInfo, keyArgs), 
getAllowFallbackStoragePolicy(bucketInfo));
 
     // Set modification time and normalize key if required.
     KeyArgs.Builder newKeyArgs =
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCreateRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCreateRequest.java
index 528085a7021..45ce32a8f0f 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCreateRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCreateRequest.java
@@ -31,7 +31,9 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
+import org.apache.hadoop.hdds.client.OzoneStoragePolicy;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.StoragePolicy;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import org.apache.hadoop.hdds.scm.container.common.helpers.ExcludeList;
 import org.apache.hadoop.hdds.utils.UniqueId;
@@ -143,6 +145,9 @@ public OMRequest preExecute(OzoneManager ozoneManager) 
throws IOException {
       //  As for a client for the first time this can be executed on any OM,
       //  till leader is identified.
 
+      final StoragePolicy storagePolicy = getStoragePolicy(bucketInfo, 
keyArgs);
+      final boolean allowFallbackStoragePolicy = 
getAllowFallbackStoragePolicy(bucketInfo);
+
       final List<OmKeyLocationInfo> omKeyLocationInfoList;
       final long effectiveDataSize;
       // Skip block allocation if dataSize <= 0. We also consider unspecified 
dataSize as
@@ -154,12 +159,14 @@ public OMRequest preExecute(OzoneManager ozoneManager) 
throws IOException {
         effectiveDataSize = keyArgs.getDataSize();
         omKeyLocationInfoList = 
captureLatencyNs(perfMetrics.getCreateKeyAllocateBlockLatencyNs(),
             () -> allocateBlock(repConfig, new ExcludeList(), 
effectiveDataSize,
-                keyArgs.getSortDatanodes(), userInfo, ozoneManager));
+                keyArgs.getSortDatanodes(), userInfo, ozoneManager,
+                storagePolicy, allowFallbackStoragePolicy));
       }
 
       newKeyArgs = keyArgs.toBuilder().setModificationTime(Time.now())
               .setType(type).setFactor(factor)
-              .setDataSize(effectiveDataSize);
+              .setDataSize(effectiveDataSize)
+              .setStoragePolicy(OzoneStoragePolicy.toProto(storagePolicy));
 
       newKeyArgs.addAllKeyLocations(omKeyLocationInfoList.stream()
           .map(info -> info.getProtobuf(false,
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCreateRequestWithFSO.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCreateRequestWithFSO.java
index 94caccffc6d..0d3f6ef3189 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCreateRequestWithFSO.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyCreateRequestWithFSO.java
@@ -52,12 +52,17 @@
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
 import org.apache.hadoop.util.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Handles CreateKey request layout version1.
  */
 public class OMKeyCreateRequestWithFSO extends OMKeyCreateRequest {
 
+  private static final Logger LOG =
+      LoggerFactory.getLogger(OMKeyCreateRequestWithFSO.class);
+
   public OMKeyCreateRequestWithFSO(OMRequest omRequest,
       BucketLayout bucketLayout) {
     super(omRequest, bucketLayout);
@@ -140,6 +145,13 @@ public OMClientResponse 
validateAndUpdateCache(OzoneManager ozoneManager, Execut
       OmBucketInfo bucketInfo = omMetadataManager.getBucketTable().get(
               omMetadataManager.getBucketKey(volumeName, bucketName));
 
+      if (!keyArgs.getIsMultipartKey() && !keyArgs.hasStoragePolicy()) {
+        // preExecute always sets a storage policy, so a missing one means 
this request
+        // was journaled by an older OM and is being replayed during an 
upgrade.
+        LOG.warn("Key {} has no StoragePolicy. This is expected only while 
replaying " +
+            "journals written by an OM without storage policy support.", 
keyArgs.getKeyName());
+      }
+
       // add all missing parents to dir table
       missingParentInfos = getAllMissingParentDirInfo(
           ozoneManager, keyArgs, bucketInfo, pathInfoFSO, trxnLogIndex);
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequest.java
index 111d80d1222..49f96cc5df4 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/key/OMKeyRequest.java
@@ -58,6 +58,7 @@
 import org.apache.hadoop.hdds.client.ECReplicationConfig;
 import org.apache.hadoop.hdds.client.OzoneStoragePolicy;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.StoragePolicy;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.BlockTokenSecretProto.AccessModeProto;
@@ -188,11 +189,14 @@ protected KeyArgs 
resolveBucketAndCheckOpenKeyAcls(KeyArgs keyArgs,
   }
 
   /** Allocate multiple blocks using one rpc to SCM. */
+  @SuppressWarnings("checkstyle:ParameterNumber")
   protected List<OmKeyLocationInfo> allocateBlock(
       ReplicationConfig replicationConfig, ExcludeList excludeList,
       long requestedSize, boolean shouldSortDatanodes,
-      UserInfo userInfo, OzoneManager ozoneManager)
+      UserInfo userInfo, OzoneManager ozoneManager,
+      @Nonnull StoragePolicy storagePolicy, boolean allowFallbackStoragePolicy)
       throws IOException {
+    Objects.requireNonNull(storagePolicy, "storagePolicy == null");
     final long scmBlockSize = ozoneManager.getScmBlockSize();
     final KeyManager keyManager = ozoneManager.getKeyManager();
 
@@ -232,10 +236,9 @@ protected List<OmKeyLocationInfo> allocateBlock(
     String remoteUser = getRemoteUser().getShortUserName();
     final List<AllocatedBlock> allocatedBlocks;
     try {
-      // TODO Use the actually passed `allowFallbackStoragePolicy` instead of 
`true`
       allocatedBlocks = 
ozoneManager.getScmClient().getBlockClient().allocateBlock(
           scmBlockSize, numBlocks, replicationConfig, 
ozoneManager.getOMServiceId(), excludeList, scmClientMachine,
-          OzoneStoragePolicy.getDefaultPolicy(), true);
+          storagePolicy, allowFallbackStoragePolicy);
     } catch (IOException ex) {
       ozoneManager.getMetrics().incNumBlockAllocateCallFails();
       if (ex instanceof SCMException) {
@@ -987,6 +990,33 @@ public static OmBucketInfo 
getBucketInfoForUpdate(OMMetadataManager omMetadataMa
     return omBucketInfo != null ? omBucketInfo.copyObject() : null;
   }
 
+  /**
+   * Resolves the storage policy for a key: the policy explicitly requested on 
the
+   * key wins, otherwise the bucket's policy, otherwise the cluster default.
+   */
+  @Nonnull
+  protected StoragePolicy getStoragePolicy(OmBucketInfo bucketInfo, KeyArgs 
keyArgs) {
+    if (keyArgs.hasStoragePolicy()) {
+      return OzoneStoragePolicy.fromProto(keyArgs.getStoragePolicy());
+    }
+    if (bucketInfo != null && bucketInfo.getStoragePolicy() != null) {
+      return bucketInfo.getStoragePolicy();
+    }
+    return OzoneStoragePolicy.getDefaultPolicy();
+  }
+
+  /**
+   * Whether SCM may fall back to the policy's fallback tier when the creation 
tier
+   * has no space. Taken from the bucket; defaults to true when the bucket 
does not
+   * set it, matching the behaviour before storage policy support.
+   */
+  protected boolean getAllowFallbackStoragePolicy(OmBucketInfo bucketInfo) {
+    if (bucketInfo == null || bucketInfo.getAllowFallbackStoragePolicy() == 
null) {
+      return true;
+    }
+    return bucketInfo.getAllowFallbackStoragePolicy();
+  }
+
   /**
    * Prepare OmKeyInfo which will be persisted to openKeyTable.
    * @return OmKeyInfo
@@ -1060,6 +1090,11 @@ protected OmKeyInfo prepareFileInfo(
       if (keyArgs.hasExpectedDataGeneration()) {
         builder.setExpectedDataGeneration(keyArgs.getExpectedDataGeneration());
       }
+      // The new blocks were allocated with the policy in keyArgs, so record 
that policy instead
+      // of the one copied from the overwritten key.
+      if (keyArgs.hasStoragePolicy()) {
+        
builder.setStoragePolicy(OzoneStoragePolicy.fromProto(keyArgs.getStoragePolicy()));
+      }
 
       return builder.build();
     }
@@ -1110,6 +1145,9 @@ protected OmKeyInfo createFileInfo(
     if (keyArgs.hasExpectedDataGeneration()) {
       builder.setExpectedDataGeneration(keyArgs.getExpectedDataGeneration());
     }
+    if (keyArgs.hasStoragePolicy()) {
+      
builder.setStoragePolicy(OzoneStoragePolicy.fromProto(keyArgs.getStoragePolicy()));
+    }
     if (omPathInfo instanceof OMFileRequest.OMPathInfoWithFSO) {
       // FileTable metadata format
       OMFileRequest.OMPathInfoWithFSO omPathInfoFSO
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMAllocateBlockRequest.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMAllocateBlockRequest.java
index da85a37e106..fa57c68000d 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMAllocateBlockRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/key/TestOMAllocateBlockRequest.java
@@ -43,6 +43,7 @@
 import java.util.List;
 import java.util.UUID;
 import org.apache.hadoop.hdds.client.ContainerBlockID;
+import org.apache.hadoop.hdds.client.OzoneStoragePolicy;
 import org.apache.hadoop.hdds.client.RatisReplicationConfig;
 import org.apache.hadoop.hdds.client.StandaloneReplicationConfig;
 import org.apache.hadoop.hdds.client.StoragePolicy;
@@ -359,7 +360,8 @@ public void testAllocateBlockSortsSharedPipelineOnce() 
throws Exception {
     // requestedSize spans two scmBlockSize blocks on the same pipeline.
     List<OmKeyLocationInfo> locations = 
request.allocateBlock(replicationConfig,
         new ExcludeList(), 2 * scmBlockSize, true,
-        UserInfo.newBuilder().setRemoteAddress("1.2.3.4").build(), 
ozoneManager);
+        UserInfo.newBuilder().setRemoteAddress("1.2.3.4").build(), 
ozoneManager,
+        OzoneStoragePolicy.getDefaultPolicy(), true);
 
     // Sorted once for the shared pipeline...
     verify(mockKeyManager, times(1)).sortDatanodesForWrite(any(), 
eq("1.2.3.4"), any());
@@ -415,7 +417,8 @@ public void 
testAllocateBlockKeepsPerPipelineOrderWhenSortSkipped() throws Excep
         getOmAllocateBlockRequest(createAllocateBlockRequest());
     List<OmKeyLocationInfo> locations = 
request.allocateBlock(replicationConfig,
         new ExcludeList(), 2 * scmBlockSize, true,
-        UserInfo.newBuilder().setRemoteAddress("1.2.3.4").build(), 
ozoneManager);
+        UserInfo.newBuilder().setRemoteAddress("1.2.3.4").build(), 
ozoneManager,
+        OzoneStoragePolicy.getDefaultPolicy(), true);
 
     assertEquals(2, locations.size());
     // Each pipeline keeps its own order; the skipped-sort result is not 
shared.
@@ -457,7 +460,8 @@ public void 
testAllocateBlockKeepsOrderWhenRemoteAddressEmpty() throws Exception
         getOmAllocateBlockRequest(createAllocateBlockRequest());
     List<OmKeyLocationInfo> locations = 
request.allocateBlock(replicationConfig,
         new ExcludeList(), scmBlockSize, true,
-        UserInfo.newBuilder().setRemoteAddress("").build(), ozoneManager);
+        UserInfo.newBuilder().setRemoteAddress("").build(), ozoneManager,
+        OzoneStoragePolicy.getDefaultPolicy(), true);
 
     assertEquals("", clientMachine.getValue());
     verify(mockKeyManager, never()).sortDatanodesForWrite(any(), anyString(), 
any());


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

Reply via email to