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

ChenSammi 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 9ac58d9f16c HDDS-16142. Fix double-decrement of deleted-block summary 
on Ratis timeout (#11043)
9ac58d9f16c is described below

commit 9ac58d9f16cca23374cc5ec1c01fce1659f36feb
Author: Priyesh Karatha <[email protected]>
AuthorDate: Wed Aug 19 20:25:37 2026 +0530

    HDDS-16142. Fix double-decrement of deleted-block summary on Ratis timeout 
(#11043)
---
 .../SCMDeletedBlockTransactionStatusManager.java   |  22 +++
 .../hadoop/hdds/scm/block/TestDeletedBlockLog.java | 168 +++++++++++++++++++++
 2 files changed, 190 insertions(+)

diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/block/SCMDeletedBlockTransactionStatusManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/block/SCMDeletedBlockTransactionStatusManager.java
index 43405ed90f7..3df8622608f 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/block/SCMDeletedBlockTransactionStatusManager.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/block/SCMDeletedBlockTransactionStatusManager.java
@@ -52,6 +52,7 @@
 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.exceptions.SCMException;
 import org.apache.hadoop.hdds.upgrade.HDDSLayoutFeature;
 import org.apache.hadoop.hdds.utils.db.Table;
 import org.apache.hadoop.ozone.container.upgrade.VersionedDatanodeFeatures;
@@ -468,6 +469,9 @@ public void 
addTransactions(ArrayList<DeletedBlocksTransaction> txList) throws I
       try {
         deletedBlockLogStateManager.addTransactionsToDB(txList, getSummary());
       } catch (IOException e) {
+        if (isRatisTimeout(e)) {
+          throw e;
+        }
         // Revert the in-memory changes if the DB update fails
         for (DeletedBlocksTransaction tx: txList) {
           if (tx.hasTotalBlockSize()) {
@@ -482,6 +486,21 @@ public void 
addTransactions(ArrayList<DeletedBlocksTransaction> txList) throws I
     deletedBlockLogStateManager.addTransactionsToDB(txList);
   }
 
+  private static boolean isRatisTimeout(IOException e) {
+    if (!(e instanceof SCMException)) {
+      return false;
+    }
+    SCMException.ResultCodes code = ((SCMException) e).getResult();
+    if (code == SCMException.ResultCodes.TIMEOUT) {
+      return true;
+    }
+    if (code == SCMException.ResultCodes.INTERNAL_ERROR) {
+      Throwable cause = e.getCause();
+      return cause instanceof InterruptedException;
+    }
+    return false;
+  }
+
   private void rollbackDeletedBlocksSummary(TxBlockInfo txBlockInfo) {
     totalTxCount.addAndGet(1);
     totalBlockCount.addAndGet(txBlockInfo.getTotalBlockCount());
@@ -516,6 +535,9 @@ public void removeTransactions(ArrayList<Long> txIDs) 
throws IOException {
       try {
         deletedBlockLogStateManager.removeTransactionsFromDB(txIDs, 
getSummary());
       } catch (IOException e) {
+        if (isRatisTimeout(e)) {
+          throw e;
+        }
         // Revert the in-memory changes if the DB update fails
         for (TxBlockInfo txBlockInfo : removedTxBlockInfos) {
           txSizeMap.put(txBlockInfo.getTxId(), txBlockInfo);
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/block/TestDeletedBlockLog.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/block/TestDeletedBlockLog.java
index 2bfa5f379b7..a418e333424 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/block/TestDeletedBlockLog.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/block/TestDeletedBlockLog.java
@@ -21,11 +21,14 @@
 import static org.apache.hadoop.ozone.common.BlockGroup.SIZE_NOT_AVAILABLE;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.params.provider.Arguments.arguments;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -72,6 +75,7 @@
 import org.apache.hadoop.hdds.scm.container.ContainerReplica;
 import org.apache.hadoop.hdds.scm.container.replication.ContainerHealthResult;
 import org.apache.hadoop.hdds.scm.container.replication.ReplicationManager;
+import org.apache.hadoop.hdds.scm.exceptions.SCMException;
 import org.apache.hadoop.hdds.scm.ha.SCMHADBTransactionBuffer;
 import org.apache.hadoop.hdds.scm.ha.SCMHADBTransactionBufferStub;
 import org.apache.hadoop.hdds.scm.ha.SCMHAManagerStub;
@@ -946,6 +950,170 @@ private void mockStandAloneContainerInfo(long 
containerID, DatanodeDetails dd)
         .thenReturn(replicaSet);
   }
 
+  private SCMException ratisTimeout() {
+    return new SCMException("Simulated Ratis submit timeout", 
SCMException.ResultCodes.TIMEOUT);
+  }
+
+  private SCMException hardDbFailure() {
+    return new SCMException("Simulated hard DB failure", 
SCMException.ResultCodes.IO_EXCEPTION);
+  }
+
+  private DeletedBlocksTransaction buildTx(long txId, long containerId, long 
blockSize, long blockCount) {
+    return DeletedBlocksTransaction.newBuilder()
+        .setTxID(txId)
+        .setContainerID(containerId)
+        .setCount(0)
+        .addLocalID(1L)
+        .setTotalBlockSize(blockSize * blockCount)
+        .setTotalBlockReplicatedSize(blockSize * blockCount * 3L)
+        .build();
+  }
+
+  /**
+   * Ratis timeout on addTransactionsToDB: the abandoned future may still 
commit,
+   * so the in-memory increment must NOT be rolled back (HDDS-16142).
+   */
+  @Test
+  public void testSummaryDoesNotDoubleDecrementOnRatisSubmitTimeout() throws 
IOException {
+    long txId = 1L;
+    long containerId = 100L;
+    long blockSize = 64L * 1024 * 1024;
+    long blockCount = 5L;
+    ArrayList<DeletedBlocksTransaction> txList = new ArrayList<>();
+    txList.add(buildTx(txId, containerId, blockSize, blockCount));
+
+    DeletedBlockLogStateManager mockStateManager = 
mock(DeletedBlockLogStateManager.class);
+    
doThrow(ratisTimeout()).when(mockStateManager).addTransactionsToDB(any(ArrayList.class),
 any());
+
+    SCMDeletedBlockTransactionStatusManager statusManager =
+        new SCMDeletedBlockTransactionStatusManager(
+            mockStateManager,
+            scm.getScmMetadataStore().getStatefulServiceConfigTable(),
+            containerManager, metrics, Long.MAX_VALUE);
+
+    assertThrows(IOException.class, () -> 
statusManager.addTransactions(txList));
+    // Increment must NOT be rolled back: the late commit can still apply.
+    assertEquals(1, statusManager.getSummary().getTotalTransactionCount(),
+        "Ratis timeout on add: summary must stay incremented (no rollback)");
+
+    // Simulate the late Ratis commit landing and the tx being found by 
getTransactions.
+    statusManager.getTxSizeMap().put(txId,
+        new TxBlockInfo(txId, containerId, blockCount, blockSize * blockCount, 
blockSize * blockCount * 3L));
+    ArrayList<Long> txIds = new ArrayList<>();
+    txIds.add(txId);
+    statusManager.removeTransactions(txIds);
+
+    assertEquals(0, statusManager.getSummary().getTotalTransactionCount(),
+        "One add (no rollback) + one remove must yield 0, not -1");
+  }
+
+  /**
+   * Hard DB failure on addTransactionsToDB: the write definitively did not 
land,
+   * so the in-memory increment must be rolled back.
+   */
+  @Test
+  public void testSummaryRollsBackOnHardAddFailure() throws IOException {
+    long txId = 3L;
+    long containerId = 300L;
+    long blockSize = 16L * 1024 * 1024;
+    long blockCount = 2L;
+    ArrayList<DeletedBlocksTransaction> txList = new ArrayList<>();
+    txList.add(buildTx(txId, containerId, blockSize, blockCount));
+
+    DeletedBlockLogStateManager mockStateManager = 
mock(DeletedBlockLogStateManager.class);
+    
doThrow(hardDbFailure()).when(mockStateManager).addTransactionsToDB(any(ArrayList.class),
 any());
+
+    SCMDeletedBlockTransactionStatusManager statusManager =
+        new SCMDeletedBlockTransactionStatusManager(
+            mockStateManager,
+            scm.getScmMetadataStore().getStatefulServiceConfigTable(),
+            containerManager, metrics, Long.MAX_VALUE);
+
+    assertThrows(IOException.class, () -> 
statusManager.addTransactions(txList));
+    // Hard failure: write did not land, rollback must restore summary to 0.
+    assertEquals(0, statusManager.getSummary().getTotalTransactionCount(),
+        "Hard add failure: summary must be rolled back to 0");
+  }
+
+  /**
+   * Ratis timeout on removeTransactionsFromDB: the abandoned future may still 
commit
+   * and remove the tx durably, so the in-memory decrement must NOT be rolled 
back (HDDS-16142).
+   */
+  @Test
+  public void testSummaryDoesNotOvercountOnRatisRemoveTimeout() throws 
IOException {
+    long txId = 2L;
+    long containerId = 200L;
+    long blockSize = 32L * 1024 * 1024;
+    long blockCount = 3L;
+
+    DeletedBlockLogStateManager mockStateManager = 
mock(DeletedBlockLogStateManager.class);
+
+    SCMDeletedBlockTransactionStatusManager statusManager =
+        new SCMDeletedBlockTransactionStatusManager(
+            mockStateManager,
+            scm.getScmMetadataStore().getStatefulServiceConfigTable(),
+            containerManager, metrics, Long.MAX_VALUE);
+
+    statusManager.addTransactions(new ArrayList<>(
+        Collections.singletonList(buildTx(txId, containerId, blockSize, 
blockCount))));
+    assertEquals(1, statusManager.getSummary().getTotalTransactionCount());
+
+    statusManager.getTxSizeMap().put(txId,
+        new TxBlockInfo(txId, containerId, blockCount, blockSize * blockCount, 
blockSize * blockCount * 3L));
+
+    
doThrow(ratisTimeout()).when(mockStateManager).removeTransactionsFromDB(any(ArrayList.class),
 any());
+
+    ArrayList<Long> txIds = new ArrayList<>();
+    txIds.add(txId);
+    assertThrows(IOException.class, () -> 
statusManager.removeTransactions(txIds));
+
+    // Decrement must NOT be rolled back: the late commit may have removed the 
tx.
+    assertEquals(0, statusManager.getSummary().getTotalTransactionCount(),
+        "Ratis timeout on remove: summary must stay decremented (no 
rollback)");
+    // txSizeMap must also not be restored (same reasoning).
+    assertFalse(statusManager.getTxSizeMap().containsKey(txId),
+        "Ratis timeout on remove: txSizeMap entry must not be restored");
+  }
+
+  /**
+   * Hard DB failure on removeTransactionsFromDB: the write definitively did 
not land,
+   * so the in-memory decrement must be rolled back and txSizeMap restored.
+   */
+  @Test
+  public void testSummaryRollsBackOnHardRemoveFailure() throws IOException {
+    long txId = 4L;
+    long containerId = 400L;
+    long blockSize = 8L * 1024 * 1024;
+    long blockCount = 4L;
+
+    DeletedBlockLogStateManager mockStateManager = 
mock(DeletedBlockLogStateManager.class);
+
+    SCMDeletedBlockTransactionStatusManager statusManager =
+        new SCMDeletedBlockTransactionStatusManager(
+            mockStateManager,
+            scm.getScmMetadataStore().getStatefulServiceConfigTable(),
+            containerManager, metrics, Long.MAX_VALUE);
+
+    statusManager.addTransactions(new ArrayList<>(
+        Collections.singletonList(buildTx(txId, containerId, blockSize, 
blockCount))));
+    assertEquals(1, statusManager.getSummary().getTotalTransactionCount());
+
+    statusManager.getTxSizeMap().put(txId,
+        new TxBlockInfo(txId, containerId, blockCount, blockSize * blockCount, 
blockSize * blockCount * 3L));
+
+    
doThrow(hardDbFailure()).when(mockStateManager).removeTransactionsFromDB(any(ArrayList.class),
 any());
+
+    ArrayList<Long> txIds = new ArrayList<>();
+    txIds.add(txId);
+    assertThrows(IOException.class, () -> 
statusManager.removeTransactions(txIds));
+
+    // Hard failure: write did not land, rollback must restore summary to 1.
+    assertEquals(1, statusManager.getSummary().getTotalTransactionCount(),
+        "Hard remove failure: summary must be rolled back to 1");
+    assertTrue(statusManager.getTxSizeMap().containsKey(txId),
+        "Hard remove failure: txSizeMap entry must be restored for retry");
+  }
+
   private void mockInadequateReplicaUnhealthyContainerInfo(long containerID,
       int count) throws IOException {
     List<DatanodeDetails> dns = dnList.subList(0, 2);


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

Reply via email to