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

sodonnell pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new a7c71f2eba HDDS-9481. A reformatted datanode node cannot be 
decommissioned (#5458)
a7c71f2eba is described below

commit a7c71f2eba2426a3bc4eb587d6a33bb1a074bca3
Author: Stephen O'Donnell <[email protected]>
AuthorDate: Wed Oct 18 15:04:11 2023 +0100

    HDDS-9481. A reformatted datanode node cannot be decommissioned (#5458)
---
 .../hdds/scm/node/NodeDecommissionManager.java     |  87 +++++++++++++--
 .../apache/hadoop/hdds/scm/node/NodeManager.java   |   8 ++
 .../hadoop/hdds/scm/node/SCMNodeManager.java       |  16 +++
 .../hadoop/hdds/scm/container/MockNodeManager.java |   5 +
 .../hdds/scm/container/SimpleMockNodeManager.java  |   5 +
 .../hdds/scm/node/TestNodeDecommissionManager.java | 122 ++++++++++++++++++++-
 .../testutils/ReplicationNodeManagerMock.java      |   5 +
 7 files changed, 233 insertions(+), 15 deletions(-)

diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeDecommissionManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeDecommissionManager.java
index a84b07d513..cedca7d3bf 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeDecommissionManager.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeDecommissionManager.java
@@ -18,6 +18,7 @@ package org.apache.hadoop.hdds.scm.node;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.apache.commons.lang3.tuple.Pair;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeOperationalState;
@@ -37,11 +38,13 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * Class used to manage datanodes scheduled for maintenance or decommission.
@@ -125,11 +128,10 @@ public class NodeDecommissionManager {
         dnsName = addr.getHostAddress();
       }
       List<DatanodeDetails> found = nodeManager.getNodesByAddress(dnsName);
-      if (found.size() == 0) {
+      if (found.isEmpty()) {
         throw new InvalidHostStringException("Host " + host.getRawHostname()
             + " (" + dnsName + ") is not running any datanodes registered"
-            + " with SCM."
-            + " Please check the host name.");
+            + " with SCM. Please check the host name.");
       } else if (found.size() == 1) {
         if (host.getPort() != -1 &&
             !validateDNPortMatch(host.getPort(), found.get(0))) {
@@ -139,26 +141,87 @@ public class NodeDecommissionManager {
               + " Please check the port number.");
         }
         results.add(found.get(0));
-      } else if (found.size() > 1) {
-        DatanodeDetails match = null;
-        for (DatanodeDetails dn : found) {
-          if (validateDNPortMatch(host.getPort(), dn)) {
-            match = dn;
-            break;
-          }
+      } else {
+        // Here we either have multiple DNs on the same host / IP, and they
+        // should have different ports. Or, we have a case where a DN was
+        // registered from a host, then stopped and formatted, changing its
+        // UUID and registered again. In that case, the ports of all hosts
+        // should be the same, and we should just use the one with the most
+        // recent heartbeat.
+        if (host.getPort() != -1) {
+          found.removeIf(dn -> !validateDNPortMatch(host.getPort(), dn));
         }
-        if (match == null) {
+        if (found.isEmpty()) {
+          throw new InvalidHostStringException("Host " + host.getRawHostname()
+              + " is running multiple datanodes registered with SCM,"
+              + " but no port numbers match."
+              + " Please check the port number.");
+        } else if (found.size() == 1) {
+          results.add(found.get(0));
+          continue;
+        }
+        // Here we have at least 2 DNs matching the passed in port, or no port
+        // was passed so we may have all the same ports in SCM or a mix of
+        // ports.
+        if (allPortsMatch(found)) {
+          // All ports match, so just use the most recent heartbeat as it is
+          // not possible for a host to have 2 DNs coming from the same port.
+          DatanodeDetails mostRecent = findDnWithMostRecentHeartbeat(found);
+          if (mostRecent == null) {
+            throw new InvalidHostStringException("Host " + 
host.getRawHostname()
+                + " has multiple datanodes registered with SCM."
+                + " All have identical ports, but none have a newest"
+                + " heartbeat.");
+          }
+          results.add(mostRecent);
+        } else {
+          // We have no passed in port or the ports in SCM do not all match, so
+          // we cannot decide which DN to use.
           throw new InvalidHostStringException("Host " + host.getRawHostname()
               + " is running multiple datanodes registered with SCM,"
               + " but no port numbers match."
               + " Please check the port number.");
         }
-        results.add(match);
       }
     }
     return results;
   }
 
+  private boolean allPortsMatch(List<DatanodeDetails> dns) {
+    if (dns.size() < 2) {
+      return true;
+    }
+    int port = dns.get(0).getPort(DatanodeDetails.Port.Name.RATIS).getValue();
+    for (int i = 1; i < dns.size(); i++) {
+      if (dns.get(i).getPort(DatanodeDetails.Port.Name.RATIS).getValue()
+          != port) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private DatanodeDetails findDnWithMostRecentHeartbeat(
+      List<DatanodeDetails> dns) {
+    if (dns.size() < 2) {
+      return dns.isEmpty() ? null : dns.get(0);
+    }
+    List<Pair<DatanodeDetails, Long>> dnsWithHeartbeat = dns.stream()
+        .map(dn -> Pair.of(dn, nodeManager.getLastHeartbeat(dn)))
+        .sorted(Comparator.comparingLong(Pair::getRight))
+        .collect(Collectors.toList());
+    // The last element should have the largest (newest) heartbeat. But also
+    // check it is not identical to the last but 1 element, as then we cannot
+    // determine which node to decommission.
+    Pair<DatanodeDetails, Long> last = dnsWithHeartbeat.get(
+        dnsWithHeartbeat.size() - 1);
+    if (last.getRight() > dnsWithHeartbeat.get(
+        dnsWithHeartbeat.size() - 2).getRight()) {
+      return last.getLeft();
+    }
+    return null;
+  }
+
   /**
    * Check if the passed port is used by the given DatanodeDetails object. If
    * it is, return true, otherwise return false.
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeManager.java
index b35a19b402..011b361d62 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeManager.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/NodeManager.java
@@ -380,6 +380,14 @@ public interface NodeManager extends 
StorageContainerNodeProtocol,
    */
   List<DatanodeDetails> getNodesByAddress(String address);
 
+  /**
+   * For the given node, retried the last heartbeat time.
+   * @param datanodeDetails DatanodeDetails of the node.
+   * @return The last heartbeat time in milliseconds or -1 if the node does not
+   *         existing in the nodeManager.
+   */
+  long getLastHeartbeat(DatanodeDetails datanodeDetails);
+
   /**
    * Get cluster map as in network topology for this node manager.
    * @return cluster map
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeManager.java
index 3466fa4234..0d323b996c 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeManager.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeManager.java
@@ -1393,6 +1393,22 @@ public class SCMNodeManager implements NodeManager {
     return clusterMap;
   }
 
+  /**
+   * For the given node, retried the last heartbeat time.
+   * @param datanodeDetails DatanodeDetails of the node.
+   * @return The last heartbeat time in milliseconds or -1 if the node does not
+   *         exist.
+   */
+  @Override
+  public long getLastHeartbeat(DatanodeDetails datanodeDetails) {
+    try {
+      DatanodeInfo node = nodeStateManager.getNode(datanodeDetails);
+      return node.getLastHeartbeatTime();
+    } catch (NodeNotFoundException e) {
+      return -1;
+    }
+  }
+
   private String nodeResolve(String hostname) {
     List<String> hosts = new ArrayList<>(1);
     hosts.add(hostname);
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/MockNodeManager.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/MockNodeManager.java
index 6ce5caa21e..98638ebe00 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/MockNodeManager.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/MockNodeManager.java
@@ -922,6 +922,11 @@ public class MockNodeManager implements NodeManager {
     return numPipelinePerDatanode;
   }
 
+  @Override
+  public long getLastHeartbeat(DatanodeDetails datanodeDetails) {
+    return -1;
+  }
+
   public void setNumPipelinePerDatanode(int value) {
     numPipelinePerDatanode = value;
   }
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/SimpleMockNodeManager.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/SimpleMockNodeManager.java
index 8c3d027d1d..2bd13d4489 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/SimpleMockNodeManager.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/SimpleMockNodeManager.java
@@ -381,6 +381,11 @@ public class SimpleMockNodeManager implements NodeManager {
     return 0;
   }
 
+  @Override
+  public long getLastHeartbeat(DatanodeDetails datanodeDetails) {
+    return -1;
+  }
+
   @Override
   public void close() throws IOException {
 
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestNodeDecommissionManager.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestNodeDecommissionManager.java
index f4e02a495a..c0e3b804bf 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestNodeDecommissionManager.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestNodeDecommissionManager.java
@@ -39,6 +39,8 @@ import java.util.List;
 import java.util.UUID;
 import java.util.Arrays;
 import java.util.ArrayList;
+
+import static 
org.apache.hadoop.ozone.container.upgrade.UpgradeUtils.defaultLayoutVersionProto;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.assertj.core.api.Fail.fail;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -163,9 +165,18 @@ public class TestNodeDecommissionManager {
     assertEquals(HddsProtos.NodeOperationalState.DECOMMISSIONING,
         nodeManager.getNodeStatus(multiDn).getOperationalState());
 
+    // Attempt to decommission on dn(9) which has another instance at
+    // dn(11) with identical ports.
+    nodeManager.processHeartbeat(dns.get(9), defaultLayoutVersionProto());
+    DatanodeDetails duplicatePorts = dns.get(9);
+    decom.decommissionNodes(Arrays.asList(duplicatePorts.getIpAddress()));
+    assertEquals(HddsProtos.NodeOperationalState.DECOMMISSIONING,
+        nodeManager.getNodeStatus(duplicatePorts).getOperationalState());
+
     // Recommission all 3 hosts
     decom.recommissionNodes(Arrays.asList(
-        multiAddr, dns.get(1).getIpAddress(), dns.get(2).getIpAddress()));
+        multiAddr, dns.get(1).getIpAddress(), dns.get(2).getIpAddress(),
+        duplicatePorts.getIpAddress()));
     decom.getMonitor().run();
     assertEquals(HddsProtos.NodeOperationalState.IN_SERVICE,
         nodeManager.getNodeStatus(dns.get(1)).getOperationalState());
@@ -173,6 +184,76 @@ public class TestNodeDecommissionManager {
         nodeManager.getNodeStatus(dns.get(2)).getOperationalState());
     assertEquals(HddsProtos.NodeOperationalState.IN_SERVICE,
         nodeManager.getNodeStatus(dns.get(10)).getOperationalState());
+    assertEquals(HddsProtos.NodeOperationalState.IN_SERVICE,
+        nodeManager.getNodeStatus(duplicatePorts).getOperationalState());
+  }
+
+  @Test
+  public void testNodesCanBeDecommissionedAndRecommissionedMixedPorts()
+      throws InvalidHostStringException, NodeNotFoundException {
+    List<DatanodeDetails> dns = generateDatanodes();
+
+    // From the generateDatanodes method we have DNs at index 9 and 11 with the
+    // same IP and port. We can add another DN with a different port on the
+    // same IP so we have 3 registered from the same host and 2 distinct ports.
+    DatanodeDetails sourceDN = dns.get(9);
+    int ratisPort = sourceDN
+        .getPort(DatanodeDetails.Port.Name.RATIS).getValue();
+    DatanodeDetails.Builder builder = DatanodeDetails.newBuilder();
+    builder.setUuid(UUID.randomUUID())
+        .setHostName(sourceDN.getHostName())
+        .setIpAddress(sourceDN.getIpAddress())
+        .addPort(DatanodeDetails.newPort(
+            DatanodeDetails.Port.Name.STANDALONE,
+            sourceDN.getPort(DatanodeDetails.Port.Name.STANDALONE)
+                .getValue() + 1))
+        .addPort(DatanodeDetails.newPort(
+            DatanodeDetails.Port.Name.RATIS,
+            ratisPort + 1))
+        .addPort(DatanodeDetails.newPort(
+            DatanodeDetails.Port.Name.REST,
+            sourceDN.getPort(DatanodeDetails.Port.Name.REST).getValue() + 1))
+        .setNetworkLocation(sourceDN.getNetworkLocation());
+    DatanodeDetails extraDN = builder.build();
+    dns.add(extraDN);
+    nodeManager.register(extraDN, null, null);
+
+    // Attempt to decommission with just the IP, which should fail.
+    try {
+      decom.decommissionNodes(Arrays.asList(extraDN.getIpAddress()));
+      fail("InvalidHostStringException expected");
+    } catch (InvalidHostStringException e) {
+    }
+    // Now try the one with the unique port
+    decom.decommissionNodes(Arrays.asList(
+        extraDN.getIpAddress() + ":" + ratisPort + 1));
+
+    assertEquals(HddsProtos.NodeOperationalState.DECOMMISSIONING,
+        nodeManager.getNodeStatus(extraDN).getOperationalState());
+
+    decom.recommissionNodes(Arrays.asList(
+        extraDN.getIpAddress() + ":" + ratisPort + 1));
+    decom.getMonitor().run();
+    assertEquals(HddsProtos.NodeOperationalState.IN_SERVICE,
+        nodeManager.getNodeStatus(extraDN).getOperationalState());
+
+    // Now decommission one of the DNs with the duplicate port
+    DatanodeDetails expectedDN = dns.get(9);
+    nodeManager.processHeartbeat(expectedDN, defaultLayoutVersionProto());
+
+    decom.decommissionNodes(Arrays.asList(
+        expectedDN.getIpAddress() + ":" + ratisPort));
+    assertEquals(HddsProtos.NodeOperationalState.DECOMMISSIONING,
+        nodeManager.getNodeStatus(expectedDN).getOperationalState());
+    // The other duplicate is still in service
+    assertEquals(HddsProtos.NodeOperationalState.IN_SERVICE,
+        nodeManager.getNodeStatus(dns.get(11)).getOperationalState());
+
+    decom.recommissionNodes(Arrays.asList(
+        expectedDN.getIpAddress() + ":" + ratisPort));
+    decom.getMonitor().run();
+    assertEquals(HddsProtos.NodeOperationalState.IN_SERVICE,
+        nodeManager.getNodeStatus(expectedDN).getOperationalState());
   }
 
   @Test
@@ -206,9 +287,19 @@ public class TestNodeDecommissionManager {
     assertEquals(HddsProtos.NodeOperationalState.ENTERING_MAINTENANCE,
         nodeManager.getNodeStatus(multiDn).getOperationalState());
 
+    // Attempt to enable maintenance on dn(9) which has another instance at
+    // dn(11) with identical ports.
+    nodeManager.processHeartbeat(dns.get(9), defaultLayoutVersionProto());
+    DatanodeDetails duplicatePorts = dns.get(9);
+    decom.startMaintenanceNodes(Arrays.asList(duplicatePorts.getIpAddress()),
+        100);
+    assertEquals(HddsProtos.NodeOperationalState.ENTERING_MAINTENANCE,
+        nodeManager.getNodeStatus(duplicatePorts).getOperationalState());
+
     // Recommission all 3 hosts
     decom.recommissionNodes(Arrays.asList(
-        multiAddr, dns.get(1).getIpAddress(), dns.get(2).getIpAddress()));
+        multiAddr, dns.get(1).getIpAddress(), dns.get(2).getIpAddress(),
+        duplicatePorts.getIpAddress()));
     decom.getMonitor().run();
     assertEquals(HddsProtos.NodeOperationalState.IN_SERVICE,
         nodeManager.getNodeStatus(dns.get(1)).getOperationalState());
@@ -216,6 +307,8 @@ public class TestNodeDecommissionManager {
         nodeManager.getNodeStatus(dns.get(2)).getOperationalState());
     assertEquals(HddsProtos.NodeOperationalState.IN_SERVICE,
         nodeManager.getNodeStatus(dns.get(10)).getOperationalState());
+    assertEquals(HddsProtos.NodeOperationalState.IN_SERVICE,
+        nodeManager.getNodeStatus(duplicatePorts).getOperationalState());
   }
 
   @Test
@@ -301,7 +394,7 @@ public class TestNodeDecommissionManager {
       nodeManager.register(dn, null, null);
     }
     // We have 10 random DNs, we want to create another one that is on the same
-    // host as some of the others.
+    // host as some of the others, but with a different port
     DatanodeDetails multiDn = dns.get(0);
 
     DatanodeDetails.Builder builder = DatanodeDetails.newBuilder();
@@ -317,8 +410,31 @@ public class TestNodeDecommissionManager {
         .setNetworkLocation(multiDn.getNetworkLocation());
 
     DatanodeDetails dn = builder.build();
+    dns.add(dn);
     nodeManager.register(dn, null, null);
+
+    // Now add another DN with the same host and IP as dns(9), and with the
+    // same port.
+    DatanodeDetails duplicatePorts = dns.get(9);
+    builder = DatanodeDetails.newBuilder();
+    builder.setUuid(UUID.randomUUID())
+        .setHostName(duplicatePorts.getHostName())
+        .setIpAddress(duplicatePorts.getIpAddress())
+        .addPort(DatanodeDetails.newPort(
+            DatanodeDetails.Port.Name.STANDALONE,
+            duplicatePorts.getPort(DatanodeDetails.Port.Name.STANDALONE)
+                .getValue()))
+        .addPort(DatanodeDetails.newPort(
+            DatanodeDetails.Port.Name.RATIS,
+            
duplicatePorts.getPort(DatanodeDetails.Port.Name.RATIS).getValue()))
+        .addPort(DatanodeDetails.newPort(
+            DatanodeDetails.Port.Name.REST,
+            duplicatePorts.getPort(DatanodeDetails.Port.Name.REST).getValue()))
+        .setNetworkLocation(multiDn.getNetworkLocation());
+    dn = builder.build();
     dns.add(dn);
+    nodeManager.register(dn, null, null);
+
     return dns;
   }
 
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/ozone/container/testutils/ReplicationNodeManagerMock.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/ozone/container/testutils/ReplicationNodeManagerMock.java
index 21defdb08b..0a86504335 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/ozone/container/testutils/ReplicationNodeManagerMock.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/ozone/container/testutils/ReplicationNodeManagerMock.java
@@ -534,4 +534,9 @@ public class ReplicationNodeManagerMock implements 
NodeManager {
   public int minPipelineLimit(List<DatanodeDetails> dn) {
     return 0;
   }
+
+  @Override
+  public long getLastHeartbeat(DatanodeDetails datanodeDetails) {
+    return -1;
+  }
 }


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

Reply via email to