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

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


The following commit(s) were added to refs/heads/HDDS-14496-zdu by this push:
     new 79728a163a3 HDDS-16154. Datanode should assign 
DatanodeDetails#currentVersion based on its apparent version (#11006)
79728a163a3 is described below

commit 79728a163a308cec2009f65e6ca201d4953b69f0
Author: Ethan Rose <[email protected]>
AuthorDate: Fri Aug 21 05:49:10 2026 -0400

    HDDS-16154. Datanode should assign DatanodeDetails#currentVersion based on 
its apparent version (#11006)
---
 .../hadoop/hdds/protocol/DatanodeDetails.java      |   5 +-
 .../hadoop/hdds/protocol/TestDatanodeDetails.java  |  20 ++--
 .../apache/hadoop/ozone/HddsDatanodeService.java   |  11 +--
 .../container/common/helpers/DatanodeIdYaml.java   |  17 +---
 .../states/datanode/RunningDatanodeState.java      |   2 -
 .../states/endpoint/HeartbeatEndpointTask.java     |  53 ++---------
 .../states/endpoint/RegisterEndpointTask.java      |  38 +-------
 .../container/upgrade/DatanodeVersionManager.java  |  10 ++
 .../common/helpers/TestContainerUtils.java         |  20 +++-
 .../states/endpoint/TestHeartbeatEndpointTask.java | 104 +++++++++++++++++++--
 .../upgrade/TestDatanodeVersionManager.java        |  23 ++++-
 .../ozone/upgrade/ComponentVersionManager.java     |   8 ++
 .../hdds/scm/server/upgrade/ScmVersionManager.java |  10 ++
 .../scm/server/upgrade/TestScmVersionManager.java  |  23 ++++-
 .../ozone/container/common/TestEndPoint.java       |  38 +++++---
 .../client/rpc/TestBlockDataStreamOutput.java      |  13 ++-
 .../hadoop/ozone/om/upgrade/OMVersionManager.java  |  10 ++
 .../ozone/om/upgrade/TestOMVersionManager.java     |  20 ++++
 .../ozone/recon/upgrade/ReconVersionManager.java   |   9 ++
 .../recon/upgrade/TestReconVersionManager.java     |  12 +++
 20 files changed, 298 insertions(+), 148 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeDetails.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeDetails.java
index 27da3567310..66856efcb4e 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeDetails.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/protocol/DatanodeDetails.java
@@ -88,7 +88,7 @@ public class DatanodeDetails extends NodeImpl implements 
Comparable<DatanodeDeta
   private volatile HddsProtos.NodeOperationalState persistedOpState;
   private volatile long persistedOpStateExpiryEpochSec;
   private HDDSVersion initialVersion;
-  private HDDSVersion currentVersion;
+  private volatile HDDSVersion currentVersion;
 
   private DatanodeDetails(Builder b) {
     super(b.hostName, b.networkLocation, NetConstants.NODE_COST_DEFAULT);
@@ -464,9 +464,6 @@ public static DatanodeDetails.Builder newBuilder(
     }
     if (datanodeDetailsProto.hasCurrentVersion()) {
       
builder.setCurrentVersion(HDDSVersion.deserialize(datanodeDetailsProto.getCurrentVersion()));
-    } else {
-      // fallback to version 1 if not present
-      builder.setCurrentVersion(HDDSVersion.SEPARATE_RATIS_PORTS_AVAILABLE);
     }
     return builder;
   }
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/protocol/TestDatanodeDetails.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/protocol/TestDatanodeDetails.java
index 6af228d52d3..2df255aae1d 100644
--- 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/protocol/TestDatanodeDetails.java
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/protocol/TestDatanodeDetails.java
@@ -69,23 +69,21 @@ void testRequiredPortsProto() {
 
   @Test
   public void testNewBuilderCurrentVersion() {
-    // test that if the current version is not set (Ozone 1.4.0 and earlier),
-    // it falls back to SEPARATE_RATIS_PORTS_AVAILABLE
+    // When the current version proto field is absent, the builder default 
(DEFAULT_VERSION) applies.
     DatanodeDetails dn = MockDatanodeDetails.randomDatanodeDetails();
     Set<Port.Name> requiredPorts = Stream.of(Port.Name.STANDALONE, 
Port.Name.RATIS)
         .collect(Collectors.toSet());
-    HddsProtos.DatanodeDetailsProto.Builder protoBuilder =
-        dn.toProtoBuilder(DEFAULT_VERSION, requiredPorts);
+    HddsProtos.DatanodeDetailsProto.Builder protoBuilder = 
dn.toProtoBuilder(ClientVersion.CURRENT, requiredPorts);
     protoBuilder.clearCurrentVersion();
     DatanodeDetails dn2 = 
DatanodeDetails.newBuilder(protoBuilder.build()).build();
-    assertEquals(HDDSVersion.SEPARATE_RATIS_PORTS_AVAILABLE,
-        dn2.getCurrentVersion());
+    assertEquals(HDDSVersion.DEFAULT_VERSION, dn2.getCurrentVersion());
 
-    // test that if the current version is set, it is used
-    protoBuilder = dn.toProtoBuilder(DEFAULT_VERSION, requiredPorts);
-    
protoBuilder.setCurrentVersion(HDDSVersion.COMBINED_PUTBLOCK_WRITECHUNK_RPC.serialize());
-    DatanodeDetails dn3 = 
DatanodeDetails.newBuilder(protoBuilder.build()).build();
-    assertEquals(HDDSVersion.COMBINED_PUTBLOCK_WRITECHUNK_RPC, 
dn3.getCurrentVersion());
+    // When the proto field is present, it round-trips correctly.
+    protoBuilder = dn.toProtoBuilder(ClientVersion.CURRENT, requiredPorts);
+    DatanodeDetails dn3 = DatanodeDetails.newBuilder(
+        
protoBuilder.setCurrentVersion(HDDSVersion.SOFTWARE_VERSION.serialize()).build())
+        .build();
+    assertEquals(HDDSVersion.SOFTWARE_VERSION, dn3.getCurrentVersion());
   }
 
   public static void assertPorts(HddsProtos.DatanodeDetailsProto dn,
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/HddsDatanodeService.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/HddsDatanodeService.java
index 274de843b9b..8e482457516 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/HddsDatanodeService.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/HddsDatanodeService.java
@@ -122,6 +122,7 @@ public class HddsDatanodeService extends GenericCli 
implements Callable<Void>, S
       HddsDatanodeService.class);
 
   public static final String TESTING_DATANODE_VERSION_INITIAL = 
"testing.hdds.datanode.version.initial";
+  // TODO(HDDS-16044): TESTING_DATANODE_VERSION_CURRENT is unused until 
SCM-side version setting lands.
   public static final String TESTING_DATANODE_VERSION_CURRENT = 
"testing.hdds.datanode.version.current";
 
   private OzoneConfiguration conf;
@@ -478,8 +479,6 @@ private DatanodeDetails initializeDatanodeDetails()
       details = 
DatanodeDetails.newBuilder().setID(DatanodeID.randomID()).build();
       details.setInitialVersion(getInitialVersion());
     }
-    // Current version is always overridden to the latest
-    details.setCurrentVersion(getCurrentVersion());
     return details;
   }
 
