smengcl commented on code in PR #11307:
URL: https://github.com/apache/ozone/pull/11307#discussion_r4319110499


##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/block/SCMDeletedBlockTransactionStatusManager.java:
##########
@@ -628,6 +664,63 @@ public void 
commitTransactions(List<DeleteBlockTransactionResult> transactionRes
     }
   }
 
+  private boolean hasPendingAddReplica(ContainerID containerId) {
+    return 
containerReplicaPendingOpsSupplier.get().getPendingOps(containerId).stream()
+        .anyMatch(op -> op.getOpType() == 
ContainerReplicaOp.PendingOpType.ADD);
+  }
+
+  /**
+   * Whether a transaction whose purge {@link #commitTransactions} deferred 
can be purged without another ACK:
+   * no replica ADD is in flight and every current replica has already 
committed it. A deferral caused only by
+   * an in-flight copy that later fails or expires is otherwise never retried, 
since no replica is left to ACK
+   * a resend and the entry would stay in {@link #transactionToDNsCommitMap} 
indefinitely.
+   */
+  boolean isPurgeableWithoutAck(long txID, ContainerID containerId) {
+    final Set<DatanodeID> committedDns = transactionToDNsCommitMap.get(txID);
+    if (committedDns == null || committedDns.isEmpty() || 
hasPendingAddReplica(containerId)) {
+      return false;
+    }
+    try {
+      final Set<ContainerReplica> replicas = 
containerManager.getContainerReplicas(containerId);
+      return !replicas.isEmpty()
+          && min(replicas.size(), committedDns.size())
+              >= 
containerManager.getContainer(containerId).getReplicationConfig().getRequiredNodes()
+          && replicas.stream().map(r -> 
r.getDatanodeDetails().getID()).allMatch(committedDns::contains);
+    } catch (ContainerNotFoundException e) {
+      return false;
+    }
+  }
+
+  /**
+   * Checks that every replica in the pre-purge snapshot has committed the 
deletion. A missing snapshot means the
+   * container has an in-flight replica ADD or could not be looked up, so the 
transaction is kept for resend.
+   *
+   * <p>Not covered: a copy exported before the deletion was recorded whose 
pending ADD expired before its replica

Review Comment:
   Optional, not blocking. SCM expires the ADD after 
`hdds.scm.replication.event.timeout` (12m), the datanode only has to start 
within 12m minus the 6m offset, and the import itself is not bounded (5 GB at 
10 MB/s takes over 8 minutes).
   
   Purging only when the container is HEALTHY, as `getTransactions` already 
requires via `checkInadequateReplica`, would cover copies made for under 
replication, since the container stays under replicated until a copy registers. 
HDDS-17152 would then only need the case of a late copy registering after a 
replacement copy.



##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/block/SCMDeletedBlockTransactionStatusManager.java:
##########
@@ -628,6 +664,63 @@ public void 
commitTransactions(List<DeleteBlockTransactionResult> transactionRes
     }
   }
 
+  private boolean hasPendingAddReplica(ContainerID containerId) {
+    return 
containerReplicaPendingOpsSupplier.get().getPendingOps(containerId).stream()
+        .anyMatch(op -> op.getOpType() == 
ContainerReplicaOp.PendingOpType.ADD);
+  }
+
+  /**
+   * Whether a transaction whose purge {@link #commitTransactions} deferred 
can be purged without another ACK:
+   * no replica ADD is in flight and every current replica has already 
committed it. A deferral caused only by
+   * an in-flight copy that later fails or expires is otherwise never retried, 
since no replica is left to ACK
+   * a resend and the entry would stay in {@link #transactionToDNsCommitMap} 
indefinitely.
+   */
+  boolean isPurgeableWithoutAck(long txID, ContainerID containerId) {
+    final Set<DatanodeID> committedDns = transactionToDNsCommitMap.get(txID);
+    if (committedDns == null || committedDns.isEmpty() || 
hasPendingAddReplica(containerId)) {
+      return false;
+    }
+    try {
+      final Set<ContainerReplica> replicas = 
containerManager.getContainerReplicas(containerId);
+      return !replicas.isEmpty()
+          && min(replicas.size(), committedDns.size())
+              >= 
containerManager.getContainer(containerId).getReplicationConfig().getRequiredNodes()
+          && replicas.stream().map(r -> 
r.getDatanodeDetails().getID()).allMatch(committedDns::contains);

Review Comment:
   Same check as line 675 to 681. Can both use one helper? 
`!replicas.isEmpty()` is redundant since `min(0, n) >= requiredNodes` is false. 
Pls also note why replicas are re-read here instead of reusing the set from 
`getTransactions`: that set was read before the pending ADD check.
   
   ```suggestion
         // Read after the pending ADD check: an earlier read can miss a 
replica whose ADD completed in between.
         final Set<ContainerReplica> replicas = 
containerManager.getContainerReplicas(containerId);
         return 
allReplicasCommitted(containerManager.getContainer(containerId), replicas, 
committedDns);
   ```
   
   ```diff
   +  private static boolean allReplicasCommitted(ContainerInfo container, 
Set<ContainerReplica> replicas,
   +      Set<DatanodeID> committedDns) {
   +    return min(replicas.size(), committedDns.size()) >= 
container.getReplicationConfig().getRequiredNodes()
   +        && replicas.stream().map(r -> 
r.getDatanodeDetails().getID()).allMatch(committedDns::contains);
   +  }
   ```



##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/block/SCMDeletedBlockTransactionStatusManager.java:
##########
@@ -619,6 +624,37 @@ public void 
commitTransactions(List<DeleteBlockTransactionResult> transactionRes
             transactionResult.getTxID(), e);
       }
     }
