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

weichiu 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 e08b001044 HDDS-13006. Use yaml files to host Ozone snapshot local 
properties (#8555)
e08b001044 is described below

commit e08b0010449342abca417510714a08e49f7ad132
Author: Siyao Meng <50227127+smen...@users.noreply.github.com>
AuthorDate: Wed Jul 9 19:31:35 2025 -0700

    HDDS-13006. Use yaml files to host Ozone snapshot local properties (#8555)
    
    Co-authored-by: Wei-Chiu Chuang <weic...@apache.org>
    
    Generated-by:  Claude 3.7 Sonnet
---
 .../java/org/apache/hadoop/ozone/OzoneConsts.java  |   9 +
 hadoop-ozone/ozone-manager/pom.xml                 |   4 +
 .../hadoop/ozone/om/OmSnapshotLocalData.java       | 288 +++++++++++++++++++++
 .../hadoop/ozone/om/OmSnapshotLocalDataYaml.java   | 259 ++++++++++++++++++
 .../apache/hadoop/ozone/om/OmSnapshotManager.java  |   5 +
 .../ozone/om/TestOmSnapshotLocalDataYaml.java      | 209 +++++++++++++++
 6 files changed, 774 insertions(+)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
index 6bee926336..c87fcc4bf0 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
@@ -202,6 +202,15 @@ public final class OzoneConsts {
 
   public static final String SCM_CONTEXT_ATTRIBUTE = "ozone.scm";
 
+  // YAML field constants for OmSnapshotLocalData (thus the OM_SLD_ prefix) 
YAML files
+  public static final String OM_SLD_VERSION = "version";
+  public static final String OM_SLD_CHECKSUM = "checksum";
+  public static final String OM_SLD_IS_SST_FILTERED = "isSSTFiltered";
+  public static final String OM_SLD_UNCOMPACTED_SST_FILE_LIST = 
"uncompactedSSTFileList";
+  public static final String OM_SLD_LAST_COMPACTION_TIME = 
"lastCompactionTime";
+  public static final String OM_SLD_NEEDS_COMPACTION = "needsCompaction";
+  public static final String OM_SLD_COMPACTED_SST_FILE_LIST = 
"compactedSSTFileList";
+
   // YAML fields for .container files
   public static final String CONTAINER_ID = "containerID";
   public static final String CONTAINER_TYPE = "containerType";
diff --git a/hadoop-ozone/ozone-manager/pom.xml 
b/hadoop-ozone/ozone-manager/pom.xml
index ba32592990..6347ee2722 100644
--- a/hadoop-ozone/ozone-manager/pom.xml
+++ b/hadoop-ozone/ozone-manager/pom.xml
@@ -298,6 +298,10 @@
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.yaml</groupId>
+      <artifactId>snakeyaml</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.apache.ozone</groupId>
       <artifactId>hdds-docs</artifactId>
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotLocalData.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotLocalData.java
new file mode 100644
index 0000000000..ce127fb3a6
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotLocalData.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * OmSnapshotLocalData is the in-memory representation of snapshot local 
metadata.
+ * Inspired by org.apache.hadoop.ozone.container.common.impl.ContainerData
+ */
+public abstract class OmSnapshotLocalData {
+
+  // Version of the snapshot local data. A valid version shall be greater than 
0.
+  private int version;
+
+  // Checksum of the YAML representation
+  private String checksum;
+
+  // Whether SST is filtered
+  private boolean isSSTFiltered;
+
+  // Map of Table to uncompacted SST file list on snapshot create
+  private Map<String, List<String>> uncompactedSSTFileList;
+
+  // Time of last compaction, in epoch milliseconds
+  private long lastCompactionTime;
+
+  // Whether the snapshot needs compaction
+  private boolean needsCompaction;
+
+  // Map of version to compacted SST file list
+  // Map<version, Map<Table, sstFileList>>
+  private Map<Integer, Map<String, List<String>>> compactedSSTFileList;
+
+  public static final Charset CHARSET_ENCODING = StandardCharsets.UTF_8;
+  private static final String DUMMY_CHECKSUM = new String(new byte[64], 
CHARSET_ENCODING);
+
+  /**
+   * Creates a OmSnapshotLocalData object with default values.
+   */
+  public OmSnapshotLocalData() {
+    this.isSSTFiltered = false;
+    this.uncompactedSSTFileList = new HashMap<>();
+    this.lastCompactionTime = 0L;
+    this.needsCompaction = false;
+    this.compactedSSTFileList = new HashMap<>();
+    this.version = 0;
+    setChecksumTo0ByteArray();
+  }
+
+  /**
+   * Copy constructor to create a deep copy of OmSnapshotLocalData object.
+   * @param source The source OmSnapshotLocalData to copy from
+   */
+  public OmSnapshotLocalData(OmSnapshotLocalData source) {
+    // Copy primitive fields directly
+    this.isSSTFiltered = source.isSSTFiltered;
+    this.lastCompactionTime = source.lastCompactionTime;
+    this.needsCompaction = source.needsCompaction;
+    this.checksum = source.checksum;
+    this.version = source.version;
+
+    // Deep copy for uncompactedSSTFileList
+    this.uncompactedSSTFileList = new HashMap<>();
+    for (Map.Entry<String, List<String>> entry :
+        source.uncompactedSSTFileList.entrySet()) {
+      this.uncompactedSSTFileList.put(
+          entry.getKey(),
+          Lists.newArrayList(entry.getValue()));
+    }
+
+    // Deep copy for compactedSSTFileList
+    this.compactedSSTFileList = new HashMap<>();
+    for (Map.Entry<Integer, Map<String, List<String>>> versionEntry :
+        source.compactedSSTFileList.entrySet()) {
+      Map<String, List<String>> tableMap = new HashMap<>();
+
+      for (Map.Entry<String, List<String>> tableEntry :
+          versionEntry.getValue().entrySet()) {
+        tableMap.put(
+            tableEntry.getKey(),
+            Lists.newArrayList(tableEntry.getValue()));
+      }
+
+      this.compactedSSTFileList.put(versionEntry.getKey(), tableMap);
+    }
+  }
+
+  /**
+   * Returns whether SST is filtered for this snapshot.
+   * @return true if SST is filtered, false otherwise
+   */
+  public boolean getSstFiltered() {
+    return isSSTFiltered;
+  }
+
+  /**
+   * Sets whether SST is filtered for this snapshot.
+   * @param sstFiltered
+   */
+  public void setSstFiltered(boolean sstFiltered) {
+    this.isSSTFiltered = sstFiltered;
+  }
+
+  /**
+   * Returns the uncompacted SST file list.
+   * @return Map of Table to uncompacted SST file list
+   */
+  public Map<String, List<String>> getUncompactedSSTFileList() {
+    return Collections.unmodifiableMap(this.uncompactedSSTFileList);
+  }
+
+  /**
+   * Sets the uncompacted SST file list.
+   * @param uncompactedSSTFileList Map of Table to uncompacted SST file list
+   */
+  public void setUncompactedSSTFileList(
+      Map<String, List<String>> uncompactedSSTFileList) {
+    this.uncompactedSSTFileList.clear();
+    this.uncompactedSSTFileList.putAll(uncompactedSSTFileList);
+  }
+
+  /**
+   * Adds an entry to the uncompacted SST file list.
+   * @param table Table name
+   * @param sstFile SST file name
+   */
+  public void addUncompactedSSTFile(String table, String sstFile) {
+    this.uncompactedSSTFileList.computeIfAbsent(table, k -> 
Lists.newArrayList())
+        .add(sstFile);
+  }
+
+  /**
+   * Returns the last compaction time, in epoch milliseconds.
+   * @return Timestamp of the last compaction
+   */
+  public long getLastCompactionTime() {
+    return lastCompactionTime;
+  }
+
+  /**
+   * Sets the last compaction time, in epoch milliseconds.
+   * @param lastCompactionTime Timestamp of the last compaction
+   */
+  public void setLastCompactionTime(Long lastCompactionTime) {
+    this.lastCompactionTime = lastCompactionTime;
+  }
+
+  /**
+   * Returns whether the snapshot needs compaction.
+   * @return true if the snapshot needs compaction, false otherwise
+   */
+  public boolean getNeedsCompaction() {
+    return needsCompaction;
+  }
+
+  /**
+   * Sets whether the snapshot needs compaction.
+   * @param needsCompaction true if the snapshot needs compaction, false 
otherwise
+   */
+  public void setNeedsCompaction(boolean needsCompaction) {
+    this.needsCompaction = needsCompaction;
+  }
+
+  /**
+   * Returns the compacted SST file list.
+   * @return Map of version to compacted SST file list
+   */
+  public Map<Integer, Map<String, List<String>>> getCompactedSSTFileList() {
+    return Collections.unmodifiableMap(this.compactedSSTFileList);
+  }
+
+  /**
+   * Sets the compacted SST file list.
+   * @param compactedSSTFileList Map of version to compacted SST file list
+   */
+  public void setCompactedSSTFileList(
+      Map<Integer, Map<String, List<String>>> compactedSSTFileList) {
+    this.compactedSSTFileList.clear();
+    this.compactedSSTFileList.putAll(compactedSSTFileList);
+  }
+
+  /**
+   * Adds an entry to the compacted SST file list.
+   * @param ver Version number (TODO: to be clarified)
+   * @param table Table name
+   * @param sstFile SST file name
+   */
+  public void addCompactedSSTFile(Integer ver, String table, String sstFile) {
+    this.compactedSSTFileList.computeIfAbsent(ver, k -> Maps.newHashMap())
+        .computeIfAbsent(table, k -> Lists.newArrayList())
+        .add(sstFile);
+  }
+
+  /**
+   * Returns the checksum of the YAML representation.
+   * @return checksum
+   */
+  public String getChecksum() {
+    return checksum;
+  }
+
+  /**
+   * Sets the checksum of the YAML representation.
+   * @param checksum checksum
+   */
+  public void setChecksum(String checksum) {
+    this.checksum = checksum;
+  }
+
+  /**
+   * Sets the checksum to a 0 byte array.
+   */
+  public void setChecksumTo0ByteArray() {
+    this.checksum = DUMMY_CHECKSUM;
+  }
+
+  /**
+   * Compute and set checksum for the snapshot data.
+   * @param yaml Yaml instance for serialization
+   * @throws IOException if checksum computation fails
+   */
+  public void computeAndSetChecksum(Yaml yaml) throws IOException {
+    // Set checksum to dummy value - 0 byte array, to calculate the checksum
+    // of rest of the data.
+    setChecksumTo0ByteArray();
+
+    // Dump yaml data into a string to compute its checksum
+    String snapshotDataYamlStr = yaml.dump(this);
+
+    this.checksum = getChecksum(snapshotDataYamlStr);
+  }
+
+  /**
+   * Computes SHA-256 hash for a given string.
+   * @param data String data for which checksum needs to be calculated
+   * @return SHA-256 checksum as hex string
+   * @throws IOException If checksum calculation fails
+   */
+  private static String getChecksum(String data) throws IOException {
+    try {
+      return DigestUtils.sha256Hex(data.getBytes(StandardCharsets.UTF_8));
+    } catch (Exception ex) {
+      throw new IOException("Unable to calculate checksum", ex);
+    }
+  }
+
+  /**
+   * Returns the version of the snapshot local data.
+   * @return version
+   */
+  public int getVersion() {
+    return version;
+  }
+
+  /**
+   * Sets the version of the snapshot local data. A valid version shall be 
greater than 0.
+   * @param version version
+   */
+  public void setVersion(int version) {
+    this.version = version;
+  }
+}
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
new file mode 100644
index 0000000000..97b401d8cb
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotLocalDataYaml.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om;
+
+import com.google.common.base.Preconditions;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Map;
+import org.apache.hadoop.hdds.server.YamlUtils;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.AbstractConstruct;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.error.YAMLException;
+import org.yaml.snakeyaml.introspector.BeanAccess;
+import org.yaml.snakeyaml.introspector.PropertyUtils;
+import org.yaml.snakeyaml.nodes.MappingNode;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.Tag;
+import org.yaml.snakeyaml.representer.Representer;
+
+/**
+ * Class for creating and reading snapshot local properties / data YAML files.
+ * Checksum of the YAML fields are computed and stored in the YAML file 
transparently to callers.
+ * Inspired by org.apache.hadoop.ozone.container.common.impl.ContainerDataYaml
+ */
+public final class OmSnapshotLocalDataYaml extends OmSnapshotLocalData {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(OmSnapshotLocalDataYaml.class);
+
+  public static final Tag SNAPSHOT_YAML_TAG = new Tag("OmSnapshotLocalData");
+
+  /**
+   * Creates a new OmSnapshotLocalDataYaml with default values.
+   */
+  public OmSnapshotLocalDataYaml() {
+    super();
+  }
+
+  /**
+   * Copy constructor to create a deep copy.
+   * @param source The source OmSnapshotLocalData to copy from
+   */
+  public OmSnapshotLocalDataYaml(OmSnapshotLocalData source) {
+    super(source);
+  }
+
+  /**
+   * Verifies the checksum of the snapshot data.
+   * @param snapshotData The snapshot data to verify
+   * @return true if the checksum is valid, false otherwise
+   * @throws IOException if there's an error computing the checksum
+   */
+  public static boolean verifyChecksum(OmSnapshotLocalData snapshotData)
+      throws IOException {
+    Preconditions.checkNotNull(snapshotData, "snapshotData cannot be null");
+
+    // Get the stored checksum
+    String storedChecksum = snapshotData.getChecksum();
+    if (storedChecksum == null) {
+      LOG.warn("No checksum found in snapshot data for verification");
+      return false;
+    }
+
+    // Create a copy of the snapshot data for computing checksum
+    OmSnapshotLocalDataYaml snapshotDataCopy = new 
OmSnapshotLocalDataYaml(snapshotData);
+
+    // Clear the existing checksum in the copy
+    snapshotDataCopy.setChecksum(null);
+
+    // Get the YAML representation
+    final Yaml yaml = getYamlForSnapshotLocalData();
+
+    // Compute new checksum
+    snapshotDataCopy.computeAndSetChecksum(yaml);
+
+    // Compare the stored and computed checksums
+    String computedChecksum = snapshotDataCopy.getChecksum();
+    boolean isValid = storedChecksum.equals(computedChecksum);
+
+    if (!isValid) {
+      LOG.warn("Checksum verification failed for snapshot local data. " +
+          "Stored: {}, Computed: {}", storedChecksum, computedChecksum);
+    }
+
+    return isValid;
+  }
+
+  /**
+   * Constructor class for OmSnapshotLocalData.
+   * This is used when parsing YAML files into OmSnapshotLocalDataYaml objects.
+   */
+  private static class SnapshotLocalDataConstructor extends SafeConstructor {
+    SnapshotLocalDataConstructor() {
+      super(new LoaderOptions());
+      //Adding our own specific constructors for tags.
+      this.yamlConstructors.put(SNAPSHOT_YAML_TAG, new 
ConstructSnapshotLocalData());
+    }
+
+    private final class ConstructSnapshotLocalData extends AbstractConstruct {
+      @SuppressWarnings("unchecked")
+      @Override
+      public Object construct(Node node) {
+        MappingNode mnode = (MappingNode) node;
+        Map<Object, Object> nodes = constructMapping(mnode);
+
+        OmSnapshotLocalDataYaml snapshotLocalData = new 
OmSnapshotLocalDataYaml();
+
+        // Set version from YAML
+        Integer version = (Integer) nodes.get(OzoneConsts.OM_SLD_VERSION);
+        // Validate version.
+        if (version <= 0) {
+          // If version is not set or invalid, log a warning, but do not throw.
+          LOG.warn("Invalid version ({}) detected in snapshot local data YAML. 
Proceed with caution.", version);
+        }
+        snapshotLocalData.setVersion(version);
+
+        // Set other fields from parsed YAML
+        snapshotLocalData.setSstFiltered((Boolean) 
nodes.getOrDefault(OzoneConsts.OM_SLD_IS_SST_FILTERED, false));
+
+        Map<String, List<String>> uncompactedSSTFileList =
+            (Map<String, List<String>>) 
nodes.get(OzoneConsts.OM_SLD_UNCOMPACTED_SST_FILE_LIST);
+        if (uncompactedSSTFileList != null) {
+          snapshotLocalData.setUncompactedSSTFileList(uncompactedSSTFileList);
+        }
+
+        snapshotLocalData.setLastCompactionTime(
+            (Long) nodes.getOrDefault(OzoneConsts.OM_SLD_LAST_COMPACTION_TIME, 
-1L));
+        snapshotLocalData.setNeedsCompaction((Boolean) 
nodes.getOrDefault(OzoneConsts.OM_SLD_NEEDS_COMPACTION, false));
+
+        Map<Integer, Map<String, List<String>>> compactedSSTFileList =
+            (Map<Integer, Map<String, List<String>>>) 
nodes.get(OzoneConsts.OM_SLD_COMPACTED_SST_FILE_LIST);
+        if (compactedSSTFileList != null) {
+          snapshotLocalData.setCompactedSSTFileList(compactedSSTFileList);
+        }
+
+        String checksum = (String) nodes.get(OzoneConsts.OM_SLD_CHECKSUM);
+        if (checksum != null) {
+          snapshotLocalData.setChecksum(checksum);
+        }
+
+        return snapshotLocalData;
+      }
+    }
+  }
+
+  /**
+   * Returns the YAML representation of this object as a String
+   * (without triggering checksum computation or persistence).
+   * @return YAML string representation
+   */
+  public String getYaml() {
+    final Yaml yaml = getYamlForSnapshotLocalData();
+    return yaml.dump(this);
+  }
+
+  /**
+   * Computes checksum (stored in this object), and writes this object to a 
YAML file.
+   * @param yamlFile The file to write to
+   * @throws IOException If there's an error writing to the file
+   */
+  public void writeToYaml(File yamlFile) throws IOException {
+    // Create Yaml
+    final Yaml yaml = getYamlForSnapshotLocalData();
+    // Compute Checksum and update SnapshotData
+    computeAndSetChecksum(yaml);
+    // Write the SnapshotData with checksum to Yaml file.
+    YamlUtils.dump(yaml, this, yamlFile, LOG);
+  }
+
+  /**
+   * Creates a OmSnapshotLocalDataYaml instance from a YAML file.
+   * @param yamlFile The YAML file to read from
+   * @return A new OmSnapshotLocalDataYaml instance
+   * @throws IOException If there's an error reading the file
+   */
+  public static OmSnapshotLocalDataYaml getFromYamlFile(File yamlFile) throws 
IOException {
+    Preconditions.checkNotNull(yamlFile, "yamlFile cannot be null");
+    try (InputStream inputFileStream = 
Files.newInputStream(yamlFile.toPath())) {
+      return getFromYamlStream(inputFileStream);
+    }
+  }
+
+  /**
+   * Returns a Yaml representation of the snapshot properties.
+   * @return Yaml representation of snapshot properties
+   */
+  public static Yaml getYamlForSnapshotLocalData() {
+    PropertyUtils propertyUtils = new PropertyUtils();
+    propertyUtils.setBeanAccess(BeanAccess.FIELD);
+    propertyUtils.setAllowReadOnlyProperties(true);
+
+    DumperOptions options = new DumperOptions();
+    Representer representer = new Representer(options);
+    representer.setPropertyUtils(propertyUtils);
+    representer.addClassTag(OmSnapshotLocalDataYaml.class, SNAPSHOT_YAML_TAG);
+
+    SafeConstructor snapshotDataConstructor = new 
SnapshotLocalDataConstructor();
+    return new Yaml(snapshotDataConstructor, representer);
+  }
+
+  /**
+   * Read the YAML content InputStream, and return OmSnapshotLocalDataYaml 
instance.
+   * @throws IOException
+   */
+  public static OmSnapshotLocalDataYaml getFromYamlStream(InputStream input) 
throws IOException {
+    OmSnapshotLocalDataYaml dataYaml;
+
+    PropertyUtils propertyUtils = new PropertyUtils();
+    propertyUtils.setBeanAccess(BeanAccess.FIELD);
+    propertyUtils.setAllowReadOnlyProperties(true);
+
+    DumperOptions options = new DumperOptions();
+    Representer representer = new Representer(options);
+    representer.setPropertyUtils(propertyUtils);
+
+    SafeConstructor snapshotDataConstructor = new 
SnapshotLocalDataConstructor();
+
+    Yaml yaml = new Yaml(snapshotDataConstructor, representer);
+
+    try {
+      dataYaml = yaml.load(input);
+    } catch (YAMLException ex) {
+      // Unchecked exception. Convert to IOException
+      throw new IOException(ex);
+    }
+
+    if (dataYaml == null) {
+      // If Yaml#load returned null, then the file is empty. This is valid yaml
+      // but considered an error in this case since we have lost data about
+      // the snapshot.
+      throw new IOException("Failed to load snapshot file. File is empty.");
+    }
+
+    return dataYaml;
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotManager.java
index 24ae4e03ce..7457fdfb4f 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotManager.java
@@ -769,6 +769,11 @@ public static String getSnapshotPath(OzoneConfiguration 
conf,
         OM_DB_NAME + checkpointDirName;
   }
 
+  public static String getSnapshotLocalPropertyYamlPath(OzoneConfiguration 
conf,
+      SnapshotInfo snapshotInfo) {
+    return getSnapshotPath(conf, snapshotInfo) + ".yaml";
+  }
+
   public static boolean isSnapshotKey(String[] keyParts) {
     return (keyParts.length > 1) &&
         (keyParts[0].compareTo(OM_SNAPSHOT_INDICATOR) == 0);
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
new file mode 100644
index 0000000000..00fea3e360
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmSnapshotLocalDataYaml.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.apache.hadoop.fs.FileSystemTestHelper;
+import org.apache.hadoop.fs.FileUtil;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * This class tests creating and reading snapshot data YAML files.
+ */
+public class TestOmSnapshotLocalDataYaml {
+
+  private static String testRoot = new FileSystemTestHelper().getTestRootDir();
+
+  private static final Instant NOW = Instant.now();
+
+  @BeforeEach
+  public void setUp() {
+    assertTrue(new File(testRoot).mkdirs());
+  }
+
+  @AfterEach
+  public void cleanup() {
+    FileUtil.fullyDelete(new File(testRoot));
+  }
+
+  /**
+   * Creates a snapshot local data YAML file.
+   */
+  private File writeToYaml(String snapshotName) throws IOException {
+    String yamlFilePath = snapshotName + ".yaml";
+
+    OmSnapshotLocalDataYaml dataYaml = new OmSnapshotLocalDataYaml();
+
+    // Set version
+    dataYaml.setVersion(42);
+    // Set SST filtered flag
+    dataYaml.setSstFiltered(true);
+
+    // Add some uncompacted SST files
+    dataYaml.addUncompactedSSTFile("table1", "sst1");
+    dataYaml.addUncompactedSSTFile("table1", "sst2");
+    dataYaml.addUncompactedSSTFile("table2", "sst3");
+
+    // Set last compaction time
+    dataYaml.setLastCompactionTime(NOW.toEpochMilli());
+
+    // Set needs compaction flag
+    dataYaml.setNeedsCompaction(true);
+
+    // Add some compacted SST files
+    dataYaml.addCompactedSSTFile(1, "table1", "compacted-sst1");
+    dataYaml.addCompactedSSTFile(1, "table2", "compacted-sst2");
+    dataYaml.addCompactedSSTFile(2, "table1", "compacted-sst3");
+
+    File yamlFile = new File(testRoot, yamlFilePath);
+
+    // Create YAML file with SnapshotData
+    dataYaml.writeToYaml(yamlFile);
+
+    // Check YAML file exists
+    assertTrue(yamlFile.exists());
+
+    return yamlFile;
+  }
+
+  @Test
+  public void testWriteToYaml() throws IOException {
+    File yamlFile = writeToYaml("snapshot1");
+
+    // Read from YAML file
+    OmSnapshotLocalDataYaml snapshotData = 
OmSnapshotLocalDataYaml.getFromYamlFile(yamlFile);
+
+    // Verify fields
+    assertEquals(42, snapshotData.getVersion());
+    assertTrue(snapshotData.getSstFiltered());
+
+    Map<String, List<String>> uncompactedFiles = 
snapshotData.getUncompactedSSTFileList();
+    assertEquals(2, uncompactedFiles.size());
+    assertEquals(2, uncompactedFiles.get("table1").size());
+    assertEquals(1, uncompactedFiles.get("table2").size());
+    assertTrue(uncompactedFiles.get("table1").contains("sst1"));
+    assertTrue(uncompactedFiles.get("table1").contains("sst2"));
+    assertTrue(uncompactedFiles.get("table2").contains("sst3"));
+
+    assertEquals(NOW.toEpochMilli(), snapshotData.getLastCompactionTime());
+    assertTrue(snapshotData.getNeedsCompaction());
+
+    Map<Integer, Map<String, List<String>>> compactedFiles = 
snapshotData.getCompactedSSTFileList();
+    assertEquals(2, compactedFiles.size());
+    assertTrue(compactedFiles.containsKey(1));
+    assertTrue(compactedFiles.containsKey(2));
+    assertEquals(2, compactedFiles.get(1).size());
+    assertEquals(1, compactedFiles.get(2).size());
+    assertTrue(compactedFiles.get(1).get("table1").contains("compacted-sst1"));
+    assertTrue(compactedFiles.get(1).get("table2").contains("compacted-sst2"));
+    assertTrue(compactedFiles.get(2).get("table1").contains("compacted-sst3"));
+  }
+
+  @Test
+  public void testUpdateSnapshotDataFile() throws IOException {
+    File yamlFile = writeToYaml("snapshot2");
+
+    // Read from YAML file
+    OmSnapshotLocalDataYaml dataYaml =
+        OmSnapshotLocalDataYaml.getFromYamlFile(yamlFile);
+
+    // Update snapshot data
+    dataYaml.setSstFiltered(false);
+    dataYaml.setNeedsCompaction(false);
+    dataYaml.addUncompactedSSTFile("table3", "sst4");
+    dataYaml.addCompactedSSTFile(3, "table3", "compacted-sst4");
+
+    // Write updated data back to file
+    dataYaml.writeToYaml(yamlFile);
+
+    // Read back the updated data
+    dataYaml = OmSnapshotLocalDataYaml.getFromYamlFile(yamlFile);
+
+    // Verify updated data
+    assertThat(dataYaml.getSstFiltered()).isFalse();
+    assertThat(dataYaml.getNeedsCompaction()).isFalse();
+
+    Map<String, List<String>> uncompactedFiles = 
dataYaml.getUncompactedSSTFileList();
+    assertEquals(3, uncompactedFiles.size());
+    assertTrue(uncompactedFiles.containsKey("table3"));
+    assertTrue(uncompactedFiles.get("table3").contains("sst4"));
+
+    Map<Integer, Map<String, List<String>>> compactedFiles = 
dataYaml.getCompactedSSTFileList();
+    assertEquals(3, compactedFiles.size());
+    assertTrue(compactedFiles.containsKey(3));
+    assertTrue(compactedFiles.get(3).containsKey("table3"));
+    assertTrue(compactedFiles.get(3).get("table3").contains("compacted-sst4"));
+  }
+
+  @Test
+  public void testEmptyFile() throws IOException {
+    File emptyFile = new File(testRoot, "empty.yaml");
+    assertTrue(emptyFile.createNewFile());
+
+    IOException ex = assertThrows(IOException.class, () ->
+        OmSnapshotLocalDataYaml.getFromYamlFile(emptyFile));
+
+    assertThat(ex).hasMessageContaining("Failed to load snapshot file. File is 
empty.");
+  }
+
+  @Test
+  public void testChecksum() throws IOException {
+    File yamlFile = writeToYaml("snapshot3");
+
+    // Read from YAML file
+    OmSnapshotLocalDataYaml snapshotData = 
OmSnapshotLocalDataYaml.getFromYamlFile(yamlFile);
+
+    // Get the original checksum
+    String originalChecksum = snapshotData.getChecksum();
+
+    // Verify the checksum is not null or empty
+    assertThat(originalChecksum).isNotNull().isNotEmpty();
+
+    assertTrue(OmSnapshotLocalDataYaml.verifyChecksum(snapshotData));
+  }
+
+  @Test
+  public void testYamlContainsAllFields() throws IOException {
+    File yamlFile = writeToYaml("snapshot4");
+
+    String content = FileUtils.readFileToString(yamlFile, 
Charset.defaultCharset());
+
+    // Verify the YAML content contains all expected fields
+    assertThat(content).contains(OzoneConsts.OM_SLD_VERSION);
+    assertThat(content).contains(OzoneConsts.OM_SLD_CHECKSUM);
+    assertThat(content).contains(OzoneConsts.OM_SLD_IS_SST_FILTERED);
+    assertThat(content).contains(OzoneConsts.OM_SLD_UNCOMPACTED_SST_FILE_LIST);
+    assertThat(content).contains(OzoneConsts.OM_SLD_LAST_COMPACTION_TIME);
+    assertThat(content).contains(OzoneConsts.OM_SLD_NEEDS_COMPACTION);
+    assertThat(content).contains(OzoneConsts.OM_SLD_COMPACTED_SST_FILE_LIST);
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@ozone.apache.org
For additional commands, e-mail: commits-h...@ozone.apache.org

Reply via email to