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

smengcl 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 aed7ceac7cc HDDS-16465. Validate snapshot chains before mutation 
(#11253)
aed7ceac7cc is described below

commit aed7ceac7cc515a796863b79066c9d5389732979
Author: Andy Huang <[email protected]>
AuthorDate: Tue Sep 22 06:30:07 2026 +0800

    HDDS-16465. Validate snapshot chains before mutation (#11253)
---
 .../hadoop/ozone/om/SnapshotChainManager.java      | 163 +++++++++++++--------
 .../ozone/om/snapshot/TestSnapshotChain.java       |  45 ++++++
 2 files changed, 146 insertions(+), 62 deletions(-)

diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/SnapshotChainManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/SnapshotChainManager.java
index 5951c1303ff..360f1f86b05 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/SnapshotChainManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/SnapshotChainManager.java
@@ -70,18 +70,19 @@ public SnapshotChainManager(OMMetadataManager 
metadataManager) {
   }
 
   /**
-   * Add snapshot to global snapshot chain.
+   * Validate adding a snapshot to the global snapshot chain.
    */
-  private void addSnapshotGlobal(UUID snapshotID, UUID prevGlobalID)
+  private void validateAddSnapshotGlobal(UUID snapshotID, UUID prevGlobalID)
       throws IOException {
     if (globalSnapshotChain.containsKey(snapshotID)) {
       throw new IOException(String.format(
           "Global Snapshot chain corruption. Snapshot with snapshotId: %s is " 
+
               "already present in the chain.", snapshotID));
     }
+
     if (!globalSnapshotChain.isEmpty() && prevGlobalID == null) {
       throw new IOException(String.format("Snapshot chain " +
-          "corruption. Adding snapshot %s as head node while there are %d " +
+              "corruption. Adding snapshot %s as head node while there are %d 
" +
               "snapshots in the global snapshot chain.", snapshotID,
           globalSnapshotChain.size()));
     }
@@ -94,16 +95,22 @@ private void addSnapshotGlobal(UUID snapshotID, UUID 
prevGlobalID)
               "snapshot chain.", prevGlobalID, snapshotID));
     }
 
