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

jojochuang 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 78784ba9fac HDDS-15071. [SCM] Add configuration and global EC 
reconstruction limit (#11054)
78784ba9fac is described below

commit 78784ba9fac6db9051a43f3bfa2a125bbc016b39
Author: Wei-Chiu Chuang <[email protected]>
AuthorDate: Thu Oct 8 15:59:16 2026 -0600

    HDDS-15071. [SCM] Add configuration and global EC reconstruction limit 
(#11054)
    
    Generated-by: Cursor <[email protected]>
---
 .../container/replication/ReplicationManager.java  | 296 +++++++++++++-
 .../replication/TestReplicationManager.java        | 440 +++++++++++++++++++++
 2 files changed, 716 insertions(+), 20 deletions(-)

diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java
index 1742f735cc9..29f00a10d3c 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java
@@ -37,6 +37,7 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -158,12 +159,15 @@ public class ReplicationManager implements SCMService, 
ContainerReplicaPendingOp
   private final Map<DatanodeDetails, Integer> excludedNodes =
       new ConcurrentHashMap<>();
 
+  private final GlobalReconstructionTracker globalReconstructionTracker =
+      new GlobalReconstructionTracker();
+
   /**
    * SCMService related variables.
    * After leaving safe mode, replicationMonitor needs to wait for a while
    * before really take effect.
    */
-  private final Lock serviceLock = new ReentrantLock();
+  private final Lock serviceLifecycleLock = new ReentrantLock();
   private ServiceStatus serviceStatus = ServiceStatus.PAUSING;
   private final long waitTimeInMillis;
   private long lastTimeToBeReadyInMillis = 0;
@@ -421,6 +425,36 @@ public long getInflightReplicationCount() {
         .getPendingOpCount(ContainerReplicaOp.PendingOpType.ADD);
   }
 
+  /**
+   * Returns the number of active EC reconstruction commands currently in
+   * progress across the cluster.
+   */
+  public int getInflightReconstructionCount() {
+    return globalReconstructionTracker.getInflightCount();
+  }
+
+  int getReconstructionPendingFragmentCount(long cmdId) {
+    return globalReconstructionTracker.getPendingFragmentCount(cmdId);
+  }
+
+  /**
+   * Returns the maximum number of inflight reconstruction commands allowed
+   * across the cluster at any given time.
+   * @return the maximum number of inflight reconstruction commands allowed
+   */
+  public int getReconstructionInFlightLimit() {
+    return rmConf.getReconstructionGlobalLimit();
+  }
+
+  /**
+   * Returns true if the number of inflight reconstruction commands has reached
+   * the global limit.
+   * @return true if the limit is reached, false otherwise
+   */
+  public boolean isReconstructionLimitReached() {
+    return globalReconstructionTracker.isLimitReached();
+  }
+
   /**
    * Sends delete container command for the given container to the given
    * datanode.
@@ -533,20 +567,45 @@ public void sendThrottledReplicationCommand(ContainerInfo 
containerInfo,
     sendDatanodeCommand(cmd, containerInfo, source);
   }
 
+  /**
+   * Send a {@link ReconstructECContainersCommand} to a datanode with capacity,
+   * after reserving a cluster-wide reconstruction slot.
+   *
+   * <p>The command may encode multiple missing EC indexes (fragments). One
+   * global slot is reserved per command; {@link #opCompleted} releases it
+   * incrementally as each fragment's pending ADD op completes or expires.
+   *
+   * <p>If the global reconstruction limit is reached, or no target datanode
+   * has spare replication capacity, a {@link CommandTargetOverloadedException}
+   * is thrown and the command is not sent. Callers (e.g.
+   * {@code ECUnderReplicationHandler}) can fall back to 1-1 replication.
+   *
+   * @param containerInfo the container to reconstruct
+   * @param command the reconstruction command to send
+   */
   public void sendThrottledReconstructionCommand(ContainerInfo containerInfo,
       ReconstructECContainersCommand command)
       throws CommandTargetOverloadedException, NotLeaderException {
-    List<DatanodeDetails> targets = command.getTargetDatanodes();
-    List<Pair<Integer, DatanodeDetails>> targetWithCmds =
-        getAvailableDatanodesForReplication(targets);
-    if (targetWithCmds.isEmpty()) {
-      metrics.incrECReconstructionCmdsDeferredTotal();
-      throw new CommandTargetOverloadedException("No target with capacity " +
-          "available for reconstruction of " + containerInfo.getContainerID());
-    }
-    DatanodeDetails target = selectAndOptionallyExcludeDatanode(
-        rmConf.getReconstructionCommandWeight(), targetWithCmds);
-    sendDatanodeCommand(command, containerInfo, target);
+    globalReconstructionTracker.reserveAndSend(
+        command.getId(),
+        command.getMissingContainerIndexes().size(),
+        containerInfo.getContainerID(),
+        () -> {
+          // Pick the least-loaded target datanode that can accept the command.
+          List<DatanodeDetails> targets = command.getTargetDatanodes();
+          List<Pair<Integer, DatanodeDetails>> targetWithCmds =
+              getAvailableDatanodesForReplication(targets);
+          if (targetWithCmds.isEmpty()) {
+            metrics.incrECReconstructionCmdsDeferredTotal();
+            throw new CommandTargetOverloadedException("No target with 
capacity " +
+                "available for reconstruction of " + 
containerInfo.getContainerID());
+          }
+          DatanodeDetails target = selectAndOptionallyExcludeDatanode(
+              rmConf.getReconstructionCommandWeight(), targetWithCmds);
+          // sendDatanodeCommand schedules one pending ADD per missing index 
and
+          // increments ecReconstructionCmdsSentTotal.
+          sendDatanodeCommand(command, containerInfo, target);
+        });
   }
 
   private DatanodeDetails selectAndOptionallyExcludeDatanode(
@@ -1055,6 +1114,12 @@ ReplicationQueue getQueue() {
 
   @Override
   public void opCompleted(ContainerReplicaOp op, ContainerID containerID, 
boolean timedOut) {
+    if (op.getOpType() == ContainerReplicaOp.PendingOpType.ADD
+        && op.getCommand() != null
+        && op.getCommand().getType() == Type.reconstructECContainersCommand) {
+      globalReconstructionTracker.onFragmentComplete(op.getCommand().getId());
+    }
+
     if (!(timedOut && op.getOpType() == 
ContainerReplicaOp.PendingOpType.DELETE)) {
       // We only care about expired delete ops. All others should be ignored.
       return;
@@ -1262,6 +1327,41 @@ public static class ReplicationManagerConfiguration
     )
     private int containerSampleLimit = 100;
 
+    @Config(key = 
"hdds.scm.replication.decommission.ec.reconstruction.enabled",
+        type = ConfigType.BOOLEAN,
+        defaultValue = "false",
+        reconfigurable = true,
+        tags = { SCM },
+        description = "Reserved for HDDS-15072. When implemented, if true, SCM 
" +
+            "will switch from 1-1 replication to multi-source reconstruction 
for " +
+            "EC containers on decommissioning nodes when the node's load 
exceeds " +
+            "the threshold. Not read by SCM in this release."
+    )
+    private boolean ecDecommissionReconstructionEnabled = false;
+
+    @Config(key = 
"hdds.scm.replication.decommission.ec.reconstruction.load.factor",
+        type = ConfigType.DOUBLE,
+        defaultValue = "0.9",
+        reconfigurable = true,
+        tags = { SCM },
+        description = "Reserved for HDDS-15072. When implemented, the 
threshold " +
+            "factor (between 0 and 1) of a node's replication limit at which " 
+
+            "SCM switches to reconstruction for EC decommission. Not read by " 
+
+            "SCM in this release. Default is 0.9."
+    )
+    private double ecDecommissionReconstructionLoadFactor = 0.9;
+
+    @Config(key = "hdds.scm.replication.reconstruction.global.limit",
+        type = ConfigType.INT,
+        defaultValue = "0",
+        reconfigurable = true,
+        tags = { SCM },
+        description = "A cluster-wide limit to restrict the total number of " +
+            "active EC reconstruction commands across the cluster. A setting " 
+
+            "of zero disables global reconstruction limit checking."
+    )
+    private int reconstructionGlobalLimit = 0;
+
     @Config(key = "hdds.scm.replication.quasi.closed.stuck.best.origin.copies",
         type = ConfigType.INT,
         defaultValue = "3",
@@ -1318,6 +1418,30 @@ public void setDatanodeReplicationLimit(int limit) {
       this.datanodeReplicationLimit = limit;
     }
 
+    public boolean isEcDecommissionReconstructionEnabled() {
+      return ecDecommissionReconstructionEnabled;
+    }
+
+    public void setEcDecommissionReconstructionEnabled(boolean enabled) {
+      this.ecDecommissionReconstructionEnabled = enabled;
+    }
+
+    public double getEcDecommissionReconstructionLoadFactor() {
+      return ecDecommissionReconstructionLoadFactor;
+    }
+
+    public void setEcDecommissionReconstructionLoadFactor(double factor) {
+      this.ecDecommissionReconstructionLoadFactor = factor;
+    }
+
+    public int getReconstructionGlobalLimit() {
+      return reconstructionGlobalLimit;
+    }
+
+    public void setReconstructionGlobalLimit(int limit) {
+      this.reconstructionGlobalLimit = limit;
+    }
+
     public void setMaintenanceRemainingRedundancy(int redundancy) {
       this.maintenanceRemainingRedundancy = redundancy;
     }
@@ -1411,7 +1535,8 @@ public void validate() {
             + " must be >= datanode.reconstruction.weight: "
             + reconstructionCommandWeight);
       }
-      if (inflightReplicationLimitFactor < 0) {
+      if (!Double.isFinite(inflightReplicationLimitFactor)
+          || inflightReplicationLimitFactor < 0) {
         throw new IllegalArgumentException(
             "inflight.limit.factor is set to " + inflightReplicationLimitFactor
                 + " and must be >= 0");
@@ -1421,12 +1546,28 @@ public void validate() {
             "inflight.limit.factor is set to " + inflightReplicationLimitFactor
                 + " and must be <= 1");
       }
+      if (!Double.isFinite(ecDecommissionReconstructionLoadFactor)
+          || ecDecommissionReconstructionLoadFactor < 0) {
+        throw new IllegalArgumentException(
+            "decommission.ec.reconstruction.load.factor is set to "
+                + ecDecommissionReconstructionLoadFactor + " and must be >= 
0");
+      }
+      if (ecDecommissionReconstructionLoadFactor > 1) {
+        throw new IllegalArgumentException(
+            "decommission.ec.reconstruction.load.factor is set to "
+                + ecDecommissionReconstructionLoadFactor + " and must be <= 
1");
+      }
+      if (reconstructionGlobalLimit < 0) {
+        throw new IllegalArgumentException(
+            "reconstruction.global.limit is set to " + 
reconstructionGlobalLimit
+                + " and must be >= 0");
+      }
     }
   }
 
   @Override
   public void notifyStatusChanged() {
-    serviceLock.lock();
+    serviceLifecycleLock.lock();
     try {
       // 1) SCMContext#isLeaderReady returns true.
       // 2) not in safe mode.
@@ -1439,28 +1580,28 @@ public void notifyStatusChanged() {
           // and then back to a leader in a short time, there may be old 
pending
           // Ops in the ContainerReplicaPendingOps table. They are no longer
           // needed as the DN will discard any commands when the term changes.
-          // Therefore we should clear the table so RM starts from a clean
-          // state.
-          containerReplicaPendingOps.clear();
+          // Reset pending ops and reconstruction tracking atomically under the
+          // tracker lock (see GlobalReconstructionTracker#clear).
+          globalReconstructionTracker.clear();
           serviceStatus = ServiceStatus.RUNNING;
         }
       } else {
         serviceStatus = ServiceStatus.PAUSING;
       }
     } finally {
-      serviceLock.unlock();
+      serviceLifecycleLock.unlock();
     }
   }
 
   @Override
   public boolean shouldRun() {
-    serviceLock.lock();
+    serviceLifecycleLock.lock();
     try {
       // If safe mode is off, then this SCMService starts to run with a delay.
       return serviceStatus == ServiceStatus.RUNNING &&
           clock.millis() - lastTimeToBeReadyInMillis >= waitTimeInMillis;
     } finally {
-      serviceLock.unlock();
+      serviceLifecycleLock.unlock();
     }
   }
 
@@ -1586,5 +1727,120 @@ public synchronized boolean notifyNodeStateChange() {
       return false;
     }
   }
+
+  /**
+   * Cluster-wide EC reconstruction slot accounting.
+   *
+   * <p>All mutations to the inflight reconstruction counter and per-command
+   * fragment map go through this class. External callers do not acquire locks.
+   *
+   * <p>Lock policy:
+   * <ul>
+   *   <li>{@link #reserveAndSend} holds {@link #lock} across reserve, map
+   *       registration, and send so {@link #clear()} on leader transition
+   *       cannot run mid-send.</li>
+   *   <li>{@link #clear()} clears {@link ContainerReplicaPendingOps} under
+   *       the same lock so pending ADD ops cannot be scheduled after the
+   *       pending-op table is reset.</li>
+   *   <li>{@link #onFragmentComplete} runs without {@link #lock}; it uses
+   *       {@code ConcurrentHashMap.compute} and tolerates a prior
+   *       {@link #clear()}.</li>
+   * </ul>
+   */
+  private final class GlobalReconstructionTracker {
+
+    private final Lock lock = new ReentrantLock();
+    private final AtomicInteger inflightCount = new AtomicInteger(0);
+    private final Map<Long, Integer> pendingFragmentCountByCommandId =
+        new ConcurrentHashMap<>();
+
+    int getInflightCount() {
+      return inflightCount.get();
+    }
+
+    int getPendingFragmentCount(long cmdId) {
+      return pendingFragmentCountByCommandId.getOrDefault(cmdId, 0);
+    }
+
+    boolean isLimitReached() {
+      int limit = getReconstructionInFlightLimit();
+      return limit > 0 && getInflightCount() >= limit;
+    }
+
+    void reserveAndSend(long cmdId, int fragmentCount, long containerId,
+        ReconstructionSendAction send)
+        throws CommandTargetOverloadedException, NotLeaderException {
+      if (fragmentCount <= 0) {
+        throw new IllegalArgumentException(
+            "Reconstruction command " + cmdId + " for container " + containerId
+                + " has no fragments");
+      }
+      lock.lock();
+      try {
+        if (!tryReserve()) {
+          metrics.incrECReconstructionCmdsDeferredTotal();
+          throw new CommandTargetOverloadedException(
+              "Global reconstruction limit (" + 
getReconstructionInFlightLimit()
+                  + ") reached for container " + containerId);
+        }
+        pendingFragmentCountByCommandId.put(cmdId, fragmentCount);
+        boolean sent = false;
+        try {
+          send.run();
+          sent = true;
+        } finally {
+          if (!sent) {
+            pendingFragmentCountByCommandId.remove(cmdId);
+            releaseSlot();
+          }
+        }
+      } finally {
+        lock.unlock();
+      }
+    }
+
+    void onFragmentComplete(long cmdId) {
+      pendingFragmentCountByCommandId.compute(cmdId, (k, pendingFragments) -> {
+        if (pendingFragments == null) {
+          return null;
+        }
+        if (pendingFragments <= 1) {
+          releaseSlot();
+          return null;
+        }
+        return pendingFragments - 1;
+      });
+    }
+
+    void clear() {
+      lock.lock();
+      try {
+        containerReplicaPendingOps.clear();
+        pendingFragmentCountByCommandId.clear();
+        inflightCount.set(0);
+      } finally {
+        lock.unlock();
+      }
+    }
+
+    private boolean tryReserve() {
+      int limit = getReconstructionInFlightLimit();
+      int current = inflightCount.get();
+      if (limit > 0 && current >= limit) {
+        return false;
+      }
+      inflightCount.incrementAndGet();
+      return true;
+    }
+
+    private void releaseSlot() {
+      inflightCount.updateAndGet(count -> Math.max(0, count - 1));
+    }
+  }
+
+  @FunctionalInterface
+  private interface ReconstructionSendAction {
+    void run() throws CommandTargetOverloadedException, NotLeaderException;
+  }
 }
 
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 bcb3dea9768..032789d180a 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
@@ -44,10 +44,12 @@
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.isNull;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.protobuf.UnsafeByteOperations;
 import java.io.IOException;
@@ -61,8 +63,13 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.hadoop.hdds.client.ECReplicationConfig;
@@ -1739,4 +1746,437 @@ private void mockReplicationCommandCounts(
         });
   }
 
+  @Test
+  public void testReconstructionGlobalLimitDisabledByDefault()
+      throws IOException, NodeNotFoundException {
+    assertEquals(0, rmConf.getReconstructionGlobalLimit());
+    ReplicationManager rm = createReplicationManager();
+    assertEquals(0, rm.getReconstructionInFlightLimit());
+    assertFalse(rm.isReconstructionLimitReached());
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+    ReconstructECContainersCommand cmd = new ReconstructECContainersCommand(
+        1L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(1)),
+        (ECReplicationConfig) repConfig);
+    rm.sendThrottledReconstructionCommand(container, cmd);
+    assertEquals(1, rm.getInflightReconstructionCount());
+    assertFalse(rm.isReconstructionLimitReached());
+  }
+
+  @Test
+  public void testInflightReconstructionLimit() throws IOException, 
NodeNotFoundException {
+    rmConf.setReconstructionGlobalLimit(2);
+    ReplicationManager rm = createReplicationManager();
+    assertEquals(2, rm.getReconstructionInFlightLimit());
+    assertEquals(0, rm.getInflightReconstructionCount());
+    assertFalse(rm.isReconstructionLimitReached());
+
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+
+    // Send one reconstruction command with 2 fragments
+    ReconstructECContainersCommand cmd1 = new ReconstructECContainersCommand(
+        1L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails(),
+            MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(1, 2)),
+        (ECReplicationConfig) repConfig);
+
+    rm.sendThrottledReconstructionCommand(container, cmd1);
+    assertEquals(1, rm.getInflightReconstructionCount());
+    assertFalse(rm.isReconstructionLimitReached());
+
+    // Send another reconstruction command with 1 fragment
+    ReconstructECContainersCommand cmd2 = new ReconstructECContainersCommand(
+        2L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(3)),
+        (ECReplicationConfig) repConfig);
+    rm.sendThrottledReconstructionCommand(container, cmd2);
+    assertEquals(2, rm.getInflightReconstructionCount());
+    assertTrue(rm.isReconstructionLimitReached());
+
+    // Complete one fragment of cmd1
+    ContainerReplicaOp op1 = new ContainerReplicaOp(
+        ContainerReplicaOp.PendingOpType.ADD,
+        cmd1.getTargetDatanodes().get(0), 1, cmd1, Long.MAX_VALUE, 0);
+    rm.opCompleted(op1, container.containerID(), false);
+    // Still 2 because cmd1 is not fully finished
+    assertEquals(2, rm.getInflightReconstructionCount());
+
+    // Complete second fragment of cmd1
+    ContainerReplicaOp op2 = new ContainerReplicaOp(
+        ContainerReplicaOp.PendingOpType.ADD,
+        cmd1.getTargetDatanodes().get(1), 2, cmd1, Long.MAX_VALUE, 0);
+    rm.opCompleted(op2, container.containerID(), false);
+    // Now 1
+    assertEquals(1, rm.getInflightReconstructionCount());
+    assertFalse(rm.isReconstructionLimitReached());
+
+    // Complete cmd2
+    ContainerReplicaOp op3 = new ContainerReplicaOp(
+        ContainerReplicaOp.PendingOpType.ADD,
+        cmd2.getTargetDatanodes().get(0), 3, cmd2, Long.MAX_VALUE, 0);
+    rm.opCompleted(op3, container.containerID(), false);
+    assertEquals(0, rm.getInflightReconstructionCount());
+  }
+
+  @Test
+  public void testSendReconstructionCommandRejectedWhenGlobalLimitReached()
+      throws IOException, NodeNotFoundException {
+    rmConf.setReconstructionGlobalLimit(2);
+    ReplicationManager rm = createReplicationManager();
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+
+    ReconstructECContainersCommand cmd1 = new ReconstructECContainersCommand(
+        1L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(1)),
+        (ECReplicationConfig) repConfig);
+    ReconstructECContainersCommand cmd2 = new ReconstructECContainersCommand(
+        2L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(2)),
+        (ECReplicationConfig) repConfig);
+    ReconstructECContainersCommand cmd3 = new ReconstructECContainersCommand(
+        3L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(3)),
+        (ECReplicationConfig) repConfig);
+
+    rm.sendThrottledReconstructionCommand(container, cmd1);
+    rm.sendThrottledReconstructionCommand(container, cmd2);
+    assertEquals(2, rm.getInflightReconstructionCount());
+
+    assertThrows(CommandTargetOverloadedException.class,
+        () -> rm.sendThrottledReconstructionCommand(container, cmd3));
+    assertEquals(2, rm.getInflightReconstructionCount());
+  }
+
+  @Test
+  public void testNotifyStatusChangedClearsReconstructionCounters()
+      throws IOException, NodeNotFoundException {
+    rmConf.setReconstructionGlobalLimit(10);
+    ReplicationManager rm = createReplicationManager();
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+    enableProcessAll();
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+    ReconstructECContainersCommand cmd = new ReconstructECContainersCommand(
+        1L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(1)),
+        (ECReplicationConfig) repConfig);
+    rm.sendThrottledReconstructionCommand(container, cmd);
+    assertEquals(1, rm.getInflightReconstructionCount());
+
+    when(scmContext.isLeaderReady()).thenReturn(false);
+    rm.notifyStatusChanged();
+    when(scmContext.isLeaderReady()).thenReturn(true);
+    when(scmContext.isInSafeMode()).thenReturn(false);
+    rm.notifyStatusChanged();
+
+    assertEquals(0, rm.getInflightReconstructionCount());
+  }
+
+  @Test
+  public void testInflightReconstructionCountNotNegativeAfterFailoverClear()
+      throws IOException, NodeNotFoundException {
+    rmConf.setReconstructionGlobalLimit(10);
+    ReplicationManager rm = createReplicationManager();
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+    enableProcessAll();
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+    ReconstructECContainersCommand cmd = new ReconstructECContainersCommand(
+        1L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails(),
+            MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(1, 2)),
+        (ECReplicationConfig) repConfig);
+    rm.sendThrottledReconstructionCommand(container, cmd);
+    assertEquals(1, rm.getInflightReconstructionCount());
+
+    when(scmContext.isLeaderReady()).thenReturn(false);
+    rm.notifyStatusChanged();
+    when(scmContext.isLeaderReady()).thenReturn(true);
+    when(scmContext.isInSafeMode()).thenReturn(false);
+    rm.notifyStatusChanged();
+    assertEquals(0, rm.getInflightReconstructionCount());
+
+    ContainerReplicaOp op1 = new ContainerReplicaOp(
+        ContainerReplicaOp.PendingOpType.ADD,
+        cmd.getTargetDatanodes().get(0), 1, cmd, Long.MAX_VALUE, 0);
+    ContainerReplicaOp op2 = new ContainerReplicaOp(
+        ContainerReplicaOp.PendingOpType.ADD,
+        cmd.getTargetDatanodes().get(1), 2, cmd, Long.MAX_VALUE, 0);
+
+    rm.opCompleted(op1, container.containerID(), false);
+    assertEquals(0, rm.getInflightReconstructionCount());
+    rm.opCompleted(op2, container.containerID(), false);
+    assertEquals(0, rm.getInflightReconstructionCount());
+    rm.opCompleted(op1, container.containerID(), false);
+    assertEquals(0, rm.getInflightReconstructionCount());
+  }
+
+  @Test
+  public void testReconstructionConfigValidation() {
+    ReplicationManager.ReplicationManagerConfiguration config =
+        new ReplicationManager.ReplicationManagerConfiguration();
+
+    config.setEcDecommissionReconstructionLoadFactor(-0.1);
+    assertThrows(IllegalArgumentException.class, config::validate);
+
+    config.setEcDecommissionReconstructionLoadFactor(Double.NaN);
+    assertThrows(IllegalArgumentException.class, config::validate);
+
+    config.setEcDecommissionReconstructionLoadFactor(1.1);
+    assertThrows(IllegalArgumentException.class, config::validate);
+
+    config.setEcDecommissionReconstructionLoadFactor(0.9);
+    config.setReconstructionGlobalLimit(-1);
+    assertThrows(IllegalArgumentException.class, config::validate);
+
+    config.setReconstructionGlobalLimit(0);
+    config.setInflightReplicationLimitFactor(Double.NaN);
+    assertThrows(IllegalArgumentException.class, config::validate);
+
+    config.setInflightReplicationLimitFactor(0.75);
+    config.validate();
+  }
+
+  @Test
+  public void testReconstructionGlobalLimitEnforcedConcurrently()
+      throws InterruptedException, NodeNotFoundException, IOException {
+    rmConf.setReconstructionGlobalLimit(2);
+    ReplicationManager rm = createReplicationManager();
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+    int threadCount = 8;
+    int attemptsPerThread = 10;
+    ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+    CountDownLatch startLatch = new CountDownLatch(1);
+    CountDownLatch doneLatch = new CountDownLatch(threadCount);
+    AtomicInteger accepted = new AtomicInteger();
+    AtomicInteger rejected = new AtomicInteger();
+
+    for (int t = 0; t < threadCount; t++) {
+      final long commandIdBase = t + 100;
+      executor.submit(() -> {
+        try {
+          startLatch.await();
+          for (int i = 0; i < attemptsPerThread; i++) {
+            ReconstructECContainersCommand cmd = new 
ReconstructECContainersCommand(
+                commandIdBase + i, Collections.emptyList(),
+                ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails()),
+                
ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(1)),
+                (ECReplicationConfig) repConfig);
+            try {
+              rm.sendThrottledReconstructionCommand(container, cmd);
+              accepted.incrementAndGet();
+            } catch (CommandTargetOverloadedException | NotLeaderException e) {
+              rejected.incrementAndGet();
+            }
+          }
+        } catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+        } finally {
+          doneLatch.countDown();
+        }
+      });
+    }
+
+    try {
+      startLatch.countDown();
+      assertTrue(doneLatch.await(30, TimeUnit.SECONDS));
+      assertEquals(2, rm.getInflightReconstructionCount());
+      assertEquals(2, accepted.get());
+      assertTrue(rejected.get() > 0);
+    } finally {
+      executor.shutdown();
+    }
+  }
+
+  @Test
+  public void testReconstructionMapRegisteredBeforePendingOpsScheduled()
+      throws IOException, NodeNotFoundException {
+    rmConf.setReconstructionGlobalLimit(10);
+    ReplicationManager rm = createReplicationManager();
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+    ReconstructECContainersCommand cmd = new ReconstructECContainersCommand(
+        1L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails(),
+            MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(1, 2)),
+        (ECReplicationConfig) repConfig);
+
+    rm.sendThrottledReconstructionCommand(container, cmd);
+    assertEquals(2, rm.getReconstructionPendingFragmentCount(cmd.getId()));
+    assertEquals(1, rm.getInflightReconstructionCount());
+
+    ContainerReplicaOp op = new ContainerReplicaOp(
+        ContainerReplicaOp.PendingOpType.ADD,
+        cmd.getTargetDatanodes().get(0), 1, cmd, Long.MAX_VALUE, 0);
+    rm.opCompleted(op, container.containerID(), false);
+    assertEquals(1, rm.getReconstructionPendingFragmentCount(cmd.getId()));
+    assertEquals(1, rm.getInflightReconstructionCount());
+  }
+
+  @Test
+  public void testInflightReconstructionCountDecrementsWhenAddOpExpires()
+      throws IOException, NodeNotFoundException {
+    rmConf.setReconstructionGlobalLimit(10);
+    ReplicationManager rm = createReplicationManager();
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+    ReconstructECContainersCommand cmd = new ReconstructECContainersCommand(
+        1L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails(),
+            MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(1, 2)),
+        (ECReplicationConfig) repConfig);
+
+    rm.sendThrottledReconstructionCommand(container, cmd);
+    assertEquals(2, rm.getReconstructionPendingFragmentCount(cmd.getId()));
+    assertEquals(1, rm.getInflightReconstructionCount());
+
+    ContainerReplicaOp firstAdd = new ContainerReplicaOp(
+        ContainerReplicaOp.PendingOpType.ADD,
+        cmd.getTargetDatanodes().get(0), 1, cmd, Long.MAX_VALUE, 0);
+    rm.opCompleted(firstAdd, container.containerID(), true);
+    assertEquals(1, rm.getReconstructionPendingFragmentCount(cmd.getId()));
+    assertEquals(1, rm.getInflightReconstructionCount());
+
+    ContainerReplicaOp secondAdd = new ContainerReplicaOp(
+        ContainerReplicaOp.PendingOpType.ADD,
+        cmd.getTargetDatanodes().get(1), 2, cmd, Long.MAX_VALUE, 0);
+    rm.opCompleted(secondAdd, container.containerID(), true);
+    assertEquals(0, rm.getReconstructionPendingFragmentCount(cmd.getId()));
+    assertEquals(0, rm.getInflightReconstructionCount());
+  }
+
+  @Test
+  public void testReconstructionWithNoFragmentsDoesNotReserveSlot()
+      throws NodeNotFoundException, IOException {
+    rmConf.setReconstructionGlobalLimit(10);
+    ReplicationManager rm = createReplicationManager();
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+    ReconstructECContainersCommand cmd = new ReconstructECContainersCommand(
+        1L, Collections.emptyList(),
+        Collections.emptyList(),
+        ECUnderReplicationHandler.integers2ByteString(Collections.emptyList()),
+        (ECReplicationConfig) repConfig);
+
+    assertThrows(IllegalArgumentException.class,
+        () -> rm.sendThrottledReconstructionCommand(container, cmd));
+    assertEquals(0, rm.getInflightReconstructionCount());
+    assertEquals(0, rm.getReconstructionPendingFragmentCount(cmd.getId()));
+    verify(nodeManager, never()).addDatanodeCommand(any(), any());
+  }
+
+  @Test
+  public void testReconstructionSlotReleasedAndMapClearedOnSendFailure()
+      throws NodeNotFoundException, IOException {
+    rmConf.setReconstructionGlobalLimit(10);
+    ReplicationManager rm = createReplicationManager();
+
+    int limit = rm.getConfig().getDatanodeReplicationLimit();
+    int reconstructionWeight = rm.getConfig().getReconstructionCommandWeight();
+    int reconstructionCount = 2;
+    int replicationCount = limit - reconstructionCount * reconstructionWeight;
+    mockReplicationCommandCounts(any -> replicationCount, any -> 
reconstructionCount);
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+    ReconstructECContainersCommand cmd = createReconstructionCommand(
+        container, MockDatanodeDetails.randomDatanodeDetails(),
+        MockDatanodeDetails.randomDatanodeDetails());
+
+    assertThrows(CommandTargetOverloadedException.class,
+        () -> rm.sendThrottledReconstructionCommand(container, cmd));
+    assertEquals(0, rm.getInflightReconstructionCount());
+    assertEquals(0, rm.getReconstructionPendingFragmentCount(cmd.getId()));
+  }
+
+  @Test
+  public void testNotifyStatusChangedBlocksUntilReconstructionSendCompletes()
+      throws Exception {
+    rmConf.setReconstructionGlobalLimit(10);
+    ReplicationManager rm = createReplicationManager();
+    mockReplicationCommandCounts(dn -> 0, dn -> 0);
+    enableProcessAll();
+
+    CountDownLatch sendReachedAddCommand = new CountDownLatch(1);
+    CountDownLatch releaseSend = new CountDownLatch(1);
+    doAnswer(invocation -> {
+      sendReachedAddCommand.countDown();
+      try {
+        releaseSend.await();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        throw new RuntimeException(e);
+      }
+      return null;
+    }).when(nodeManager).addDatanodeCommand(any(), any());
+
+    ContainerInfo container = ReplicationTestUtil.createContainerInfo(
+        repConfig, 1, HddsProtos.LifeCycleState.CLOSED, 10, 20);
+    ReconstructECContainersCommand cmd = new ReconstructECContainersCommand(
+        1L, Collections.emptyList(),
+        ImmutableList.of(MockDatanodeDetails.randomDatanodeDetails()),
+        ECUnderReplicationHandler.integers2ByteString(ImmutableList.of(1)),
+        (ECReplicationConfig) repConfig);
+
+    Thread sender = new Thread(() -> {
+      try {
+        rm.sendThrottledReconstructionCommand(container, cmd);
+      } catch (CommandTargetOverloadedException | NotLeaderException e) {
+        throw new RuntimeException(e);
+      }
+    });
+    sender.start();
+    assertTrue(sendReachedAddCommand.await(30, TimeUnit.SECONDS));
+    assertEquals(1, rm.getInflightReconstructionCount());
+    assertEquals(1, rm.getReconstructionPendingFragmentCount(cmd.getId()));
+
+    AtomicBoolean notifyCompleted = new AtomicBoolean(false);
+    Thread notifyThread = new Thread(() -> {
+      rm.notifyStatusChanged();
+      notifyCompleted.set(true);
+    });
+    notifyThread.start();
+    notifyThread.join(500);
+    assertFalse(notifyCompleted.get(),
+        "notifyStatusChanged must wait for in-flight reconstruction send");
+    assertEquals(1, rm.getInflightReconstructionCount());
+
+    releaseSend.countDown();
+    sender.join(TimeUnit.SECONDS.toMillis(30));
+    notifyThread.join(TimeUnit.SECONDS.toMillis(30));
+    assertTrue(notifyCompleted.get());
+    
assertTrue(containerReplicaPendingOps.getPendingOps(container.containerID())
+        .isEmpty());
+    assertEquals(0, rm.getInflightReconstructionCount());
+  }
 }


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

Reply via email to