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 0eb1c67cd6a HDDS-15760. Handle large or invalid OM snapshot local data 
YAMLs (#10679)
0eb1c67cd6a is described below

commit 0eb1c67cd6ab0d8bd99cc705170dc646650000b7
Author: Siyao Meng <[email protected]>
AuthorDate: Tue Jul 7 11:28:36 2026 -0700

    HDDS-15760. Handle large or invalid OM snapshot local data YAMLs (#10679)
---
 .../hadoop/ozone/om/OmSnapshotLocalDataYaml.java   | 12 +++-
 .../om/snapshot/OmSnapshotLocalDataManager.java    | 83 +++++++++++++++++++---
 .../ozone/om/TestOmSnapshotLocalDataYaml.java      | 36 ++++++++++
 .../snapshot/TestOmSnapshotLocalDataManager.java   | 49 +++++++++++--
 4 files changed, 166 insertions(+), 14 deletions(-)

diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotLocalDataYaml.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotLocalDataYaml.java
index ed6b3feda7e..24c7fb3e3e7 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotLocalDataYaml.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotLocalDataYaml.java
@@ -56,6 +56,9 @@ public final class OmSnapshotLocalDataYaml {
   public static final Tag SNAPSHOT_VERSION_META_TAG = new Tag("VersionMeta");
   public static final Tag SST_FILE_INFO_TAG = new Tag("SstFileInfo");
   public static final String YAML_FILE_EXTENSION = ".yaml";
+  // Maximum number of Unicode code points the YAML parser will read 
(SnakeYAML code point limit).
+  // For the ASCII snapshot local data YAML this is effectively the maximum 
file size in bytes.
+  public static final int SNAPSHOT_LOCAL_DATA_YAML_CODE_POINT_LIMIT = 128 * 
1024 * 1024;
 
   private OmSnapshotLocalDataYaml() {
   }
@@ -125,7 +128,7 @@ protected NodeTuple representJavaBeanProperty(
    */
   private static class SnapshotLocalDataConstructor extends SafeConstructor {
     SnapshotLocalDataConstructor() {
-      super(new LoaderOptions());
+      super(createLoaderOptions());
       //Adding our own specific constructors for tags.
       this.yamlConstructors.put(SNAPSHOT_YAML_TAG, new 
ConstructSnapshotLocalData());
       this.yamlConstructors.put(SNAPSHOT_VERSION_META_TAG, new 
ConstructVersionMeta());
@@ -138,6 +141,13 @@ private static class SnapshotLocalDataConstructor extends 
SafeConstructor {
       this.addTypeDescription(versionMetaDesc);
     }
 
+    private static LoaderOptions createLoaderOptions() {
+      LoaderOptions options = new LoaderOptions();
+      // Snapshot local data is trusted local metadata, but keep a finite 
parser bound to avoid unbounded memory use.
+      options.setCodePointLimit(SNAPSHOT_LOCAL_DATA_YAML_CODE_POINT_LIMIT);
+      return options;
+    }
+
     private final class ConstructSstFileInfo extends AbstractConstruct {
       @Override
       public Object construct(Node node) {
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/OmSnapshotLocalDataManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/OmSnapshotLocalDataManager.java
index a2203f675d8..994cfec7b37 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/OmSnapshotLocalDataManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/OmSnapshotLocalDataManager.java
@@ -288,8 +288,28 @@ private void addMissingSnapshotYamlFiles(
   }
 
   void addVersionNodeWithDependents(OmSnapshotLocalData snapshotLocalData) 
throws IOException {
+    addVersionNodeWithDependents(snapshotLocalData, null);
+  }
+
+  /**
+   * Adds version nodes for the supplied snapshot local data and any unloaded 
previous snapshots it depends on.
+   * The graph must contain the previous snapshot's version node before the 
current snapshot's version node can be
+   * added. This method walks the previous-snapshot chain using persisted YAML 
metadata and processes the stack in
+   * dependency order.
+   *
+   * @param snapshotLocalData snapshot local data to add to the version graph
+   * @param failedFilePaths when non-null, a previous snapshot YAML that 
cannot be loaded or whose snapshotId does not
+   *     match its path is skipped instead of thrown, and its path is recorded 
here so it is not reloaded during
+   *     startup (a path recorded here is not logged again on subsequent 
lookups)
+   * @return true if the snapshot local data was added or was already present, 
false if skipped due to an unloadable or
+   *     mismatched previous snapshot
+   * @throws IOException if a required YAML load fails, or the loaded 
snapshotId does not match, when failedFilePaths is
+   *     null
+   */
+  private boolean addVersionNodeWithDependents(OmSnapshotLocalData 
snapshotLocalData, Set<String> failedFilePaths)
+      throws IOException {
     if (versionNodeMap.containsKey(snapshotLocalData.getSnapshotId())) {
-      return;
+      return true;
     }
     Set<UUID> visitedSnapshotIds = new HashSet<>();
     Stack<Pair<UUID, SnapshotVersionsMeta>> stack = new Stack<>();
@@ -305,17 +325,52 @@ void addVersionNodeWithDependents(OmSnapshotLocalData 
snapshotLocalData) throws
         UUID prevSnapId = snapshotVersionsMeta.getPreviousSnapshotId();
         if (prevSnapId != null && !versionNodeMap.containsKey(prevSnapId)) {
           File previousSnapshotLocalDataFile = new 
File(getSnapshotLocalPropertyYamlPath(prevSnapId));
-          OmSnapshotLocalData prevSnapshotLocalData = 
snapshotLocalDataSerializer.load(previousSnapshotLocalDataFile);
+          OmSnapshotLocalData prevSnapshotLocalData;
+          if (failedFilePaths != null) {
+            Optional<OmSnapshotLocalData> loadedLocalData =
+                tryLoadSnapshotLocalData(previousSnapshotLocalDataFile, 
failedFilePaths);
+            if (!loadedLocalData.isPresent()) {
+              // tryLoadSnapshotLocalData recorded this path in 
failedFilePaths (logging the underlying failure the
+              // first time it was seen). Skip this snapshot so it is not 
added to the version graph.
+              return false;
+            }
+            prevSnapshotLocalData = loadedLocalData.get();
+          } else {
+            prevSnapshotLocalData = 
snapshotLocalDataSerializer.load(previousSnapshotLocalDataFile);
+          }
           if (!prevSnapId.equals(prevSnapshotLocalData.getSnapshotId())) {
-            throw new IOException("SnapshotId mismatch: expected " + 
prevSnapId +
+            String mismatch = "Expected SnapshotId " + prevSnapId +
                 " but found " + prevSnapshotLocalData.getSnapshotId() +
-                " in file " + previousSnapshotLocalDataFile.getAbsolutePath());
+                " in file " + previousSnapshotLocalDataFile.getAbsolutePath();
+            if (failedFilePaths != null) {
+              
failedFilePaths.add(previousSnapshotLocalDataFile.getAbsolutePath());
+              LOG.error("Skipping snapshot local data for snapshot {} because 
previous snapshot local data yaml {} " +
+                  "has a mismatched snapshotId: {}", snapId, 
previousSnapshotLocalDataFile.getAbsolutePath(), mismatch);
+              return false;
+            }
+            throw new IOException(mismatch);
           }
           stack.push(Pair.of(prevSnapshotLocalData.getSnapshotId(), new 
SnapshotVersionsMeta(prevSnapshotLocalData)));
         }
         visitedSnapshotIds.add(snapId);
       }
     }
+    return true;
+  }
+
+  private Optional<OmSnapshotLocalData> tryLoadSnapshotLocalData(File 
localDataFile, Set<String> failedFilePaths) {
+    String path = localDataFile.getAbsolutePath();
+    if (failedFilePaths.contains(path)) {
+      return Optional.empty();
+    }
+    try {
+      return Optional.of(snapshotLocalDataSerializer.load(localDataFile));
+    } catch (IOException e) {
+      failedFilePaths.add(path);
+      LOG.error("Skipping snapshot local data file {} because it could not be 
loaded. " +
+          "Snapshot defrag and snapshot diff may be unavailable for the 
affected snapshot.", path, e);
+      return Optional.empty();
+    }
   }
 
   private void incrementOrphanCheckCount(UUID snapshotId) {
@@ -360,16 +415,28 @@ private void init(OzoneConfiguration configuration, 
SnapshotChainManager chainMa
       throw new IOException("Error while listing yaml files inside directory: 
" + snapshotDir.getAbsolutePath());
     }
     Arrays.sort(localDataFiles, Comparator.comparing(File::getName));
+    Set<String> failedFilePaths = new HashSet<>();
     for (File localDataFile : localDataFiles) {
-      OmSnapshotLocalData snapshotLocalData = 
snapshotLocalDataSerializer.load(localDataFile);
+      Optional<OmSnapshotLocalData> loadedLocalData = 
tryLoadSnapshotLocalData(localDataFile, failedFilePaths);
+      if (!loadedLocalData.isPresent()) {
+        continue;
+      }
+      OmSnapshotLocalData snapshotLocalData = loadedLocalData.get();
       File file = new 
File(getSnapshotLocalPropertyYamlPath(snapshotLocalData.getSnapshotId()));
       String expectedPath = file.getAbsolutePath();
       String actualPath = localDataFile.getAbsolutePath();
       if (!expectedPath.equals(actualPath)) {
-        throw new IOException("Unexpected path for local data file with 
snapshotId:" + snapshotLocalData.getSnapshotId()
-            + " : " + actualPath + ". " + "Expected: " + expectedPath);
+        failedFilePaths.add(actualPath);
+        LOG.error("Skipping snapshot local data file {} because its stored 
snapshotId {} does not match its path. " +
+            "Expected path: {}.", actualPath, 
snapshotLocalData.getSnapshotId(), expectedPath);
+        continue;
+      }
+      if (!addVersionNodeWithDependents(snapshotLocalData, failedFilePaths)) {
+        // A previous snapshot in the dependency chain could not be loaded, so 
this snapshot was not added to the
+        // version graph. Record its path as failed so later snapshots that 
depend on it short-circuit in
+        // tryLoadSnapshotLocalData instead of reparsing this YAML.
+        failedFilePaths.add(actualPath);
       }
-      addVersionNodeWithDependents(snapshotLocalData);
     }
     for (UUID snapshotId : versionNodeMap.keySet()) {
       incrementOrphanCheckCount(snapshotId);
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmSnapshotLocalDataYaml.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmSnapshotLocalDataYaml.java
index e18adae7b40..bfcb4b0b990 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmSnapshotLocalDataYaml.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmSnapshotLocalDataYaml.java
@@ -32,6 +32,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -240,6 +241,41 @@ public void testEmptyFile() throws IOException {
     assertThat(ex).hasMessageContaining("Failed to load file. File is empty.");
   }
 
+  @Test
+  public void testLoadYamlLargerThanDefaultSnakeYamlLimit() throws IOException 
{
+    UUID snapshotId = UUID.randomUUID();
+    File yamlFile = new File(testRoot, "large-snapshot.yaml");
+    StringBuilder yaml = new StringBuilder(4 * 1024 * 1024);
+    yaml.append("!<OmSnapshotLocalData>\n")
+        .append("checksum: 
\"0000000000000000000000000000000000000000000000000000000000000000\"\n")
+        .append("dbTxSequenceNumber: 10\n")
+        .append("isSSTFiltered: false\n")
+        .append("lastDefragTime: 0\n")
+        .append("needsDefrag: false\n")
+        .append("snapshotId: \"").append(snapshotId).append("\"\n")
+        .append("version: 0\n")
+        .append("versionSstFileInfos:\n")
+        .append("  0: !<VersionMeta>\n")
+        .append("    previousSnapshotVersion: 0\n")
+        .append("    sstFiles:\n");
+    int sstFileCount = 0;
+    while (yaml.length() <= 3 * 1024 * 1024 + 1024) {
+      yaml.append("    - !<SstFileInfo>\n")
+          .append("      fileName: 
file-").append(sstFileCount).append(".sst\n")
+          .append("      startKey: 
key-start-").append(sstFileCount).append("-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n")
+          .append("      endKey: 
key-end-").append(sstFileCount).append("-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n")
+          .append("      columnFamily: fileTable\n");
+      sstFileCount++;
+    }
+    FileUtils.writeStringToFile(yamlFile, yaml.toString(), 
StandardCharsets.UTF_8);
+    assertThat(yamlFile.length()).isGreaterThan(3L * 1024 * 1024);
+
+    OmSnapshotLocalData snapshotData = 
omSnapshotLocalDataSerializer.load(yamlFile);
+
+    assertThat(snapshotData.getSnapshotId()).isEqualTo(snapshotId);
+    
assertThat(snapshotData.getVersionSstFileInfos().get(0).getSstFiles()).hasSize(sstFileCount);
+  }
+
   @Test
   public void testChecksum() throws IOException {
     UUID snapshotId = UUID.randomUUID();
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOmSnapshotLocalDataManager.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOmSnapshotLocalDataManager.java
index c73304fa122..9aecb7b1f28 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOmSnapshotLocalDataManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOmSnapshotLocalDataManager.java
@@ -26,6 +26,7 @@
 import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.KEY_TABLE;
 import static 
org.apache.hadoop.ozone.om.helpers.SnapshotInfo.SnapshotStatus.SNAPSHOT_ACTIVE;
 import static 
org.apache.hadoop.ozone.om.helpers.SnapshotInfo.SnapshotStatus.SNAPSHOT_DELETED;
+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.assertNotNull;
@@ -46,6 +47,7 @@
 import com.google.common.collect.ImmutableSet;
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
@@ -986,6 +988,42 @@ public void testInitWithExistingYamlFiles() throws 
IOException {
     assertEquals(versionMap.keySet(), new HashSet<>(versionIds));
   }
 
+  @Test
+  public void testInitSkipsYamlFilesThatCannotBeLoaded() throws IOException {
+    UUID snapshotId = UUID.fromString("00000000-0000-0000-0000-000000000001");
+    UUID validSnapshotId = 
UUID.fromString("00000000-0000-0000-0000-000000000002");
+    UUID previousSnapshotId = 
UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff");
+
+    createSnapshotLocalDataFile(snapshotId, previousSnapshotId);
+    createSnapshotLocalDataFile(validSnapshotId, null);
+    Path invalidYamlPath = Paths.get(snapshotsDir.getAbsolutePath(),
+        "db" + OM_SNAPSHOT_SEPARATOR + previousSnapshotId + 
YAML_FILE_EXTENSION);
+    Files.write(invalidYamlPath, "not: 
[valid".getBytes(StandardCharsets.UTF_8));
+
+    localDataManager = getNewOmSnapshotLocalDataManager();
+
+    
assertThat(localDataManager.getVersionNodeMapUnmodifiable()).containsOnlyKeys(validSnapshotId);
+  }
+
+  @Test
+  public void testInitSkipsPreviousSnapshotWithMismatchedSnapshotId() throws 
IOException {
+    UUID snapshotId = UUID.fromString("00000000-0000-0000-0000-000000000001");
+    UUID validSnapshotId = 
UUID.fromString("00000000-0000-0000-0000-000000000002");
+    UUID previousSnapshotId = 
UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff");
+    UUID mismatchedSnapshotId = 
UUID.fromString("00000000-0000-0000-0000-000000000003");
+
+    createSnapshotLocalDataFile(snapshotId, previousSnapshotId);
+    createSnapshotLocalDataFile(validSnapshotId, null);
+    // Write a loadable YAML at the previous snapshot's path, but whose stored 
snapshotId does not match the path.
+    Path mismatchedYamlPath = Paths.get(snapshotsDir.getAbsolutePath(),
+        "db" + OM_SNAPSHOT_SEPARATOR + previousSnapshotId + 
YAML_FILE_EXTENSION);
+    writeLocalDataToFile(createMockLocalData(mismatchedSnapshotId, null), 
mismatchedYamlPath);
+
+    localDataManager = getNewOmSnapshotLocalDataManager();
+
+    
assertThat(localDataManager.getVersionNodeMapUnmodifiable()).containsOnlyKeys(validSnapshotId);
+  }
+
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   public void testInitWithMissingYamlFiles(boolean needsUpgrade) throws 
IOException {
@@ -1081,16 +1119,17 @@ public void 
testCheckOrphanSnapshotVersionsWithStaleSnapshotChain() throws IOExc
   }
 
   @Test
-  public void testInitWithInvalidPathThrowsException() throws IOException {
+  public void testInitSkipsYamlFileWhosePathDoesNotMatchStoredSnapshotId() 
throws IOException {
     UUID snapshotId = UUID.randomUUID();
-    
+
     // Create a file with wrong location
     OmSnapshotLocalData localData = createMockLocalData(snapshotId, null);
     Path wrongPath = Paths.get(snapshotsDir.getAbsolutePath(), 
"db-wrong-name.yaml");
     writeLocalDataToFile(localData, wrongPath);
-    
-    // Should throw IOException during init
-    assertThrows(IOException.class, this::getNewOmSnapshotLocalDataManager);
+
+    localDataManager = getNewOmSnapshotLocalDataManager();
+
+    assertThat(localDataManager.getVersionNodeMapUnmodifiable()).isEmpty();
   }
 
   @Test


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

Reply via email to