+    // A container copy made by ReplicationManager misses this deletion only 
if it was exported before the source
+    // recorded the deletion: the datanode serializes export and recording 
under the container lock, and ACKs only
+    // after recording. Such a copy was therefore scheduled before the ACKs 
above, so its replica ADD is either still
+    // pending here or already completed. ContainerStateManager registers a 
replica and clears its pending ADD under
+    // the same container lock, so the pending ADD is checked before the 
replica set is read: an ADD that is no
+    // longer pending is already in that replica set. A copy scheduled after 
this check is exported after the
+    // deletion was recorded and deletes the blocks without a resend.
+    final Map<ContainerID, List<DatanodeID>> replicaSnapshots = new 
HashMap<>();
+    for (ContainerID containerId : new 
HashSet<>(purgeCandidateContainers.values())) {
+      if (hasPendingAddReplica(containerId)) {
+        LOG.info("Deferring purge of transactions for container {}: a replica 
is being added to it.", containerId);
+        continue;
+      }
+      try {
+        replicaSnapshots.put(containerId, 
containerManager.getContainerReplicas(containerId).stream()
+            .map(ContainerReplica::getDatanodeDetails)
+            .map(DatanodeDetails::getID)
+            .collect(Collectors.toList()));
+      } catch (ContainerNotFoundException e) {
+        LOG.warn("Could not re-validate replicas for container {} before 
purge; deferring its transactions.",
+            containerId, e);
+      }
+    }
+    txIDsToBeDeleted.removeIf(txID -> !isSafeToPurge(txID, 
purgeCandidateContainers.get(txID), replicaSnapshots));
+    for (Long txID : txIDsToBeDeleted) {
+      transactionToDNsCommitMap.remove(txID);
+      transactionToRetryCountMap.remove(txID);
+      if (LOG.isDebugEnabled()) {
+        LOG.debug("Purging txId: {} from block deletion log", txID);
+      }
+    }

Review Comment:
   This second pass isn't needed if the pending ADD check moves before the 
in-loop `getContainerReplicas()`. An ADD that is not pending at the check has 
either registered (so the read sees it) or was scheduled after it, when its 
source had already recorded the deletion. That drops 
`purgeCandidateContainers`, `replicaSnapshots`, `isSafeToPurge` and this block, 
plus `testCommitTransactionsDefersPurgeForReplicaAddedDuringCommit` and 
`testCommitTransactionsReadsEachContainerOnceAndDefersConsistently`.
   
   ```diff
            dnsWithCommittedTxn.add(dnId);
   +        if (hasPendingAddReplica(containerId)) {
   +          continue;
   +        }
            final ContainerInfo container =
   @@
              if (dnsWithCommittedTxn.containsAll(containerDns)) {
   -            purgeCandidateContainers.put(txID, containerId);
   +            transactionToDNsCommitMap.remove(txID);
   +            transactionToRetryCountMap.remove(txID);
                txIDsToBeDeleted.add(txID);
   ```



##########
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/block/TestDeletedBlockLog.java:
##########
@@ -412,6 +419,199 @@ public void testCommitTransactions() throws Exception {
     assertEquals(0, blocks.size());
   }
 
+  // The tests below exercise SCM's purge decision in 
SCMDeletedBlockTransactionStatusManager#commitTransactions.
+  // TestKeyValueContainer#testExportCarriesOnlyDeletionsRecordedBeforeExport 
covers the datanode side: a copy
+  // exported before the source recorded the deletion keeps the blocks with no 
pending-delete record, while a copy
+  // exported later carries it. These tests verify that SCM keeps the 
transaction for resend whenever it cannot rule
+  // out a replica of the first kind.
+  @Test
+  public void testCommitTransactionsDefersPurgeForReplicaAddedDuringCommit()
+      throws Exception {
+    deletedBlockLog.setScmCommandTimeoutMs(Long.MAX_VALUE);
+    addTransactions(generateData(1), true);
+    mockContainerHealthResult(true);
+    List<DeletedBlocksTransaction> blocks =
+        getTransactions(BLOCKS_PER_TXN * THREE);
+    assertEquals(THREE, blocks.size());
+    // All THREE entries above are the same logical transaction, one per 
replica DN;
+    // commit it one DN at a time to control exactly how many times
+    // containerManager.getContainerReplicas() is invoked per commit call.
+    List<DeletedBlocksTransaction> singleTxn =
+        Collections.singletonList(blocks.get(0));
+
+    final ContainerID cid = 
ContainerID.valueOf(blocks.get(0).getContainerID());
+    final Set<ContainerReplica> originalReplicas = new 
HashSet<>(replicas.get(cid));
+
+    // Simulate ReplicationManager landing a new, never-acked replica of the 
container
+    // in the window between the ack-set check and the durable purge: the 
first three
+    // getContainerReplicas() calls (the in-loop decision for each of the 
three commits
+    // below) see the original replica set, while every call after that (the 
pre-purge
+    // recheck) sees a fourth replica that never received or acknowledged the 
deletion.
+    ContainerReplica newReplica = ContainerReplica.newBuilder()
+        .setContainerID(cid)
+        .setContainerState(ContainerReplicaProto.State.CLOSED)
+        
.setDatanodeDetails(DatanodeDetails.newBuilder().setUuid(UUID.randomUUID()).build())
+        .build();
+    Set<ContainerReplica> grownReplicas = new HashSet<>(originalReplicas);
+    grownReplicas.add(newReplica);
+    when(containerManager.getContainerReplicas(cid)).thenReturn(
+        originalReplicas, originalReplicas, originalReplicas, grownReplicas);
+
+    commitTransactions(singleTxn, dnList.get(0));
+    commitTransactions(singleTxn, dnList.get(1));
+    commitTransactions(singleTxn, dnList.get(2));
+
+    // The transaction must remain outstanding: the new replica has not 
acknowledged the
+    // delete, so it may be a copy exported before the source recorded the 
deletion.
+    assertEquals(1, deletedBlockLog.getNumOfValidTransactions(),
+        "transaction was purged although a never-acked replica exists");
+    assertEquals(1, deletedBlockLog.getTransactionToDNsCommitMapSize(),
+        "commit tracking was dropped although the transaction was not purged");
+  }
+
+  @Test
+  public void 
testCommitTransactionsReadsEachContainerOnceAndDefersConsistently()
+      throws Exception {
+    deletedBlockLog.setScmCommandTimeoutMs(Long.MAX_VALUE);
+    mockContainerHealthResult(true);
+
+    // Two independent delete-block transactions against the SAME container (a 
container
+    // commonly accumulates several separate pending deletions over time), 
each already
+    // acknowledged by two of its three replicas before the decisive final ack 
below.
+    Map<Long, List<DeletedBlock>> firstTxBlocks = generateData(1);
+    long containerID = firstTxBlocks.keySet().iterator().next();
+    List<DeletedBlock> secondTxBlocks = firstTxBlocks.get(containerID).stream()
+        .map(b -> new DeletedBlock(new BlockID(containerID, 
b.getBlockID().getLocalID() + BLOCKS_PER_TXN),
+            b.getSize(), b.getReplicatedSize(), b.getSizePerReplica()))
+        .collect(Collectors.toList());
+    addTransactions(firstTxBlocks, true);
+    addTransactions(Collections.singletonMap(containerID, secondTxBlocks), 
true);
+
+    List<DeletedBlocksTransaction> blocks = getTransactions(2 * BLOCKS_PER_TXN 
* THREE);
+    assertEquals(2 * THREE, blocks.size());
+    Collection<DeletedBlocksTransaction> bothTxns = blocks.stream()
+        .collect(Collectors.toMap(DeletedBlocksTransaction::getTxID, t -> t, 
(a, b) -> a))
+        .values();
+    assertEquals(2, bothTxns.size());
+
+    final ContainerID cid = ContainerID.valueOf(containerID);
+    final Set<ContainerReplica> originalReplicas = new 
HashSet<>(replicas.get(cid));
+
+    commitTransactions(bothTxns, dnList.get(0));
+    commitTransactions(bothTxns, dnList.get(1));
+
+    // Simulate ReplicationManager landing a new, never-acked replica exactly 
as the
+    // decisive final ack arrives: the ack-set check for each of the two txIDs 
below sees
+    // the original replica set, while the pre-purge snapshot sees a fourth 
replica that
+    // never received or acknowledged either deletion.
+    ContainerReplica newReplica = ContainerReplica.newBuilder()
+        .setContainerID(cid)
+        .setContainerState(ContainerReplicaProto.State.CLOSED)
+        
.setDatanodeDetails(DatanodeDetails.newBuilder().setUuid(UUID.randomUUID()).build())
+        .build();
+    Set<ContainerReplica> grownReplicas = new HashSet<>(originalReplicas);
+    grownReplicas.add(newReplica);
+    when(containerManager.getContainerReplicas(cid)).thenReturn(
+        originalReplicas, originalReplicas, grownReplicas);
+    clearInvocations(containerManager);
+
+    commitTransactions(bothTxns, dnList.get(2));
+
+    // Two ack-set checks (one per txID) plus exactly one deduplicated 
pre-purge snapshot
+    // shared by both txIDs of this container: three calls total, not four.
+    verify(containerManager, times(3)).getContainerReplicas(cid);

Review Comment:
   nit: drop this `verify`. It pins the call count, which the `thenReturn(...)` 
stubbing above already depends on



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

To unsubscribe, e-mail: [email protected]

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


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

Reply via email to