+    if (prevGlobalID != null && 
globalSnapshotChain.get(prevGlobalID).hasNextSnapshotId()) {
+      throw new IOException(String.format(
+          "Global Snapshot chain corruption. Snapshot with snapshotId: %s " +
+              "already has the next snapshotId: %s. Adding snapshot %s " +
+              "with prevSnapshotId: %s will make the chain non linear.",
+          prevGlobalID,
+          globalSnapshotChain.get(prevGlobalID).getNextSnapshotId(),
+          snapshotID, prevGlobalID));
+    }
+  }
+
+  /**
+   * Add snapshot to global snapshot chain.
+   */
+  private void addSnapshotGlobal(UUID snapshotID, UUID prevGlobalID) {
     if (prevGlobalID != null) {
-      if (globalSnapshotChain.get(prevGlobalID).hasNextSnapshotId()) {
-        throw new IOException(String.format(
-            "Global Snapshot chain corruption. Snapshot with snapshotId: %s " +
-                "already has the next snapshotId: %s. Adding snapshot %s " +
-                "with prevSnapshotId: %s will make the chain non linear.",
-            prevGlobalID,
-            globalSnapshotChain.get(prevGlobalID).getNextSnapshotId(),
-            snapshotID, prevGlobalID));
-      }
       // On add snapshot, set previous snapshot entry nextSnapshotID =
       // snapshotID
       globalSnapshotChain.get(prevGlobalID).setNextSnapshotId(snapshotID);
@@ -119,11 +126,9 @@ private void addSnapshotGlobal(UUID snapshotID, UUID 
prevGlobalID)
   }
 
   /**
-   * Add snapshot to bucket snapshot chain(path based).
+   * Validate adding a snapshot to the path snapshot chain.
    */
-  private void addSnapshotPath(String snapshotPath, UUID snapshotID,
-                               UUID prevPathID) throws IOException {
-    // On add snapshot, set previous snapshot entry nextSnapshotId = snapshotId
+  private void validateAddSnapshotPath(String snapshotPath, UUID snapshotID, 
UUID prevPathID) throws IOException {
     if (prevPathID != null &&
         ((!snapshotChainByPath.containsKey(snapshotPath)) ||
             (!snapshotChainByPath.get(snapshotPath).containsKey(prevPathID)))) 
{
@@ -142,17 +147,22 @@ private void addSnapshotPath(String snapshotPath, UUID 
snapshotID,
           snapshotChainByPath.get(snapshotPath).size()));
     }
 
+    if (prevPathID != null && 
snapshotChainByPath.get(snapshotPath).get(prevPathID).hasNextSnapshotId()) {
+      throw new IOException(String.format(
+          "Path Snapshot chain corruption. Next snapshotId: %s is already " +
+              "set for snapshotId: %s. Adding snapshotId: %s with " +
+              "prevSnapshotId: %s will make the chain non linear.",
+          snapshotChainByPath.get(snapshotPath).get(prevPathID)
+              .getNextSnapshotId(), prevPathID,
+          snapshotID, prevPathID));
+    }
+  }
+
+  /**
+   * Add snapshot to bucket snapshot chain(path based).
+   */
+  private void addSnapshotPath(String snapshotPath, UUID snapshotID, UUID 
prevPathID) {
     if (prevPathID != null && snapshotChainByPath.containsKey(snapshotPath)) {
-      if (snapshotChainByPath.get(snapshotPath).get(prevPathID)
-          .hasNextSnapshotId()) {
-        throw new IOException(String.format(
-            "Path Snapshot chain corruption. Next snapshotId: %s is already " +
-                "set for snapshotId: %s. Adding snapshotId: %s with " +
-                "prevSnapshotId: %s will make the chain non linear.",
-            snapshotChainByPath.get(snapshotPath).get(prevPathID)
-                .getNextSnapshotId(), prevPathID,
-            snapshotID, prevPathID));
-      }
       snapshotChainByPath
           .get(snapshotPath)
           .get(prevPathID)
@@ -170,26 +180,31 @@ private void addSnapshotPath(String snapshotPath, UUID 
snapshotID,
     latestSnapshotIdByPath.put(snapshotPath, snapshotID);
   }
 
+  private void validateDeleteSnapshotGlobal(UUID snapshotID) throws 
IOException {
+    UUID next = globalSnapshotChain.get(snapshotID).getNextSnapshotId();
+    UUID prev = globalSnapshotChain.get(snapshotID).getPreviousSnapshotId();
+    if (prev != null && !globalSnapshotChain.containsKey(prev)) {
+      throw new IOException(String.format(
+          "Global snapshot chain corruption. " +
+              "SnapshotId: %s to be deleted has previous snapshotId: %s " +
+              "but associated snapshot is not found in snapshot chain.",
+          snapshotID, prev));
+    }
+    if (next != null && !globalSnapshotChain.containsKey(next)) {
+      throw new IOException(String.format(
+          "Global snapshot chain corruption. " +
+              "SnapshotId: {%s} to be deleted has next snapshotId: %s " +
+              "but associated snapshot is not found in snapshot chain.",
+          snapshotID, next));
+    }
+  }
+
   private boolean deleteSnapshotGlobal(UUID snapshotID) throws IOException {
     if (globalSnapshotChain.containsKey(snapshotID)) {
       // reset prev and next snapshot entries in chain ordered list
       // for node removal
       UUID next = globalSnapshotChain.get(snapshotID).getNextSnapshotId();
       UUID prev = globalSnapshotChain.get(snapshotID).getPreviousSnapshotId();
-      if (prev != null && !globalSnapshotChain.containsKey(prev)) {
-        throw new IOException(String.format(
-            "Global snapshot chain corruption. " +
-                "SnapshotId: %s to be deleted has previous snapshotId: %s " +
-                "but associated snapshot is not found in snapshot chain.",
-            snapshotID, prev));
-      }
-      if (next != null && !globalSnapshotChain.containsKey(next)) {
-        throw new IOException(String.format(
-            "Global snapshot chain corruption. " +
-                "SnapshotId: {%s} to be deleted has next snapshotId: %s " +
-                "but associated snapshot is not found in snapshot chain.",
-            snapshotID, next));
-      }
       globalSnapshotChain.remove(snapshotID);
       if (next != null) {
         globalSnapshotChain.get(next).setPreviousSnapshotId(prev);
@@ -213,6 +228,30 @@ private boolean deleteSnapshotGlobal(UUID snapshotID) 
throws IOException {
     }
   }
 
+  private void validateDeleteSnapshotPath(String snapshotPath, UUID 
snapshotId) throws IOException {
+    UUID nextSnapshotId = 
snapshotChainByPath.get(snapshotPath).get(snapshotId).getNextSnapshotId();
+    UUID previousSnapshotId = 
snapshotChainByPath.get(snapshotPath).get(snapshotId).getPreviousSnapshotId();
+    if (previousSnapshotId != null &&
+        !snapshotChainByPath.get(snapshotPath)
+            .containsKey(previousSnapshotId)) {
+      throw new IOException(String.format(
+          "Path snapshot chain corruption. " +
+              "SnapshotId: %s at snapshotPath: %s to be deleted has " +
+              "previous snapshotId: %s but associated snapshot is not " +
+              "found in snapshot chain.", snapshotId, snapshotPath,
+          previousSnapshotId));
+    }
+    if (nextSnapshotId != null && !snapshotChainByPath.get(snapshotPath)
+        .containsKey(nextSnapshotId)) {
+      throw new IOException(String.format(
+          "Path snapshot chain corruption. " +
+              "SnapshotId: %s at snapshotPath: %s to be deleted has next " +
+              "snapshotId: %s but associated snapshot is not found in " +
+              "snapshot chain.", snapshotId, snapshotPath,
+          nextSnapshotId));
+    }
+  }
+
   private boolean deleteSnapshotPath(String snapshotPath,
                                      UUID snapshotId) throws IOException {
     if (snapshotChainByPath.containsKey(snapshotPath) &&
@@ -228,26 +267,6 @@ private boolean deleteSnapshotPath(String snapshotPath,
           .get(snapshotId)
           .getPreviousSnapshotId();
 
-      if (previousSnapshotId != null &&
-          !snapshotChainByPath.get(snapshotPath)
-              .containsKey(previousSnapshotId)) {
-        throw new IOException(String.format(
-            "Path snapshot chain corruption. " +
-                "SnapshotId: %s at snapshotPath: %s to be deleted has " +
-                "previous snapshotId: %s but associated snapshot is not " +
-                "found in snapshot chain.", snapshotId, snapshotPath,
-            previousSnapshotId));
-      }
-      if (nextSnapshotId != null && !snapshotChainByPath.get(snapshotPath)
-          .containsKey(nextSnapshotId)) {
-        throw new IOException(String.format(
-            "Path snapshot chain corruption. " +
-                "SnapshotId: %s at snapshotPath: %s to be deleted has next " +
-                "snapshotId: %s but associated snapshot is not found in " +
-                "snapshot chain.", snapshotId, snapshotPath,
-            nextSnapshotId));
-      }
-
       snapshotChainByPath.get(snapshotPath).remove(snapshotId);
       if (nextSnapshotId != null) {
         snapshotChainByPath.get(snapshotPath)
@@ -349,6 +368,9 @@ private boolean loadFromSnapshotInfoTable(OMMetadataManager 
metadataManager) {
   public synchronized void addSnapshot(SnapshotInfo snapshotInfo)
       throws IOException {
     validateSnapshotChain();
+    validateAddSnapshotGlobal(snapshotInfo.getSnapshotId(), 
snapshotInfo.getGlobalPreviousSnapshotId());
+    validateAddSnapshotPath(snapshotInfo.getSnapshotPath(), 
snapshotInfo.getSnapshotId(),
+        snapshotInfo.getPathPreviousSnapshotId());
     addSnapshotGlobal(snapshotInfo.getSnapshotId(),
         snapshotInfo.getGlobalPreviousSnapshotId());
     addSnapshotPath(snapshotInfo.getSnapshotPath(),
@@ -375,8 +397,25 @@ public synchronized void updateSnapshot(SnapshotInfo 
snapshotInfo) {
   public synchronized boolean deleteSnapshot(SnapshotInfo snapshotInfo)
       throws IOException {
     validateSnapshotChain();
-    return deleteSnapshotGlobal(snapshotInfo.getSnapshotId()) &&
-        deleteSnapshotPath(snapshotInfo.getSnapshotPath(), 
snapshotInfo.getSnapshotId());
+    UUID snapshotId = snapshotInfo.getSnapshotId();
+    String snapshotPath = snapshotInfo.getSnapshotPath();
+    boolean inGlobalChain = globalSnapshotChain.containsKey(snapshotId);
+    boolean inPathChain = snapshotChainByPath.containsKey(snapshotPath)
+        && snapshotChainByPath.get(snapshotPath).containsKey(snapshotId);
+    if (inGlobalChain != inPathChain) {
+      throw new IOException(String.format(
+          "Snapshot chain corruption. SnapshotId: %s has inconsistent 
membership for snapshot path: %s.",
+          snapshotId, snapshotPath));
+    }
+    if (!inGlobalChain) {
+      LOG.warn("SnapshotId: {} is not found in the snapshot chain.", 
snapshotId);
+      return false;
+    }
+    validateDeleteSnapshotGlobal(snapshotId);
+    validateDeleteSnapshotPath(snapshotPath, snapshotId);
+    deleteSnapshotGlobal(snapshotId);
+    deleteSnapshotPath(snapshotPath, snapshotId);
+    return true;
   }
 
   /**
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotChain.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotChain.java
index e62b6489325..4534e4b7d3a 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotChain.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotChain.java
@@ -207,6 +207,51 @@ public void testAddSnapshot() throws Exception {
     }
   }
 
+  @Test
+  public void testFailedAddPreservesChains() throws Exception {
+    UUID id = UUID.randomUUID();
+    SnapshotInfo info = createSnapshotInfo(id, UUID.randomUUID(), null, 1L);
+
+    assertThrows(IOException.class, () -> chainManager.addSnapshot(info));
+
+    assertTrue(chainManager.getGlobalSnapshotChain().isEmpty());
+    assertTrue(chainManager.getSnapshotChainByPath().isEmpty());
+  }
+
+  @Test
+  public void testFailedAddPreservesExistingSnapshot() throws Exception {
+    UUID firstId = UUID.randomUUID();
+    chainManager.addSnapshot(createSnapshotInfo(firstId, null, null, 1L));
+
+    UUID rejectedId = UUID.randomUUID();
+    SnapshotInfo rejected = createSnapshotInfo(rejectedId, UUID.randomUUID(), 
firstId, 2L);
+
+    assertThrows(IOException.class, () -> chainManager.addSnapshot(rejected));
+
+    assertEquals(1, chainManager.getGlobalSnapshotChain().size());
+    assertEquals(1, chainManager.getSnapshotChainPath("vol1/bucket1").size());
+    assertEquals(firstId, chainManager.getOldestGlobalSnapshotId());
+    assertEquals(firstId, chainManager.getLatestGlobalSnapshotId());
+    assertEquals(firstId, 
chainManager.getLatestPathSnapshotId("vol1/bucket1"));
+    assertFalse(chainManager.hasNextGlobalSnapshot(firstId));
+    assertFalse(chainManager.hasNextPathSnapshot("vol1/bucket1", firstId));
+    assertFalse(chainManager.getGlobalSnapshotChain().containsKey(rejectedId));
+  }
+
+  @Test
+  public void testWrongPathDeletePreservesGlobalChain() throws Exception {
+    UUID id = UUID.randomUUID();
+    chainManager.addSnapshot(createSnapshotInfo(id, null, null, 1L));
+
+    SnapshotInfo wrongPath = createSnapshotInfo(id, null, null, 1L);
+    wrongPath.setSnapshotPath("vol1/bucket2");
+
+    assertThrows(IOException.class, () -> 
chainManager.deleteSnapshot(wrongPath));
+
+    assertTrue(chainManager.getGlobalSnapshotChain().containsKey(id));
+    
assertTrue(chainManager.getSnapshotChainPath("vol1/bucket1").containsKey(id));
+  }
+
   @Test
   public void testDeleteSnapshot() throws Exception {
     // add three snapshots


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

Reply via email to