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 b09bcff1f1b HDDS-14225. Upgrade RocksDB from 7.7.3 to 10.10.1 (#9813)
b09bcff1f1b is described below

commit b09bcff1f1b7460c1c8fd7082b98f5fd530bb0ba
Author: Siyao Meng <[email protected]>
AuthorDate: Tue Jul 7 18:24:45 2026 -0700

    HDDS-14225. Upgrade RocksDB from 7.7.3 to 10.10.1 (#9813)
    
    Generated-by: Codex (GPT-5.3-Codex, GPT-5.4), Claude Code (Opus 4.8)
---
 .../org/apache/hadoop/hdds/utils/db/DBProfile.java |   1 +
 .../org/apache/hadoop/hdds/utils/db/RDBTable.java  |  32 +++--
 .../apache/hadoop/hdds/utils/db/RocksDatabase.java |  33 +++--
 .../org/apache/hadoop/hdds/utils/db/Table.java     |  11 +-
 .../utils/db/TestRDBStoreCodecBufferIterator.java  |  34 ++---
 .../apache/hadoop/hdds/utils/db/TestRDBTable.java  | 109 ++++++++++++++++
 .../db/managed/ManagedBlockBasedTableConfig.java   |   8 ++
 .../hdds/utils/db/managed/ManagedBloomFilter.java  |  14 ++
 .../hdds/utils/db/managed/ManagedDBOptions.java    |  21 ++-
 .../hdds/utils/db/managed/ManagedRocksDB.java      |  20 ++-
 .../utils/db/managed/TestManagedDBOptions.java     |  59 +++++++++
 hadoop-hdds/rocks-native/pom.xml                   |  41 ++++--
 .../src/main/patches/rocks-native.patch            |  35 ++---
 .../hadoop/hdds/utils/db/RDBSstFileWriter.java     |   7 +
 .../rocksdiff/TestRocksDBCheckpointDiffer.java     |  83 ++++++------
 .../dist/src/main/compose/common/replicas-test.sh  | 141 +++++++++++++++++++--
 .../smoketest/debug/ozone-debug-keywords.robot     |  23 +++-
 pom.xml                                            |   2 +-
 18 files changed, 538 insertions(+), 136 deletions(-)

diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/DBProfile.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/DBProfile.java
index 8eedcf1ed49..a9a3f2cfc75 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/DBProfile.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/DBProfile.java
@@ -87,6 +87,7 @@ public ManagedBlockBasedTableConfig 
getBlockBasedTableConfig() {
       ManagedBlockBasedTableConfig config = new ManagedBlockBasedTableConfig();
       config.setBlockCache(new ManagedLRUCache(blockCacheSize))
           .setBlockSize(blockSize)
+          .setFormatVersion(ManagedBlockBasedTableConfig.FORMAT_VERSION)
           .setPinL0FilterAndIndexBlocksInCache(true)
           .setFilterPolicy(new ManagedBloomFilter());
       return config;
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/RDBTable.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/RDBTable.java
index 7d8d2f5af57..e1fc7297d4d 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/RDBTable.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/RDBTable.java
@@ -99,16 +99,16 @@ public boolean isEmpty() throws RocksDatabaseException {
   @Override
   public boolean isExist(byte[] key) throws RocksDatabaseException {
     rdbMetrics.incNumDBKeyMayExistChecks();
-    final Supplier<byte[]> holder = db.keyMayExist(family, key);
-    if (holder == null) {
+    final Supplier<byte[]> valueSupplier = db.keyMayExist(family, key);
+    if (valueSupplier == null) {
       return false;  // definitely not exists
     }
-    final byte[] value = holder.get();
+    final byte[] value = valueSupplier.get();
     if (value != null) {
       return true; // definitely exists
     }
 
-    // inconclusive: the key may or may not exist
+    // keyMayExist could not return the value; confirm via point-get.
     final boolean exists = get(key) != null;
     if (!exists) {
       rdbMetrics.incNumDBKeyMayExistMisses();
@@ -141,15 +141,16 @@ public byte[] getSkipCache(byte[] bytes) throws 
RocksDatabaseException {
   @Override
   public byte[] getIfExist(byte[] key) throws RocksDatabaseException {
     rdbMetrics.incNumDBKeyGetIfExistChecks();
-    final Supplier<byte[]> value = db.keyMayExist(family, key);
-    if (value == null) {
+    final Supplier<byte[]> valueSupplier = db.keyMayExist(family, key);
+    if (valueSupplier == null) {
       return null; // definitely not exists
     }
-    if (value.get() != null) {
-      return value.get(); // definitely exists
+    final byte[] value = valueSupplier.get();
+    if (value != null) {
+      return value; // definitely exists
     }
 
-    // inconclusive: the key may or may not exist
+    // keyMayExist could not return the value; confirm via point-get.
     rdbMetrics.incNumDBKeyGetIfExistGets();
     final byte[] val = get(key);
     if (val == null) {
@@ -160,19 +161,24 @@ public byte[] getIfExist(byte[] key) throws 
RocksDatabaseException {
 
   Integer getIfExist(ByteBuffer key, ByteBuffer outValue) throws 
RocksDatabaseException {
     rdbMetrics.incNumDBKeyGetIfExistChecks();
+    // Note: RocksDatabase.keyMayExist duplicates the key internally, so the 
caller's
+    // key buffer position is preserved for the fallback point-get below.
     final Supplier<Integer> value = db.keyMayExist(
         family, key, outValue.duplicate());
     if (value == null) {
       return null; // definitely not exists
     }
-    if (value.get() != null) {
+    final Integer length = value.get();
+    if (length != null) {
       // definitely exists, return value size.
-      return value.get();
+      return length;
     }
 
-    // inconclusive: the key may or may not exist
+    // keyMayExist could not return the value; confirm via point-get. get()
+    // advances the key position, so pass a duplicate to leave the caller's
+    // key buffer unchanged.
     rdbMetrics.incNumDBKeyGetIfExistGets();
-    final Integer val = get(key, outValue);
+    final Integer val = get(key.duplicate(), outValue);
     if (val == null) {
       rdbMetrics.incNumDBKeyGetIfExistMisses();
     }
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/RocksDatabase.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/RocksDatabase.java
index fe500e27447..c2d4b8bde2b 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/RocksDatabase.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/RocksDatabase.java
@@ -626,8 +626,11 @@ Supplier<byte[]> keyMayExist(ColumnFamily family, byte[] 
key)
   Supplier<Integer> keyMayExist(ColumnFamily family,
       ByteBuffer key, ByteBuffer out) throws RocksDatabaseException {
     try (UncheckedAutoCloseable ignored = acquire()) {
+      // keyMayExist may advance the input ByteBuffer position in native code.
+      // Always pass a duplicate so callers can safely reuse the original key
+      // buffer for a follow-up point-get.
       final KeyMayExist result = db.get().keyMayExist(
-          family.getHandle(), key, out);
+          family.getHandle(), key.duplicate(), out);
       switch (result.exists) {
       case kNotExist: return null;
       case kExistsWithValue: return () -> result.valueLength;
@@ -872,12 +875,12 @@ public void deleteFilesNotMatchingPrefix(TablePrefixInfo 
prefixInfo) throws Rock
         String sstFileColumnFamily = 
StringUtils.bytes2String(liveFileMetaData.columnFamilyName());
         int lastLevel = getLastLevel();
 
-        // RocksDB #deleteFile API allows only to delete the last level of
-        // SST Files. Any level < last level won't get deleted and
-        // only last file of level 0 can be deleted
-        // and will throw warning in the rocksdb manifest.
-        // Instead, perform the level check here
-        // itself to avoid failed delete attempts for lower level files.
+        // Restrict deletion to files at the last level (and skip entirely when
+        // the last level is 0). The old RocksDB #deleteFile API could only
+        // delete last-level SST files (and the last file of level 0);
+        // deleteSstFileRange, used below, no longer has that limitation, but
+        // this method keeps the last-level restriction to preserve its 
existing
+        // pruning behavior.
         if (liveFileMetaData.level() != lastLevel || lastLevel == 0) {
           continue;
         }
@@ -888,6 +891,12 @@ public void deleteFilesNotMatchingPrefix(TablePrefixInfo 
prefixInfo) throws Rock
         boolean isKeyWithPrefixPresent = RocksDiffUtils.isKeyWithPrefixPresent(
             prefixForColumnFamily, firstDbKey, lastDbKey);
         if (!isKeyWithPrefixPresent) {
+          ColumnFamilyHandle handle = 
getColumnFamilyHandle(sstFileColumnFamily);
+          if (handle == null) {
+            LOG.warn("Skipping sst file deletion for {}: no handle found for 
column family {}",
+                liveFileMetaData.fileName(), sstFileColumnFamily);
+            continue;
+          }
           LOG.info("Deleting sst file: {} with start key: {} and end key: {} "
                   + "corresponding to column family {} from db: {}. "
                   + "Prefix for the column family: {}.",
@@ -896,7 +905,15 @@ public void deleteFilesNotMatchingPrefix(TablePrefixInfo 
prefixInfo) throws Rock
               StringUtils.bytes2String(liveFileMetaData.columnFamilyName()),
               db.get().getName(),
               prefixForColumnFamily);
-          db.deleteFile(liveFileMetaData);
+          // deleteSstFileRange uses deleteFilesInRanges over this file's
+          // [smallestKey, largestKey]. It may also drop other files fully
+          // contained in that range, which is safe here: any such file's key
+          // range is a subset of this non-matching file's range. Because
+          // isKeyWithPrefixPresent is a monotone prefix-range test, a subset
+          // range cannot contain the prefix when the enclosing range does not,
+          // so every collaterally deleted file is likewise non-matching. This
+          // invariant holds only while isKeyWithPrefixPresent stays monotone.
+          db.deleteSstFileRange(handle, liveFileMetaData);
         }
       }
     }
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/Table.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/Table.java
index fc049034406..400547a0e36 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/Table.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/db/Table.java
@@ -64,6 +64,8 @@ public interface Table<KEY, VALUE> {
    * Check if a given key exists in Metadata store.
    * (Optimization to save on data deserialization)
    * A lock on the key / bucket needs to be acquired before invoking this API.
+   * Implementations may use fast existence checks internally, but the returned
+   * result must be definitive for the current table state.
    * @param key metadata key
    * @return true if the metadata store contains a key.
    */
@@ -107,12 +109,9 @@ default VALUE getReadCopy(KEY key) throws 
RocksDatabaseException, CodecException
    * Returns the value mapped to the given key in byte array or returns null
    * if the key is not found.
    *
-   * This method first checks using keyMayExist, if it returns false, we are
-   * 100% sure that key does not exist in DB, so it returns null with out
-   * calling db.get. If keyMayExist return true, then we use db.get and then
-   * return the value. This method will be useful in the cases where the
-   * caller is more sure that this key does not exist in DB and keyMayExist
-   * will help here.
+   * Implementations may use keyMayExist or similar fast-path checks
+   * internally, but the returned result must remain equivalent to a regular
+   * point lookup on the current table state.
    *
    * @param key metadata key
    * @return value in byte array or null if the key is not found.
diff --git 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/utils/db/TestRDBStoreCodecBufferIterator.java
 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/utils/db/TestRDBStoreCodecBufferIterator.java
index 919b3b6cdad..cddb11e9528 100644
--- 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/utils/db/TestRDBStoreCodecBufferIterator.java
+++ 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/utils/db/TestRDBStoreCodecBufferIterator.java
@@ -100,13 +100,13 @@ Answer<Integer> newAnswer(String name, byte... b) {
   public void testForEachRemaining() throws Exception {
     when(rocksIteratorMock.isValid())
         .thenReturn(true, true, true, true, true, true, true, false);
-    when(rocksIteratorMock.key(any()))
+    when(rocksIteratorMock.key(any(ByteBuffer.class)))
         .then(newAnswerInt("key1", 0x00))
         .then(newAnswerInt("key2", 0x00))
         .then(newAnswerInt("key3", 0x01))
         .then(newAnswerInt("key4", 0x02))
         .thenThrow(new NoSuchElementException());
-    when(rocksIteratorMock.value(any()))
+    when(rocksIteratorMock.value(any(ByteBuffer.class)))
         .then(newAnswerInt("val1", 0x7f))
         .then(newAnswerInt("val2", 0x7f))
         .then(newAnswerInt("val3", 0x7e))
@@ -152,8 +152,8 @@ public void 
testNextCallsIsValidThenGetsTheValueAndStepsToNext()
     }
 
     verifier.verify(rocksIteratorMock).isValid();
-    verifier.verify(rocksIteratorMock).key(any());
-    verifier.verify(rocksIteratorMock).value(any());
+    verifier.verify(rocksIteratorMock).key(any(ByteBuffer.class));
+    verifier.verify(rocksIteratorMock).value(any(ByteBuffer.class));
     verifier.verify(rocksIteratorMock).next();
 
     CodecTestUtil.gc();
@@ -192,9 +192,9 @@ public void testSeekToLastSeeks() throws Exception {
   @Test
   public void testSeekReturnsTheActualKey() throws Exception {
     when(rocksIteratorMock.isValid()).thenReturn(true);
-    when(rocksIteratorMock.key(any()))
+    when(rocksIteratorMock.key(any(ByteBuffer.class)))
         .then(newAnswerInt("key1", 0x00));
-    when(rocksIteratorMock.value(any()))
+    when(rocksIteratorMock.value(any(ByteBuffer.class)))
         .then(newAnswerInt("val1", 0x7f));
 
     try (RDBStoreCodecBufferIterator i = newIterator();
@@ -208,8 +208,8 @@ public void testSeekReturnsTheActualKey() throws Exception {
       verifier.verify(rocksIteratorMock, times(1))
           .seek(any(ByteBuffer.class));
       verifier.verify(rocksIteratorMock, times(1)).isValid();
-      verifier.verify(rocksIteratorMock, times(1)).key(any());
-      verifier.verify(rocksIteratorMock, times(1)).value(any());
+      verifier.verify(rocksIteratorMock, times(1)).key(any(ByteBuffer.class));
+      verifier.verify(rocksIteratorMock, 
times(1)).value(any(ByteBuffer.class));
       assertArrayEquals(new byte[]{0x00}, val.getKey().getArray());
       assertArrayEquals(new byte[]{0x7f}, val.getValue().getArray());
     }
@@ -220,7 +220,7 @@ public void testSeekReturnsTheActualKey() throws Exception {
   @Test
   public void testGettingTheKeyIfIteratorIsValid() throws Exception {
     when(rocksIteratorMock.isValid()).thenReturn(true);
-    when(rocksIteratorMock.key(any()))
+    when(rocksIteratorMock.key(any(ByteBuffer.class)))
         .then(newAnswerInt("key1", 0x00));
 
     byte[] key = null;
@@ -233,7 +233,7 @@ public void testGettingTheKeyIfIteratorIsValid() throws 
Exception {
     InOrder verifier = inOrder(rocksIteratorMock);
 
     verifier.verify(rocksIteratorMock, times(1)).isValid();
-    verifier.verify(rocksIteratorMock, times(1)).key(any());
+    verifier.verify(rocksIteratorMock, times(1)).key(any(ByteBuffer.class));
     assertArrayEquals(new byte[]{0x00}, key);
 
     CodecTestUtil.gc();
@@ -242,9 +242,9 @@ public void testGettingTheKeyIfIteratorIsValid() throws 
Exception {
   @Test
   public void testGettingTheValueIfIteratorIsValid() throws Exception {
     when(rocksIteratorMock.isValid()).thenReturn(true);
-    when(rocksIteratorMock.key(any()))
+    when(rocksIteratorMock.key(any(ByteBuffer.class)))
         .then(newAnswerInt("key1", 0x00));
-    when(rocksIteratorMock.value(any()))
+    when(rocksIteratorMock.value(any(ByteBuffer.class)))
         .then(newAnswerInt("val1", 0x7f));
 
     byte[] key = null;
@@ -260,7 +260,7 @@ public void testGettingTheValueIfIteratorIsValid() throws 
Exception {
     InOrder verifier = inOrder(rocksIteratorMock);
 
     verifier.verify(rocksIteratorMock, times(1)).isValid();
-    verifier.verify(rocksIteratorMock, times(1)).key(any());
+    verifier.verify(rocksIteratorMock, times(1)).key(any(ByteBuffer.class));
     assertArrayEquals(new byte[]{0x00}, key);
     assertArrayEquals(new byte[]{0x7f}, value);
 
@@ -272,7 +272,7 @@ public void testRemovingFromDBActuallyDeletesFromTable() 
throws Exception {
     final byte[] testKey = new byte[10];
     ThreadLocalRandom.current().nextBytes(testKey);
     when(rocksIteratorMock.isValid()).thenReturn(true);
-    when(rocksIteratorMock.key(any()))
+    when(rocksIteratorMock.key(any(ByteBuffer.class)))
         .then(newAnswer("key1", testKey));
 
     try (RDBStoreCodecBufferIterator i = newIterator(null)) {
@@ -320,7 +320,7 @@ public void testNullPrefixedIterator() throws Exception {
       when(rocksIteratorMock.isValid()).thenReturn(true);
       assertTrue(i.hasNext());
       verify(rocksIteratorMock, times(1)).isValid();
-      verify(rocksIteratorMock, times(0)).key(any());
+      verify(rocksIteratorMock, times(0)).key(any(ByteBuffer.class));
 
       i.seekToLast();
       verify(rocksIteratorMock, times(1)).seekToLast();
@@ -343,11 +343,11 @@ public void testNormalPrefixedIterator() throws Exception 
{
       clearInvocations(rocksIteratorMock);
 
       when(rocksIteratorMock.isValid()).thenReturn(true);
-      when(rocksIteratorMock.key(any()))
+      when(rocksIteratorMock.key(any(ByteBuffer.class)))
           .then(newAnswer("key1", prefixBytes));
       assertTrue(i.hasNext());
       verify(rocksIteratorMock, times(1)).isValid();
-      verify(rocksIteratorMock, times(1)).key(any());
+      verify(rocksIteratorMock, times(1)).key(any(ByteBuffer.class));
 
       Exception e =
           assertThrows(Exception.class, () -> i.seekToLast(), "Prefixed 
iterator does not support seekToLast");
diff --git 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/utils/db/TestRDBTable.java
 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/utils/db/TestRDBTable.java
new file mode 100644
index 00000000000..dc582a537df
--- /dev/null
+++ 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/utils/db/TestRDBTable.java
@@ -0,0 +1,109 @@
+/*
+ * 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.hdds.utils.db;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.ByteBuffer;
+import java.util.function.Supplier;
+import org.apache.hadoop.hdds.utils.db.RocksDatabase.ColumnFamily;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link RDBTable}.
+ */
+public class TestRDBTable {
+
+  @Test
+  public void testGetIfExistByteBufferFallbackUsesFreshKeyBuffer()
+      throws Exception {
+    RocksDatabase db = mock(RocksDatabase.class);
+    ColumnFamily columnFamily = mock(ColumnFamily.class);
+    RDBMetrics metrics = mock(RDBMetrics.class);
+    RDBTable table = new RDBTable(db, columnFamily, metrics);
+
+    byte[] keyBytes = "key-1".getBytes(UTF_8);
+    ByteBuffer key = ByteBuffer.wrap(keyBytes);
+    ByteBuffer outValue = ByteBuffer.allocate(64);
+
+    // RocksDatabase.keyMayExist duplicates the key internally, so it leaves 
the
+    // caller's key buffer untouched. Return an inconclusive result (value-less
+    // "may exist") to force the fallback point-get.
+    when(db.keyMayExist(eq(columnFamily), any(ByteBuffer.class),
+        any(ByteBuffer.class))).thenReturn((Supplier<Integer>) () -> null);
+
+    // get() advances the key buffer position as native RocksDB does. It must
+    // still see the full key, i.e. RDBTable must hand it a fresh duplicate.
+    when(db.get(eq(columnFamily), any(ByteBuffer.class), 
any(ByteBuffer.class)))
+        .thenAnswer(invocation -> {
+          ByteBuffer keyBuffer = invocation.getArgument(1);
+          if (keyBuffer.remaining() != keyBytes.length) {
+            return null;
+          }
+          keyBuffer.position(keyBuffer.limit());
+          return 0;
+        });
+
+    Integer result = table.getIfExist(key, outValue);
+    assertEquals(0, result);
+    assertEquals(0, key.position(), "caller key buffer position must be 
unchanged");
+  }
+
+  @Test
+  public void testGetIfExistByteBufferFastPathReturnsValue()
+      throws Exception {
+    RocksDatabase db = mock(RocksDatabase.class);
+    ColumnFamily columnFamily = mock(ColumnFamily.class);
+    RDBMetrics metrics = mock(RDBMetrics.class);
+    RDBTable table = new RDBTable(db, columnFamily, metrics);
+
+    byte[] keyBytes = "key-1".getBytes(UTF_8);
+    byte[] valueBytes = "value-1".getBytes(UTF_8);
+    ByteBuffer key = ByteBuffer.wrap(keyBytes);
+    ByteBuffer outValue = ByteBuffer.allocate(64);
+
+    // Simulate the RocksDB "exists with value" fast path: native code writes
+    // the value into the buffer handed to keyMayExist and reports its length.
+    // getIfExist passes outValue.duplicate(), so the write must land in the
+    // caller's outValue via the shared backing memory.
+    when(db.keyMayExist(eq(columnFamily), any(ByteBuffer.class),
+        any(ByteBuffer.class))).thenAnswer(invocation -> {
+          ByteBuffer valueBuffer = invocation.getArgument(2);
+          valueBuffer.put(valueBytes);
+          return (Supplier<Integer>) () -> valueBytes.length;
+        });
+
+    Integer result = table.getIfExist(key, outValue);
+    assertEquals(valueBytes.length, result);
+    // The fast path must not fall back to a point-get.
+    verify(db, never()).get(eq(columnFamily), any(ByteBuffer.class), 
any(ByteBuffer.class));
+    // Value bytes written through the duplicate are visible in the caller's 
buffer.
+    byte[] readBack = new byte[valueBytes.length];
+    outValue.duplicate().get(readBack);
+    assertArrayEquals(valueBytes, readBack);
+  }
+}
+
diff --git 
a/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedBlockBasedTableConfig.java
 
b/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedBlockBasedTableConfig.java
index 621c9e93524..28690ebe54a 100644
--- 
a/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedBlockBasedTableConfig.java
+++ 
b/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedBlockBasedTableConfig.java
@@ -25,6 +25,14 @@
  * Managed BlockBasedTableConfig.
  */
 public class ManagedBlockBasedTableConfig extends BlockBasedTableConfig {
+
+  /**
+   * Block-based table format version kept stable across RocksDB upgrades so
+   * SST files stay readable if Ozone is downgraded before finalization.
+   * RocksDB 9+ defaults to format_version 6, which RocksDB &lt; 8.6 cannot 
read.
+   */
+  public static final int FORMAT_VERSION = 5;
+
   private Cache blockCacheHolder;
   private AtomicBoolean closed = new AtomicBoolean(false);
 
diff --git 
a/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedBloomFilter.java
 
b/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedBloomFilter.java
index 406716eaf84..b2e40e75eb4 100644
--- 
a/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedBloomFilter.java
+++ 
b/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedBloomFilter.java
@@ -28,6 +28,20 @@
 public class ManagedBloomFilter extends BloomFilter {
   private final UncheckedAutoCloseable leakTracker = track(this);
 
+  // Delegate to satisfy SpotBugs EQ_DOESNT_OVERRIDE_EQUALS: BloomFilter 
defines
+  // equals()/hashCode() and this subclass adds a field (leakTracker). The 
added
+  // field is not part of the filter's identity, so BloomFilter's equality is
+  // still correct; we override only to declare that explicitly.
+  @Override
+  public boolean equals(Object obj) {
+    return super.equals(obj);
+  }
+
+  @Override
+  public int hashCode() {
+    return super.hashCode();
+  }
+
   @Override
   public void close() {
     try {
diff --git 
a/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedDBOptions.java
 
b/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedDBOptions.java
index 1809b088560..2015e4ff42c 100644
--- 
a/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedDBOptions.java
+++ 
b/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedDBOptions.java
@@ -24,7 +24,7 @@
 import org.apache.hadoop.hdds.utils.IOUtils;
 import org.apache.ratis.util.UncheckedAutoCloseable;
 import org.rocksdb.DBOptions;
-import org.rocksdb.Logger;
+import org.rocksdb.LoggerInterface;
 
 /**
  * Managed DBOptions.
@@ -32,21 +32,32 @@
 public class ManagedDBOptions extends DBOptions {
 
   private final UncheckedAutoCloseable leakTracker = track(this);
-  private final AtomicReference<Logger> loggerRef = new AtomicReference<>();
+  private final AtomicReference<LoggerInterface> loggerRef = new 
AtomicReference<>();
 
+  // DBOptions#setLogger takes LoggerInterface since RocksDB 9.x. Override that
+  // exact signature (not the pre-9.x Logger overload) so every call path,
+  // including one made through a DBOptions-typed reference, is leak-tracked.
   @Override
-  public DBOptions setLogger(Logger logger) {
-    IOUtils.close(LOG, loggerRef.getAndSet(logger));
+  public DBOptions setLogger(LoggerInterface logger) {
+    closeLogger(loggerRef.getAndSet(logger));
     return super.setLogger(logger);
   }
 
   @Override
   public void close() {
     try {
-      IOUtils.close(LOG, loggerRef.getAndSet(null));
+      closeLogger(loggerRef.getAndSet(null));
       super.close();
     } finally {
       leakTracker.close();
     }
   }
+
+  // RocksDB loggers (org.rocksdb.Logger) own native resources and are
+  // AutoCloseable; a bare LoggerInterface may not be, so only close when it 
is.
+  private static void closeLogger(LoggerInterface logger) {
+    if (logger instanceof AutoCloseable) {
+      IOUtils.close(LOG, (AutoCloseable) logger);
+    }
+  }
 }
diff --git 
a/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedRocksDB.java
 
b/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedRocksDB.java
index 3e19e95bca2..a027b61de85 100644
--- 
a/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedRocksDB.java
+++ 
b/hadoop-hdds/managed-rocksdb/src/main/java/org/apache/hadoop/hdds/utils/db/managed/ManagedRocksDB.java
@@ -19,6 +19,7 @@
 
 import java.io.File;
 import java.time.Duration;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -190,18 +191,23 @@ public static ManagedRocksDB openWithLatestOptions(
   }
 
   /**
-   * Delete liveMetaDataFile from rocks db using RocksDB#deleteFile Api.
-   * This function makes the RocksDB#deleteFile Api synchronized by waiting
-   * for the deletes to happen.
-   * @param fileToBeDeleted File to be deleted.
+   * Delete the SST file range from rocks db and wait for file deletion.
+   * @param columnFamilyHandle column family of the target sst file.
+   * @param fileToBeDeleted file metadata to be deleted.
    * @throws RocksDatabaseException if the underlying db throws an exception
    *                                or the file is not deleted within a time 
limit.
    */
-  public void deleteFile(LiveFileMetaData fileToBeDeleted) throws 
RocksDatabaseException {
-    String sstFileName = fileToBeDeleted.fileName();
+  public void deleteSstFileRange(
+      ColumnFamilyHandle columnFamilyHandle,
+      LiveFileMetaData fileToBeDeleted) throws RocksDatabaseException {
     File file = new File(fileToBeDeleted.path(), fileToBeDeleted.fileName());
+    final byte[] smallestKey = fileToBeDeleted.smallestKey();
+    final byte[] largestKey = fileToBeDeleted.largestKey();
     try {
-      get().deleteFile(sstFileName);
+      get().deleteFilesInRanges(
+          columnFamilyHandle,
+          Arrays.asList(smallestKey, largestKey),
+          true);
     } catch (RocksDBException e) {
       throw new RocksDatabaseException("Failed to delete " + file, e);
     }
diff --git 
a/hadoop-hdds/managed-rocksdb/src/test/java/org/apache/hadoop/hdds/utils/db/managed/TestManagedDBOptions.java
 
b/hadoop-hdds/managed-rocksdb/src/test/java/org/apache/hadoop/hdds/utils/db/managed/TestManagedDBOptions.java
new file mode 100644
index 00000000000..6dd9a1cc9ad
--- /dev/null
+++ 
b/hadoop-hdds/managed-rocksdb/src/test/java/org/apache/hadoop/hdds/utils/db/managed/TestManagedDBOptions.java
@@ -0,0 +1,59 @@
+/*
+ * 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.hdds.utils.db.managed;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.rocksdb.DBOptions;
+
+/**
+ * Tests for {@link ManagedDBOptions}, in particular that a logger it is given
+ * is closed on replacement and on close, regardless of the reference type used
+ * to call setLogger.
+ */
+public class TestManagedDBOptions {
+  static {
+    ManagedRocksObjectUtils.loadRocksDBLibrary();
+  }
+
+  @Test
+  public void testSetLoggerViaDBOptionsReferenceIsClosed() {
+    ManagedDBOptions managed = new ManagedDBOptions();
+    ManagedLogger first = new ManagedLogger(managed, (level, message) -> { });
+    ManagedLogger second = new ManagedLogger(managed, (level, message) -> { });
+
+    // Since RocksDB 9.x, DBOptions#setLogger takes a LoggerInterface. Calling
+    // it through the parent type must still route through ManagedDBOptions'
+    // override (not a bypassed overload) so the logger is tracked and closed.
+    DBOptions options = managed;
+
+    options.setLogger(first);
+    assertTrue(first.isOwningHandle());
+
+    // Replacing the logger closes the previous one.
+    options.setLogger(second);
+    assertFalse(first.isOwningHandle(), "previous logger should be closed on 
replace");
+    assertTrue(second.isOwningHandle());
+
+    // Closing the options closes the current logger.
+    managed.close();
+    assertFalse(second.isOwningHandle(), "current logger should be closed on 
close");
+  }
+}
diff --git a/hadoop-hdds/rocks-native/pom.xml b/hadoop-hdds/rocks-native/pom.xml
index 0e260ba0478..9837aae92db 100644
--- a/hadoop-hdds/rocks-native/pom.xml
+++ b/hadoop-hdds/rocks-native/pom.xml
@@ -121,6 +121,28 @@
       </properties>
       <build>
         <plugins>
+          <!-- rocksdbjni may carry a packaging/build suffix like 10.10.1.1,
+               but upstream RocksDB source tags and tarballs are published as
+               x.y.z only, so derive a source version for the native tools 
build. -->
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>build-helper-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>derive-rocksdb-source-version</id>
+                <goals>
+                  <goal>regex-property</goal>
+                </goals>
+                <phase>initialize</phase>
+                <configuration>
+                  <name>rocksdb.source.version</name>
+                  <value>${rocksdb.version}</value>
+                  <regex>^([0-9]+\.[0-9]+\.[0-9]+)(?:\..+)?$</regex>
+                  <replacement>$1</replacement>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
           <plugin>
             <groupId>org.codehaus.mojo</groupId>
             <artifactId>exec-maven-plugin</artifactId>
@@ -173,6 +195,7 @@
                     <artifactItem>
                       <groupId>org.rocksdb</groupId>
                       <artifactId>rocksdbjni</artifactId>
+                      <version>${rocksdb.version}</version>
                       <type>jar</type>
                       <overWrite>false</overWrite>
                       
<outputDirectory>${project.build.directory}/rocksdbjni</outputDirectory>
@@ -193,8 +216,8 @@
                 </goals>
                 <phase>generate-sources</phase>
                 <configuration>
-                  
<url>https://github.com/facebook/rocksdb/archive/refs/tags/v${rocksdb.version}.tar.gz</url>
-                  
<outputFileName>rocksdb-v${rocksdb.version}.tar.gz</outputFileName>
+                  
<url>https://github.com/facebook/rocksdb/archive/refs/tags/v${rocksdb.source.version}.tar.gz</url>
+                  
<outputFileName>rocksdb-v${rocksdb.source.version}.tar.gz</outputFileName>
                   
<outputDirectory>${project.build.directory}/rocksdb</outputDirectory>
                 </configuration>
               </execution>
@@ -206,7 +229,7 @@
             <configuration>
               
<patchFile>${basedir}/src/main/patches/rocks-native.patch</patchFile>
               <strip>1</strip>
-              
<targetDirectory>${project.build.directory}/rocksdb/rocksdb-${rocksdb.version}</targetDirectory>
+              
<targetDirectory>${project.build.directory}/rocksdb/rocksdb-${rocksdb.source.version}</targetDirectory>
             </configuration>
             <executions>
               <execution>
@@ -230,7 +253,7 @@
                 <phase>generate-sources</phase>
                 <configuration>
                   <target>
-                    <untar compression="gzip" 
dest="${project.build.directory}/rocksdb/" 
src="${project.build.directory}/rocksdb/rocksdb-v${rocksdb.version}.tar.gz" />
+                    <untar compression="gzip" 
dest="${project.build.directory}/rocksdb/" 
src="${project.build.directory}/rocksdb/rocksdb-v${rocksdb.source.version}.tar.gz"
 />
                   </target>
                 </configuration>
               </execution>
@@ -245,9 +268,9 @@
                     <exec executable="chmod" failonerror="true">
                       <arg line="-R" />
                       <arg line="775" />
-                      <arg 
line="${project.build.directory}/rocksdb/rocksdb-${rocksdb.version}" />
+                      <arg 
line="${project.build.directory}/rocksdb/rocksdb-${rocksdb.source.version}" />
                     </exec>
-                    <exec 
dir="${project.build.directory}/rocksdb/rocksdb-${rocksdb.version}" 
executable="make" failonerror="true">
+                    <exec 
dir="${project.build.directory}/rocksdb/rocksdb-${rocksdb.source.version}" 
executable="make" failonerror="true">
                       <arg line="PORTABLE=1" />
                       <arg line="DEBUG_LEVEL=0" />
                       <arg line="EXTRA_CXXFLAGS='-fPIC 
-D_GLIBCXX_USE_CXX11_ABI=0'" />
@@ -275,12 +298,12 @@
                       <arg line="-DNATIVE_DIR=${basedir}/src/main/native" />
                       <arg line="-DSST_DUMP_INCLUDE=${sstDump.include}" />
                       <arg line="-DCMAKE_STANDARDS=${cmake.standards}" />
-                      <arg 
line="-DROCKSDB_HEADERS=${project.build.directory}/rocksdb/rocksdb-${rocksdb.version}/include"
 />
-                      <arg 
line="-DROCKSDB_TOOLS_LIB=${project.build.directory}/rocksdb/rocksdb-${rocksdb.version}"
 />
+                      <arg 
line="-DROCKSDB_HEADERS=${project.build.directory}/rocksdb/rocksdb-${rocksdb.source.version}/include"
 />
+                      <arg 
line="-DROCKSDB_TOOLS_LIB=${project.build.directory}/rocksdb/rocksdb-${rocksdb.source.version}"
 />
                       <arg 
line="-DROCKSDB_LIB=${project.build.directory}/native/rocksdb/${rocksdbLibName}"
 />
                     </exec>
                     <exec dir="${project.build.directory}/native/rocksdb" 
executable="make" failonerror="true" />
-                    <delete 
dir="${project.build.directory}/rocksdb/rocksdb-${rocksdb.version}" 
includeemptydirs="true" />
+                    <delete 
dir="${project.build.directory}/rocksdb/rocksdb-${rocksdb.source.version}" 
includeemptydirs="true" />
                   </target>
                 </configuration>
               </execution>
diff --git a/hadoop-hdds/rocks-native/src/main/patches/rocks-native.patch 
b/hadoop-hdds/rocks-native/src/main/patches/rocks-native.patch
index b2627fbbb3e..ae256eba2db 100644
--- a/hadoop-hdds/rocks-native/src/main/patches/rocks-native.patch
+++ b/hadoop-hdds/rocks-native/src/main/patches/rocks-native.patch
@@ -119,10 +119,11 @@ diff --git a/src.mk b/src.mk
 index b94bc43ca..c13e5cde6 100644
 --- a/src.mk
 +++ b/src.mk
-@@ -338,11 +338,8 @@ RANGE_TREE_SOURCES =\
+@@ -367,12 +367,8 @@
    utilities/transactions/lock/range/range_tree/range_tree_lock_tracker.cc
-
+ 
  TOOL_LIB_SOURCES =                                              \
+-  db_stress_tool/db_stress_compression_manager.cc               \
 -  tools/io_tracer_parser_tool.cc                                \
 -  tools/ldb_cmd.cc                                              \
 -  tools/ldb_tool.cc                                             \
@@ -130,7 +131,7 @@ index b94bc43ca..c13e5cde6 100644
 -  utilities/blob_db/blob_dump_tool.cc                           \
 +  tools/raw_sst_file_reader.cc                                  \
 +  tools/raw_sst_file_iterator.cc                                \
-
+ 
  ANALYZER_LIB_SOURCES =                                          \
    tools/block_cache_analyzer/block_cache_trace_analyzer.cc      \
 diff --git a/tools/raw_sst_file_iterator.cc b/tools/raw_sst_file_iterator.cc
@@ -380,9 +381,9 @@ index 000000000..5ba8a82ee
 +
 +  rep_->file_.reset(new RandomAccessFileReader(std::move(file), file_path));
 +
-+  FilePrefetchBuffer prefetch_buffer(
-+      0 /* readahead_size */, 0 /* max_readahead_size */, true /* enable */,
-+      false /* track_min_offset */);
++  FilePrefetchBuffer prefetch_buffer(ReadaheadParams(),
++                                     !fopts.use_mmap_reads /* enable */,
++                                     false /* track_min_offset */);
 +  if (s.ok()) {
 +    const uint64_t kSstDumpTailPrefetchSize = 512 * 1024;
 +    uint64_t prefetch_size = (file_size > kSstDumpTailPrefetchSize)
@@ -391,11 +392,10 @@ index 000000000..5ba8a82ee
 +    uint64_t prefetch_off = file_size - prefetch_size;
 +    IOOptions opts;
 +    s = prefetch_buffer.Prefetch(opts, rep_->file_.get(), prefetch_off,
-+                                 static_cast<size_t>(prefetch_size),
-+                                 Env::IO_TOTAL /* rate_limiter_priority */);
++                                 static_cast<size_t>(prefetch_size));
 +
-+    s = ReadFooterFromFile(opts, rep_->file_.get(), &prefetch_buffer, 
file_size,
-+                           &footer);
++    s = ReadFooterFromFile(opts, rep_->file_.get(), *fs, &prefetch_buffer,
++                           file_size, &footer);
 +  }
 +  if (s.ok()) {
 +    magic_number = footer.table_magic_number();
@@ -405,16 +405,16 @@ index 000000000..5ba8a82ee
 +    if (magic_number == kPlainTableMagicNumber ||
 +        magic_number == kLegacyPlainTableMagicNumber) {
 +      rep_->soptions_.use_mmap_reads = true;
++      fopts = rep_->soptions_;
 +
 +      fs->NewRandomAccessFile(file_path, fopts, &file, nullptr);
 +      rep_->file_.reset(new RandomAccessFileReader(std::move(file), 
file_path));
 +    }
 +
 +    s = ROCKSDB_NAMESPACE::ReadTableProperties(
-+        rep_->file_.get(), file_size, magic_number, rep_->ioptions_, 
&(rep_->table_properties_),
-+        /* memory_allocator= */ nullptr, (magic_number == 
kBlockBasedTableMagicNumber)
-+                                             ? &prefetch_buffer
-+                                             : nullptr);
++        rep_->file_.get(), file_size, magic_number, rep_->ioptions_, 
rep_->read_options_,
++        &(rep_->table_properties_), /* memory_allocator= */ nullptr,
++        (magic_number == kBlockBasedTableMagicNumber) ? &prefetch_buffer : 
nullptr);
 +    // For old sst format, ReadTableProperties might fail but file can be read
 +    if (s.ok()) {
 +      s = SetTableOptionsByMagicNumber(magic_number);
@@ -448,9 +448,10 @@ index 000000000..5ba8a82ee
 +
 +Status RawSstFileReader::NewTableReader(uint64_t file_size) {
 +  auto t_opt =
-+      TableReaderOptions(rep_->ioptions_, rep_->moptions_.prefix_extractor, 
rep_->soptions_,
-+                         rep_->internal_comparator_, false /* skip_filters */,
-+                         false /* imortal */, true /* force_direct_prefetch 
*/);
++      TableReaderOptions(rep_->ioptions_, rep_->moptions_.prefix_extractor,
++                         rep_->moptions_.compression_manager.get(), 
rep_->soptions_,
++                         rep_->internal_comparator_, 0 /* 
block_protection_bytes_per_key */,
++                         false /* skip_filters */, false /* immortal */, true 
/* force_direct_prefetch */);
 +  // Allow open file with global sequence number for backward compatibility.
 +  t_opt.largest_seqno = kMaxSequenceNumber;
 +
diff --git 
a/hadoop-hdds/rocksdb-checkpoint-differ/src/main/java/org/apache/hadoop/hdds/utils/db/RDBSstFileWriter.java
 
b/hadoop-hdds/rocksdb-checkpoint-differ/src/main/java/org/apache/hadoop/hdds/utils/db/RDBSstFileWriter.java
index a689e9fdea1..294bf41b802 100644
--- 
a/hadoop-hdds/rocksdb-checkpoint-differ/src/main/java/org/apache/hadoop/hdds/utils/db/RDBSstFileWriter.java
+++ 
b/hadoop-hdds/rocksdb-checkpoint-differ/src/main/java/org/apache/hadoop/hdds/utils/db/RDBSstFileWriter.java
@@ -20,6 +20,7 @@
 import java.io.Closeable;
 import java.io.File;
 import java.util.concurrent.atomic.AtomicLong;
+import org.apache.hadoop.hdds.utils.db.managed.ManagedBlockBasedTableConfig;
 import org.apache.hadoop.hdds.utils.db.managed.ManagedDirectSlice;
 import org.apache.hadoop.hdds.utils.db.managed.ManagedEnvOptions;
 import org.apache.hadoop.hdds.utils.db.managed.ManagedOptions;
@@ -36,8 +37,13 @@ public class RDBSstFileWriter implements Closeable {
   private AtomicLong keyCounter;
   private ManagedOptions emptyOption = new ManagedOptions();
   private final ManagedEnvOptions emptyEnvOptions = new ManagedEnvOptions();
+  private final ManagedBlockBasedTableConfig tableConfig = new 
ManagedBlockBasedTableConfig();
 
   public RDBSstFileWriter(File externalFile) throws RocksDatabaseException {
+    // Pin the SST format version so files written here (e.g. snapshot defrag
+    // ingest) stay readable if Ozone is downgraded before finalization.
+    tableConfig.setFormatVersion(ManagedBlockBasedTableConfig.FORMAT_VERSION);
+    emptyOption.setTableFormatConfig(tableConfig);
     this.sstFileWriter = new ManagedSstFileWriter(emptyEnvOptions, 
emptyOption);
     this.keyCounter = new AtomicLong(0);
     this.sstFile = externalFile;
@@ -117,6 +123,7 @@ private void closeResources() {
     sstFileWriter = null;
     emptyOption.close();
     emptyEnvOptions.close();
+    tableConfig.close();
   }
 
   private void closeOnFailure() {
diff --git 
a/hadoop-hdds/rocksdb-checkpoint-differ/src/test/java/org/apache/ozone/rocksdiff/TestRocksDBCheckpointDiffer.java
 
b/hadoop-hdds/rocksdb-checkpoint-differ/src/test/java/org/apache/ozone/rocksdiff/TestRocksDBCheckpointDiffer.java
index e083be0d0c8..0fc2df2a596 100644
--- 
a/hadoop-hdds/rocksdb-checkpoint-differ/src/test/java/org/apache/ozone/rocksdiff/TestRocksDBCheckpointDiffer.java
+++ 
b/hadoop-hdds/rocksdb-checkpoint-differ/src/test/java/org/apache/ozone/rocksdiff/TestRocksDBCheckpointDiffer.java
@@ -986,11 +986,14 @@ void testDifferWithDB() throws Exception {
 
     // Confirm correct links created
     try (Stream<Path> sstPathStream = Files.list(sstBackUpDir.toPath())) {
-      List<String> expectedLinks = sstPathStream.map(Path::getFileName)
+      List<String> actualLinks = sstPathStream.map(Path::getFileName)
               .map(Object::toString).sorted().collect(Collectors.toList());
-      assertEquals(expectedLinks, asList(
-              "000017.sst", "000019.sst", "000021.sst", "000023.sst",
-          "000024.sst", "000026.sst", "000029.sst"));
+      assertThat(actualLinks).hasSize(7);
+      assertThat(actualLinks).allMatch(link -> link.matches("\\d{6}\\.sst"));
+      for (String linkName : actualLinks) {
+        assertTrue(Files.size(sstBackUpDir.toPath().resolve(linkName)) > 0,
+            "SST link should not be empty: " + linkName);
+      }
     }
     
rocksDBCheckpointDiffer.getForwardCompactionDAG().nodes().stream().forEach(compactionNode
 -> {
       Assertions.assertNotNull(compactionNode.getStartKey());
@@ -1015,48 +1018,44 @@ private static List<ColumnFamilyDescriptor> 
getColumnFamilyDescriptors() {
   void diffAllSnapshots(RocksDBCheckpointDiffer differ)
       throws IOException {
     final DifferSnapshotInfo src = snapshots.get(snapshots.size() - 1);
-
-    // Hard-coded expected output.
-    // The results are deterministic. Retrieved from a successful run.
-    final List<List<String>> expectedDifferResult = asList(
-        asList("000023", "000029", "000026", "000019", "000021", "000031"),
-        asList("000023", "000029", "000026", "000021", "000031"),
-        asList("000023", "000029", "000026", "000031"),
-        asList("000029", "000026", "000031"),
-        asList("000029", "000031"),
-        Collections.singletonList("000031"),
-        Collections.emptyList()
-    );
-    assertEquals(snapshots.size(), expectedDifferResult.size());
-
-    int index = 0;
-    List<String> expectedDiffFiles = new ArrayList<>();
+    boolean sawNonEmptyDiff = false;
     for (DifferSnapshotInfo snap : snapshots) {
       // Returns a list of SST files to be fed into RocksCheckpointDiffer Dag.
       List<String> tablesToTrack = new 
ArrayList<>(COLUMN_FAMILIES_TO_TRACK_IN_DAG);
       // Add some invalid index.
       tablesToTrack.add("compactionLogTable");
+
+      // Baseline diff when tracking every table. A subset's diff must equal
+      // this baseline filtered to the subset's column families (files with no
+      // column family are always kept). This relationship is deterministic and
+      // stable across RocksDB versions, unlike hard-coded SST file names.
+      Set<String> allTables = new HashSet<>(tablesToTrack);
+      List<SstFileInfo> baseline = differ.getSSTDiffList(
+          new DifferSnapshotVersion(src, 0, allTables),
+          new DifferSnapshotVersion(snap, 0, allTables),
+          null, allTables, true).orElse(Collections.emptyList());
+      sawNonEmptyDiff = sawNonEmptyDiff || !baseline.isEmpty();
+
+      // Independent structural oracle, not derived from getSSTDiffList's own
+      // output: a snapshot diffed against itself must have no differing SST
+      // files. Together with the sawNonEmptyDiff guard below, this bounds a
+      // systematically broken diff in both directions (returning nothing, or
+      // returning files even for identical snapshots).
+      if (snap == src) {
+        assertThat(baseline)
+            .as("diff of a snapshot against itself must be empty")
+            .isEmpty();
+      }
+
       Set<String> tableToLookUp = new HashSet<>();
       for (int i = 0; i < Math.pow(2, tablesToTrack.size()); i++) {
         tableToLookUp.clear();
-        expectedDiffFiles.clear();
         int mask = i;
         while (mask != 0) {
           int firstSetBitIndex = Integer.numberOfTrailingZeros(mask);
           tableToLookUp.add(tablesToTrack.get(firstSetBitIndex));
           mask &= mask - 1;
         }
-        for (String diffFile : expectedDifferResult.get(index)) {
-          String columnFamily;
-          if 
(rocksDBCheckpointDiffer.getCompactionNodeMap().containsKey(diffFile)) {
-            columnFamily = 
rocksDBCheckpointDiffer.getCompactionNodeMap().get(diffFile).getColumnFamily();
-          } else {
-            columnFamily = src.getSstFile(0, diffFile).getColumnFamily();
-          }
-          if (columnFamily == null || tableToLookUp.contains(columnFamily)) {
-            expectedDiffFiles.add(diffFile);
-          }
-        }
         DifferSnapshotVersion srcSnapVersion = new DifferSnapshotVersion(src, 
0, tableToLookUp);
         DifferSnapshotVersion destSnapVersion = new 
DifferSnapshotVersion(snap, 0, tableToLookUp);
         List<SstFileInfo> sstDiffList = differ.getSSTDiffList(srcSnapVersion, 
destSnapVersion, null,
@@ -1064,12 +1063,24 @@ void diffAllSnapshots(RocksDBCheckpointDiffer differ)
         LOG.info("SST diff list from '{}' to '{}': {} tables: {}",
             src.getDbPath(0), snap.getDbPath(0), sstDiffList, tableToLookUp);
 
-        assertEquals(expectedDiffFiles, 
sstDiffList.stream().map(SstFileInfo::getFileName)
-            .collect(Collectors.toList()));
+        // Expected files: baseline entries whose column family is untracked
+        // (null) or included in this subset. getSSTDiffList returns the values
+        // of a HashMap, so its ordering is not guaranteed; compare as sets.
+        List<String> expectedFiles = baseline.stream()
+            .filter(sstFileInfo -> sstFileInfo.getColumnFamily() == null
+                || tableToLookUp.contains(sstFileInfo.getColumnFamily()))
+            .map(SstFileInfo::getFileName)
+            .collect(Collectors.toList());
+        List<String> actualFiles = sstDiffList.stream()
+            .map(SstFileInfo::getFileName)
+            .collect(Collectors.toList());
+        
assertThat(actualFiles).containsExactlyInAnyOrderElementsOf(expectedFiles);
       }
-
-      ++index;
     }
+    // Guard against getSSTDiffList silently returning nothing for every input.
+    assertThat(sawNonEmptyDiff)
+        .as("expected at least one non-empty SST diff across snapshots")
+        .isTrue();
   }
 
   /**
diff --git a/hadoop-ozone/dist/src/main/compose/common/replicas-test.sh 
b/hadoop-ozone/dist/src/main/compose/common/replicas-test.sh
index f69b4b23f1a..31cc0c1f9b6 100755
--- a/hadoop-ozone/dist/src/main/compose/common/replicas-test.sh
+++ b/hadoop-ozone/dist/src/main/compose/common/replicas-test.sh
@@ -21,14 +21,49 @@ volume="cli-debug-volume${prefix}"
 bucket="cli-debug-bucket"
 key="testfile"
 
-dn_container="ozonesecure-ha-datanode1-1"
 container_db_path="/data/hdds/hdds/"
-local_db_backup_path="${COMPOSE_DIR}/container_db_backup"
+local_db_backup_path="${COMPOSE_DIR}/container_db_backup_${prefix}"
+backup_manifest="${local_db_backup_path}/container_db_paths.tsv"
 mkdir -p "${local_db_backup_path}"
 
-echo "Taking a backup of container.db"
-docker exec "${dn_container}" find "${container_db_path}" -name "container.db" 
| while read -r db; do
-  docker cp "${dn_container}:${db}" "${local_db_backup_path}/container.db"
+echo "Taking backups of existing container.db directories"
+datanodes=$(docker ps --format '{{.Names}}' | grep 
'^ozonesecure-ha-datanode[0-9]\+-1$' | sort)
+if [ -z "${datanodes}" ]; then
+  echo "Failed to find datanode containers" >&2
+  exit 1
+fi
+
+>"${backup_manifest}"
+for dn_container in ${datanodes}; do
+  while read -r db; do
+    printf '%s\t%s\n' "${dn_container}" "${db}" >> "${backup_manifest}"
+  done < <(docker exec "${dn_container}" find "${container_db_path}" -name 
"container.db")
+done
+
+echo "Stopping datanodes for a consistent container.db backup"
+for dn_container in ${datanodes}; do
+  if [ "$(docker inspect -f '{{.State.Running}}' "${dn_container}" 
2>/dev/null)" = "true" ]; then
+    docker stop "${dn_container}" >/dev/null
+  else
+    echo "${dn_container} is already stopped before backup"
+  fi
+done
+
+while IFS=$'\t' read -r dn_container db; do
+  backup_path="${local_db_backup_path}/${dn_container}${db}"
+  mkdir -p "$(dirname "${backup_path}")"
+  docker cp "${dn_container}:${db}" "${backup_path}"
+done < "${backup_manifest}"
+
+echo "Restarting datanodes after backup"
+for dn_container in ${datanodes}; do
+  if [ "$(docker inspect -f '{{.State.Running}}' "${dn_container}" 
2>/dev/null)" != "true" ]; then
+    docker start "${dn_container}" >/dev/null
+  fi
+done
+
+for dn_container in ${datanodes}; do
+  wait_for_datanode "${dn_container}" HEALTHY 60
 done
 
 execute_robot_test ${SCM} -v "PREFIX:${prefix}" debug/ozone-debug-tests.robot
@@ -40,29 +75,111 @@ host="$(jq -r '.keyLocations[0][0].datanode["hostname"]' 
${chunkinfo})"
 container="${host%%.*}"
 dn_with_num="$(sed -E 's/^.*-(datanode[0-9]+)-[0-9]+$/\1/' <<< "$container")"
 
-# corrupt the first block of key on one of the datanodes
 datafile="$(jq -r '.keyLocations[0][0].file' ${chunkinfo})"
+container_id="$(jq -r '.keyLocations[0][0].blockData.blockID.containerID' 
${chunkinfo})"
+local_block_id="$(jq -r '.keyLocations[0][0].blockData.blockID.localID // 
.keyLocations[0][0].blockData.blockID.localId' ${chunkinfo})"
+pipeline_id="$(docker-compose exec -T ${SCM} bash -c \
+  "ozone admin container info ${container_id} --json | jq -r 
'.writePipelineID.id // .writePipelineId.id'")"
+if [ -z "${pipeline_id}" ] || [ "${pipeline_id}" = "null" ]; then
+  echo "Failed to determine write pipeline for container ${container_id}" >&2
+  exit 1
+fi
+if [ -z "${local_block_id}" ] || [ "${local_block_id}" = "null" ]; then
+  echo "Failed to determine local block ID for container ${container_id}" >&2
+  exit 1
+fi
+
+# corrupt the first block of key on one of the datanodes
 docker exec "${container}" sed -i -e '1s/^/a/' "${datafile}"
 
 execute_robot_test ${SCM} -v "PREFIX:${prefix}" -v "CORRUPT_DATANODE:${host}" 
debug/corrupt-block-checksum.robot
 
-echo "Overwriting container.db with the backup db"
-target_container_dir=$(docker exec "${container}" find "${container_db_path}" 
-name "container.db" | xargs dirname)
-docker cp "${local_db_backup_path}/container.db" 
"${container}:${target_container_dir}/"
-docker exec "${container}" sudo chown -R hadoop:hadoop 
"${target_container_dir}"
+target_container_db=$(docker exec "${container}" bash -c "
+  datafile=\$1
+  dir=\$(dirname \"\$datafile\")
+  while [ \"\$dir\" != '/' ]; do
+    if [[ \$(basename \"\$dir\") == CID-* ]]; then
+      container_db=\$(find \"\$dir\" -path '*/container.db' | head -n 1)
+      if [ -n \"\$container_db\" ]; then
+        echo \"\$container_db\"
+        exit 0
+      fi
+      exit 1
+    fi
+    dir=\$(dirname \"\$dir\")
+  done
+  exit 1
+" _ "${datafile}")
+if [ -z "${target_container_db}" ]; then
+  echo "Failed to locate container.db for ${datafile} on ${container}" >&2
+  exit 1
+fi
+backup_container_db="${local_db_backup_path}/${container}${target_container_db}"
+if [ ! -e "${backup_container_db}" ]; then
+  echo "No pre-key backup found for ${target_container_db} on ${container}; 
creating rollback copy by deleting block metadata"
+
+  docker stop "${container}"
+
+  wait_for_datanode "${container}" STALE 60
+
+  mkdir -p "$(dirname "${backup_container_db}")"
+  docker cp "${container}:${target_container_db}" "${backup_container_db}"
+
+  container_image="$(docker inspect -f '{{.Config.Image}}' "${container}")"
+  docker run --rm \
+    -v "${local_db_backup_path}:${local_db_backup_path}" \
+    --entrypoint bash "${container_image}" -c '
+      set -euo pipefail
+      backup_container_db="$1"
+      local_block_id="$2"
+
+      ldb --db="${backup_container_db}" --column_family=block_data delete 
"${local_block_id}"
+    ' _ "${backup_container_db}" "${local_block_id}" || exit 1
+
+  docker start "${container}"
+
+  wait_for_datanode "${container}" HEALTHY 60
+fi
 
 docker stop "${container}"
 
 wait_for_datanode "${container}" STALE 60
+
 execute_robot_test ${SCM} -v "PREFIX:${prefix}" -v "STALE_DATANODE:${host}" 
debug/stale-datanode-checksum.robot
 
 docker start "${container}"
 
 wait_for_datanode "${container}" HEALTHY 60
 
-execute_robot_test ${SCM} -v "PREFIX:${prefix}" -v "DATANODE:${host}" 
debug/block-existence-check.robot
-
 execute_robot_test ${SCM} -v "PREFIX:${prefix}" -v "DATANODE:${host}" -v 
"FAULT_INJ_DATANODE:${dn_with_num}" debug/container-state-verifier.robot
 
 execute_robot_test ${OM} kinit.robot
 execute_robot_test ${OM} -v "PREFIX:${prefix}" 
debug/ozone-debug-tests-ec3-2.robot
+
+echo "Overwriting container.db with the backup db"
+echo "Restoring backup at ${target_container_db} on ${container}"
+echo "Removing dn.ratis state for pipeline ${pipeline_id} on ${container}"
+docker stop "${container}"
+
+wait_for_datanode "${container}" STALE 60
+
+container_image="$(docker inspect -f '{{.Config.Image}}' "${container}")"
+docker run --rm --volumes-from "${container}" \
+  -v "${local_db_backup_path}:${local_db_backup_path}:ro" \
+  --entrypoint bash "${container_image}" -c '
+    set -euo pipefail
+    target_container_db="$1"
+    pipeline_id="$2"
+    backup_container_db="$3"
+
+    rm -rf "${target_container_db}" "/data/metadata/dn.ratis/${pipeline_id}"
+    mkdir -p "$(dirname "${target_container_db}")"
+    cp -a "${backup_container_db}" "${target_container_db}"
+    chown -R hadoop:hadoop "${target_container_db}"
+  ' _ "${target_container_db}" "${pipeline_id}" "${backup_container_db}" || 
exit 1
+
+docker start "${container}"
+
+wait_for_datanode "${container}" HEALTHY 60
+
+execute_robot_test ${SCM} -v "PREFIX:${prefix}" -v "DATANODE:${host}" 
debug/block-existence-check.robot
diff --git 
a/hadoop-ozone/dist/src/main/smoketest/debug/ozone-debug-keywords.robot 
b/hadoop-ozone/dist/src/main/smoketest/debug/ozone-debug-keywords.robot
index aa51febb318..9114239d878 100644
--- a/hadoop-ozone/dist/src/main/smoketest/debug/ozone-debug-keywords.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/debug/ozone-debug-keywords.robot
@@ -63,13 +63,26 @@ Check Container State Replicas
         ${checks} =       Get From Dictionary    ${replica}    checks
         ${check} =        Get From List          ${checks}     0
         Should Be Equal    ${check['type']}    containerState
-        Should Be Equal    ${check['pass']}    ${False}
-        ${actual_message} =    Set Variable    
${check['failures'][0]['message']}
-
-        Run Keyword If    '${hostname}' == '${faulty_datanode}'    Should 
Contain    ${actual_message}    ${expected_message}
-        ...    ELSE    Should Match Regexp    ${actual_message}    Replica 
state is (OPEN|CLOSING|QUASI_CLOSED|CLOSED)
+        Run Keyword If    '${hostname}' == '${faulty_datanode}'    Check 
Replica Failed    ${replica}  containerState  ${expected_message}
+        ...    ELSE    Check Healthy Replica Container State    ${replica}
     END
 
+Check Healthy Replica Container State
+    [Arguments]    ${replica}
+    ${checks} =     Get From Dictionary    ${replica}    checks
+    ${check} =      Get From List          ${checks}     0
+    Should Be Equal    ${check['type']}    containerState
+    Run Keyword If    ${check['pass']}    Check Replica Passed    ${replica}  
containerState
+    ...    ELSE    Check Replica Failed Container State    ${replica}
+
+Check Replica Failed Container State
+    [Arguments]    ${replica}
+    ${checks} =     Get From Dictionary    ${replica}    checks
+    ${check} =      Get From List          ${checks}     0
+    Should Be Equal    ${check['type']}    containerState
+    Should Be Equal    ${check['pass']}    ${False}
+    Should Match Regexp    ${check['failures'][0]['message']}    Replica state 
is (OPEN|CLOSING|QUASI_CLOSED|CLOSED)
+
 Check Replica Failed
     [Arguments]    ${replica}  ${check_type}  ${expected_message}
     ${checks} =     Get From Dictionary    ${replica}    checks
diff --git a/pom.xml b/pom.xml
index e043273407a..97b7d0991a3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -194,7 +194,7 @@
     <reflections.version>0.10.2</reflections.version>
     <reload4j.version>1.2.26</reload4j.version>
     
<restrict-imports.enforcer.version>3.0.0</restrict-imports.enforcer.version>
-    <rocksdb.version>7.7.3</rocksdb.version>
+    <rocksdb.version>10.10.1.1</rocksdb.version>
     <servlet-api.version>3.1.0</servlet-api.version>
     <shell-executable>bash</shell-executable>
     <slf4j.version>2.0.18</slf4j.version>


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

Reply via email to