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 46b6f3def1 HDDS-10113. UNHEALTHY replicas of QUASI_CLOSED container
with unique origins should be handled during decommission (#5984)
46b6f3def1 is described below
commit 46b6f3def1d84ca769affb4d3f0d84dece6e8567
Author: Siddhant Sangwan <[email protected]>
AuthorDate: Thu Jan 11 22:11:49 2024 +0530
HDDS-10113. UNHEALTHY replicas of QUASI_CLOSED container with unique
origins should be handled during decommission (#5984)
---
.../replication/RatisContainerReplicaCount.java | 22 ++++----
.../health/VulnerableUnhealthyReplicasHandler.java | 9 ++-
.../TestRatisUnderReplicationHandler.java | 31 ++++++++++
.../replication/TestReplicationManager.java | 62 ++++++++++++++++++++
.../TestVulnerableUnhealthyReplicasHandler.java | 49 ++++++++++++++++
.../hdds/scm/node/TestDatanodeAdminMonitor.java | 66 ++++++++++++++++++++++
6 files changed, 223 insertions(+), 16 deletions(-)
diff --git
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/RatisContainerReplicaCount.java
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/RatisContainerReplicaCount.java
index d23934184e..fe771fac6a 100644
---
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/RatisContainerReplicaCount.java
+++
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/RatisContainerReplicaCount.java
@@ -470,10 +470,10 @@ public class RatisContainerReplicaCount implements
ContainerReplicaCount {
/**
* QUASI_CLOSED containers that have a mix of healthy and UNHEALTHY
- * replicas require special treatment. If the healthy replicas don't have
- * the same BCSID as the container, but the UNHEALTHY ones do, then we need
- * to save at least one copy of each such UNHEALTHY replica. This method
- * finds such UNHEALTHY replicas.
+ * replicas require special treatment. If the UNHEALTHY replicas have the
+ * correct sequence ID and have unique origins, then we need to save at least
+ * one copy of each such UNHEALTHY replicas. This method finds such UNHEALTHY
+ * replicas.
*
* @param nodeStatusFn a function used to check the {@link NodeStatus} of a
node,
* accepting a {@link DatanodeDetails} and returning {@link NodeStatus}
@@ -498,11 +498,6 @@ public class RatisContainerReplicaCount implements
ContainerReplicaCount {
if (replica.getSequenceId() == container.getSequenceId()) {
if (replica.getState() == ContainerReplicaProto.State.UNHEALTHY &&
!replica.isEmpty()) {
unhealthyReplicas.add(replica);
- } else if (replica.getState() ==
- ContainerReplicaProto.State.QUASI_CLOSED) {
- // don't need to save UNHEALTHY replicas if there's a QUASI_CLOSED
- // replica with the greatest Sequence ID.
- return Collections.emptyList();
}
}
}
@@ -517,6 +512,11 @@ public class RatisContainerReplicaCount implements
ContainerReplicaCount {
NodeStatus status = nodeStatusFn.apply(replica.getDatanodeDetails());
return status == null || !status.isHealthy();
});
+ replicas.removeIf(
+ replica -> {
+ NodeStatus status = nodeStatusFn.apply(replica.getDatanodeDetails());
+ return status == null || !status.isHealthy();
+ });
/*
At this point, the list of unhealthyReplicas contains all UNHEALTHY
non-empty
replicas with the greatest Sequence ID that are on healthy Datanodes.
@@ -531,9 +531,9 @@ public class RatisContainerReplicaCount implements
ContainerReplicaCount {
*/
// TODO should we also consider pending deletes?
Set<UUID> originsOfInServiceReplicas = new HashSet<>();
- for (ContainerReplica replica : unhealthyReplicas) {
+ for (ContainerReplica replica : replicas) {
if (replica.getDatanodeDetails().getPersistedOpState()
- .equals(IN_SERVICE)) {
+ .equals(IN_SERVICE) &&
replica.getSequenceId().equals(container.getSequenceId())) {
originsOfInServiceReplicas.add(replica.getOriginDatanodeId());
}
}
diff --git
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/health/VulnerableUnhealthyReplicasHandler.java
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/health/VulnerableUnhealthyReplicasHandler.java
index 21b2d8151d..d8a4edf31e 100644
---
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/health/VulnerableUnhealthyReplicasHandler.java
+++
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/health/VulnerableUnhealthyReplicasHandler.java
@@ -37,9 +37,8 @@ import static
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationType.R
/**
* A QUASI_CLOSED container may have some UNHEALTHY replicas with the
- * same Sequence ID as the container. RM should try to maintain one
- * copy of such replicas when there are no healthy replicas that
- * match the container's Sequence ID.
+ * same Sequence ID as the container and on unique origins. RM should try to
maintain one
+ * copy of such replicas.
*/
public class VulnerableUnhealthyReplicasHandler extends AbstractCheck {
public static final Logger LOG =
LoggerFactory.getLogger(VulnerableUnhealthyReplicasHandler.class);
@@ -52,8 +51,8 @@ public class VulnerableUnhealthyReplicasHandler extends
AbstractCheck {
/**
* Checks if the container is QUASI_CLOSED has some vulnerable UNHEALTHY
replicas that need to replicated to
* other Datanodes. These replicas have the same sequence ID as the
container while other healthy replicas don't.
- * If the node hosting such a replica is being taken offline, then the
replica may have to be replicated to another
- * node.
+ * Or, these replicas have unique origin Datanodes. If the node hosting such
a replica is being taken offline, then
+ * the replica may have to be replicated to another node.
* @param request ContainerCheckRequest object representing the container
* @return true if some vulnerable UNHEALTHY replicas were found, else false
*/
diff --git
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestRatisUnderReplicationHandler.java
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestRatisUnderReplicationHandler.java
index 02de63ee51..ca86cb689f 100644
---
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestRatisUnderReplicationHandler.java
+++
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestRatisUnderReplicationHandler.java
@@ -39,9 +39,11 @@ import
org.apache.hadoop.hdds.scm.pipeline.InsufficientDatanodesException;
import org.apache.hadoop.ozone.container.common.SCMTestUtils;
import org.apache.hadoop.ozone.protocol.commands.SCMCommand;
import org.apache.ratis.protocol.exceptions.NotLeaderException;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
import java.io.IOException;
import java.util.ArrayList;
@@ -581,6 +583,35 @@ public class TestRatisUnderReplicationHandler {
assertEquals(unhealthyReplica.getDatanodeDetails(),
commands.iterator().next().getKey());
}
+ /**
+ * A QUASI_CLOSED container may have UNHEALTHY replicas with the correct
sequence ID which have unique
+ * origin Datanodes. If any of these UNHEALTHY replicas is being taken
offline, then it needs to be replicated to
+ * another DN for decommission to progress. This test asserts that a
replicate command is sent for one such replica.
+ */
+ @Test
+ public void testUnderReplicationWithVulnerableReplicasOnUniqueOrigins()
throws IOException {
+ final long sequenceID = 20;
+ container =
ReplicationTestUtil.createContainerInfo(RATIS_REPLICATION_CONFIG, 1,
+ HddsProtos.LifeCycleState.QUASI_CLOSED, sequenceID);
+
+ final Set<ContainerReplica> replicas = new HashSet<>(4);
+ for (int i = 0; i < 3; i++) {
+ replicas.add(createContainerReplica(container.containerID(), 0,
IN_SERVICE, State.QUASI_CLOSED,
+ sequenceID));
+ }
+
+ // create an UNHEALTHY replica with a unique origin
+ final ContainerReplica unhealthyReplica =
createContainerReplica(container.containerID(), 0,
+ DECOMMISSIONING, State.UNHEALTHY, sequenceID);
+ replicas.add(unhealthyReplica);
+ UnderReplicatedHealthResult result = getUnderReplicatedHealthResult();
+ Mockito.when(result.hasVulnerableUnhealthy()).thenReturn(true);
+
+ final Set<Pair<DatanodeDetails, SCMCommand<?>>> commands =
testProcessing(replicas, Collections.emptyList(),
+ result, 2, 1);
+ Assertions.assertEquals(unhealthyReplica.getDatanodeDetails(),
commands.iterator().next().getKey());
+ }
+
/**
* In the push replication model, a replicate command is sent to the DN
hosting the replica, and that DN is
* expected to "push" the replica to another DN. If the DN hosting the
replica has too many commands already, an
diff --git
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestReplicationManager.java
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestReplicationManager.java
index b3cb962820..fe1cdcc069 100644
---
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestReplicationManager.java
+++
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestReplicationManager.java
@@ -56,6 +56,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
import java.io.IOException;
import java.time.Instant;
@@ -508,6 +509,67 @@ public class TestReplicationManager {
assertEquals(decommissioning.getDatanodeDetails().getUuid(),
command.getKey());
}
+
+ /**
+ * There is a QUASI_CLOSED container with some UNHEALTHY replicas on unique
origin nodes. If the datanode hosting
+ * one such replica is being taken offline, then the UNHEALTHY replica needs
to be replicated to another node.
+ */
+ @Test
+ public void
testQuasiClosedContainerWithUnhealthyReplicaOnDecommissioningNodeWithUniqueOrigin()
+ throws IOException, NodeNotFoundException {
+ RatisReplicationConfig ratisRepConfig =
+ RatisReplicationConfig.getInstance(THREE);
+ // create a QUASI_CLOSED container with 3 QUASI_CLOSED replicas on same
origin, and 1 UNHEALTHY on unique origin
+ ContainerInfo container = createContainerInfo(ratisRepConfig, 1,
+ HddsProtos.LifeCycleState.QUASI_CLOSED);
+ Set<ContainerReplica> replicas =
+ createReplicasWithSameOrigin(container.containerID(),
+ ContainerReplicaProto.State.QUASI_CLOSED, 0, 0, 0);
+ ContainerReplica unhealthy =
+ createContainerReplica(container.containerID(), 0, DECOMMISSIONING,
+ ContainerReplicaProto.State.UNHEALTHY);
+ replicas.add(unhealthy);
+ storeContainerAndReplicas(container, replicas);
+ Mockito.when(replicationManager.getNodeStatus(any(DatanodeDetails.class)))
+ .thenAnswer(invocation -> {
+ DatanodeDetails dn = invocation.getArgument(0);
+ if (dn.equals(unhealthy.getDatanodeDetails())) {
+ return new NodeStatus(DECOMMISSIONING,
HddsProtos.NodeState.HEALTHY);
+ }
+
+ return NodeStatus.inServiceHealthy();
+ });
+
+ // the container should be under replicated and queued to under
replication queue
+ replicationManager.processContainer(container, repQueue, repReport);
+ assertEquals(1, repReport.getStat(
+ ReplicationManagerReport.HealthState.UNDER_REPLICATED));
+ assertEquals(0, repReport.getStat(
+ ReplicationManagerReport.HealthState.OVER_REPLICATED));
+ assertEquals(1, repQueue.underReplicatedQueueSize());
+ assertEquals(0, repQueue.overReplicatedQueueSize());
+
+ // next, this test sets up some mocks to test if
RatisUnderReplicationHandler will handle this container correctly
+ Mockito.when(ratisPlacementPolicy.chooseDatanodes(anyList(), anyList(),
eq(null), eq(1), anyLong(),
+ anyLong())).thenAnswer(invocation ->
ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails()));
+
Mockito.when(nodeManager.getTotalDatanodeCommandCounts(any(DatanodeDetails.class),
any(), any()))
+ .thenAnswer(invocation -> {
+ Map<SCMCommandProto.Type, Integer> map = new HashMap<>();
+ map.put(SCMCommandProto.Type.replicateContainerCommand, 0);
+ map.put(SCMCommandProto.Type.reconstructECContainersCommand, 0);
+ return map;
+ });
+ RatisUnderReplicationHandler handler =
+ new RatisUnderReplicationHandler(ratisPlacementPolicy, configuration,
replicationManager);
+
+ handler.processAndSendCommands(replicas, Collections.emptyList(),
repQueue.dequeueUnderReplicatedContainer(), 2);
+ assertEquals(1, commandsSent.size());
+ Pair<UUID, SCMCommand<?>> command = commandsSent.iterator().next();
+ // a replicate command should have been sent for the UNHEALTHY replica
+ assertEquals(SCMCommandProto.Type.replicateContainerCommand,
command.getValue().getType());
+ assertEquals(unhealthy.getDatanodeDetails().getUuid(), command.getKey());
+ }
+
/**
* When there is Quasi Closed Replica with incorrect sequence id
* for a Closed container, it's treated as unhealthy and deleted.
diff --git
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/health/TestVulnerableUnhealthyReplicasHandler.java
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/health/TestVulnerableUnhealthyReplicasHandler.java
index 1c0a29d3eb..8fa4c974e1 100644
---
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/health/TestVulnerableUnhealthyReplicasHandler.java
+++
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/health/TestVulnerableUnhealthyReplicasHandler.java
@@ -35,6 +35,7 @@ import org.apache.hadoop.hdds.scm.node.NodeStatus;
import org.apache.hadoop.hdds.scm.node.states.NodeNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
import java.util.Collections;
import java.util.HashSet;
@@ -128,6 +129,11 @@ public class TestVulnerableUnhealthyReplicasHandler {
assertEquals(0, repQueue.overReplicatedQueueSize());
}
+ /**
+ * A QUASI_CLOSED container with 3 QUASI_CLOSED replicas with incorrect
sequence id. They're on unique origin nodes.
+ * There's an UNHEALTHY replica on a Decommissioning node, which has the
correct sequence ID and unique origin.
+ * It's expected that the UNHEALTHY replica is queued for under replication.
+ */
@Test
public void testReturnsTrueForQuasiClosedContainerWithVulnerableReplica()
throws NodeNotFoundException {
long sequenceId = 10;
@@ -156,6 +162,49 @@ public class TestVulnerableUnhealthyReplicasHandler {
assertEquals(0, repQueue.overReplicatedQueueSize());
}
+ /**
+ * A QUASI_CLOSED container with 3 QUASI_CLOSED replicas with correct
sequence id. They're on unique origin nodes.
+ * There's an UNHEALTHY replica on a Decommissioning node, which also has
the correct sequence ID and unique origin.
+ * It's expected that the UNHEALTHY replica is queued for under replication.
This is a variation of the situation
+ * where the healthy replicas have incorrect sequence id, and the unhealthy
ones have the correct sequence id.
+ * Here, all the replicas have the correct sequence id but the unhealthy
still need to be saved because they're on
+ * unique origin nodes.
+ * <p>
+ * Why do we need to save the UNHEALTHY replicas if we have enough unique
QUASI_CLOSED replicas to form a quorum?
+ * Simply because we're ensuring redundancy of replicas having unique origin
node IDs. When HDDS has the ability to
+ * restore UNHEALTHY replicas to a healthy state, they can also be used to
create a quorum. In any case, when the
+ * container transitions to CLOSED, any UNHEALTHY replicas will be deleted.
+ * </p>
+ */
+ @Test
+ public void
testReturnsTrueForQuasiClosedContainerWithVulnerableReplicaWhenAllReplicasHaveCorrectSequence()
+ throws NodeNotFoundException {
+ long sequenceId = 10;
+ ContainerInfo container = createContainerInfo(repConfig, 1,
LifeCycleState.QUASI_CLOSED, sequenceId);
+ Set<ContainerReplica> replicas = new HashSet<>(4);
+ for (int i = 0; i < 3; i++) {
+ replicas.add(createContainerReplica(container.containerID(), 0,
IN_SERVICE, State.QUASI_CLOSED,
+ container.getSequenceId()));
+ }
+ // create UNHEALTHY replica with unique origin id on a DECOMMISSIONING node
+ ContainerReplica unhealthy =
+ createContainerReplica(container.containerID(), 0, DECOMMISSIONING,
State.UNHEALTHY, sequenceId);
+ replicas.add(unhealthy);
+
Mockito.when(replicationManager.getNodeStatus(Mockito.any(DatanodeDetails.class)))
+ .thenAnswer(invocation -> {
+ DatanodeDetails dn = invocation.getArgument(0);
+ if (dn.equals(unhealthy.getDatanodeDetails())) {
+ return new NodeStatus(DECOMMISSIONING, HEALTHY);
+ }
+ return NodeStatus.inServiceHealthy();
+ });
+ requestBuilder.setContainerReplicas(replicas).setContainerInfo(container);
+
+ assertTrue(handler.handle(requestBuilder.build()));
+ assertEquals(1, repQueue.underReplicatedQueueSize());
+ assertEquals(0, repQueue.overReplicatedQueueSize());
+ }
+
@Test
public void testReturnsFalseForVulnerableReplicaWithAnotherCopy() throws
NodeNotFoundException {
long sequenceId = 10;
diff --git
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestDatanodeAdminMonitor.java
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestDatanodeAdminMonitor.java
index d4e2ce4b7f..523d4226cb 100644
---
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestDatanodeAdminMonitor.java
+++
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/node/TestDatanodeAdminMonitor.java
@@ -41,6 +41,8 @@ import org.apache.hadoop.hdds.server.events.EventQueue;
import
org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
@@ -374,6 +376,70 @@ public class TestDatanodeAdminMonitor {
nodeManager.getNodeStatus(dn1).getOperationalState());
}
+ /**
+ * Situation: A QUASI_CLOSED container has an UNHEALTHY replica with a
unique origin and three QUASI_CLOSED replicas.
+ * All the replicas have the correct Sequence ID. UNHEALTHY container is on
a decommissioning node, and there are
+ * no other copies of this replica, that is, replicas with the same Origin
ID as this replica.
+ *
+ * Expectation: Decommissioning should not complete until the UNHEALTHY
replica has been replicated to another node.
+ */
+ @Test
+ public void
testDecommissionWaitsForUnhealthyReplicaWithUniqueOriginToReplicateNewRM()
+ throws NodeNotFoundException, ContainerNotFoundException {
+ DatanodeDetails dn1 = MockDatanodeDetails.randomDatanodeDetails();
+ nodeManager.register(dn1,
+ new NodeStatus(HddsProtos.NodeOperationalState.DECOMMISSIONING,
HddsProtos.NodeState.HEALTHY));
+
+ // create a container and 3 QUASI_CLOSED replicas with containerID 1 and
same origin ID
+ ContainerID containerID = ContainerID.valueOf(1);
+ ContainerInfo container =
ReplicationTestUtil.createContainerInfo(RatisReplicationConfig.getInstance(
+ HddsProtos.ReplicationFactor.THREE), containerID.getId(),
HddsProtos.LifeCycleState.QUASI_CLOSED);
+ Set<ContainerReplica> replicas =
+ ReplicationTestUtil.createReplicasWithSameOrigin(containerID,
State.QUASI_CLOSED, 0, 0, 0);
+
+ // UNHEALTHY replica is on a unique origin and has same sequence id as the
container
+ ContainerReplica unhealthy =
+ ReplicationTestUtil.createContainerReplica(containerID, 0,
+ dn1.getPersistedOpState(), State.UNHEALTHY,
+ container.getNumberOfKeys(), container.getUsedBytes(), dn1,
+ dn1.getUuid(), container.getSequenceId());
+ replicas.add(unhealthy);
+ nodeManager.setContainers(dn1, ImmutableSet.of(containerID));
+
+ Mockito.when(repManager.getContainerReplicaCount(Mockito.eq(containerID)))
+ .thenReturn(new RatisContainerReplicaCount(container, replicas,
+ Collections.emptyList(), 2, false));
+ DatanodeAdminMonitorTestUtil.mockCheckContainerState(repManager, true);
+
+ // start monitoring dn1
+ monitor.startMonitoring(dn1);
+ monitor.run();
+ assertEquals(1, monitor.getTrackedNodeCount());
+ assertEquals(HddsProtos.NodeOperationalState.DECOMMISSIONING,
+ nodeManager.getNodeStatus(dn1).getOperationalState());
+
+ // Running the monitor again causes it to remain DECOMMISSIONING
+ // as nothing has changed.
+ monitor.run();
+ assertEquals(HddsProtos.NodeOperationalState.DECOMMISSIONING,
+ nodeManager.getNodeStatus(dn1).getOperationalState());
+
+ // add a copy of the UNHEALTHY replica on a new node, dn1 should get
+ // decommissioned now
+ ContainerReplica copyOfUnhealthyOnNewNode = unhealthy.toBuilder()
+ .setDatanodeDetails(MockDatanodeDetails.randomDatanodeDetails())
+ .build();
+ replicas.add(copyOfUnhealthyOnNewNode);
+ Mockito.when(repManager.getContainerReplicaCount(Mockito.eq(containerID)))
+ .thenReturn(new RatisContainerReplicaCount(container, replicas,
+ Collections.emptyList(), 2, false));
+ DatanodeAdminMonitorTestUtil.mockCheckContainerState(repManager, false);
+ monitor.run();
+ assertEquals(0, monitor.getTrackedNodeCount());
+ assertEquals(HddsProtos.NodeOperationalState.DECOMMISSIONED,
+ nodeManager.getNodeStatus(dn1).getOperationalState());
+ }
+
/**
* Consider a QUASI_CLOSED container with only UNHEALTHY replicas. If one
* of its nodes is decommissioned, the decommissioning should succeed.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]