@@ -840,12 +839,4 @@ private HDDSVersion getInitialVersion() {
     return HDDSVersion.deserialize(
         conf.getInt(TESTING_DATANODE_VERSION_INITIAL, 
HDDSVersion.SOFTWARE_VERSION.serialize()));
   }
-
-  /**
-   * Returns the current version of the datanode.
-   */
-  private HDDSVersion getCurrentVersion() {
-    return HDDSVersion.deserialize(
-        conf.getInt(TESTING_DATANODE_VERSION_CURRENT, 
HDDSVersion.SOFTWARE_VERSION.serialize()));
-  }
 }
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/helpers/DatanodeIdYaml.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/helpers/DatanodeIdYaml.java
index 00d232adc18..f74f3b0d8fd 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/helpers/DatanodeIdYaml.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/helpers/DatanodeIdYaml.java
@@ -110,8 +110,7 @@ public static DatanodeDetails readDatanodeIdFile(File path)
         }
       }
 
-      
builder.setInitialVersion(HDDSVersion.deserialize(datanodeDetailsYaml.getInitialVersion()))
-          
.setCurrentVersion(HDDSVersion.deserialize(datanodeDetailsYaml.getCurrentVersion()));
+      
builder.setInitialVersion(HDDSVersion.deserialize(datanodeDetailsYaml.getInitialVersion()));
 
       datanodeDetails = builder.build();
     }
