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 3b8c05e635e HDDS-13217. Test that snapshot checkpoint content is 
preserved after defrag (#10951)
3b8c05e635e is described below

commit 3b8c05e635e8a03fe9c232853220f295e8ac701b
Author: Arun Sarin <[email protected]>
AuthorDate: Sun Aug 9 04:57:59 2026 +0530

    HDDS-13217. Test that snapshot checkpoint content is preserved after defrag 
(#10951)
---
 .../TestOmSnapshotCheckpointDbContent.java         | 511 +++++++++++++++++++++
 1 file changed, 511 insertions(+)

diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOmSnapshotCheckpointDbContent.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOmSnapshotCheckpointDbContent.java
new file mode 100644
index 00000000000..1126a85a0fc
--- /dev/null
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestOmSnapshotCheckpointDbContent.java
@@ -0,0 +1,511 @@
+/*
+ * 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.snapshot;
+
+import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SNAPSHOT_DELETING_SERVICE_INTERVAL;
+import static org.apache.hadoop.ozone.OzoneConsts.OM_KEY_PREFIX;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_FILESYSTEM_SNAPSHOT_ENABLED_KEY;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SNAPSHOT_DEFRAG_SERVICE_INTERVAL;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.SNAPSHOT_DEFRAG_LIMIT_PER_TASK;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.BUCKET_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.DIRECTORY_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.FILE_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.KEY_TABLE;
+import static 
org.apache.hadoop.ozone.om.codec.OMDBDefinition.MULTIPART_INFO_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.OPEN_FILE_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.OPEN_KEY_TABLE;
+import static org.apache.hadoop.ozone.om.codec.OMDBDefinition.VOLUME_TABLE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.utils.IOUtils;
+import org.apache.hadoop.hdds.utils.db.ManagedRawSSTFileReader;
+import org.apache.hadoop.hdds.utils.db.RocksDBCheckpoint;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.hdds.utils.db.Table.KeyValue;
+import org.apache.hadoop.hdds.utils.db.Table.KeyValueIterator;
+import org.apache.hadoop.hdds.utils.db.TablePrefixInfo;
+import org.apache.hadoop.ozone.DataTestUtil;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OmMetadataManagerImpl;
+import org.apache.hadoop.ozone.om.OmSnapshot;
+import org.apache.hadoop.ozone.om.OmSnapshotManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
+import org.apache.ozone.test.GenericTestUtils;
+import org.apache.ratis.util.function.UncheckedAutoCloseableSupplier;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * HDDS-13217: verify that snapshot checkpoint DB content is preserved across 
defrag iterations.
+ *
+ * <p>After defrag compacts a snapshot checkpoint, the bucket-prefix metadata 
in the defragged
+ * checkpoint must still match the checkpoint taken at snapshot creation time 
(version 0).
+ * Version-0 directories are removed by defrag, so baselines are captured 
before the first defrag
+ * pass and compared against the active snapshot afterward.
+ *
+ * <p>{@link #testSnapshotCheckpointContentPreservedAcrossDefragIterations()} 
runs both HDDS-13217
+ * scenarios on OBS and FSO buckets:
+ * <ol>
+ *   <li>Create S1, S2, and S3 with insert, overwrite, and delete deltas 
between snapshots,
+ *       run defrag, and verify each snapshot still matches its version-0 
baseline.</li>
+ *   <li>Delete the middle snapshot (S2), run defrag again, and verify the 
remaining snapshot
+ *       (S3) still matches its baseline.</li>
+ * </ol>
+ *
+ * <p>OBS checks {@code keyTable}; FSO checks {@code fileTable} and {@code 
directoryTable}.
+ * A fresh cluster is started between the two layouts to avoid interference on 
the snapshot
+ * defrag chain.
+ */
+public class TestOmSnapshotCheckpointDbContent {
+
+  private static final byte[] TEST_KEY_CONTENT = new byte[] {0x61, 0x62, 0x63};
+  private static final byte[] OVERWRITE_KEY_CONTENT = new byte[] {0x64, 0x65, 
0x66};
+  private static final int CHECKPOINT_WAIT_MS = 120_000;
+  private static final int PURGE_WAIT_MS = 180_000;
+  private static final int DEFRAG_WAIT_MS = 600_000;
+  private static final int KEY_DELETE_WAIT_MS = 60_000;
+
+  private MiniOzoneCluster cluster;
+  private OzoneConfiguration conf;
+  private OzoneClient client;
+  private ObjectStore store;
+
+  @BeforeEach
+  void initCluster() throws Exception {
+    startCluster();
+  }
+
+  private void startCluster() throws Exception {
+    assumeTrue(ManagedRawSSTFileReader.tryLoadLibrary(),
+        "Snapshot defrag requires rocks-tools native library");
+
+    conf = new OzoneConfiguration();
+    conf.setBoolean(OZONE_FILESYSTEM_SNAPSHOT_ENABLED_KEY, true);
+    // Keep background defrag idle during the test; manual 
triggerSnapshotDefrag() still requires
+    // the service to be initialized (interval must be > 0).
+    conf.setTimeDuration(OZONE_SNAPSHOT_DEFRAG_SERVICE_INTERVAL, 2, 
TimeUnit.HOURS);
+    conf.setInt(SNAPSHOT_DEFRAG_LIMIT_PER_TASK, 10);
+    conf.setTimeDuration(OZONE_SNAPSHOT_DELETING_SERVICE_INTERVAL, 1, 
TimeUnit.SECONDS);
+
+    cluster = MiniOzoneCluster.newBuilder(conf).setNumDatanodes(3).build();
+    cluster.waitForClusterToBeReady();
+    client = cluster.newClient();
+    store = client.getObjectStore();
+    resumeBackgroundServices();
+  }
+
+  private void restartCluster() throws Exception {
+    IOUtils.closeQuietly(client, cluster);
+    startCluster();
+  }
+
+  private void resumeBackgroundServices() {
+    OzoneManager om = cluster.getOzoneManager();
+    om.getKeyManager().getDeletingService().resume();
+    om.getKeyManager().getDirDeletingService().resume();
+    om.getKeyManager().getSnapshotDeletingService().resume();
+  }
+
+  @AfterEach
+  void shutdownCluster() {
+    IOUtils.closeQuietly(client, cluster);
+  }
+
+  /**
+   * HDDS-13217 scenarios for OBS and FSO buckets:
+   * <ol>
+   *   <li>Create S1, S2, S3 with insert, overwrite, and delete deltas between 
snapshots, run defrag,
+   *       and verify each defragged checkpoint still matches its version-0 
baseline.</li>
+   *   <li>Delete the middle snapshot, run defrag again, and verify the 
remaining youngest snapshot
+   *       checkpoint still matches its baseline.</li>
+   * </ol>
+   */
+  @Test
+  public void testSnapshotCheckpointContentPreservedAcrossDefragIterations()
+      throws Exception {
+    runDefragIntegrityScenario(BucketLayout.OBJECT_STORE);
+    // Use a fresh cluster for FSO to avoid interference on the global 
snapshot defrag chain.
+    restartCluster();
+    runDefragIntegrityScenario(BucketLayout.FILE_SYSTEM_OPTIMIZED);
+  }
+
+  private void runDefragIntegrityScenario(BucketLayout layout)
+      throws Exception {
+    ThreeSnapshotSetup setup = createThreeSnapshotsOnNewBucket(layout);
+    triggerDefragUntilDone(setup.snapshots);
+    assertCheckpointMatchesBaseline(setup.baselines, setup.snapshots, layout);
+
+    SnapshotInfo s2 = setup.snapshots.get(1);
+    SnapshotInfo s3 = setup.snapshots.get(2);
+    int s3VersionAfterFirstDefrag = readSnapshotVersion(s3);
+
+    store.deleteSnapshot(setup.volumeName, setup.bucketName, s2.getName());
+    waitForSnapshotPurged(s2);
+    // Reload S3 so pathPreviousSnapshotId reflects the purged chain, not 
deleted S2.
+    s3 = loadSnapshotInfo(setup.volumeName, setup.bucketName, s3.getName());
+
+    triggerDefragUntilVersionIncreases(s3, s3VersionAfterFirstDefrag);
+
+    assertCheckpointMatchesBaseline(
+        Collections.singletonMap(s3.getSnapshotId(),
+            setup.baselines.get(s3.getSnapshotId())),
+        Arrays.asList(s3),
+        layout);
+  }
+
+  private ThreeSnapshotSetup createThreeSnapshotsOnNewBucket(BucketLayout 
layout)
+      throws IOException, InterruptedException, TimeoutException {
+    OzoneBucket bucket = DataTestUtil.createVolumeAndBucket(client, layout);
+    String volumeName = bucket.getVolumeName();
+    String bucketName = bucket.getName();
+
+    String keyA = objectKey(layout, "key-a");
+    String keyB = objectKey(layout, "key-b");
+    String keyS2 = objectKey(layout, "key-s2");
+    String keyS3 = objectKey(layout, "key-s3");
+
+    DataTestUtil.createKey(bucket, keyA, TEST_KEY_CONTENT);
+    DataTestUtil.createKey(bucket, keyB, TEST_KEY_CONTENT);
+    store.createSnapshot(volumeName, bucketName, "snap-s1");
+
+    DataTestUtil.createKey(bucket, keyA, OVERWRITE_KEY_CONTENT);
+    DataTestUtil.createKey(bucket, keyS2, TEST_KEY_CONTENT);
+    store.createSnapshot(volumeName, bucketName, "snap-s2");
+
+    bucket.deleteKey(keyB);
+    waitForKeyDeleted(bucket, keyB);
+    DataTestUtil.createKey(bucket, keyS3, TEST_KEY_CONTENT);
+    store.createSnapshot(volumeName, bucketName, "snap-s3");
+
+    List<SnapshotInfo> snapshots = Arrays.asList(
+        loadSnapshotInfo(volumeName, bucketName, "snap-s1"),
+        loadSnapshotInfo(volumeName, bucketName, "snap-s2"),
+        loadSnapshotInfo(volumeName, bucketName, "snap-s3"));
+
+    for (SnapshotInfo snapshotInfo : snapshots) {
+      waitForCheckpointReady(snapshotInfo);
+    }
+
+    OMMetadataManager liveMm = cluster.getOzoneManager().getMetadataManager();
+    TablePrefixInfo prefixes = liveMm.getTableBucketPrefix(volumeName, 
bucketName);
+    Map<UUID, SnapshotBaseline> baselines = new HashMap<>();
+    for (SnapshotInfo snapshotInfo : snapshots) {
+      baselines.put(snapshotInfo.getSnapshotId(), 
captureBaseline(snapshotInfo, prefixes, layout));
+    }
+    return new ThreeSnapshotSetup(volumeName, bucketName, snapshots, 
baselines);
+  }
+
+  private static String objectKey(BucketLayout layout, String name) {
+    return layout.isFileSystemOptimized() ? "dir/" + name : name;
+  }
+
+  private SnapshotInfo loadSnapshotInfo(String volumeName, String bucketName,
+      String snapshotName) throws IOException {
+    OzoneManager om = cluster.getOzoneManager();
+    SnapshotInfo snapshotInfo = 
om.getMetadataManager().getSnapshotInfoTable().get(
+        SnapshotInfo.getTableKey(volumeName, bucketName, snapshotName));
+    assertNotNull(snapshotInfo, "Snapshot row should exist for " + 
snapshotName);
+    assertEquals(snapshotName, snapshotInfo.getName());
+    return snapshotInfo;
+  }
+
+  private void waitForCheckpointReady(SnapshotInfo snapshotInfo)
+      throws TimeoutException, InterruptedException {
+    String currentPath = OmSnapshotManager.getSnapshotPath(conf, snapshotInfo, 
0)
+        + OM_KEY_PREFIX + "CURRENT";
+    GenericTestUtils.waitFor(() -> new File(currentPath).exists(), 1000, 
CHECKPOINT_WAIT_MS);
+  }
+
+  private void waitForSnapshotPurged(SnapshotInfo snapshotInfo)
+      throws TimeoutException, InterruptedException {
+    OzoneManager om = cluster.getOzoneManager();
+    resumeBackgroundServices();
+    GenericTestUtils.waitFor(() -> {
+      try {
+        return om.getMetadataManager().getSnapshotInfoTable()
+            .get(snapshotInfo.getTableKey()) == null;
+      } catch (IOException e) {
+        return false;
+      }
+    }, 1000, PURGE_WAIT_MS);
+  }
+
+  private void waitForKeyDeleted(OzoneBucket bucket, String keyName)
+      throws TimeoutException, InterruptedException {
+    GenericTestUtils.waitFor(() -> {
+      try {
+        bucket.getKey(keyName);
+        return false;
+      } catch (IOException e) {
+        return true;
+      }
+    }, 1000, KEY_DELETE_WAIT_MS);
+  }
+
+  /**
+   * Wait for a follow-up defrag pass after snapshot-chain rewiring (e.g. 
middle snapshot purge).
+   * Unlike {@link #triggerDefragUntilDone}, this requires the snapshot 
version to increase so we
+   * do not treat an already-defragged checkpoint from an earlier pass as 
complete.
+   */
+  private void triggerDefragUntilVersionIncreases(SnapshotInfo snapshotInfo,
+      int baselineVersion) throws TimeoutException, InterruptedException {
+    OzoneManager om = cluster.getOzoneManager();
+    String volumeName = snapshotInfo.getVolumeName();
+    String bucketName = snapshotInfo.getBucketName();
+    String snapshotName = snapshotInfo.getName();
+    GenericTestUtils.waitFor(() -> {
+      try {
+        SnapshotInfo currentSnapshot = loadSnapshotInfo(volumeName, 
bucketName, snapshotName);
+        if (readSnapshotVersion(currentSnapshot) > baselineVersion
+            && isSnapshotDefragComplete(currentSnapshot)) {
+          return true;
+        }
+        om.triggerSnapshotDefrag(false);
+        currentSnapshot = loadSnapshotInfo(volumeName, bucketName, 
snapshotName);
+        return readSnapshotVersion(currentSnapshot) > baselineVersion
+            && isSnapshotDefragComplete(currentSnapshot);
+      } catch (IOException e) {
+        return false;
+      }
+    }, 2000, DEFRAG_WAIT_MS);
+  }
+
+  private void triggerDefragUntilDone(List<SnapshotInfo> snapshots)
+      throws TimeoutException, InterruptedException {
+    OzoneManager om = cluster.getOzoneManager();
+    GenericTestUtils.waitFor(() -> {
+      if (areAllSnapshotsDefragComplete(snapshots)) {
+        return true;
+      }
+      try {
+        om.triggerSnapshotDefrag(false);
+      } catch (IOException e) {
+        return false;
+      }
+      return areAllSnapshotsDefragComplete(snapshots);
+    }, 2000, DEFRAG_WAIT_MS);
+  }
+
+  private boolean areAllSnapshotsDefragComplete(List<SnapshotInfo> snapshots) {
+    for (SnapshotInfo snapshotInfo : snapshots) {
+      if (!isSnapshotDefragComplete(snapshotInfo)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean isSnapshotDefragComplete(SnapshotInfo snapshotInfo) {
+    try {
+      OmSnapshotLocalDataManager localDataManager =
+          
cluster.getOzoneManager().getOmSnapshotManager().getSnapshotLocalDataManager();
+      try (OmSnapshotLocalDataManager.ReadableOmSnapshotLocalDataProvider 
provider =
+               localDataManager.getOmSnapshotLocalData(snapshotInfo)) {
+        return provider.getVersion() > 0 && !provider.needsDefrag();
+      }
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
+  private int readSnapshotVersion(SnapshotInfo snapshotInfo) throws 
IOException {
+    OmSnapshotLocalDataManager localDataManager =
+        
cluster.getOzoneManager().getOmSnapshotManager().getSnapshotLocalDataManager();
+    try (OmSnapshotLocalDataManager.ReadableOmSnapshotLocalDataProvider 
provider =
+             localDataManager.getOmSnapshotLocalData(snapshotInfo)) {
+      return (int) provider.getVersion();
+    }
+  }
+
+  private SnapshotBaseline captureBaseline(SnapshotInfo snapshotInfo,
+      TablePrefixInfo prefixes, BucketLayout layout) throws IOException {
+    try (OmMetadataManagerImpl checkpointMm = openCheckpoint(snapshotInfo, 0)) 
{
+      return new SnapshotBaseline(
+          readAllBucketPrefixTables(checkpointMm, prefixes, layout));
+    }
+  }
+
+  private void assertCheckpointMatchesBaseline(
+      Map<UUID, SnapshotBaseline> baselines, List<SnapshotInfo> snapshots,
+      BucketLayout layout) throws IOException {
+    OzoneManager om = cluster.getOzoneManager();
+    for (SnapshotInfo snapshotInfo : snapshots) {
+      SnapshotBaseline baseline = baselines.get(snapshotInfo.getSnapshotId());
+      assertNotNull(baseline, "Missing baseline for " + 
snapshotInfo.getName());
+      TablePrefixInfo prefixes = om.getMetadataManager().getTableBucketPrefix(
+          snapshotInfo.getVolumeName(), snapshotInfo.getBucketName());
+      try (UncheckedAutoCloseableSupplier<OmSnapshot> activeSnapshot =
+               om.getOmSnapshotManager().getActiveSnapshot(
+                   snapshotInfo.getVolumeName(),
+                   snapshotInfo.getBucketName(),
+                   snapshotInfo.getName())) {
+        OMMetadataManager currentMm = 
activeSnapshot.get().getMetadataManager();
+        assertBucketPrefixTablesMatch(baseline.getTableData(), currentMm, 
prefixes, layout);
+      }
+    }
+  }
+
+  private OmMetadataManagerImpl openCheckpoint(SnapshotInfo snapshotInfo, int 
version)
+      throws IOException {
+    RocksDBCheckpoint checkpoint = new RocksDBCheckpoint(
+        Paths.get(OmSnapshotManager.getSnapshotPath(conf, snapshotInfo, 
version)));
+    return OmMetadataManagerImpl.createCheckpointMetadataManager(conf, 
checkpoint);
+  }
+
+  /**
+   * Collects the snapshot OM metadata for the bucket-relevant tables and 
returns
+   * a map from table name to sorted key-value entries scoped to the bucket 
prefix.
+   */
+  private static Map<String, SortedMap<String, ?>> readAllBucketPrefixTables(
+      OMMetadataManager mm,
+      TablePrefixInfo prefixes,
+      BucketLayout layout) throws IOException {
+    Map<String, SortedMap<String, ?>> tables = new HashMap<>();
+    tables.put(VOLUME_TABLE, filterTableEntriesByKeyPrefix(mm.getVolumeTable(),
+        prefixes.getTablePrefix(VOLUME_TABLE)));
+    tables.put(BUCKET_TABLE, filterTableEntriesByKeyPrefix(mm.getBucketTable(),
+        prefixes.getTablePrefix(BUCKET_TABLE)));
+    if (layout.isFileSystemOptimized()) {
+      tables.put(FILE_TABLE, filterTableEntriesByKeyPrefix(mm.getFileTable(),
+          prefixes.getTablePrefix(FILE_TABLE)));
+      tables.put(DIRECTORY_TABLE, 
filterTableEntriesByKeyPrefix(mm.getDirectoryTable(),
+          prefixes.getTablePrefix(DIRECTORY_TABLE)));
+      tables.put(OPEN_FILE_TABLE, 
filterTableEntriesByKeyPrefix(mm.getOpenKeyTable(layout),
+          prefixes.getTablePrefix(OPEN_FILE_TABLE)));
+    } else {
+      tables.put(KEY_TABLE, 
filterTableEntriesByKeyPrefix(mm.getKeyTable(layout),
+          prefixes.getTablePrefix(KEY_TABLE)));
+      tables.put(OPEN_KEY_TABLE, 
filterTableEntriesByKeyPrefix(mm.getOpenKeyTable(layout),
+          prefixes.getTablePrefix(OPEN_KEY_TABLE)));
+    }
+    tables.put(MULTIPART_INFO_TABLE, 
filterTableEntriesByKeyPrefix(mm.getMultipartInfoTable(),
+        prefixes.getTablePrefix(MULTIPART_INFO_TABLE)));
+    return tables;
+  }
+
+  private static void assertBucketPrefixTablesMatch(
+      Map<String, SortedMap<String, ?>> baseline,
+      OMMetadataManager current,
+      TablePrefixInfo prefixes,
+      BucketLayout layout) throws IOException {
+
+    assertPrefixEquals(VOLUME_TABLE, baseline.get(VOLUME_TABLE),
+        current.getVolumeTable(), prefixes);
+    assertPrefixEquals(BUCKET_TABLE, baseline.get(BUCKET_TABLE),
+        current.getBucketTable(), prefixes);
+    if (layout.isFileSystemOptimized()) {
+      assertPrefixEquals(FILE_TABLE, baseline.get(FILE_TABLE),
+          current.getFileTable(), prefixes);
+      assertPrefixEquals(DIRECTORY_TABLE, baseline.get(DIRECTORY_TABLE),
+          current.getDirectoryTable(), prefixes);
+      assertPrefixEquals(OPEN_FILE_TABLE, baseline.get(OPEN_FILE_TABLE),
+          current.getOpenKeyTable(layout), prefixes);
+    } else {
+      assertPrefixEquals(KEY_TABLE, baseline.get(KEY_TABLE),
+          current.getKeyTable(layout), prefixes);
+      assertPrefixEquals(OPEN_KEY_TABLE, baseline.get(OPEN_KEY_TABLE),
+          current.getOpenKeyTable(layout), prefixes);
+    }
+    assertPrefixEquals(MULTIPART_INFO_TABLE, 
baseline.get(MULTIPART_INFO_TABLE),
+        current.getMultipartInfoTable(), prefixes);
+  }
+
+  private static <V> void assertPrefixEquals(
+      String tableName,
+      SortedMap<String, ?> expected,
+      Table<String, V> current,
+      TablePrefixInfo prefixes) throws IOException {
+    String prefix = prefixes.getTablePrefix(tableName);
+    assertTrue(prefix != null && !prefix.isEmpty(),
+        "Expected non-empty prefix for " + tableName);
+    assertEquals(expected, filterTableEntriesByKeyPrefix(current, prefix), 
tableName);
+  }
+
+  /**
+   * Filters table entries whose keys start with {@code prefix} and returns 
them in a sorted map.
+   */
+  private static <V> SortedMap<String, V> filterTableEntriesByKeyPrefix(
+      Table<String, V> table, String prefix) throws IOException {
+    SortedMap<String, V> map = new TreeMap<>();
+    if (prefix == null || prefix.isEmpty()) {
+      return map;
+    }
+    try (KeyValueIterator<String, V> it = table.iterator(prefix)) {
+      while (it.hasNext()) {
+        KeyValue<String, V> kv = it.next();
+        if (!kv.getKey().startsWith(prefix)) {
+          break;
+        }
+        map.put(kv.getKey(), kv.getValue());
+      }
+    }
+    return map;
+  }
+
+  private static final class SnapshotBaseline {
+    private final Map<String, SortedMap<String, ?>> tableData;
+
+    private SnapshotBaseline(Map<String, SortedMap<String, ?>> tableData) {
+      this.tableData = tableData;
+    }
+
+    private Map<String, SortedMap<String, ?>> getTableData() {
+      return tableData;
+    }
+  }
+
+  private static final class ThreeSnapshotSetup {
+    private final String volumeName;
+    private final String bucketName;
+    private final List<SnapshotInfo> snapshots;
+    private final Map<UUID, SnapshotBaseline> baselines;
+
+    private ThreeSnapshotSetup(String volumeName, String bucketName,
+        List<SnapshotInfo> snapshots, Map<UUID, SnapshotBaseline> baselines) {
+      this.volumeName = volumeName;
+      this.bucketName = bucketName;
+      this.snapshots = new ArrayList<>(snapshots);
+      this.baselines = baselines;
+    }
+  }
+}


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

Reply via email to