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


##########
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/block/TestDeletedBlockLog.java:
##########
@@ -412,6 +419,198 @@ public void testCommitTransactions() throws Exception {
     assertEquals(0, blocks.size());
   }
 
+  // The tests below exercise SCM's purge decision in 
SCMDeletedBlockTransactionStatusManager#commitTransactions.
+  // A replica added by a real container copy may already carry the 
pending-delete record and self-heal on the
+  // datanode; these tests only verify that SCM keeps the transaction for 
resend whenever it cannot rule out a
+  // replica that missed the deletion.
+  @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 never received 
or
+    // acknowledged the delete, so purging now would leak its blocks forever.
+    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);
+
+    // Both transactions must stay outstanding and get the SAME verdict: a 
never-acked
+    // replica now exists, so purging either would leak its blocks with no way 
to resend.
+    assertEquals(2, deletedBlockLog.getNumOfValidTransactions(),
+        "both transactions for the container must stay outstanding once a 
never-acked "
+            + "replica exists");
+    assertEquals(2, deletedBlockLog.getTransactionToDNsCommitMapSize(),
+        "commit tracking was dropped although neither transaction was purged");
+  }
+
+  @Test
+  public void 
testCommitTransactionsDefersPurgeOnlyForContainerWithPendingAdd() throws 
Exception {
+    deletedBlockLog.setScmCommandTimeoutMs(Long.MAX_VALUE);
+    mockContainerHealthResult(true);
+    addTransactions(generateData(2), true);
+    List<DeletedBlocksTransaction> blocks = getTransactions(2 * BLOCKS_PER_TXN 
* THREE);
+    Collection<DeletedBlocksTransaction> bothTxns = blocks.stream()
+        .collect(Collectors.toMap(DeletedBlocksTransaction::getTxID, t -> t, 
(a, b) -> a))
+        .values();
+    assertEquals(2, bothTxns.size());
+
+    // A copy of the first container is in flight; the other container has 
none. The pending op does not
+    // depend on the order in which the candidates are checked.
+    final ContainerID pendingCid = 
ContainerID.valueOf(bothTxns.iterator().next().getContainerID());
+    pendingOps.scheduleAddReplica(pendingCid, 
DatanodeDetails.newBuilder().setUuid(UUID.randomUUID()).build(),
+        0, mock(SCMCommand.class), Long.MAX_VALUE, 0L, 0L);
+
+    commitTransactions(bothTxns, dnList.get(0), dnList.get(1), dnList.get(2));
+
+    assertEquals(1, deletedBlockLog.getNumOfValidTransactions(),
+        "only the container without an in-flight replica ADD may be purged");
+    assertEquals(1, deletedBlockLog.getTransactionToDNsCommitMapSize());
+  }
+
+  @Test
+  public void testCommitTransactionsPurgesAfterCopiedReplicaAcknowledges() 
throws Exception {
+    deletedBlockLog.setScmCommandTimeoutMs(Long.MAX_VALUE);
+    addTransactions(generateData(1), true);
+    mockContainerHealthResult(true);
+    List<DeletedBlocksTransaction> blocks = getTransactions(BLOCKS_PER_TXN * 
THREE);
+    List<DeletedBlocksTransaction> singleTxn = 
Collections.singletonList(blocks.get(0));
+    final ContainerID cid = 
ContainerID.valueOf(blocks.get(0).getContainerID());
+
+    // A copy is scheduled (its export may predate the source recording the 
deletion) and all original replicas

Review Comment:
   This test only schedules a mocked ADD and mutates SCM's replica set. It does 
not export a container before the source records the deletion, import that 
copy, or process its container report.
   
   Because pending-delete records are copied with the container DB, can we 
cover that ordering explicitly? Without it, the test verifies transaction 
retention but not the claimed permanent block leak.



##########
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/block/SCMDeletedBlockTransactionStatusManager.java:
##########
@@ -619,6 +624,35 @@ public void 
commitTransactions(List<DeleteBlockTransactionResult> transactionRes
             transactionResult.getTxID(), e);
       }
     }
+    // A container copy (ReplicationManager) can add a replica after the 
ack-set check above but before the
+    // durable removal below; purging then would leave that replica's blocks 
undeleted with no way to resend.
+    // Each candidate container is therefore checked once: first for an 
in-flight replica ADD, then against a
+    // fresh replica set. The order matters: ContainerStateManager registers a 
replica and clears its pending
+    // ADD under the same container lock, so an ADD that is no longer pending 
is already in the replica set.
+    final Map<ContainerID, List<DatanodeID>> replicaSnapshots = new 
HashMap<>();
+    for (ContainerID containerId : new 
HashSet<>(purgeCandidateContainers.values())) {
+      if (hasPendingAddReplica(containerId)) {

Review Comment:
   The pending-operation check and replica snapshot are separate observations. 
If the ADD expires or is scheduled after this check, but the datanode reports 
the new replica after the snapshot and before removeTransactions(), this 
transaction can still be purged before that replica acknowledges the deletion. 
The new Javadoc also explicitly leaves this case uncovered below.
   
   Can we coordinate purge with replica registration/finalization, or reconcile 
delete progress when a late replica reports?



-- 
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