errose28 commented on code in PR #11692:
URL: https://github.com/apache/ozone/pull/11692#discussion_r4350279586


##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeManager.java:
##########
@@ -408,6 +408,16 @@ void processNodeReport(DatanodeDetails datanodeDetails,
    */
   void processVersionReport(DatanodeDetails datanodeDetails, 
DatanodeVersionProto versionReport);
 
+  /**
+   * Returns true if the datanode should be blocked from joining the cluster 
due to a version
+   * incompatibility with this SCM. See {@code 
SCMNodeManager.shouldFenceDatanode} for the full
+   * set of conditions. Returns false by default for implementations that do 
not enforce versioning.

Review Comment:
   ```suggestion
      * set of conditions. Returns false by default for test implementations 
that do not enforce versioning.
   ```
   
   Another option is to explicitly update each test implementation with `return 
false`, although I am fine with this approach too. Just want it to be clear 
that prod instances are not expected to use the default implementation.



##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMDatanodeHeartbeatDispatcher.java:
##########
@@ -182,6 +196,25 @@ public List<SCMCommand<?>> 
dispatch(SCMHeartbeatRequestProto heartbeat) {
     return commands;
   }
 
+  /**
+   * Returns true if the node is not healthy (stale or dead) and its reported 
version is
+   * incompatible with this SCM. Healthy nodes are always allowed through — 
their version was
+   * already validated at registration. If the node status cannot be 
determined, returns false
+   * so the heartbeat is processed normally.
+   */
+  private boolean isUnhealthyWithIncompatibleVersion(DatanodeDetails 
datanodeDetails,
+      SCMHeartbeatRequestProto heartbeat) {
+    if (!heartbeat.hasDatanodeVersion()) {
+      return false;
+    }
+    try {
+      return !nodeManager.getNodeStatus(datanodeDetails).isHealthy()
+          && nodeManager.shouldFenceDatanode(datanodeDetails, 
heartbeat.getDatanodeVersion());
+    } catch (NodeNotFoundException e) {

Review Comment:
   I think the intent of the copilot comment is valid, although I don't like 
the way it rewrote the method. Here we are defining the server side protocol 
which is:
   - Stale or Dead nodes must re-register
   - Registration is the only time a version is required to be attached to the 
heartbeat
   
   So this method should first check if the node is healthy. If not, it should 
call `shouldFenceDatanode`. `shouldFenceDatanode` already returns `true` if the 
version is absent so this method does not need that check.



##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMDatanodeHeartbeatDispatcher.java:
##########
@@ -104,6 +105,18 @@ public List<SCMCommand<?>> 
dispatch(SCMHeartbeatRequestProto heartbeat) {
       if (heartbeat.hasCommandQueueReport()) {
         commandQueueReport = heartbeat.getCommandQueueReport();
       }
+
+      // Stale and dead nodes must pass a version compatibility check before 
their heartbeat
+      // timestamp is updated. Without this gate a node that went unhealthy 
during a rolling
+      // upgrade could resurrect at an incompatible software version. If the 
check fails, ask
+      // the node to re-register so the full registration fence runs with 
current version info.
+      if (isUnhealthyWithIncompatibleVersion(datanodeDetails, heartbeat)) {

Review Comment:
   Do we need to process the command queue report for these nodes?
   
   Also are we intentionally processing the version report for the node before 
this check?  I guess that will update SCM's metadata of the node's versions 
which may help with debugging when it is fenced. However this method also calls 
`sendFinalizeToDatanodeIfNeeded` which calls `shouldFenceDatanode`, so the 
fencing check logs twice.



##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMDatanodeHeartbeatDispatcher.java:
##########
@@ -104,6 +105,18 @@ public List<SCMCommand<?>> 
dispatch(SCMHeartbeatRequestProto heartbeat) {
       if (heartbeat.hasCommandQueueReport()) {
         commandQueueReport = heartbeat.getCommandQueueReport();
       }
+
+      // Stale and dead nodes must pass a version compatibility check before 
their heartbeat
+      // timestamp is updated. Without this gate a node that went unhealthy 
during a rolling
+      // upgrade could resurrect at an incompatible software version. If the 
check fails, ask
+      // the node to re-register so the full registration fence runs with 
current version info.
+      if (isUnhealthyWithIncompatibleVersion(datanodeDetails, heartbeat)) {
+        LOG.info("Node {} is not healthy and reported an incompatible version. 
Asking to re-register.",
+            datanodeDetails);
+        nodeManager.addDatanodeCommand(datanodeDetails.getID(), new 
ReregisterCommand());
+        return nodeManager.getCommandQueue(datanodeDetails.getID());

Review Comment:
   The first block which tells unknown nodes to re-register doesn't return so 
that the commands sent in the heartbeat are picked up by a debug log. Maybe 
this new case should be contained in an `else if` block and follow a similar 
pattern.



##########
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMDatanodeHeartbeatDispatcher.java:
##########
@@ -141,9 +144,10 @@ public <PAYLOAD, EVENT extends Event<PAYLOAD>> void 
fireEvent(
   }
 
   @Test
-  public void testVersionReportProcessedOnlyWhenPresent() throws IOException {
+  public void testVersionReportProcessedOnlyWhenPresent() throws IOException, 
NodeNotFoundException {
     NodeManager mockNodeManager = mock(NodeManager.class);
     when(mockNodeManager.isNodeRegistered(any())).thenReturn(true);
+    
when(mockNodeManager.getNodeStatus(any())).thenReturn(NodeStatus.inServiceStale());

Review Comment:
   Why was stale chosen here?



##########
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMDatanodeHeartbeatDispatcher.java:
##########
@@ -166,6 +170,74 @@ public void testVersionReportProcessedOnlyWhenPresent() 
throws IOException {
     verify(mockNodeManager, times(1)).processVersionReport(any(), any());
   }
 
+  @Test
+  public void testStaleNodeWithIncompatibleVersionGetsReregisterCommand() 
throws Exception {

Review Comment:
   Looks like these stale and dead tests can be combined and parameterized by 
health state. We can also test that re-register is skipped for these health 
states when the version is allowed.



##########
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMDatanodeHeartbeatDispatcher.java:
##########
@@ -166,6 +170,74 @@ public void testVersionReportProcessedOnlyWhenPresent() 
throws IOException {
     verify(mockNodeManager, times(1)).processVersionReport(any(), any());
   }
 
+  @Test
+  public void testStaleNodeWithIncompatibleVersionGetsReregisterCommand() 
throws Exception {
+    NodeManager mockNodeManager = mock(NodeManager.class);
+    when(mockNodeManager.isNodeRegistered(any())).thenReturn(true);
+    
when(mockNodeManager.getNodeStatus(any())).thenReturn(NodeStatus.inServiceStale());
+    when(mockNodeManager.shouldFenceDatanode(any(), any())).thenReturn(true);
+
+    SCMDatanodeHeartbeatDispatcher dispatcher =
+        new SCMDatanodeHeartbeatDispatcher(mockNodeManager, 
mock(EventPublisher.class));
+
+    DatanodeVersionProto version = DatanodeVersionProto.newBuilder()
+        .setApparentVersion(1).setSoftwareVersion(1).build();
+    SCMHeartbeatRequestProto heartbeat = SCMHeartbeatRequestProto.newBuilder()
+        .setDatanodeDetails(randomDatanodeDetails().getProtoBufMessage())
+        .setDatanodeVersion(version)
+        .build();
+
+    dispatcher.dispatch(heartbeat);
+
+    verify(mockNodeManager, 
times(1)).addDatanodeCommand(any(DatanodeID.class), 
any(ReregisterCommand.class));
+    verify(mockNodeManager, never()).processHeartbeat(any(), any());
+  }
+
+  @Test
+  public void testDeadNodeWithIncompatibleVersionGetsReregisterCommand() 
throws Exception {
+    NodeManager mockNodeManager = mock(NodeManager.class);
+    when(mockNodeManager.isNodeRegistered(any())).thenReturn(true);
+    
when(mockNodeManager.getNodeStatus(any())).thenReturn(NodeStatus.inServiceDead());
+    when(mockNodeManager.shouldFenceDatanode(any(), any())).thenReturn(true);
+
+    SCMDatanodeHeartbeatDispatcher dispatcher =
+        new SCMDatanodeHeartbeatDispatcher(mockNodeManager, 
mock(EventPublisher.class));
+
+    DatanodeVersionProto version = DatanodeVersionProto.newBuilder()
+        .setApparentVersion(1).setSoftwareVersion(1).build();
+    SCMHeartbeatRequestProto heartbeat = SCMHeartbeatRequestProto.newBuilder()
+        .setDatanodeDetails(randomDatanodeDetails().getProtoBufMessage())
+        .setDatanodeVersion(version)
+        .build();
+
+    dispatcher.dispatch(heartbeat);
+
+    verify(mockNodeManager, 
times(1)).addDatanodeCommand(any(DatanodeID.class), 
any(ReregisterCommand.class));
+    verify(mockNodeManager, never()).processHeartbeat(any(), any());
+  }
+
+  @Test
+  public void testUnhealthyNodeWithoutVersionReportIsNotFenced() throws 
Exception {
+    // A stale or dead node that omits datanodeVersion (e.g. an older binary) 
must not be

Review Comment:
   I don't think this test is valid. We don't have to handle older binaries 
because ZDU support doesn't start until we release this. Actually the stale or 
dead nodes should not be allowed back if they do not report a version. The 
comment I left on `SCMDatanodeHeartbeatDispatcher` about delegating this to 
`shouldFenceDatanode` should fix this.



##########
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMDatanodeHeartbeatDispatcher.java:
##########


Review Comment:
   We should probably add a test that healthy nodes are let through the 
dispatcher gate. Their software version is only expected to change if the node 
restarts, which would cause it to re-register on its own.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to