@@ -131,7 +130,6 @@ public static class DatanodeDetailsYaml {
     private long persistedOpStateExpiryEpochSec = 0;
     private Map<String, Integer> portDetails;
     private int initialVersion;
-    private int currentVersion;
 
     public DatanodeDetailsYaml() {
       // Needed for snake-yaml introspection.
@@ -142,7 +140,7 @@ private DatanodeDetailsYaml(String uuid, String ipAddress,
         String hostName, String certSerialId,
         String persistedOpState, long persistedOpStateExpiryEpochSec,
         Map<String, Integer> portDetails,
-        int initialVersion, int currentVersion) {
+        int initialVersion) {
       this.uuid = uuid;
       this.ipAddress = ipAddress;
       this.hostName = hostName;
@@ -151,7 +149,6 @@ private DatanodeDetailsYaml(String uuid, String ipAddress,
       this.persistedOpStateExpiryEpochSec = persistedOpStateExpiryEpochSec;
       this.portDetails = portDetails;
       this.initialVersion = initialVersion;
-      this.currentVersion = currentVersion;
     }
 
     public String getUuid() {
@@ -218,12 +215,9 @@ public void setInitialVersion(int version) {
       this.initialVersion = version;
     }
 
-    public int getCurrentVersion() {
-      return currentVersion;
-    }
-
     public void setCurrentVersion(int version) {
-      this.currentVersion = version;
+      // Setter retained so SnakeYAML can bind existing datanode.id files
+      // that contain this key without throwing on unknown properties.
     }
 
     @Override
@@ -249,7 +243,6 @@ private static DatanodeDetailsYaml getDatanodeDetailsYaml(
         persistedOpString,
         datanodeDetails.getPersistedOpStateExpiryEpochSec(),
         
VersionedDatanodeFeatures.DatanodePorts.getPortsToPersist(datanodeDetails, 
conf),
-        datanodeDetails.getInitialVersion().serialize(),
-        datanodeDetails.getCurrentVersion().serialize());
+        datanodeDetails.getInitialVersion().serialize());
   }
 }
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/datanode/RunningDatanodeState.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/datanode/RunningDatanodeState.java
index 6b8ea712a93..9dcf81603d1 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/datanode/RunningDatanodeState.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/datanode/RunningDatanodeState.java
@@ -150,14 +150,12 @@ private Callable<EndPointStates> buildEndPointTask(
           .setConfig(conf)
           .setEndpointStateMachine(endpoint)
           .setContext(context)
-          .setDatanodeDetails(context.getParent().getDatanodeDetails())
           .setOzoneContainer(context.getParent().getContainer())
           .build();
     case HEARTBEAT:
       return HeartbeatEndpointTask.newBuilder()
           .setConfig(conf)
           .setEndpointStateMachine(endpoint)
-          .setDatanodeDetails(context.getParent().getDatanodeDetails())
           .setContext(context)
           .build();
     default:
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/HeartbeatEndpointTask.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/HeartbeatEndpointTask.java
index 61fb248ef20..e7bed34a6f7 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/HeartbeatEndpointTask.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/HeartbeatEndpointTask.java
@@ -72,7 +72,7 @@ public class HeartbeatEndpointTask
     implements Callable<EndpointStateMachine.EndPointStates> {
   private static final Logger LOG = 
LoggerFactory.getLogger(HeartbeatEndpointTask.class);
   private final EndpointStateMachine rpcEndpoint;
-  private DatanodeDetailsProto datanodeDetailsProto;
+  private final DatanodeDetails datanodeDetails;
   private StateContext context;
   private int maxContainerActionsPerHB;
   private int maxPipelineActionsPerHB;
@@ -93,28 +93,10 @@ public HeartbeatEndpointTask(EndpointStateMachine 
rpcEndpoint,
         HDDS_CONTAINER_ACTION_MAX_LIMIT_DEFAULT);
     this.maxPipelineActionsPerHB = conf.getInt(HDDS_PIPELINE_ACTION_MAX_LIMIT,
         HDDS_PIPELINE_ACTION_MAX_LIMIT_DEFAULT);
+    this.datanodeDetails = context.getParent().getDatanodeDetails();
     this.versionManager = context.getParent().getVersionManager();
   }
 
-  /**
-   * Get the container Node ID proto.
-   *
-   * @return ContainerNodeIDProto
-   */
-  public DatanodeDetailsProto getDatanodeDetailsProto() {
-    return datanodeDetailsProto;
-  }
-
-  /**
-   * Set container node ID proto.
-   *
-   * @param datanodeDetailsProto - the node id.
-   */
-  public void setDatanodeDetailsProto(DatanodeDetailsProto
-      datanodeDetailsProto) {
-    this.datanodeDetailsProto = datanodeDetailsProto;
-  }
-
   /**
    * Computes a result, or throws an exception if unable to do so.
    *
@@ -126,12 +108,13 @@ public EndpointStateMachine.EndPointStates call() throws 
Exception {
     rpcEndpoint.lock();
     SCMHeartbeatRequestProto.Builder requestBuilder = null;
     try {
-      Preconditions.checkState(this.datanodeDetailsProto != null);
-
       DatanodeVersionProto versionInfo = toVersionProto(
           versionManager.getApparentVersion(),
           versionManager.getSoftwareVersion());
 
+      datanodeDetails.setCurrentVersion(versionManager.getVersionForClient());
+      DatanodeDetailsProto datanodeDetailsProto = 
datanodeDetails.getProtoBufMessage();
+
       requestBuilder = SCMHeartbeatRequestProto.newBuilder()
           .setDatanodeDetails(datanodeDetailsProto)
           .setDatanodeVersion(versionInfo);
@@ -265,9 +248,9 @@ public static Builder newBuilder() {
    * @param response - SCMHeartbeat response.
    */
   private void processResponse(SCMHeartbeatResponseProto response,
-      final DatanodeDetailsProto datanodeDetails) {
+      final DatanodeDetailsProto datanodeDetailsProto) {
     Preconditions.checkState(response.getDatanodeUUID()
-            .equalsIgnoreCase(datanodeDetails.getUuid()),
+            .equalsIgnoreCase(datanodeDetailsProto.getUuid()),
         "Unexpected datanode ID in the response.");
     if (response.hasTerm()) {
       context.updateTermOfLeaderSCM(response.getTerm());
@@ -433,7 +416,6 @@ private void processReregisterCommand() {
   public static class Builder {
     private EndpointStateMachine endPointStateMachine;
     private ConfigurationSource conf;
-    private DatanodeDetails datanodeDetails;
     private StateContext context;
 
     /**
@@ -464,17 +446,6 @@ public Builder setConfig(ConfigurationSource config) {
       return this;
     }
 
-    /**
-     * Sets the NodeID.
-     *
-     * @param dnDetails - NodeID proto
-     * @return Builder
-     */
-    public Builder setDatanodeDetails(DatanodeDetails dnDetails) {
-      this.datanodeDetails = dnDetails;
-      return this;
-    }
-
     /**
      * Sets the context.
      * @param stateContext - State context.
@@ -498,16 +469,8 @@ public HeartbeatEndpointTask build() {
             " construct HeartbeatEndpointTask task");
       }
 
-      if (datanodeDetails == null) {
-        LOG.error("No datanode specified.");
-        throw new IllegalArgumentException("A valid Node ID is needed to " +
-            "construct HeartbeatEndpointTask task");
-      }
-
-      HeartbeatEndpointTask task = new HeartbeatEndpointTask(this
+      return new HeartbeatEndpointTask(this
           .endPointStateMachine, this.conf, this.context);
-      task.setDatanodeDetailsProto(datanodeDetails.getProtoBufMessage());
-      return task;
     }
   }
 }
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/RegisterEndpointTask.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/RegisterEndpointTask.java
index 08bff1a422d..31ff3821882 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/RegisterEndpointTask.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/states/endpoint/RegisterEndpointTask.java
@@ -47,7 +47,7 @@ public final class RegisterEndpointTask implements
   static final Logger LOG = 
LoggerFactory.getLogger(RegisterEndpointTask.class);
 
   private final EndpointStateMachine rpcEndPoint;
-  private DatanodeDetails datanodeDetails;
+  private final DatanodeDetails datanodeDetails;
   private final OzoneContainer datanodeContainerManager;
   private StateContext stateContext;
   private final DatanodeVersionManager versionManager;
@@ -66,6 +66,7 @@ public RegisterEndpointTask(EndpointStateMachine rpcEndPoint,
     this.rpcEndPoint = rpcEndPoint;
     this.datanodeContainerManager = ozoneContainer;
     this.stateContext = context;
+    this.datanodeDetails = context.getParent().getDatanodeDetails();
     this.versionManager = context.getParent().getVersionManager();
   }
 
@@ -78,16 +79,6 @@ public DatanodeDetails getDatanodeDetails() {
     return datanodeDetails;
   }
 
-  /**
-   * Set the contiainerNodeID Proto.
-   *
-   * @param datanodeDetails - Container Node ID.
-   */
-  public void setDatanodeDetails(
-      DatanodeDetails datanodeDetails) {
-    this.datanodeDetails = datanodeDetails;
-  }
-
   /**
    * Computes a result, or throws an exception if unable to do so.
    *
@@ -119,6 +110,7 @@ public EndpointStateMachine.EndPointStates call() throws 
Exception {
         NodeReportProto nodeReport = datanodeContainerManager.getNodeReport();
         PipelineReportsProto pipelineReportsProto =
             datanodeContainerManager.getPipelineReport();
+        
datanodeDetails.setCurrentVersion(versionManager.getVersionForClient());
         // TODO : Add responses to the command Queue.
         SCMRegisteredResponseProto response = rpcEndPoint.getEndPoint()
             .register(datanodeDetails.getExtendedProtoBufMessage(),
@@ -177,7 +169,6 @@ public static Builder newBuilder() {
   public static class Builder {
     private EndpointStateMachine endPointStateMachine;
     private ConfigurationSource conf;
-    private DatanodeDetails datanodeDetails;
     private OzoneContainer container;
     private StateContext context;
 
@@ -209,17 +200,6 @@ public Builder setConfig(ConfigurationSource config) {
       return this;
     }
 
-    /**
-     * Sets the NodeID.
-     *
-     * @param dnDetails - NodeID proto
-     * @return Builder
-     */
-    public Builder setDatanodeDetails(DatanodeDetails dnDetails) {
-      this.datanodeDetails = dnDetails;
-      return this;
-    }
-
     /**
      * Sets the ozonecontainer.
      * @param ozoneContainer
@@ -249,12 +229,6 @@ public RegisterEndpointTask build() {
                 + "task");
       }
 
-      if (datanodeDetails == null) {
-        LOG.error("No datanode specified.");
-        throw new IllegalArgumentException("A valid Node ID is needed to " +
-            "construct RegisterEndpoint task");
-      }
-
       if (container == null) {
         LOG.error("Container is not specified");
         throw new IllegalArgumentException("Container is not specified to " +
@@ -267,10 +241,8 @@ public RegisterEndpointTask build() {
             "construct RegisterEndpoint task");
       }
 
-      RegisterEndpointTask task = new RegisterEndpointTask(this
-          .endPointStateMachine, this.container, this.context);
-      task.setDatanodeDetails(datanodeDetails);
-      return task;
+      return new RegisterEndpointTask(this.endPointStateMachine,
+          this.container, this.context);
     }
   }
 }
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/upgrade/DatanodeVersionManager.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/upgrade/DatanodeVersionManager.java
index a4a762ffa27..b925a4b8759 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/upgrade/DatanodeVersionManager.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/upgrade/DatanodeVersionManager.java
@@ -65,6 +65,16 @@ public int getPersistedApparentVersion() {
     return storage.getApparentVersion();
   }
 
+  @Override
+  public HDDSVersion getVersionForClient() {
+    ComponentVersion apparentVersion = getApparentVersion();
+    // Once ZDU is finalized, the apparent version should always belong to the 
HDDSVersion enum.
+    if (isAllowed(HDDSVersion.ZDU) && apparentVersion instanceof HDDSVersion) {
+      return (HDDSVersion) apparentVersion;
+    }
+    return HDDSVersion.values()[HDDSVersion.ZDU.ordinal() - 1];
+  }
+
   @VisibleForTesting
   public Map<ComponentVersion, DatanodeUpgradeAction> 
getUpgradeActionsForTesting() {
     return upgradeActions;
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/helpers/TestContainerUtils.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/helpers/TestContainerUtils.java
index 9735c5eeb13..0feb3c03fb8 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/helpers/TestContainerUtils.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/helpers/TestContainerUtils.java
@@ -24,6 +24,7 @@
 import static 
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Type.ReadChunk;
 import static 
org.apache.hadoop.hdds.scm.protocolPB.ContainerCommandResponseBuilders.getReadChunkResponse;
 import static 
org.apache.hadoop.ozone.container.ContainerTestHelper.getDummyCommandRequestProto;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -112,6 +113,10 @@ public void testTarName() throws IOException {
   public void testDatanodeIDPersistent(@TempDir File tempDir) throws Exception 
{
     // Generate IDs for testing
     DatanodeDetails id1 = randomDatanodeDetails();
+    // `randomDatanodeDetails()` populates the current version to 
SOFTWARE_VERSION by default.
+    // Datanodes do not populate this value when persisting the proto, only 
when sending it over the wire,
+    // so override the setter to the default value to match what Datanodes 
actually persist internally.
+    id1.setCurrentVersion(HDDSVersion.DEFAULT_VERSION);
     try (MockedStatic<InetAddress> mockedStaticInetAddress = 
mockStatic(InetAddress.class)) {
       InetAddress mockedInetAddress = mock(InetAddress.class);
       mockedStaticInetAddress.when(() -> 
InetAddress.getByName(id1.getHostName()))
@@ -204,7 +209,20 @@ private void assertWriteRead(@TempDir File tempDir,
     DatanodeDetails read = ContainerUtils.readDatanodeDetailsFrom(file, conf);
 
     assertDetailsEquals(details, read);
-    assertEquals(details.getCurrentVersion(), read.getCurrentVersion());
+    // currentVersion is updated by SCM for clients to consume. When unset for 
cases like persisting to the datanode,
+    // it should return DEFAULT_VERSION. This is an in-memory placeholder 
which is not written to the file.
+    assertEquals(HDDSVersion.DEFAULT_VERSION, read.getCurrentVersion());
+  }
+
+  @Test
+  public void writtenYamlOmitsCurrentVersion(@TempDir File tempDir) throws 
IOException {
+    DatanodeDetails details = randomDatanodeDetails();
+    details.setCurrentVersion(HDDSVersion.SOFTWARE_VERSION);
+    File file = new File(tempDir, "no-current-version.id");
+    ContainerUtils.writeDatanodeDetailsTo(details, file, conf);
+
+    String yaml = new String(Files.readAllBytes(file.toPath()), UTF_8);
+    assertThat(yaml).doesNotContain("currentVersion");
   }
 
   private void assertWriteReadWithChangedIpAddress(@TempDir File tempDir,
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/endpoint/TestHeartbeatEndpointTask.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/endpoint/TestHeartbeatEndpointTask.java
index 4c49b634aa1..1dd6fd02ad0 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/endpoint/TestHeartbeatEndpointTask.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/states/endpoint/TestHeartbeatEndpointTask.java
@@ -50,6 +50,7 @@
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.SCMHeartbeatRequestProto;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.SCMHeartbeatResponseProto;
 import org.apache.hadoop.hdds.scm.net.HostAndPort;
+import org.apache.hadoop.hdds.upgrade.HDDSLayoutFeature;
 import org.apache.hadoop.hdfs.util.EnumCounters;
 import 
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine;
 import 
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine.DatanodeStates;
@@ -395,6 +396,94 @@ public void testheartbeatWithAllReports() throws Exception 
{
     }
   }
 
+  @Test
+  public void testDatanodeCurrentVersionPassThrough() throws Exception {
+    OzoneConfiguration conf = new OzoneConfiguration();
+    DatanodeStateMachine datanodeStateMachine = mockDatanodeStateMachine();
+    StateContext context = new StateContext(conf, DatanodeStates.RUNNING,
+        datanodeStateMachine, "");
+
+    // Set the expected versions to return.
+    DatanodeVersionManager versionManager = 
datanodeStateMachine.getVersionManager();
+    when(versionManager.getVersionForClient())
+        .thenReturn(HDDSVersion.DEFAULT_VERSION)
+        .thenReturn(HDDSVersion.ZDU)
+        .thenReturn(HDDSVersion.SOFTWARE_VERSION);
+
+    when(datanodeStateMachine.getQueuedCommandCount())
+        .thenReturn(new EnumCounters<>(SCMCommandProto.Type.class));
+
+    StorageContainerDatanodeProtocolClientSideTranslatorPB scm =
+        mock(StorageContainerDatanodeProtocolClientSideTranslatorPB.class);
+    ArgumentCaptor<SCMHeartbeatRequestProto> argument = ArgumentCaptor
+        .forClass(SCMHeartbeatRequestProto.class);
+    when(scm.sendHeartbeat(argument.capture()))
+        .thenAnswer(invocation ->
+            SCMHeartbeatResponseProto.newBuilder()
+                .setDatanodeUUID(
+                    ((SCMHeartbeatRequestProto)invocation.getArgument(0))
+                        .getDatanodeDetails().getUuid())
+                .build());
+
+    HeartbeatEndpointTask endpointTask = getHeartbeatEndpointTask(
+        conf, context, scm);
+
+    // Assert the expected versions were returned.
+    endpointTask.call();
+    assertEquals(HDDSVersion.DEFAULT_VERSION.serialize(),
+        argument.getValue().getDatanodeDetails().getCurrentVersion());
+    endpointTask.call();
+    assertEquals(HDDSVersion.ZDU.serialize(),
+        argument.getValue().getDatanodeDetails().getCurrentVersion());
+    endpointTask.call();
+    assertEquals(HDDSVersion.SOFTWARE_VERSION.serialize(),
+        argument.getValue().getDatanodeDetails().getCurrentVersion());
+  }
+
+  @Test
+  public void testDatanodeApparentVersionPassThrough() throws Exception {
+    OzoneConfiguration conf = new OzoneConfiguration();
+    DatanodeStateMachine datanodeStateMachine = mockDatanodeStateMachine();
+    StateContext context = new StateContext(conf, DatanodeStates.RUNNING,
+        datanodeStateMachine, "");
+
+    // Set the expected versions to return.
+    DatanodeVersionManager versionManager = 
datanodeStateMachine.getVersionManager();
+    when(versionManager.getApparentVersion())
+        .thenReturn(HDDSLayoutFeature.INITIAL_VERSION)
+        .thenReturn(HDDSVersion.ZDU)
+        .thenReturn(HDDSVersion.SOFTWARE_VERSION);
+
+    when(datanodeStateMachine.getQueuedCommandCount())
+        .thenReturn(new EnumCounters<>(SCMCommandProto.Type.class));
+
+    StorageContainerDatanodeProtocolClientSideTranslatorPB scm =
+        mock(StorageContainerDatanodeProtocolClientSideTranslatorPB.class);
+    ArgumentCaptor<SCMHeartbeatRequestProto> argument = ArgumentCaptor
+        .forClass(SCMHeartbeatRequestProto.class);
+    when(scm.sendHeartbeat(argument.capture()))
+        .thenAnswer(invocation ->
+            SCMHeartbeatResponseProto.newBuilder()
+                .setDatanodeUUID(
+                    ((SCMHeartbeatRequestProto)invocation.getArgument(0))
+                        .getDatanodeDetails().getUuid())
+                .build());
+
+    HeartbeatEndpointTask endpointTask = getHeartbeatEndpointTask(
+        conf, context, scm);
+
+    // Assert the expected versions were returned.
+    endpointTask.call();
+    assertEquals(HDDSLayoutFeature.INITIAL_VERSION.serialize(),
+        argument.getValue().getDatanodeVersion().getApparentVersion());
+    endpointTask.call();
+    assertEquals(HDDSVersion.ZDU.serialize(),
+        argument.getValue().getDatanodeVersion().getApparentVersion());
+    endpointTask.call();
+    assertEquals(HDDSVersion.SOFTWARE_VERSION.serialize(),
+        argument.getValue().getDatanodeVersion().getApparentVersion());
+  }
+
   /**
    * Creates HeartbeatEndpointTask with the given conf, context and
    * StorageContainerManager client side proxy.
@@ -409,28 +498,29 @@ private HeartbeatEndpointTask getHeartbeatEndpointTask(
       ConfigurationSource conf,
       StateContext context,
       StorageContainerDatanodeProtocolClientSideTranslatorPB proxy) {
-    DatanodeDetails datanodeDetails = DatanodeDetails.newBuilder()
-        .setUuid(UUID.randomUUID())
-        .setHostName("localhost")
-        .setIpAddress("127.0.0.1")
-        .build();
     EndpointStateMachine endpointStateMachine = 
mock(EndpointStateMachine.class);
     when(endpointStateMachine.getEndPoint()).thenReturn(proxy);
     when(endpointStateMachine.getAddress())
         .thenReturn(TEST_SCM_ENDPOINT);
     return HeartbeatEndpointTask.newBuilder()
         .setConfig(conf)
-        .setDatanodeDetails(datanodeDetails)
         .setContext(context)
         .setEndpointStateMachine(endpointStateMachine)
         .build();
   }
-  
+
   private DatanodeStateMachine mockDatanodeStateMachine() {
+    DatanodeDetails datanodeDetails = DatanodeDetails.newBuilder()
+        .setUuid(UUID.randomUUID())
+        .setHostName("localhost")
+        .setIpAddress("127.0.0.1")
+        .build();
     DatanodeVersionManager versionManager = mock(DatanodeVersionManager.class);
     
when(versionManager.getSoftwareVersion()).thenReturn(HDDSVersion.SOFTWARE_VERSION);
     
when(versionManager.getApparentVersion()).thenReturn(HDDSVersion.SOFTWARE_VERSION);
+    
when(versionManager.getVersionForClient()).thenReturn(HDDSVersion.SOFTWARE_VERSION);
     DatanodeStateMachine mockDSM = mock(DatanodeStateMachine.class);
+    when(mockDSM.getDatanodeDetails()).thenReturn(datanodeDetails);
     when(mockDSM.getVersionManager()).thenReturn(versionManager);
     return mockDSM;
   }
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/upgrade/TestDatanodeVersionManager.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/upgrade/TestDatanodeVersionManager.java
index 572b7baa679..a01dba245be 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/upgrade/TestDatanodeVersionManager.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/upgrade/TestDatanodeVersionManager.java
@@ -59,7 +59,6 @@
 import 
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine;
 import org.apache.hadoop.ozone.upgrade.AbstractComponentVersionManagerTest;
 import org.apache.hadoop.ozone.upgrade.ComponentUpgradeActionProvider;
-import org.apache.hadoop.ozone.upgrade.ComponentVersionManager;
 import org.apache.hadoop.ozone.upgrade.UpgradeException;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -102,7 +101,7 @@ public static Stream<Arguments> preFinalizedVersionArgs() {
   }
 
   @Override
-  protected ComponentVersionManager createManager(int 
serializedApparentVersion) throws IOException {
+  protected DatanodeVersionManager createManager(int 
serializedApparentVersion) throws IOException {
     return createManager(serializedApparentVersion, HashMap::new);
   }
 
@@ -224,6 +223,26 @@ public void testPersistFailureRollsBack() throws Exception 
{
     }
   }
 
+  @Test
+  public void testGetVersionForClientWhenFinalized() throws Exception {
+    // Apparent version >= ZDU (finalized) reports the apparent version itself.
+    try (DatanodeVersionManager versionManager = 
createManager(HDDSVersion.ZDU.serialize())) {
+      assertEquals(HDDSVersion.ZDU, versionManager.getVersionForClient());
+    }
+
+    try (DatanodeVersionManager versionManager = 
createManager(HDDSVersion.SOFTWARE_VERSION.serialize())) {
+      assertEquals(HDDSVersion.SOFTWARE_VERSION, 
versionManager.getVersionForClient());
+    }
+  }
+
+  @Test
+  public void testGetVersionForClientWhenPreZdu() throws Exception {
+    // A pre-ZDU apparent version clamps to the last pre-ZDU wire version.
+    try (DatanodeVersionManager versionManager = 
createManager(INITIAL_VERSION.serialize())) {
+      assertEquals(HDDSVersion.STREAM_BLOCK_SUPPORT, 
versionManager.getVersionForClient());
+    }
+  }
+
   private static void assertDatanodeApparentVersionOnDisk(OzoneConfiguration 
conf, int expected)
       throws IOException {
     DatanodeStorage reloaded = new DatanodeStorage(conf, 
UUID.randomUUID().toString());
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/ComponentVersionManager.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/ComponentVersionManager.java
index 516158f76c6..5aaa48aec1a 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/ComponentVersionManager.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/ComponentVersionManager.java
@@ -101,6 +101,14 @@ public void finalizeUpgrade() throws UpgradeException {
     }
   }
 
+  /**
+   * Returns the version that the server should advertise to the client for 
new client/oldserver compatibility.
+   * If ZDU is finalized, we can use the same apparent version for both client 
and disk compatibility.
+   * If the server is not yet finalized for ZDU, we must continue to use the 
separate component version system for
+   * clients and layout feature versioning system for internal apparent 
version.
+   */
+  public abstract ComponentVersion getVersionForClient();
+
   /**
    * @return An Iterable of all versions after the current apparent version 
which still need to be finalized. If this
    *    component is already finalized, the Iterable will be empty.
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/upgrade/ScmVersionManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/upgrade/ScmVersionManager.java
index c5a44fb249d..3f79bd0ddcb 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/upgrade/ScmVersionManager.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/upgrade/ScmVersionManager.java
@@ -72,6 +72,16 @@ public Map<ComponentVersion, ScmUpgradeAction> 
getUpgradeActionsForTesting() {
     return upgradeActions;
   }
 
+  @Override
+  public HDDSVersion getVersionForClient() {
+    ComponentVersion apparentVersion = getApparentVersion();
+    // Once ZDU is finalized, the apparent version should always belong to the 
HDDSVersion enum.
+    if (isAllowed(HDDSVersion.ZDU) && apparentVersion instanceof HDDSVersion) {
+      return (HDDSVersion) apparentVersion;
+    }
+    return HDDSVersion.values()[HDDSVersion.ZDU.ordinal() - 1];
+  }
+
   @Override
   protected void runUpgradeAction(ComponentVersion version) throws 
UpgradeException {
     ScmUpgradeAction action = upgradeActions.get(version);
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/upgrade/TestScmVersionManager.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/upgrade/TestScmVersionManager.java
index 5c32cd8f71b..bb8c0ddde95 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/upgrade/TestScmVersionManager.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/upgrade/TestScmVersionManager.java
@@ -58,7 +58,6 @@
 import org.apache.hadoop.hdds.upgrade.ScmUpgradeActionProvider;
 import org.apache.hadoop.ozone.upgrade.AbstractComponentVersionManagerTest;
 import org.apache.hadoop.ozone.upgrade.ComponentUpgradeActionProvider;
-import org.apache.hadoop.ozone.upgrade.ComponentVersionManager;
 import org.apache.hadoop.ozone.upgrade.UpgradeException;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -100,7 +99,7 @@ public static Stream<Arguments> preFinalizedVersionArgs() {
   }
 
   @Override
-  protected ComponentVersionManager createManager(int 
serializedApparentVersion) throws IOException {
+  protected ScmVersionManager createManager(int serializedApparentVersion) 
throws IOException {
     return createManager(serializedApparentVersion, HashMap::new);
   }
 
@@ -221,6 +220,26 @@ public void testPersistFailureRollsBack() throws Exception 
{
     }
   }
 
+  @Test
+  public void testGetVersionForClientWhenFinalized() throws Exception {
+    // Apparent version >= ZDU (finalized) reports the apparent version itself.
+    try (ScmVersionManager versionManager = 
createManager(HDDSVersion.ZDU.serialize())) {
+      assertEquals(HDDSVersion.ZDU, versionManager.getVersionForClient());
+    }
+
+    try (ScmVersionManager versionManager = 
createManager(HDDSVersion.SOFTWARE_VERSION.serialize())) {
+      assertEquals(HDDSVersion.SOFTWARE_VERSION, 
versionManager.getVersionForClient());
+    }
+  }
+
+  @Test
+  public void testGetVersionForClientWhenPreZdu() throws Exception {
+    // A pre-ZDU apparent version clamps to the last pre-ZDU wire version.
+    try (ScmVersionManager versionManager = 
createManager(INITIAL_VERSION.serialize())) {
+      assertEquals(HDDSVersion.STREAM_BLOCK_SUPPORT, 
versionManager.getVersionForClient());
+    }
+  }
+
   private static void assertScmApparentVersionOnDisk(OzoneConfiguration conf, 
int expected)
       throws IOException {
     SCMStorageConfig reloaded = new SCMStorageConfig(conf);
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/ozone/container/common/TestEndPoint.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/ozone/container/common/TestEndPoint.java
index 2b76ac820ff..caf36cf7743 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/ozone/container/common/TestEndPoint.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/ozone/container/common/TestEndPoint.java
@@ -42,7 +42,6 @@
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.protocol.DatanodeID;
-import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.CloseContainerCommandProto;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.CommandStatus.Status;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.DeleteBlocksCommandProto;
@@ -438,17 +437,16 @@ private RegisterEndpointTask 
getRegisterEndpointTask(boolean clearDatanodeDetail
         .thenReturn(HDDSVersion.SOFTWARE_VERSION);
     when(versionManager.getSoftwareVersion())
         .thenReturn(HDDSVersion.SOFTWARE_VERSION);
+    when(versionManager.getVersionForClient())
+        .thenReturn(HDDSVersion.SOFTWARE_VERSION);
     DatanodeStateMachine dn = mock(DatanodeStateMachine.class);
     when(dn.getVersionManager()).thenReturn(versionManager);
-    StateContext context = mock(StateContext.class);
-    when(context.getParent()).thenReturn(dn);
-    RegisterEndpointTask endpointTask =
-        new RegisterEndpointTask(rpcEndPoint, ozoneContainer, context);
     if (!clearDatanodeDetails) {
-      DatanodeDetails datanodeDetails = randomDatanodeDetails();
-      endpointTask.setDatanodeDetails(datanodeDetails);
+      when(dn.getDatanodeDetails()).thenReturn(randomDatanodeDetails());
     }
-    return endpointTask;
+    StateContext context = mock(StateContext.class);
+    when(context.getParent()).thenReturn(dn);
+    return new RegisterEndpointTask(rpcEndPoint, ozoneContainer, context);
   }
 
   private EndpointStateMachine registerTaskHelper(InetSocketAddress scmAddress,
@@ -471,6 +469,26 @@ public void testRegisterTask() throws Exception {
     }
   }
 
+  @Test
+  public void testRegisterAssignsCurrentVersionFromVersionManager()
+      throws Exception {
+    OzoneConfiguration conf = SCMTestUtils.getConf(tempDir);
+    try (EndpointStateMachine rpcEndPoint =
+        createEndpoint(conf, serverAddress, 1000)) {
+      rpcEndPoint.setState(EndpointStateMachine.EndPointStates.REGISTER);
+      RegisterEndpointTask endpointTask =
+          getRegisterEndpointTask(false, conf, rpcEndPoint);
+      // Simulate stale version on the persisted datanode details. The task
+      // should overwrite it with the version manager's current value, matching
+      // the heartbeat behavior.
+      endpointTask.getDatanodeDetails()
+          .setCurrentVersion(HDDSVersion.DEFAULT_VERSION);
+      endpointTask.call();
+      assertEquals(HDDSVersion.SOFTWARE_VERSION,
+          endpointTask.getDatanodeDetails().getCurrentVersion());
+    }
+  }
+
   @Test
   public void testRegisterToInvalidEndpoint() throws Exception {
     InetSocketAddress address = SCMTestUtils.getReuseableAddress();
@@ -619,8 +637,6 @@ private StateContext heartbeatTaskHelper(
         randomDatanodeDetails(), conf);
         EndpointStateMachine rpcEndPoint =
             createEndpoint(conf, scmAddress, rpcTimeout)) {
-      HddsProtos.DatanodeDetailsProto datanodeDetailsProto =
-          randomDatanodeDetails().getProtoBufMessage();
       rpcEndPoint.setState(EndpointStateMachine.EndPointStates.HEARTBEAT);
 
       final StateContext stateContext =
@@ -629,9 +645,7 @@ private StateContext heartbeatTaskHelper(
 
       HeartbeatEndpointTask endpointTask =
           new HeartbeatEndpointTask(rpcEndPoint, conf, stateContext);
-      endpointTask.setDatanodeDetailsProto(datanodeDetailsProto);
       endpointTask.call();
-      assertNotNull(endpointTask.getDatanodeDetailsProto());
 
       assertEquals(EndpointStateMachine.EndPointStates.HEARTBEAT, 
rpcEndPoint.getState());
       return stateContext;
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestBlockDataStreamOutput.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestBlockDataStreamOutput.java
index 97958c324e5..c915c393014 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestBlockDataStreamOutput.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestBlockDataStreamOutput.java
@@ -63,6 +63,7 @@
 import org.apache.hadoop.ozone.container.ContainerTestHelper;
 import org.apache.hadoop.ozone.container.TestHelper;
 import org.apache.ozone.test.tag.Flaky;
+import org.apache.ozone.test.tag.Unhealthy;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.TestInstance;
@@ -333,17 +334,17 @@ public void testTotalAckDataLength(boolean flushDelay) 
throws Exception {
     }
   }
 
+  @Unhealthy("Requires HDDS-16044 to finish implementing datanode version 
passing to client.")
   @ParameterizedTest
   @MethodSource("clientParameters")
   public void testDatanodeVersion(boolean flushDelay) throws Exception {
     OzoneClientConfig config = newClientConfig(cluster.getConf(), flushDelay);
     try (OzoneClient client = newClient(cluster.getConf(), config)) {
-      // Each datanode advertises its own (older) currentVersion internally.
+      // Verify all DNs internally have versions set correctly
       List<HddsDatanodeService> dns = cluster.getHddsDatanodes();
       for (HddsDatanodeService dn : dns) {
         DatanodeDetails details = dn.getDatanodeDetails();
-        assertEquals(DN_OLD_VERSION,
-            details.getCurrentVersion());
+        assertEquals(DN_OLD_VERSION, details.getCurrentVersion());
       }
 
       String keyName = getKeyName();
@@ -351,12 +352,10 @@ public void testDatanodeVersion(boolean flushDelay) 
throws Exception {
       KeyDataStreamOutput keyDataStreamOutput = (KeyDataStreamOutput) 
key.getByteBufStreamOutput();
       BlockDataStreamOutputEntry stream = 
keyDataStreamOutput.getStreamEntries().get(0);
 
-      // The cluster is finalized for ZDU, so the pipeline SCM returns on 
block allocation
-      // stamps each member's currentVersion with the lowest apparent version 
across the
-      // pipeline, which here is the software version the datanodes have 
finalized to.
+     // Now check 3 DNs in a random pipeline returns the correct DN versions
       List<DatanodeDetails> streamDnDetails = stream.getPipeline().getNodes();
       for (DatanodeDetails details : streamDnDetails) {
-        assertEquals(HDDSVersion.SOFTWARE_VERSION, 
details.getCurrentVersion());
+        assertEquals(DN_OLD_VERSION, details.getCurrentVersion());
       }
     }
   }
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMVersionManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMVersionManager.java
index f03e14819d6..65a94196a41 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMVersionManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMVersionManager.java
@@ -88,6 +88,16 @@ protected ComponentVersion computeApparentVersion(int 
serializedApparentVersion)
     return computeApparentVersionInternal(serializedApparentVersion);
   }
 
+  @Override
+  public OzoneManagerVersion getVersionForClient() {
+    ComponentVersion apparentVersion = getApparentVersion();
+    // Once ZDU is finalized, the apparent version should always belong to the 
OzoneManagerVersion enum.
+    if (isAllowed(OzoneManagerVersion.ZDU) && apparentVersion instanceof 
OzoneManagerVersion) {
+      return (OzoneManagerVersion) apparentVersion;
+    }
+    return OzoneManagerVersion.values()[OzoneManagerVersion.ZDU.ordinal() - 1];
+  }
+
   /**
    * Maps a serialized apparent version to a {@link ComponentVersion}.
    * If the value is &gt;= {@link OzoneManagerVersion#ZDU} serialized, the OM 
has been finalized for ZDU and the
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/upgrade/TestOMVersionManager.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/upgrade/TestOMVersionManager.java
index 5a927fa3614..434f23b010d 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/upgrade/TestOMVersionManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/upgrade/TestOMVersionManager.java
@@ -213,6 +213,26 @@ public void testPersistFailureRollsBack() throws Exception 
{
     }
   }
 
+  @Test
+  public void testGetVersionForClientWhenFinalized() throws Exception {
+    // Apparent version >= ZDU (finalized) reports the apparent version itself.
+    try (OMVersionManager versionManager = createManager(ZDU.serialize())) {
+      assertEquals(ZDU, versionManager.getVersionForClient());
+    }
+
+    try (OMVersionManager versionManager = 
createManager(SOFTWARE_VERSION.serialize())) {
+      assertEquals(SOFTWARE_VERSION, versionManager.getVersionForClient());
+    }
+  }
+
+  @Test
+  public void testGetVersionForClientWhenPreZdu() throws Exception {
+    // A pre-ZDU apparent version clamps to the last pre-ZDU client version.
+    try (OMVersionManager versionManager = 
createManager(INITIAL_VERSION.serialize())) {
+      assertEquals(OzoneManagerVersion.S3_BUCKET_TAGGING_API, 
versionManager.getVersionForClient());
+    }
+  }
+
   private OMStorage newOmStorage(int apparentVersion)
       throws IOException {
     // Reinitialize the configuration to point to a new unique storage 
location.
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersionManager.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersionManager.java
index eeffc03e443..03b8cc0cc72 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersionManager.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersionManager.java
@@ -135,6 +135,15 @@ public int getPersistedApparentVersion() {
     }
   }
 
+  /**
+   * Recon never used separate layout features and component versions, so its 
apparent version can always be used for
+   * both network and disk compatibility.
+   */
+  @Override
+  public ReconVersion getVersionForClient() {
+    return (ReconVersion) getApparentVersion();
+  }
+
   @Override
   protected void runUpgradeAction(ComponentVersion version) throws 
UpgradeException {
     ReconUpgradeAction action = upgradeActions.get(version);
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
index 24033063e31..c32c713e22a 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
@@ -214,6 +214,18 @@ public void testUpgradeActionFailureAbortsFinalize() 
throws Exception {
     }
   }
 
+  @Test
+  public void testGetVersionForClientReturnsApparentVersion() throws Exception 
{
+    // Recon has no separate ZDU/layout-feature split, so the client version 
is always the apparent version.
+    try (ComponentVersionManager versionManager = 
createManager(ReconVersion.SOFTWARE_VERSION.serialize())) {
+      assertEquals(ReconVersion.SOFTWARE_VERSION, 
versionManager.getVersionForClient());
+    }
+
+    try (ComponentVersionManager versionManager = 
createManager(ReconVersion.INITIAL_VERSION.serialize())) {
+      assertEquals(ReconVersion.INITIAL_VERSION, 
versionManager.getVersionForClient());
+    }
+  }
+
   @Override
   @Test
   public void testPersistFailureRollsBack() throws Exception {


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

Reply via email to