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

adoroszlai 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 39ee6037b4 HDDS-9877. ReplicationManager: Create a data driven test 
framework (#5746)
39ee6037b4 is described below

commit 39ee6037b450c1a4bd3d2dd981f8d948ec0c6fed
Author: Stephen O'Donnell <[email protected]>
AuthorDate: Sun Dec 24 19:29:31 2023 +0000

    HDDS-9877. ReplicationManager: Create a data driven test framework (#5746)
---
 hadoop-hdds/pom.xml                                |   1 +
 .../container/replication/ReplicationTestUtil.java |   8 +
 .../TestReplicationManagerScenarios.java           | 797 +++++++++++++++++++++
 .../resources/replicationManagerTests/basic.json   | 113 +++
 .../mismatched_replicas.json                       |  15 +
 .../simple_decommission.json                       |  23 +
 .../simple_maintenance.json                        |  46 ++
 7 files changed, 1003 insertions(+)

diff --git a/hadoop-hdds/pom.xml b/hadoop-hdds/pom.xml
index d523430cfb..d35392982a 100644
--- a/hadoop-hdds/pom.xml
+++ b/hadoop-hdds/pom.xml
@@ -257,6 +257,7 @@ https://maven.apache.org/xsd/maven-4.0.0.xsd";>
         <artifactId>apache-rat-plugin</artifactId>
         <configuration>
           <excludes>
+            <exclude>**/*.json</exclude>
             <exclude>**/hs_err*.log</exclude>
             <exclude>**/.attach_*</exclude>
             <exclude>**/**.rej</exclude>
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationTestUtil.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationTestUtil.java
index 3f0f0e78a2..693349dca8 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationTestUtil.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationTestUtil.java
@@ -24,12 +24,14 @@ import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto;
+import org.apache.hadoop.hdds.scm.ContainerPlacementStatus;
 import org.apache.hadoop.hdds.scm.PlacementPolicy;
 import org.apache.hadoop.hdds.scm.SCMCommonPlacementPolicy;
 import org.apache.hadoop.hdds.scm.container.ContainerID;
 import org.apache.hadoop.hdds.scm.container.ContainerInfo;
 import org.apache.hadoop.hdds.scm.container.ContainerReplica;
 import org.apache.hadoop.hdds.scm.container.TestContainerInfo;
+import 
org.apache.hadoop.hdds.scm.container.placement.algorithms.ContainerPlacementStatusDefault;
 import org.apache.hadoop.hdds.scm.exceptions.SCMException;
 import org.apache.hadoop.hdds.scm.net.Node;
 import org.apache.hadoop.hdds.scm.node.NodeManager;
@@ -276,6 +278,12 @@ public final class ReplicationTestUtil {
         // Make it look like a single rack cluster
         return rackNode;
       }
+
+      @Override
+      public ContainerPlacementStatus
+          validateContainerPlacement(List<DatanodeDetails> dns, int replicas) {
+        return new ContainerPlacementStatusDefault(2, 2, 3);
+      }
     };
   }
 
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestReplicationManagerScenarios.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestReplicationManagerScenarios.java
new file mode 100644
index 0000000000..656a018143
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/replication/TestReplicationManagerScenarios.java
@@ -0,0 +1,797 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hdds.scm.container.replication;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.hdds.client.ECReplicationConfig;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
+import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto;
+import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.SCMCommandProto;
+import org.apache.hadoop.hdds.scm.PlacementPolicy;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import org.apache.hadoop.hdds.scm.container.ContainerManager;
+import org.apache.hadoop.hdds.scm.container.ContainerReplica;
+import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport;
+import org.apache.hadoop.hdds.scm.ha.SCMContext;
+import org.apache.hadoop.hdds.scm.node.NodeManager;
+import org.apache.hadoop.hdds.scm.node.NodeStatus;
+import org.apache.hadoop.hdds.scm.node.states.NodeNotFoundException;
+import org.apache.hadoop.hdds.server.events.EventPublisher;
+import org.apache.hadoop.ozone.protocol.commands.ReplicateContainerCommand;
+import org.apache.hadoop.ozone.protocol.commands.SCMCommand;
+import org.apache.ozone.test.TestClock;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_WAIT_TIME_AFTER_SAFE_MODE_EXIT;
+import static org.mockito.ArgumentMatchers.any;
+
+/**
+ * This class tests the replication manager using a set of scenarios defined in
+ * JSON files. The scenarios define a container and a set of replicas, and the
+ * expected results from the replication manager. The scenarios are defined in
+ * JSON files in the test resources directory. The test files are loaded in
+ * {@link #init()} and the tests are run in {@link 
#testAllScenarios(Scenario)}.
+ *
+ * There are several inner class defined within this class, and they are used 
to
+ * deserialize the JSON files into Java objects. In general any field which is 
a
+ * setter on the inner class can be set in the JSON file.
+ *
+ * TODO - The framework does not allow for testing mis-replicated containers.
+ */
+
+public class TestReplicationManagerScenarios {
+  private static final Map<String, UUID> ORIGINS = new HashMap<>();
+  private static final Map<String, DatanodeDetails> DATANODE_ALIASES
+      = new HashMap<>();
+  private static final Map<DatanodeDetails, NodeStatus> NODE_STATUS_MAP
+      = new HashMap<>();
+  private static final String TEST_RESOURCE_PATH = "/replicationManagerTests";
+  private static final List<Scenario> TEST_SCENARIOS = new ArrayList<>();
+
+  private Map<ContainerID, Set<ContainerReplica>> containerReplicaMap;
+  private Set<ContainerInfo> containerInfoSet;
+  private ContainerReplicaPendingOps containerReplicaPendingOps;
+  private Set<Pair<UUID, SCMCommand<?>>> commandsSent;
+
+  private OzoneConfiguration configuration;
+  private ReplicationManager replicationManager;
+  private LegacyReplicationManager legacyReplicationManager;
+  private ContainerManager containerManager;
+  private PlacementPolicy ratisPlacementPolicy;
+  private PlacementPolicy ecPlacementPolicy;
+  private EventPublisher eventPublisher;
+  private SCMContext scmContext;
+  private NodeManager nodeManager;
+  private TestClock clock;
+
+  private static List<URI> getTestFiles() throws URISyntaxException {
+    File[] fileList = (new File(TestReplicationManagerScenarios.class
+        .getResource(TEST_RESOURCE_PATH)
+        .toURI())).listFiles();
+    if (fileList == null) {
+      Assertions.fail("No test file resources found");
+      // Make findbugs happy.
+      return Collections.emptyList();
+    }
+    List<URI> uris = new ArrayList<>();
+    for (File file : fileList) {
+      uris.add(file.toURI());
+    }
+    return uris;
+  }
+
+  private static List<Scenario> loadTestsInFile(URI testFile)
+      throws IOException {
+    ObjectReader reader = new ObjectMapper().readerFor(Scenario.class);
+    try (InputStream stream = testFile.toURL().openStream()) {
+      try (MappingIterator<Scenario> iterator = reader.readValues(stream)) {
+        return iterator.readAll();
+      }
+    } catch (Exception e) {
+      System.out.println("Failed to load test file: " + testFile);
+      throw e;
+    }
+  }
+
+  /**
+   * Load all the JSON files in the test resources directory and add them to 
the
+   * list of tests to run. If there is a parsing failure in any of the json
+   * files, the entire test will fail.
+   */
+  @BeforeAll
+  public static void init() throws IOException, URISyntaxException {
+    List<URI> testFiles = getTestFiles();
+    for (URI file : testFiles) {
+      List<Scenario> scenarios = loadTestsInFile(file);
+      Set<String> names = new HashSet<>();
+      for (Scenario scenario : scenarios) {
+        if (!names.add(scenario.getDescription())) {
+          Assertions.fail("Duplicate test name: " + scenario.getDescription() 
+ " in file: " + file);
+        }
+        scenario.setResourceName(file.toString());
+      }
+      TEST_SCENARIOS.addAll(scenarios);
+    }
+  }
+
+  @BeforeEach
+  public void setup() throws IOException, NodeNotFoundException {
+    configuration = new OzoneConfiguration();
+    configuration.set(HDDS_SCM_WAIT_TIME_AFTER_SAFE_MODE_EXIT, "0s");
+    containerManager = Mockito.mock(ContainerManager.class);
+
+    scmContext = Mockito.mock(SCMContext.class);
+    nodeManager = Mockito.mock(NodeManager.class);
+
+    ratisPlacementPolicy = 
ReplicationTestUtil.getSimpleTestPlacementPolicy(nodeManager, configuration);
+    ecPlacementPolicy = 
ReplicationTestUtil.getSimpleTestPlacementPolicy(nodeManager, configuration);
+
+    commandsSent = new HashSet<>();
+    eventPublisher = Mockito.mock(EventPublisher.class);
+    Mockito.doAnswer(invocation -> {
+      commandsSent.add(Pair.of(invocation.getArgument(0),
+          invocation.getArgument(1)));
+      return null;
+    }).when(nodeManager).addDatanodeCommand(any(), any());
+
+    legacyReplicationManager = Mockito.mock(LegacyReplicationManager.class);
+    clock = new TestClock(Instant.now(), ZoneId.systemDefault());
+    containerReplicaPendingOps = new ContainerReplicaPendingOps(clock);
+
+    
Mockito.when(containerManager.getContainerReplicas(Mockito.any(ContainerID.class))).thenAnswer(
+        invocation -> {
+          ContainerID cid = invocation.getArgument(0);
+          return containerReplicaMap.get(cid);
+        });
+
+    Mockito.when(containerManager.getContainers()).thenAnswer(
+        invocation -> new ArrayList<>(containerInfoSet));
+
+    Mockito.when(nodeManager.getNodeStatus(any(DatanodeDetails.class)))
+        .thenAnswer(invocation -> {
+          DatanodeDetails dn = invocation.getArgument(0);
+          return NODE_STATUS_MAP.getOrDefault(dn, 
NodeStatus.inServiceHealthy());
+        });
+
+    final HashMap<SCMCommandProto.Type, Integer> countMap = new HashMap<>();
+    for (SCMCommandProto.Type type : SCMCommandProto.Type.values()) {
+      countMap.put(type, 0);
+    }
+    Mockito.when(
+        nodeManager.getTotalDatanodeCommandCounts(any(DatanodeDetails.class),
+            any(SCMCommandProto.Type.class), any(SCMCommandProto.Type.class)))
+        .thenReturn(countMap);
+
+    // Ensure that RM will run when asked.
+    Mockito.when(scmContext.isLeaderReady()).thenReturn(true);
+    Mockito.when(scmContext.isInSafeMode()).thenReturn(false);
+    containerReplicaMap = new HashMap<>();
+    containerInfoSet = new HashSet<>();
+    ORIGINS.clear();
+    DATANODE_ALIASES.clear();
+    NODE_STATUS_MAP.clear();
+  }
+
+  private ReplicationManager createReplicationManager() throws IOException {
+    return new ReplicationManager(
+        configuration,
+        containerManager,
+        ratisPlacementPolicy,
+        ecPlacementPolicy,
+        eventPublisher,
+        scmContext,
+        nodeManager,
+        clock,
+        legacyReplicationManager,
+        containerReplicaPendingOps) {
+      @Override
+      protected void startSubServices() {
+        // do not start any threads for processing
+      }
+    };
+  }
+
+  protected static UUID getOrCreateOrigin(String origin) {
+    return ORIGINS.computeIfAbsent(origin, (k) -> UUID.randomUUID());
+  }
+
+  private static Stream<Scenario> getTestScenarios() {
+    return TEST_SCENARIOS.stream();
+  }
+
+  private void loadPendingOps(ContainerInfo container, Scenario scenario) {
+    for (PendingReplica r : scenario.getPendingReplicas()) {
+      if (r.getType() == ContainerReplicaOp.PendingOpType.ADD) {
+        containerReplicaPendingOps.scheduleAddReplica(container.containerID(), 
r.getDatanodeDetails(),
+            r.getReplicaIndex(), Long.MAX_VALUE);
+      } else if (r.getType() == ContainerReplicaOp.PendingOpType.DELETE) {
+        
containerReplicaPendingOps.scheduleDeleteReplica(container.containerID(), 
r.getDatanodeDetails(),
+            r.getReplicaIndex(), Long.MAX_VALUE);
+      }
+    }
+  }
+
+  @ParameterizedTest
+  @MethodSource("getTestScenarios")
+  public void testAllScenarios(Scenario scenario) throws IOException {
+    ReplicationManagerReport repReport = new ReplicationManagerReport();
+    ReplicationQueue repQueue = new ReplicationQueue();
+    ReplicationManager.ReplicationManagerConfiguration conf =
+        new ReplicationManager.ReplicationManagerConfiguration();
+    
conf.setMaintenanceRemainingRedundancy(scenario.getEcMaintenanceRedundancy());
+    conf.setMaintenanceReplicaMinimum(scenario.getRatisMaintenanceMinimum());
+    configuration.setFromObject(conf);
+    replicationManager = createReplicationManager();
+
+    ContainerInfo containerInfo = scenario.buildContainerInfo();
+    loadPendingOps(containerInfo, scenario);
+
+    Set<ContainerReplica> replicas = new HashSet<>();
+    for (TestReplica replica : scenario.getReplicas()) {
+      replicas.add(replica.buildContainerReplica());
+    }
+    // Set up the maps used by the mocks passed into Replication Manager, so it
+    // can find the replicas and containers created here.
+    containerInfoSet.add(containerInfo);
+    containerReplicaMap.put(containerInfo.containerID(), replicas);
+
+    // Run the replication manager check phase.
+    replicationManager.processContainer(containerInfo, repQueue, repReport);
+
+    // Check the results in the report and queue against the expected results.
+    assertExpectations(scenario, repReport);
+    Expectation expectation = scenario.getExpectation();
+    Assertions.assertEquals(expectation.getUnderReplicatedQueue(), 
repQueue.underReplicatedQueueSize(),
+        "Test: " + scenario + ": Unexpected count for underReplicatedQueue");
+    Assertions.assertEquals(expectation.getOverReplicatedQueue(), 
repQueue.overReplicatedQueueSize(),
+        "Test: " + scenario + ": Unexpected count for overReplicatedQueue");
+
+    assertExpectedCommands(scenario, scenario.getCheckCommands());
+    commandsSent.clear();
+
+    ReplicationManagerReport roReport = new ReplicationManagerReport();
+    replicationManager.checkContainerStatus(containerInfo, roReport);
+    Assertions.assertEquals(0, commandsSent.size());
+    assertExpectations(scenario, roReport);
+
+    // Now run the replication manager execute phase, where we expect commands
+    // to be sent to fix the under and over replicated containers.
+    if (repQueue.underReplicatedQueueSize() > 0) {
+      
replicationManager.processUnderReplicatedContainer(repQueue.dequeueUnderReplicatedContainer());
+    } else if (repQueue.overReplicatedQueueSize() > 0) {
+      
replicationManager.processOverReplicatedContainer(repQueue.dequeueOverReplicatedContainer());
+    }
+    assertExpectedCommands(scenario, scenario.getCommands());
+  }
+
+  private void assertExpectations(Scenario scenario,
+      ReplicationManagerReport report) {
+    Expectation expectation = scenario.getExpectation();
+    for (ReplicationManagerReport.HealthState state :
+        ReplicationManagerReport.HealthState.values()) {
+      Assertions.assertEquals(expectation.getExpected(state), 
report.getStat(state),
+          "Test: " + scenario + ": Unexpected count for " + state);
+    }
+  }
+
+  private void assertExpectedCommands(Scenario scenario,
+      List<ExpectedCommands> expectedCommands) {
+    Assertions.assertEquals(expectedCommands.size(), commandsSent.size(),
+        "Test: " + scenario + ": Unexpected count for commands sent");
+    // Iterate the expected commands and check that they were all sent. If we
+    // have a target datanode, then we need to check that the command was sent
+    // to that target. The targets in the tests work off aliases for the
+    // datanodes.
+    for (ExpectedCommands expectedCommand : expectedCommands) {
+      boolean found = false;
+      for (Pair<UUID, SCMCommand<?>> command : commandsSent) {
+        if (command.getRight().getType() == expectedCommand.getType()) {
+          DatanodeDetails targetDatanode = expectedCommand.getTargetDatanode();
+          if (targetDatanode != null) {
+            // We need to assert against the command the datanode is sent to
+            DatanodeDetails commandDatanode = 
findDatanodeFromUUID(command.getKey());
+            if (commandDatanode != null && 
commandDatanode.equals(targetDatanode)) {
+              found = true;
+              commandsSent.remove(command);
+              break;
+            }
+          } else {
+            // We don't care what datanode the command is sent to.
+            found = true;
+            commandsSent.remove(command);
+            break;
+          }
+        }
+      }
+      Assertions.assertTrue(found, "Test: " + scenario + ": Expected command 
not sent: " + expectedCommand.getType());
+    }
+  }
+
+  private DatanodeDetails findDatanodeFromUUID(UUID uuid) {
+    for (DatanodeDetails dn : DATANODE_ALIASES.values()) {
+      if (dn.getUuid().equals(uuid)) {
+        return dn;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * This class is used to define the replicas used in the test scenarios. It 
is
+   * created by deserializing JSON files.
+   */
+  public static class TestReplica {
+    private ContainerReplicaProto.State state = 
ContainerReplicaProto.State.CLOSED;
+    private long containerId = 1;
+    // This is a string identifier for a datanode that can be referenced in
+    // test expectations and commands. The real datanode will be generated.
+    private String datanode;
+    private DatanodeDetails datanodeDetails;
+    private HddsProtos.NodeOperationalState operationalState = 
HddsProtos.NodeOperationalState.IN_SERVICE;
+    private HddsProtos.NodeState healthState = HddsProtos.NodeState.HEALTHY;
+
+    private int index = 0;
+    private int sequenceId = 0;
+    private long keys = 10;
+    private long used = 10;
+    private boolean isEmpty = false;
+    private String origin;
+    private UUID originId;
+
+    public void setContainerId(long containerId) {
+      this.containerId = containerId;
+    }
+
+    public void setDatanode(String datanode) {
+      this.datanode = datanode;
+    }
+
+    public void setOrigin(String origin) {
+      this.origin = origin;
+    }
+
+    public void setIndex(int index) {
+      this.index = index;
+    }
+
+    public void setSequenceId(int sequenceId) {
+      this.sequenceId = sequenceId;
+    }
+
+    public void setState(String state) {
+      this.state = ContainerReplicaProto.State.valueOf(state);
+    }
+
+    public void setKeys(long keys) {
+      this.keys = keys;
+    }
+
+    public void setUsed(long used) {
+      this.used = used;
+    }
+
+    public void setHealthState(String healthState) {
+      this.healthState = HddsProtos.NodeState.valueOf(
+          healthState.toUpperCase());
+    }
+
+    public void setIsEmpty(boolean empty) {
+      isEmpty = empty;
+    }
+
+    public void setOperationalState(String operationalState) {
+      this.operationalState = 
HddsProtos.NodeOperationalState.valueOf(operationalState.toUpperCase());
+    }
+
+    public String getOrigin() {
+      createOrigin();
+      return origin;
+    }
+
+    // This returns the datanode identifier, not the real datanode.
+    public String getDatanode() {
+      return datanode;
+    }
+
+    public DatanodeDetails getDatanodeDetails() {
+      createDatanodeDetails();
+      return datanodeDetails;
+    }
+
+    public ContainerReplica buildContainerReplica() {
+      createDatanodeDetails();
+      createOrigin();
+      NODE_STATUS_MAP.put(datanodeDetails, new NodeStatus(operationalState, 
healthState));
+      datanodeDetails.setPersistedOpState(operationalState);
+
+      ContainerReplica.ContainerReplicaBuilder builder = new 
ContainerReplica.ContainerReplicaBuilder();
+      return builder.setReplicaIndex(index)
+          .setContainerID(new ContainerID(containerId))
+          .setContainerState(state)
+          .setSequenceId(sequenceId)
+          .setDatanodeDetails(datanodeDetails)
+          .setKeyCount(keys)
+          .setBytesUsed(used)
+          .setEmpty(isEmpty)
+          .setOriginNodeId(originId).build();
+    }
+
+    private void createDatanodeDetails() {
+      if (datanodeDetails != null) {
+        return;
+      }
+      if (datanode != null) {
+        datanodeDetails = DATANODE_ALIASES.computeIfAbsent(datanode, (k) ->
+            MockDatanodeDetails.randomDatanodeDetails());
+      } else {
+        datanodeDetails = MockDatanodeDetails.randomDatanodeDetails();
+      }
+    }
+
+    private void createOrigin() {
+      if (originId != null) {
+        return;
+      }
+      if (origin != null) {
+        originId = getOrCreateOrigin(origin);
+      } else {
+        originId = UUID.randomUUID();
+      }
+    }
+  }
+
+  /**
+   * This class is used to define the expected counts for each health state and
+   * queues. It is created by deserializing JSON files.
+   */
+  public static class Expectation {
+
+    // The expected counts for each health state, as would be seen in the 
ReplicationManagerReport.
+    private Map<ReplicationManagerReport.HealthState, Integer> stateCounts = 
new HashMap<>();
+    // The expected count for each queue after running the RM check phase.
+    private int underReplicatedQueue = 0;
+    private int overReplicatedQueue = 0;
+
+    private void setUnderReplicated(int underReplicated) {
+      stateCounts.put(ReplicationManagerReport.HealthState.UNDER_REPLICATED, 
underReplicated);
+    }
+
+    private void setOverReplicated(int overReplicated) {
+      stateCounts.put(ReplicationManagerReport.HealthState.OVER_REPLICATED, 
overReplicated);
+    }
+
+    private void setMisReplicated(int misReplicated) {
+      stateCounts.put(ReplicationManagerReport.HealthState.MIS_REPLICATED, 
misReplicated);
+    }
+
+    private void setUnhealthy(int unhealthy) {
+      stateCounts.put(ReplicationManagerReport.HealthState.UNHEALTHY, 
unhealthy);
+    }
+
+    private void setMissing(int missing) {
+      stateCounts.put(ReplicationManagerReport.HealthState.MISSING, missing);
+    }
+
+    private void setEmpty(int empty) {
+      stateCounts.put(ReplicationManagerReport.HealthState.EMPTY,  empty);
+    }
+
+    private void setQuasiClosedStuck(int quasiClosedStuck) {
+      stateCounts.put(ReplicationManagerReport.HealthState.QUASI_CLOSED_STUCK, 
quasiClosedStuck);
+    }
+
+    private void setOpenUnhealthy(int openUnhealthy) {
+      stateCounts.put(ReplicationManagerReport.HealthState.OPEN_UNHEALTHY, 
openUnhealthy);
+    }
+
+    public int getExpected(ReplicationManagerReport.HealthState state) {
+      return stateCounts.getOrDefault(state, 0);
+    }
+
+    public int getUnderReplicatedQueue() {
+      return underReplicatedQueue;
+    }
+
+    public int getOverReplicatedQueue() {
+      return overReplicatedQueue;
+    }
+  }
+
+  /**
+   * This class is used to define the expected commands for each replica. It is
+   * created by deserializing JSON files.
+   */
+  public static class ExpectedCommands {
+    private SCMCommandProto.Type type;
+    private String datanode;
+
+    public void setDatanode(String datanode) {
+      this.datanode = datanode;
+    }
+
+    public void setType(String command) {
+      ReplicateContainerCommand replicateContainerCommand;
+      this.type = SCMCommandProto.Type.valueOf(command);
+    }
+
+    public SCMCommandProto.Type getType() {
+      return type;
+    }
+
+    public DatanodeDetails getTargetDatanode() {
+      if (datanode == null) {
+        return null;
+      }
+      DatanodeDetails datanodeDetails = DATANODE_ALIASES.get(this.datanode);
+      if (datanodeDetails == null) {
+        Assertions.fail("Unable to find a datanode for the alias: " + datanode 
+ " in the expected commands.");
+      }
+      return datanodeDetails;
+    }
+  }
+
+  /**
+   * This class is used to define the pending replicas for the container. It is
+   * created by deserializing JSON files.
+   */
+  public static class PendingReplica {
+    private ContainerReplicaOp.PendingOpType type;
+    private String datanode;
+    private int replicaIndex;
+
+    public void setReplicaIndex(int replicaIndex) {
+      this.replicaIndex = replicaIndex;
+    }
+
+    public void setDatanode(String datanode) {
+      this.datanode = datanode;
+    }
+
+    public void setType(String type) {
+      this.type = ContainerReplicaOp.PendingOpType.valueOf(type);
+    }
+
+    public DatanodeDetails getDatanodeDetails() {
+      if (datanode == null) {
+        return MockDatanodeDetails.randomDatanodeDetails();
+      } else {
+        return DATANODE_ALIASES.computeIfAbsent(datanode, (k) ->
+            MockDatanodeDetails.randomDatanodeDetails());
+      }
+    }
+
+    public ContainerReplicaOp.PendingOpType getType() {
+      return type;
+    }
+
+    public int getReplicaIndex() {
+      return this.replicaIndex;
+    }
+
+  }
+
+  /**
+   * This class is used to define the test scenarios. It is created by
+   * deserializing JSON files. It defines the base container used for the test,
+   * and provides getter for the replicas and expected results.
+   */
+  public static class Scenario {
+    // The test description
+    private String description;
+    // The resource name of the test file this scenario was loaded from. NOTE 
- this does not come
+    // from the json definition,
+    private String resourceName;
+    private int ecMaintenanceRedundancy;
+    private int ratisMaintenanceMinimum;
+    private HddsProtos.LifeCycleState containerState = 
HddsProtos.LifeCycleState.CLOSED;
+    // Used bytes in the container under test.
+    private long used = 10;
+    // Number of keys in the container under test.
+    private long keys = 10;
+    // Container ID of the container under test.
+    private long id = 1;
+    // Owner of the container under test.
+    private String owner = "theowner";
+    // Sequence ID of the container under test.
+    private int sequenceId = 0;
+    // Replication config for the container under test.
+    private ReplicationConfig replicationConfig = RatisReplicationConfig
+        .getInstance(HddsProtos.ReplicationFactor.THREE);
+    // Replicas for the container under test.
+    private List<TestReplica> replicas = new ArrayList<>();
+    // Replicas pending add or delete for the container under test.
+    private List<PendingReplica> pendingReplicas = new ArrayList<>();
+    // Object that defines the expected counts for each health state and queue.
+    private Expectation expectation = new Expectation();
+    // Commands expected to be sent during the check phase of replication 
manager.
+    private List<ExpectedCommands> checkCommands = new ArrayList<>();
+    // Commands expected to be sent when processing the under / over 
replicated queue
+    private List<ExpectedCommands> commands = new ArrayList<>();
+
+    public Scenario() {
+      ReplicationManager.ReplicationManagerConfiguration conf =
+          new ReplicationManager.ReplicationManagerConfiguration();
+      ecMaintenanceRedundancy = conf.getMaintenanceRemainingRedundancy();
+      ratisMaintenanceMinimum = conf.getMaintenanceReplicaMinimum();
+    }
+
+    public void setDescription(String description) {
+      this.description = description;
+    }
+
+    public void setEcMaintenanceRedundancy(int ecMaintenanceRedundancy) {
+      this.ecMaintenanceRedundancy = ecMaintenanceRedundancy;
+    }
+
+    public void setRatisMaintenanceMinimum(int ratisMaintenanceMinimum) {
+      this.ratisMaintenanceMinimum = ratisMaintenanceMinimum;
+    }
+
+    public void setUsed(long used) {
+      this.used = used;
+    }
+
+    public void setKeys(long keys) {
+      this.keys = keys;
+    }
+
+    public void setId(long id) {
+      this.id = id;
+    }
+
+    public void setOwner(String owner) {
+      this.owner = owner;
+    }
+
+    public void setSequenceId(int sequenceId) {
+      this.sequenceId = sequenceId;
+    }
+
+    public void setReplicas(List<TestReplica> replicas) {
+      this.replicas = replicas;
+    }
+
+    public void setPendingReplicas(List<PendingReplica> pendingReplicas) {
+      this.pendingReplicas = pendingReplicas;
+    }
+
+    public void setExpectation(Expectation expectation) {
+      this.expectation = expectation;
+    }
+
+    public void setCheckCommands(List<ExpectedCommands> checkCommands) {
+      this.checkCommands = checkCommands;
+    }
+
+    public void setCommands(List<ExpectedCommands> commands) {
+      this.commands = commands;
+    }
+
+    public void setResourceName(String resourceName) {
+      this.resourceName = resourceName;
+    }
+
+    public int getEcMaintenanceRedundancy() {
+      return this.ecMaintenanceRedundancy;
+    }
+
+    public int getRatisMaintenanceMinimum() {
+      return this.ratisMaintenanceMinimum;
+    }
+
+    public String getDescription() {
+      return description;
+    }
+
+    public void setContainerState(String containerState) {
+      this.containerState = HddsProtos.LifeCycleState.valueOf(containerState);
+    }
+
+    public List<PendingReplica> getPendingReplicas() {
+      return this.pendingReplicas;
+    }
+
+    public List<ExpectedCommands> getCheckCommands() {
+      return checkCommands;
+    }
+
+    public List<TestReplica> getReplicas() {
+      return replicas;
+    }
+
+    public Expectation getExpectation() {
+      return expectation;
+    }
+
+    /**
+     * Should be in the format of "type:factor".
+     * Eg RATIS:THREE
+     *    EC:rs-3-2-1024k
+     * @param replicationConfig
+     */
+    public void setReplicationConfig(String replicationConfig) {
+      String[] parts = replicationConfig.split(":");
+      if (parts.length != 2) {
+        throw new IllegalArgumentException(
+            "Replication config should be in the format of \"type:factor\". Eg 
RATIS:THREE");
+      }
+      switch (parts[0].toUpperCase()) {
+      case "RATIS":
+        this.replicationConfig = 
RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.valueOf(parts[1]));
+        break;
+      case "EC":
+        this.replicationConfig = new ECReplicationConfig(parts[1]);
+        break;
+      default:
+        throw new IllegalArgumentException("Unknown replication type: " + 
parts[0]);
+      }
+    }
+
+    public ContainerInfo buildContainerInfo() {
+      ContainerInfo.Builder builder = new ContainerInfo.Builder();
+      builder.setState(containerState)
+          .setSequenceId(sequenceId)
+          .setReplicationConfig(replicationConfig)
+          .setNumberOfKeys(keys)
+          .setOwner(owner)
+          .setContainerID(id)
+          .setUsedBytes(used);
+      return builder.build();
+    }
+
+    public List<ExpectedCommands> getCommands() {
+      return commands;
+    }
+
+    @Override
+    public String toString() {
+      return resourceName + ": " + description;
+    }
+  }
+}
diff --git 
a/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/basic.json 
b/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/basic.json
new file mode 100644
index 0000000000..1c29c4dde8
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/basic.json
@@ -0,0 +1,113 @@
+[
+  { "description": "Perfect Replication Ratis", "containerState": "CLOSED", 
"replicationConfig": "RATIS:THREE", "sequenceId": 12,
+    "replicas": [
+        { "state": "CLOSED", "index": 0, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+        { "state": "CLOSED", "index": 0, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+        { "state": "CLOSED", "index": 0, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"}
+    ],
+    "expectation": {},
+    "commands": []
+  },
+
+  { "description": "Perfect Replication EC", "containerState": "CLOSED", 
"replicationConfig": "EC:RS-3-2-1024k",
+    "replicas": [
+      { "state": "CLOSED", "index": 1, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+      { "state": "CLOSED", "index": 2, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+      { "state": "CLOSED", "index": 3, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"},
+      { "state": "CLOSED", "index": 4, "datanode": "d4", "sequenceId": 0, 
"isEmpty": false, "origin": "o4"},
+      { "state": "CLOSED", "index": 5, "datanode": "d5", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"}
+    ],
+    "expectation": {},
+    "commands": []
+  },
+
+  { "description": "Ratis Under Replication", "containerState": "CLOSED", 
"replicationConfig": "RATIS:THREE", "sequenceId": 12,
+    "replicas": [
+        { "state": "CLOSED", "index": 0, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+        { "state": "CLOSED", "index": 0, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"}
+    ],
+    "expectation": { "underReplicated": 1, "underReplicatedQueue": 1 },
+    "commands": [ { "type": "replicateContainerCommand"} ]
+  },
+
+  { "description": "Ratis Under Replication pending add", "containerState": 
"CLOSED", "replicationConfig": "RATIS:THREE", "sequenceId": 12,
+    "replicas": [
+      { "state": "CLOSED", "index": 0, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+      { "state": "CLOSED", "index": 0, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"}
+    ],
+    "pendingReplicas": [ { "type": "ADD" } ],
+    "expectation": { "underReplicated": 1, "underReplicatedQueue": 0 }
+  },
+
+  { "description": "Under Replication EC", "containerState": "CLOSED", 
"replicationConfig": "EC:RS-3-2-1024k",
+    "replicas": [
+      { "state": "CLOSED", "index": 1, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+      { "state": "CLOSED", "index": 2, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+      { "state": "CLOSED", "index": 3, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"}
+    ],
+    "expectation": { "underReplicated": 1, "underReplicatedQueue": 1 },
+    "commands": [ { "type": "reconstructECContainersCommand"} ]
+  },
+
+  { "description": "Over Replicated", "containerState": "CLOSED", 
"replicationConfig": "RATIS:THREE", "sequenceId": 12,
+    "replicas": [
+        { "state": "CLOSED", "index": 0, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+        { "state": "CLOSED", "index": 0, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+        { "state": "CLOSED", "index": 0, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"},
+        { "state": "CLOSED", "index": 0, "datanode": "d4", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"}
+    ],
+    "expectation": { "overReplicated": 1, "overReplicatedQueue": 1 },
+    "commands": [ { "type": "deleteContainerCommand"} ]
+  },
+
+  { "description": "Over Replication EC", "containerState": "CLOSED", 
"replicationConfig": "EC:RS-3-2-1024k",
+    "replicas": [
+      { "state": "CLOSED", "index": 1, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+      { "state": "CLOSED", "index": 2, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+      { "state": "CLOSED", "index": 3, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"},
+      { "state": "CLOSED", "index": 4, "datanode": "d4", "sequenceId": 0, 
"isEmpty": false, "origin": "o4"},
+      { "state": "CLOSED", "index": 5, "datanode": "d5", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"},
+      { "state": "CLOSED", "index": 5, "datanode": "d6", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"}
+    ],
+    "expectation": { "overReplicated": 1, "overReplicatedQueue": 1 },
+    "commands": [ { "type": "deleteContainerCommand"} ]
+  },
+
+  { "description": "Over Replication EC pending delete", "containerState": 
"CLOSED", "replicationConfig": "EC:RS-3-2-1024k",
+    "replicas": [
+      { "state": "CLOSED", "index": 1, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+      { "state": "CLOSED", "index": 2, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+      { "state": "CLOSED", "index": 3, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"},
+      { "state": "CLOSED", "index": 4, "datanode": "d4", "sequenceId": 0, 
"isEmpty": false, "origin": "o4"},
+      { "state": "CLOSED", "index": 5, "datanode": "d5", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"},
+      { "state": "CLOSED", "index": 5, "datanode": "d6", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"}
+    ],
+    "pendingReplicas": [ { "type": "DELETE", "replicaIndex": 5, "datanode": 
"d6" } ],
+    "expectation": { "overReplicated": 1, "overReplicatedQueue": 0 }
+  },
+
+  { "description": "Over and Under Replication EC", "containerState": 
"CLOSED", "replicationConfig": "EC:RS-3-2-1024k",
+    "replicas": [
+      { "state": "CLOSED", "index": 1, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+      { "state": "CLOSED", "index": 2, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+      { "state": "CLOSED", "index": 4, "datanode": "d4", "sequenceId": 0, 
"isEmpty": false, "origin": "o4"},
+      { "state": "CLOSED", "index": 5, "datanode": "d5", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"},
+      { "state": "CLOSED", "index": 5, "datanode": "d6", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"}
+    ],
+    "expectation": { "overReplicated": 0, "overReplicatedQueue": 0, 
"underReplicated": 1, "underReplicatedQueue": 1 },
+    "commands": [ { "type": "reconstructECContainersCommand" } ]
+  },
+
+  { "description": "Replication Over Replicated Ratis Pending Delete", 
"containerState": "CLOSED", "replicationConfig": "RATIS:THREE", "sequenceId": 
12,
+    "replicas": [
+      { "state": "CLOSED", "index": 0, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1"},
+      { "state": "CLOSED", "index": 0, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+      { "state": "CLOSED", "index": 0, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"},
+      { "state": "CLOSED", "index": 0, "datanode": "d4", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"}
+    ],
+    "pendingReplicas": [ { "type":  "DELETE", "datanode":  "d2" } ],
+    "expectation": { "overReplicated":  1, "overReplicatedQueue":  0},
+    "checkCommands": [],
+    "commands": []
+  }
+]
diff --git 
a/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/mismatched_replicas.json
 
b/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/mismatched_replicas.json
new file mode 100644
index 0000000000..195c3f9f09
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/mismatched_replicas.json
@@ -0,0 +1,15 @@
+[
+  { "description": "Mis-matched replicas", "containerState": "CLOSED", 
"replicationConfig": "RATIS:THREE", "sequenceId": 12,
+    "replicas": [
+      { "state": "OPEN", "index": 0,   "datanode": "d1", "sequenceId": 12, 
"isEmpty": false, "origin": "o1"},
+      { "state": "OPEN", "index": 0,   "datanode": "d2", "sequenceId": 12, 
"isEmpty": false, "origin": "o2"},
+      { "state": "CLOSED", "index": 0, "datanode": "d3", "sequenceId": 12, 
"isEmpty": false, "origin": "o3"}
+    ],
+    "expectation": { "overReplicated": 0, "overReplicatedQueue":  0},
+    "checkCommands": [
+      { "type":  "closeContainerCommand", "datanode": "d1"},
+      { "type":  "closeContainerCommand", "datanode": "d2"}
+    ],
+    "commands": []
+  }
+]
\ No newline at end of file
diff --git 
a/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/simple_decommission.json
 
b/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/simple_decommission.json
new file mode 100644
index 0000000000..757ef4e38f
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/simple_decommission.json
@@ -0,0 +1,23 @@
+[
+  { "description": "Simple Decommission Ratis", "containerState": "CLOSED", 
"replicationConfig": "RATIS:THREE", "sequenceId": 12,
+    "replicas": [
+      { "state": "CLOSED", "index": 0, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1", "operationalState": "DECOMMISSIONING"},
+      { "state": "CLOSED", "index": 0, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+      { "state": "CLOSED", "index": 0, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"}
+    ],
+    "expectation": { "underReplicated": 1, "underReplicatedQueue": 1 },
+    "commands": [ { "type": "replicateContainerCommand"} ]
+  },
+
+  { "description": "Simple Decommission EC", "containerState": "CLOSED", 
"replicationConfig": "EC:RS-3-2-1024k",
+    "replicas": [
+      { "state": "CLOSED", "index": 1, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1", "operationalState": "DECOMMISSIONING"},
+      { "state": "CLOSED", "index": 2, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2"},
+      { "state": "CLOSED", "index": 3, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"},
+      { "state": "CLOSED", "index": 4, "datanode": "d4", "sequenceId": 0, 
"isEmpty": false, "origin": "o4"},
+      { "state": "CLOSED", "index": 5, "datanode": "d5", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"}
+    ],
+    "expectation": { "underReplicated": 1, "underReplicatedQueue": 1 },
+    "commands": [ { "type": "replicateContainerCommand"} ]
+  }
+]
\ No newline at end of file
diff --git 
a/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/simple_maintenance.json
 
b/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/simple_maintenance.json
new file mode 100644
index 0000000000..d4b7eba9d3
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/test/resources/replicationManagerTests/simple_maintenance.json
@@ -0,0 +1,46 @@
+[
+
+  { "description": "Ratis Simple Maintenance", "containerState": "CLOSED", 
"replicationConfig": "RATIS:THREE", "sequenceId": 12,
+    "ratisMaintenanceMinimum": 1,
+    "replicas": [
+      { "state": "CLOSED", "index": 0, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1", "operationalState": "ENTERING_MAINTENANCE" },
+      { "state": "CLOSED", "index": 0, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2", "operationalState": "ENTERING_MAINTENANCE" },
+      { "state": "CLOSED", "index": 0, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"}
+    ]
+  },
+
+  { "description": "EC Simple Maintenance", "containerState": "CLOSED", 
"replicationConfig": "EC:RS-3-2-1024k",
+    "ecMaintenanceRedundancy": 0,
+    "replicas": [
+      { "state": "CLOSED", "index": 1, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1", "operationalState": "ENTERING_MAINTENANCE" },
+      { "state": "CLOSED", "index": 2, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2", "operationalState": "ENTERING_MAINTENANCE" },
+      { "state": "CLOSED", "index": 3, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"},
+      { "state": "CLOSED", "index": 4, "datanode": "d4", "sequenceId": 0, 
"isEmpty": false, "origin": "o4"},
+      { "state": "CLOSED", "index": 5, "datanode": "d5", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"}
+    ]
+  },
+
+  { "description": "Ratis Simple Maintenance Requires Replication", 
"containerState": "CLOSED", "replicationConfig": "RATIS:THREE", "sequenceId": 
12,
+    "ratisMaintenanceMinimum": 2,
+    "replicas": [
+      { "state": "CLOSED", "index": 0, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1", "operationalState": "ENTERING_MAINTENANCE" },
+      { "state": "CLOSED", "index": 0, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2", "operationalState": "ENTERING_MAINTENANCE" },
+      { "state": "CLOSED", "index": 0, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"}
+    ],
+    "expectation": { "underReplicated": 1, "underReplicatedQueue": 1 },
+    "commands": [ { "type": "replicateContainerCommand"} ]
+  },
+
+  { "description": "EC Simple Maintenance requires replication", 
"containerState": "CLOSED", "replicationConfig": "EC:RS-3-2-1024k",
+    "ecMaintenanceRedundancy": 2,
+    "replicas": [
+      { "state": "CLOSED", "index": 1, "datanode": "d1", "sequenceId": 0, 
"isEmpty": false, "origin": "o1", "operationalState": "ENTERING_MAINTENANCE" },
+      { "state": "CLOSED", "index": 2, "datanode": "d2", "sequenceId": 0, 
"isEmpty": false, "origin": "o2", "operationalState": "ENTERING_MAINTENANCE" },
+      { "state": "CLOSED", "index": 3, "datanode": "d3", "sequenceId": 0, 
"isEmpty": false, "origin": "o3"},
+      { "state": "CLOSED", "index": 4, "datanode": "d4", "sequenceId": 0, 
"isEmpty": false, "origin": "o4"},
+      { "state": "CLOSED", "index": 5, "datanode": "d5", "sequenceId": 0, 
"isEmpty": false, "origin": "o5"}
+    ],
+    "expectation": { "underReplicated": 1, "underReplicatedQueue": 1 },
+    "commands": [ { "type": "replicateContainerCommand"}, { "type": 
"replicateContainerCommand"} ]
+  }
+]
\ No newline at end of file


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


Reply via email to