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 8d61861f48f HDDS-15860. Always rewrite live SSTs before snapshot 
defrag ingestion (#10772)
8d61861f48f is described below

commit 8d61861f48fb9589fe5a775878d80de20c95b43d
Author: Siyao Meng <[email protected]>
AuthorDate: Tue Jul 21 11:17:33 2026 -0700

    HDDS-15860. Always rewrite live SSTs before snapshot defrag ingestion 
(#10772)
    
    Generated-by: Codex (GPT-5.6 Sol)
---
 .../om/snapshot/defrag/SnapshotDefragService.java  |  35 ++--
 .../snapshot/defrag/TestSnapshotDefragService.java | 216 +++++++++++++++++----
 2 files changed, 195 insertions(+), 56 deletions(-)

diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/defrag/SnapshotDefragService.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/defrag/SnapshotDefragService.java
index 4f018ed4641..d96e4869b9d 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/defrag/SnapshotDefragService.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/defrag/SnapshotDefragService.java
@@ -377,7 +377,6 @@ private Pair<Path, Boolean> 
spillTableDiffIntoSstFile(List<Path> deltaFilePaths,
    * @param previousSnapshotInfo information about the previous snapshot.
    * @param snapshotInfo information about the current snapshot for which
    *                     incremental defragmentation is performed.
-   * @param snapshotVersion the version of the snapshot to be processed.
    * @param checkpointStore the dbStore instance where data
    *                        updates are ingested after being processed.
    * @param bucketPrefixInfo table prefix information associated with buckets,
@@ -387,7 +386,7 @@ private Pair<Path, Boolean> 
spillTableDiffIntoSstFile(List<Path> deltaFilePaths,
    */
   @VisibleForTesting
   void performIncrementalDefragmentation(SnapshotInfo previousSnapshotInfo, 
SnapshotInfo snapshotInfo,
-      int snapshotVersion, DBStore checkpointStore, TablePrefixInfo 
bucketPrefixInfo, Set<String> incrementalTables)
+      DBStore checkpointStore, TablePrefixInfo bucketPrefixInfo, Set<String> 
incrementalTables)
       throws IOException {
     // Map of delta files grouped on the basis of the tableName.
     Collection<Pair<Path, SstFileInfo>> allTableDeltaFiles = 
this.deltaDiffComputer.getDeltaFiles(
@@ -411,22 +410,18 @@ void performIncrementalDefragmentation(SnapshotInfo 
previousSnapshotInfo, Snapsh
       for (Map.Entry<String, List<Path>> entry : 
tableGroupedDeltaFiles.entrySet()) {
         String table = entry.getKey();
         List<Path> deltaFiles = entry.getValue();
-        Path fileToBeIngested;
-        if (deltaFiles.size() == 1 && snapshotVersion > 0) {
-          // If there is only one delta file for the table and the snapshot 
version is also not 0 then the same delta
-          // file can reingested into the checkpointStore.
-          fileToBeIngested = deltaFiles.get(0);
-        } else {
-          Table<String, CodecBuffer> snapshotTable = 
snapshot.get().getMetadataManager().getStore()
-              .getTable(table, StringCodec.get(), CodecBufferCodec.get(true));
-          Table<String, CodecBuffer> previousSnapshotTable = 
previousSnapshot.get().getMetadataManager().getStore()
-              .getTable(table, StringCodec.get(), CodecBufferCodec.get(true));
-          String tableBucketPrefix = bucketPrefixInfo.getTablePrefix(table);
-          Pair<Path, Boolean> spillResult = 
spillTableDiffIntoSstFile(deltaFiles, snapshotTable,
-              previousSnapshotTable, tableBucketPrefix);
-          fileToBeIngested = spillResult.getValue() ? spillResult.getLeft() : 
null;
-          filesToBeDeleted.add(spillResult.getLeft());
-        }
+        // Delta candidates are live RocksDB SSTs selected at file granularity,
+        // not valid external SSTs containing an exact bucket-level delta. 
Always
+        // rebuild the logical delta, even when there is only one candidate 
file.
+        Table<String, CodecBuffer> snapshotTable = 
snapshot.get().getMetadataManager().getStore()
+            .getTable(table, StringCodec.get(), CodecBufferCodec.get(true));
+        Table<String, CodecBuffer> previousSnapshotTable = 
previousSnapshot.get().getMetadataManager().getStore()
+            .getTable(table, StringCodec.get(), CodecBufferCodec.get(true));
+        String tableBucketPrefix = bucketPrefixInfo.getTablePrefix(table);
+        Pair<Path, Boolean> spillResult = 
spillTableDiffIntoSstFile(deltaFiles, snapshotTable,
+            previousSnapshotTable, tableBucketPrefix);
+        Path fileToBeIngested = spillResult.getValue() ? spillResult.getLeft() 
: null;
+        filesToBeDeleted.add(spillResult.getLeft());
         if (fileToBeIngested != null) {
           if (!fileToBeIngested.toFile().exists()) {
             throw new IOException("Delta file does not exist: " + 
fileToBeIngested);
@@ -687,8 +682,8 @@ boolean checkAndDefragSnapshot(SnapshotChainManager 
chainManager, UUID snapshotI
         LOG.info("Performing incremental defragmentation for snapshot: {} (ID: 
{})", snapshotInfo.getTableKey(),
             snapshotInfo.getSnapshotId());
         try {
-          performIncrementalDefragmentation(checkpointSnapshotInfo, 
snapshotInfo, needsDefragVersionPair.getValue(),
-              checkpointDBStore, prefixInfo, 
COLUMN_FAMILIES_TO_TRACK_IN_SNAPSHOT);
+          performIncrementalDefragmentation(checkpointSnapshotInfo, 
snapshotInfo, checkpointDBStore, prefixInfo,
+              COLUMN_FAMILIES_TO_TRACK_IN_SNAPSHOT);
           perfMetrics.setSnapshotDefragServiceIncLatencyMs(Time.monotonicNow() 
- defragStart);
         } catch (IOException e) {
           snapshotMetrics.incNumSnapshotIncDefragFails();
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/defrag/TestSnapshotDefragService.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/defrag/TestSnapshotDefragService.java
index f93d95d6ac9..99beb715c50 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/defrag/TestSnapshotDefragService.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/defrag/TestSnapshotDefragService.java
@@ -52,7 +52,9 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -61,6 +63,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
@@ -82,9 +85,11 @@
 import org.apache.hadoop.hdds.utils.db.CodecException;
 import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
 import org.apache.hadoop.hdds.utils.db.DBStore;
+import org.apache.hadoop.hdds.utils.db.DBStoreBuilder;
 import org.apache.hadoop.hdds.utils.db.InMemoryTestTable;
 import org.apache.hadoop.hdds.utils.db.ManagedRawSSTFileReader;
 import org.apache.hadoop.hdds.utils.db.RDBSstFileWriter;
+import org.apache.hadoop.hdds.utils.db.RDBStore;
 import org.apache.hadoop.hdds.utils.db.RocksDBCheckpoint;
 import org.apache.hadoop.hdds.utils.db.RocksDatabaseException;
 import org.apache.hadoop.hdds.utils.db.SstFileSetReader;
@@ -122,6 +127,7 @@
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.EnumSource;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.InOrder;
@@ -130,6 +136,7 @@
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.rocksdb.LiveFileMetaData;
 
 /**
  * Unit tests for SnapshotDefragService.
@@ -170,6 +177,11 @@ public class TestSnapshotDefragService {
   private Map<String, CodecBuffer> dummyTableValues;
   private Set<CodecBuffer> closeSet = new HashSet<>();
 
+  private enum LiveSstType {
+    DB_GENERATED,
+    PREVIOUSLY_INGESTED
+  }
+
   @BeforeEach
   public void setup() throws IOException {
     mocks = MockitoAnnotations.openMocks(this);
@@ -224,6 +236,83 @@ private String getFromCodecBuffer(CodecBuffer buffer) {
     return StringCodec.get().fromCodecBuffer(buffer);
   }
 
+  private void putString(Table<String, CodecBuffer> table, String key,
+      String value) throws RocksDatabaseException, CodecException {
+    table.put(key, StringCodec.get().toDirectCodecBuffer(value));
+  }
+
+  private DBStore createDBStore(String name, String tableName)
+      throws RocksDatabaseException {
+    return DBStoreBuilder.newBuilder(configuration)
+        .setName(name)
+        .setPath(tempDir)
+        .addTable(tableName)
+        .build();
+  }
+
+  private Path createLiveSstDelta(DBStore sourceStore, String tableName,
+      String key, LiveSstType liveSstType) throws Exception {
+    Table<String, CodecBuffer> sourceTable = sourceStore.getTable(
+        tableName, StringCodec.get(), CodecBufferCodec.get(true));
+    putString(sourceTable, key, "source-value");
+    sourceStore.flushDB();
+    Set<String> liveFilesBeforeIngestion = ((RDBStore) sourceStore).getDb()
+        .getLiveFilesMetaData().stream()
+        .map(LiveFileMetaData::fileName)
+        .collect(Collectors.toSet());
+
+    if (liveSstType == LiveSstType.PREVIOUSLY_INGESTED) {
+      File externalFile = tempDir.resolve("external-" + UUID.randomUUID()
+          + ".sst").toFile();
+      try (RDBSstFileWriter writer = new RDBSstFileWriter(externalFile);
+           CodecBuffer keyBuffer = StringCodec.get().toDirectCodecBuffer(key);
+           CodecBuffer valueBuffer = StringCodec.get()
+               .toDirectCodecBuffer("ingested-value")) {
+        writer.put(keyBuffer, valueBuffer);
+      }
+      sourceTable.loadFromFile(externalFile);
+    }
+
+    List<LiveFileMetaData> candidateFiles = ((RDBStore) sourceStore).getDb()
+        .getLiveFilesMetaData().stream()
+        .filter(file -> tableName.equals(
+            StringUtils.bytes2String(file.columnFamilyName())))
+        .filter(file -> liveSstType != LiveSstType.PREVIOUSLY_INGESTED ||
+            !liveFilesBeforeIngestion.contains(file.fileName()))
+        .collect(Collectors.toList());
+    if (candidateFiles.size() != 1) {
+      throw new IllegalStateException("Expected one live SST file, found " +
+          candidateFiles.size());
+    }
+    LiveFileMetaData liveFile = candidateFiles.get(0);
+    Path sourceFile = Paths.get(liveFile.path(), liveFile.fileName());
+    Path deltaFile = tempDir.resolve("delta-" + UUID.randomUUID() + ".sst");
+    Files.createLink(deltaFile, sourceFile);
+    return deltaFile;
+  }
+
+  private void createMockSnapshot(SnapshotInfo snapshotInfo,
+      DBStore snapshotStore) throws IOException {
+    OmSnapshot snapshot = mock(OmSnapshot.class);
+    UncheckedAutoCloseableSupplier<OmSnapshot> snapshotSupplier =
+        new UncheckedAutoCloseableSupplier<OmSnapshot>() {
+          @Override
+          public void close() {
+          }
+
+          @Override
+          public OmSnapshot get() {
+            return snapshot;
+          }
+        };
+    OMMetadataManager snapshotMetadataManager = mock(OMMetadataManager.class);
+    when(snapshot.getMetadataManager()).thenReturn(snapshotMetadataManager);
+    when(snapshotMetadataManager.getStore()).thenReturn(snapshotStore);
+    when(omSnapshotManager.getActiveSnapshot(
+        eq(snapshotInfo.getVolumeName()), eq(snapshotInfo.getBucketName()),
+        eq(snapshotInfo.getName()))).thenReturn(snapshotSupplier);
+  }
+
   @AfterEach
   public void tearDown() throws Exception {
     if (defragService != null) {
@@ -639,12 +728,87 @@ public OmSnapshot get() {
         eq(snapshotInfo.getName()))).thenReturn(snapshotSupplier);
   }
 
+  @ParameterizedTest
+  @EnumSource(LiveSstType.class)
+  public void testRewritesSingleLiveSstBeforeIngestion(
+      LiveSstType liveSstType) throws Exception {
+    String tableName = "cf1";
+    String key = "ab001";
+    String previousValue = "previous-value";
+    String currentValue = "current-value";
+    SnapshotInfo previousSnapshotInfo = createMockSnapshotInfo(
+        UUID.randomUUID(), "vol1", "bucket1", "snap1");
+    SnapshotInfo snapshotInfo = createMockSnapshotInfo(
+        UUID.randomUUID(), "vol1", "bucket1", "snap2");
+    TablePrefixInfo prefixInfo = new TablePrefixInfo(
+        ImmutableMap.of(tableName, "ab"));
+
+    try (DBStore sourceStore = createDBStore(
+             "source-" + liveSstType + "-" + UUID.randomUUID(), tableName);
+         DBStore previousStore = createDBStore(
+             "previous-" + liveSstType + "-" + UUID.randomUUID(), tableName);
+         DBStore snapshotStore = createDBStore(
+             "snapshot-" + liveSstType + "-" + UUID.randomUUID(), tableName);
+         DBStore checkpointStore = createDBStore(
+             "checkpoint-" + liveSstType + "-" + UUID.randomUUID(),
+             tableName)) {
+      putString(previousStore.getTable(
+          tableName, StringCodec.get(), CodecBufferCodec.get(true)),
+          key, previousValue);
+      previousStore.flushDB();
+      putString(snapshotStore.getTable(
+          tableName, StringCodec.get(), CodecBufferCodec.get(true)),
+          key, currentValue);
+      snapshotStore.flushDB();
+      createMockSnapshot(previousSnapshotInfo, previousStore);
+      createMockSnapshot(snapshotInfo, snapshotStore);
+      Path deltaFile = createLiveSstDelta(
+          sourceStore, tableName, key, liveSstType);
+      when(deltaFileComputer.getDeltaFiles(eq(previousSnapshotInfo),
+          eq(snapshotInfo), eq(ImmutableSet.of(tableName))))
+          .thenReturn(ImmutableList.of(Pair.of(deltaFile,
+              new SstFileInfo(deltaFile.toFile().getName(), key, key,
+                  tableName))));
+
+      try (MockedConstruction<SstFileSetReader> ignored = mockConstruction(
+          SstFileSetReader.class, (mock, context) -> when(mock
+              .getKeyStreamWithTombstone(anyString(), anyString()))
+              .thenReturn(new ClosableIterator<String>() {
+                private boolean hasNext = true;
+
+                @Override
+                public void close() {
+                }
+
+                @Override
+                public boolean hasNext() {
+                  return hasNext;
+                }
+
+                @Override
+                public String next() {
+                  if (!hasNext) {
+                    throw new NoSuchElementException();
+                  }
+                  hasNext = false;
+                  return key;
+                }
+              }))) {
+        defragService.performIncrementalDefragmentation(previousSnapshotInfo,
+            snapshotInfo, checkpointStore, prefixInfo,
+            ImmutableSet.of(tableName));
+      }
+
+      assertEquals(currentValue, checkpointStore.getTable(
+          tableName, StringCodec.get(), StringCodec.get()).get(key));
+    }
+  }
+
   /**
    * Tests the incremental defragmentation process between two snapshots.
    *
-   * <p>This parameterized test validates the {@code 
performIncrementalDefragmentation} method
-   * across different version scenarios (0, 1, 2, 10) to ensure proper 
handling of snapshot
-   * delta files and version-specific optimizations.</p>
+   * <p>This test validates that all snapshot delta files are rewritten into
+   * fresh external SST files before ingestion.</p>
    *
    * <h3>Test Data Generation:</h3>
    * Creates 67,600 synthetic key-value pairs (26×26×100) distributed across 
two snapshots
@@ -667,38 +831,26 @@ public OmSnapshot get() {
    *   <li>SstFileSetReader mock returns keys with indices 0-4 (i % 6 < 5)</li>
    * </ul>
    *
-   * <h3>Version-Specific Behavior:</h3>
+   * <h3>Expected Behavior:</h3>
    * <ul>
-   *   <li><b>currentVersion == 0 (initial version):</b>
-   *     <ul>
-   *       <li>All incremental tables are dumped to new SST files</li>
-   *       <li>All dumped files are ingested into the checkpoint database</li>
-   *     </ul>
-   *   </li>
-   *   <li><b>currentVersion > 0 (subsequent versions):</b>
-   *     <ul>
-   *       <li>Single delta file tables (cf1) are ingested directly without 
merging</li>
-   *       <li>Multiple delta file tables (cf2) are merged and dumped before 
ingestion</li>
-   *       <li>Optimization: avoids unnecessary file I/O for single delta 
files</li>
-   *     </ul>
-   *   </li>
+   *   <li>Every incremental table is dumped to a fresh external SST file</li>
+   *   <li>All dumped files are ingested into the checkpoint database</li>
+   *   <li>Behavior is independent of snapshot version and delta-file 
count</li>
    * </ul>
    *
    * <h3>Assertions:</h3>
    * <ul>
-   *   <li>Verifies correct tables are dumped and ingested based on 
version</li>
+   *   <li>Verifies all incremental tables are dumped and ingested</li>
    *   <li>Validates that only modified keys (i % 6 < 3) appear in delta 
files</li>
    *   <li>Confirms written values match snap2's values or null for 
deletions</li>
    *   <li>Ensures all incremental tables are ultimately ingested</li>
    * </ul>
    *
-   * @param currentVersion the snapshot version being defragmented (0 for 
initial, >0 for subsequent)
    * @throws Exception if any error occurs during the test execution
    */
   @SuppressWarnings("checkstyle:MethodLength")
-  @ParameterizedTest
-  @ValueSource(ints = {0, 1, 2, 10})
-  public void testPerformIncrementalDefragmentation(int currentVersion) throws 
Exception {
+  @Test
+  public void testPerformIncrementalDefragmentation() throws Exception {
     DBStore checkpointDBStore = mock(DBStore.class);
     String samePrefix = "samePrefix";
     String snap1Prefix = "snap1Prefix";
@@ -839,18 +991,10 @@ public String next() {
         String tableName = i.getArgument(0, String.class);
         return checkpointTables.get(tableName);
       }).when(checkpointDBStore).getTable(anyString());
-      defragService.performIncrementalDefragmentation(snap1Info, snap2Info, 
currentVersion, checkpointDBStore,
+      defragService.performIncrementalDefragmentation(snap1Info, snap2Info, 
checkpointDBStore,
           prefixInfo, incrementalTables);
-      if (currentVersion == 0) {
-        assertEquals(incrementalTables, new 
HashSet<>(dumpedFileName.values()));
-        assertEquals(ingestedFiles, dumpedFileName);
-      } else {
-        assertEquals(ImmutableSet.of("cf2"), new 
HashSet<>(dumpedFileName.values()));
-        assertEquals("cf1", 
ingestedFiles.get(deltaFiles.get(0).getLeft().toAbsolutePath().toString()));
-        assertEquals(ingestedFiles.entrySet().stream().filter(e -> 
e.getValue().equals(
-            "cf2")).collect(Collectors.toSet()), 
dumpedFileName.entrySet().stream().filter(e -> e.getValue().equals(
-            "cf2")).collect(Collectors.toSet()));
-      }
+      assertEquals(incrementalTables, new HashSet<>(dumpedFileName.values()));
+      assertEquals(ingestedFiles, dumpedFileName);
       assertEquals(incrementalTables, new HashSet<>(ingestedFiles.values()));
       for (Map.Entry<Path, List<Pair<String, String>>> deltaFileContent : 
deltaFileContents.entrySet()) {
         int idx = 0;
@@ -938,7 +1082,7 @@ public void testCheckAndDefragSnapshotFailure(boolean 
previousSnapshotExists) th
       IOException defragException = new IOException("Defrag failed");
       if (previousSnapshotExists) {
         
Mockito.doThrow(defragException).when(spyDefragService).performIncrementalDefragmentation(
-            eq(previousSnapshotInfo), eq(snapshotInfo), eq(10), 
eq(checkpointDBStore), eq(prefixInfo),
+            eq(previousSnapshotInfo), eq(snapshotInfo), eq(checkpointDBStore), 
eq(prefixInfo),
             eq(COLUMN_FAMILIES_TO_TRACK_IN_SNAPSHOT));
       } else {
         
Mockito.doThrow(defragException).when(spyDefragService).performFullDefragmentation(
@@ -1045,7 +1189,7 @@ public void testCheckAndDefragActiveSnapshot(boolean 
previousSnapshotExists) thr
       
doNothing().when(spyDefragService).performFullDefragmentation(eq(checkpointDBStore),
 eq(prefixInfo),
           eq(COLUMN_FAMILIES_TO_TRACK_IN_SNAPSHOT));
       
doNothing().when(spyDefragService).performIncrementalDefragmentation(eq(previousSnapshotInfo),
-          eq(snapshotInfo), eq(10), eq(checkpointDBStore), eq(prefixInfo),
+          eq(snapshotInfo), eq(checkpointDBStore), eq(prefixInfo),
           eq(COLUMN_FAMILIES_TO_TRACK_IN_SNAPSHOT));
       AtomicInteger lockAcquired = new AtomicInteger(0);
       AtomicInteger lockReleased = new AtomicInteger(0);
@@ -1094,7 +1238,7 @@ public void testCheckAndDefragActiveSnapshot(boolean 
previousSnapshotExists) thr
           eq(COLUMN_FAMILIES_TO_TRACK_IN_SNAPSHOT));
       if (previousSnapshotExists) {
         
verifier.verify(spyDefragService).performIncrementalDefragmentation(eq(previousSnapshotInfo),
-            eq(snapshotInfo), eq(10), eq(checkpointDBStore), eq(prefixInfo),
+            eq(snapshotInfo), eq(checkpointDBStore), eq(prefixInfo),
             eq(COLUMN_FAMILIES_TO_TRACK_IN_SNAPSHOT));
       } else {
         
verifier.verify(spyDefragService).performFullDefragmentation(eq(checkpointDBStore),
 eq(prefixInfo),


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

Reply via email to