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

SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git


The following commit(s) were added to refs/heads/main by this push:
     new 0198bcf53 [CELEBORN-2334] Automatically restore RocksDB in case of 
failures
0198bcf53 is described below

commit 0198bcf53a602f585df3a72cc1e5f147dff419a1
Author: AmandeepSingh285 <[email protected]>
AuthorDate: Mon Jun 22 10:48:46 2026 +0800

    [CELEBORN-2334] Automatically restore RocksDB in case of failures
    
    ### What changes were proposed in this pull request?
    
    The patch re-instantiates RocksDB in case of failures. In the current 
implementation, when RocksDB enters a read-only mode due to failures, Celeborn 
metadata operations fail and remain blocked until manual intervention or 
restart. This pull request adds logic to detect such RocksDB failures and 
re-instantiate the RocksDB instance so that metadata operations can recover 
automatically and continue functioning without prolonged disruption. RocksDB 
can enter a read-only or unusable state  [...]
    
    ### Why are the changes needed?
    
    Once RocksDB enters a read-only or error state, Celeborn metadata 
operations become unavailable because the existing RocksDB instance remains 
unusable, which could lead to failures in metadata updates.
    
    ### Does this PR resolve a correctness bug?
    
    No.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Unit tests.
    
    Closes #3695 from AmandeepSingh285/auto-recover-rocks-db.
    
    Lead-authored-by: AmandeepSingh285 <[email protected]>
    Co-authored-by: amandeeps.28 <[email protected]>
    Signed-off-by: Nicholas Jiang <[email protected]>
---
 .../common/protocol/RocksDBCompressionType.java    |  30 +++
 .../org/apache/celeborn/common/CelebornConf.scala  |  56 ++++
 docs/configuration/worker.md                       |   4 +
 .../deploy/worker/shuffledb/DBProvider.java        |  11 +-
 .../deploy/worker/shuffledb/ManagedRocksDB.java    | 107 ++++++++
 .../service/deploy/worker/shuffledb/RocksDB.java   | 178 ++++++++++++-
 .../deploy/worker/shuffledb/RocksDBIterator.java   |  25 +-
 .../deploy/worker/shuffledb/RocksDBProvider.java   |  75 ++++--
 .../worker/storage/PartitionFilesSorter.java       |   3 +-
 .../deploy/worker/storage/StorageManager.scala     |   2 +-
 .../deploy/worker/shuffledb/DBProviderSuiteJ.java  |   6 +-
 .../worker/shuffledb/ManagedRocksDBSuiteJ.java     | 224 ++++++++++++++++
 .../worker/shuffledb/RocksDBRecoverySuiteJ.java    | 285 +++++++++++++++++++++
 13 files changed, 965 insertions(+), 41 deletions(-)

diff --git 
a/common/src/main/java/org/apache/celeborn/common/protocol/RocksDBCompressionType.java
 
b/common/src/main/java/org/apache/celeborn/common/protocol/RocksDBCompressionType.java
new file mode 100644
index 000000000..49f1005ef
--- /dev/null
+++ 
b/common/src/main/java/org/apache/celeborn/common/protocol/RocksDBCompressionType.java
@@ -0,0 +1,30 @@
+/*
+ * 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.celeborn.common.protocol;
+
+public enum RocksDBCompressionType {
+  NO_COMPRESSION,
+  SNAPPY_COMPRESSION,
+  ZLIB_COMPRESSION,
+  BZLIB2_COMPRESSION,
+  LZ4_COMPRESSION,
+  LZ4HC_COMPRESSION,
+  XPRESS_COMPRESSION,
+  ZSTD_COMPRESSION,
+  DISABLE_COMPRESSION_OPTION;
+}
diff --git 
a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala 
b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
index 668fbcf79..ea6b819fc 100644
--- a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
+++ b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
@@ -1368,6 +1368,14 @@ class CelebornConf(loadDefaults: Boolean) extends 
Cloneable with Logging with Se
   def workerGracefulShutdownRecoverPath: String = 
get(WORKER_GRACEFUL_SHUTDOWN_RECOVER_PATH)
   def workerGracefulShutdownRecoverDbBackend: String =
     get(WORKER_GRACEFUL_SHUTDOWN_RECOVER_DB_BACKEND)
+  def workerRecoverDbRocksDBAutoRecoveryEnabled: Boolean =
+    get(WORKER_RECOVERDB_ROCKSDB_AUTORECOVERY_ENABLED)
+  def workerRecoverDbRocksDBCompression: String =
+    get(WORKER_RECOVERDB_ROCKSDB_COMPRESSION)
+  def workerRecoverDbRocksDBBottommostCompression: String =
+    get(WORKER_RECOVERDB_ROCKSDB_BOTTOMMOST_COMPRESSION)
+  def workerRecoverDbRocksDBBloomFilterBitsPerKey: Double =
+    get(WORKER_RECOVERDB_ROCKSDB_BLOOMFILTER_BITSPERKEY)
   def workerGracefulShutdownPartitionSorterCloseAwaitTimeMs: Long =
     get(WORKER_PARTITION_SORTER_SHUTDOWN_TIMEOUT)
   def workerGracefulShutdownFlusherShutdownTimeoutMs: Long = 
get(WORKER_FLUSHER_SHUTDOWN_TIMEOUT)
@@ -4533,6 +4541,54 @@ object CelebornConf extends Logging {
       .checkValues(Set("LEVELDB", "ROCKSDB"))
       .createWithDefault("ROCKSDB")
 
+  val WORKER_RECOVERDB_ROCKSDB_AUTORECOVERY_ENABLED: ConfigEntry[Boolean] =
+    
buildConf("celeborn.worker.graceful.shutdown.recoverDb.rocksdb.autoRecovery.enabled")
+      .categories("worker")
+      .doc("If true, the metadata DB will automatically attempt to recover 
from RocksDBException " +
+        "errors during put/get/delete operations. Recovery tries a safe 
reopen. " +
+        "If false, RocksDBException errors are propagated directly to the 
caller.")
+      .version("0.7.0")
+      .booleanConf
+      .createWithDefault(false)
+
+  private val rocksDBCompressionTypes: Set[String] =
+    RocksDBCompressionType.values().map(_.name()).toSet
+
+  val WORKER_RECOVERDB_ROCKSDB_COMPRESSION: ConfigEntry[String] =
+    
buildConf("celeborn.worker.graceful.shutdown.recoverDb.rocksdb.compression")
+      .categories("worker")
+      .doc("Compression type for the recover DB RocksDB upper levels. " +
+        "Must be a valid org.rocksdb.CompressionType value, e.g. 
LZ4_COMPRESSION, " +
+        "ZSTD_COMPRESSION, SNAPPY_COMPRESSION, NO_COMPRESSION.")
+      .version("0.7.0")
+      .stringConf
+      .transform(_.toUpperCase(Locale.ROOT))
+      .checkValues(rocksDBCompressionTypes)
+      .createWithDefault(RocksDBCompressionType.LZ4_COMPRESSION.name())
+
+  val WORKER_RECOVERDB_ROCKSDB_BOTTOMMOST_COMPRESSION: ConfigEntry[String] =
+    
buildConf("celeborn.worker.graceful.shutdown.recoverDb.rocksdb.bottommostCompression")
+      .categories("worker")
+      .doc("Compression type for the recover DB RocksDB bottommost level. " +
+        "Higher-ratio codecs (e.g. ZSTD_COMPRESSION) trade CPU for disk space 
and " +
+        "are typically a good fit since the bottommost level holds the most 
data. " +
+        "Must be a valid org.rocksdb.CompressionType value.")
+      .version("0.7.0")
+      .stringConf
+      .transform(_.toUpperCase(Locale.ROOT))
+      .checkValues(rocksDBCompressionTypes)
+      .createWithDefault(RocksDBCompressionType.ZSTD_COMPRESSION.name())
+
+  val WORKER_RECOVERDB_ROCKSDB_BLOOMFILTER_BITSPERKEY: ConfigEntry[Double] =
+    
buildConf("celeborn.worker.graceful.shutdown.recoverDb.rocksdb.bloomFilter.bitsPerKey")
+      .categories("worker")
+      .doc("Bits per key for the bloom filter used by the recover DB RocksDB. 
" +
+        "Higher values reduce false positives (fewer wasted disk reads) at the 
cost " +
+        "of more memory per key.")
+      .version("0.7.0")
+      .doubleConf
+      .createWithDefault(10.0)
+
   val WORKER_PARTITION_SORTER_SHUTDOWN_TIMEOUT: ConfigEntry[Long] =
     
buildConf("celeborn.worker.graceful.shutdown.partitionSorter.shutdownTimeout")
       .categories("worker")
diff --git a/docs/configuration/worker.md b/docs/configuration/worker.md
index bb2cec89b..f33a16993 100644
--- a/docs/configuration/worker.md
+++ b/docs/configuration/worker.md
@@ -105,6 +105,10 @@ license: |
 | celeborn.worker.graceful.shutdown.dbDeleteFailurePolicy | IGNORE | false | 
Policy for handling DB delete failures during graceful shutdown. THROW: throw 
exception, EXIT: trigger graceful shutdown, IGNORE: log error and continue 
(default). | 0.7.0 |  | 
 | celeborn.worker.graceful.shutdown.enabled | false | false | When true, 
during worker shutdown, the worker will wait for all released slots to be 
committed or destroyed. | 0.2.0 |  | 
 | celeborn.worker.graceful.shutdown.partitionSorter.shutdownTimeout | 120s | 
false | The wait time of waiting for sorting partition files during worker 
graceful shutdown. | 0.2.0 |  | 
+| celeborn.worker.graceful.shutdown.recoverDb.rocksdb.autoRecovery.enabled | 
false | false | If true, the metadata DB will automatically attempt to recover 
from RocksDBException errors during put/get/delete operations. Recovery tries a 
safe reopen. If false, RocksDBException errors are propagated directly to the 
caller. | 0.7.0 |  | 
+| celeborn.worker.graceful.shutdown.recoverDb.rocksdb.bloomFilter.bitsPerKey | 
10.0 | false | Bits per key for the bloom filter used by the recover DB 
RocksDB. Higher values reduce false positives (fewer wasted disk reads) at the 
cost of more memory per key. | 0.7.0 |  | 
+| celeborn.worker.graceful.shutdown.recoverDb.rocksdb.bottommostCompression | 
ZSTD_COMPRESSION | false | Compression type for the recover DB RocksDB 
bottommost level. Higher-ratio codecs (e.g. ZSTD_COMPRESSION) trade CPU for 
disk space and are typically a good fit since the bottommost level holds the 
most data. Must be a valid org.rocksdb.CompressionType value. | 0.7.0 |  | 
+| celeborn.worker.graceful.shutdown.recoverDb.rocksdb.compression | 
LZ4_COMPRESSION | false | Compression type for the recover DB RocksDB upper 
levels. Must be a valid org.rocksdb.CompressionType value, e.g. 
LZ4_COMPRESSION, ZSTD_COMPRESSION, SNAPPY_COMPRESSION, NO_COMPRESSION. | 0.7.0 
|  | 
 | celeborn.worker.graceful.shutdown.recoverDbBackend | ROCKSDB | false | 
Specifies a disk-based store used in local db. ROCKSDB or LEVELDB (deprecated). 
| 0.4.0 |  | 
 | celeborn.worker.graceful.shutdown.recoverPath | &lt;tmp&gt;/recover | false 
| The path to store DB. | 0.2.0 |  | 
 | celeborn.worker.graceful.shutdown.saveCommittedFileInfo.interval | 5s | 
false | Interval for a Celeborn worker to flush committed file infos into DB. | 
0.3.1 |  | 
diff --git 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/DBProvider.java
 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/DBProvider.java
index 8022a62e3..37be632b8 100644
--- 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/DBProvider.java
+++ 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/DBProvider.java
@@ -23,6 +23,7 @@ import java.io.IOException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.celeborn.common.CelebornConf;
 import org.apache.celeborn.common.metrics.source.AbstractSource;
 
 /** Note: code copied from Apache Spark. */
@@ -30,7 +31,11 @@ public class DBProvider {
   private static final Logger logger = 
LoggerFactory.getLogger(DBProvider.class);
 
   public static DB initDB(
-      DBBackend dbBackend, File dbFile, StoreVersion version, AbstractSource 
source)
+      DBBackend dbBackend,
+      File dbFile,
+      StoreVersion version,
+      AbstractSource source,
+      CelebornConf conf)
       throws IOException {
     if (dbFile != null) {
       switch (dbBackend) {
@@ -39,8 +44,8 @@ public class DBProvider {
           logger.warn("The LEVELDB is deprecated. Please use ROCKSDB 
instead.");
           return levelDB != null ? new LevelDB(levelDB, source, dbBackend) : 
null;
         case ROCKSDB:
-          org.rocksdb.RocksDB rocksDB = RocksDBProvider.initRockDB(dbFile, 
version);
-          return rocksDB != null ? new RocksDB(rocksDB, source, dbBackend) : 
null;
+          ManagedRocksDB rocksDB = RocksDBProvider.initRockDB(dbFile, version, 
conf);
+          return rocksDB != null ? new RocksDB(rocksDB, source, dbBackend, 
dbFile, conf) : null;
         default:
           throw new IllegalArgumentException("Unsupported DBBackend: " + 
dbBackend);
       }
diff --git 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/ManagedRocksDB.java
 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/ManagedRocksDB.java
new file mode 100644
index 000000000..3245f7ead
--- /dev/null
+++ 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/ManagedRocksDB.java
@@ -0,0 +1,107 @@
+/*
+ * 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.celeborn.service.deploy.worker.shuffledb;
+
+import java.io.Closeable;
+
+import org.rocksdb.BloomFilter;
+import org.rocksdb.Options;
+import org.rocksdb.RocksDBException;
+import org.rocksdb.RocksIterator;
+import org.rocksdb.WriteOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Encapsulates a {@link org.rocksdb.RocksDB} instance together with the 
resources it depends on.
+ * {@link org.rocksdb.RocksDB#close()} releases only the DB handle (and column 
family handles); the
+ * {@link Options}, {@link BloomFilter}, and {@link org.rocksdb.Logger} each 
own separate off-heap
+ * memory and must be closed explicitly.
+ *
+ * <p>Instances are produced by {@link RocksDBProvider}; callers interact with 
the underlying DB
+ * through the forwarding {@code put/get/delete/newIterator} methods and 
release everything via a
+ * single {@link #close()} call.
+ */
+public class ManagedRocksDB implements Closeable {
+  private static final Logger logger = 
LoggerFactory.getLogger(ManagedRocksDB.class);
+
+  private final Options dbOptions;
+  private final BloomFilter bloomFilter;
+  private final org.rocksdb.Logger rocksDBLogger;
+  private org.rocksdb.RocksDB db;
+
+  ManagedRocksDB(Options dbOptions, BloomFilter bloomFilter, 
org.rocksdb.Logger rocksDBLogger) {
+    this.dbOptions = dbOptions;
+    this.bloomFilter = bloomFilter;
+    this.rocksDBLogger = rocksDBLogger;
+  }
+
+  /** Attaches the DB handle */
+  void setDb(org.rocksdb.RocksDB db) {
+    this.db = db;
+  }
+
+  /** Package-private accessor */
+  Options options() {
+    return dbOptions;
+  }
+
+  /** Package-private accessor for {@link RocksDBProvider#checkVersion}. */
+  org.rocksdb.RocksDB db() {
+    return db;
+  }
+
+  public void put(byte[] key, byte[] value) throws RocksDBException {
+    db.put(key, value);
+  }
+
+  public void put(WriteOptions writeOptions, byte[] key, byte[] value) throws 
RocksDBException {
+    db.put(writeOptions, key, value);
+  }
+
+  public byte[] get(byte[] key) throws RocksDBException {
+    return db.get(key);
+  }
+
+  public void delete(byte[] key) throws RocksDBException {
+    db.delete(key);
+  }
+
+  public RocksIterator newIterator() {
+    return db.newIterator();
+  }
+
+  @Override
+  public void close() {
+    closeQuietly(db, "RocksDB");
+    closeQuietly(dbOptions, "RocksDB Options");
+    closeQuietly(bloomFilter, "RocksDB BloomFilter");
+    closeQuietly(rocksDBLogger, "RocksDB logger");
+  }
+
+  private static void closeQuietly(AutoCloseable resource, String name) {
+    if (resource == null) {
+      return;
+    }
+    try {
+      resource.close();
+    } catch (Exception e) {
+      logger.warn("Failed to close {}", name, e);
+    }
+  }
+}
diff --git 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDB.java
 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDB.java
index b45988ca0..02c1b659c 100644
--- 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDB.java
+++ 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDB.java
@@ -17,63 +17,217 @@
 
 package org.apache.celeborn.service.deploy.worker.shuffledb;
 
+import java.io.File;
 import java.io.IOException;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 import org.rocksdb.RocksDBException;
 import org.rocksdb.WriteOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import org.apache.celeborn.common.CelebornConf;
 import org.apache.celeborn.common.metrics.source.AbstractSource;
 
 /**
  * RocksDB implementation of the local KV storage used to persist the shuffle 
state.
  *
+ * <p>This class supports automatic recovery from RocksDB failures when {@code 
autoRecoveryEnabled}
+ * is set to {@code true}. When a put/get/delete operation encounters a {@link 
RocksDBException},
+ * the DB instance is closed and reopened. If the safe reopen fails, the 
exception is propagated.
+ * When {@code autoRecoveryEnabled} is {@code false}, exceptions are 
propagated directly without any
+ * recovery attempt.
+ *
+ * <p>Iterators obtained via {@link #iterator()} are invalidated after a 
recovery event and will
+ * throw {@link IllegalStateException} on subsequent use.
+ *
  * <p>Note: code copied from Apache Spark.
  */
 public class RocksDB extends DB {
-  private final org.rocksdb.RocksDB db;
+  private static final Logger logger = LoggerFactory.getLogger(RocksDB.class);
+
+  private volatile ManagedRocksDB db;
   private final WriteOptions SYNC_WRITE_OPTIONS = new 
WriteOptions().setSync(true);
+  private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
+  private final AtomicLong dbGeneration = new AtomicLong(0);
+  private final File dbFile;
+  private final boolean autoRecoveryEnabled;
+  private final CelebornConf conf;
+  private volatile boolean closed = false;
 
-  public RocksDB(org.rocksdb.RocksDB db, AbstractSource source, DBBackend 
dbBackend) {
+  public RocksDB(
+      ManagedRocksDB db,
+      AbstractSource source,
+      DBBackend dbBackend,
+      File dbFile,
+      CelebornConf conf) {
     super(source, dbBackend);
     this.db = db;
+    this.dbFile = dbFile;
+    this.autoRecoveryEnabled = 
conf.workerRecoverDbRocksDBAutoRecoveryEnabled();
+    this.conf = conf;
+  }
+
+  /** Attempts to recover the DB by closing and safely reopening it. */
+  private void tryRecoverDBInstance(long failedGeneration) {
+    if (isClosed()) {
+      return;
+    }
+
+    rwLock.writeLock().lock();
+    try {
+      if (dbGeneration.get() != failedGeneration) {
+        logger.info(
+            "Recovery already attempted by another thread (generation {} -> 
{}); "
+                + "if DB is still unhealthy, the next operation will retry",
+            failedGeneration,
+            dbGeneration.get());
+        return;
+      }
+
+      if (isClosed()) {
+        return;
+      }
+
+      try {
+        if (db != null) {
+          db.close();
+        }
+      } catch (Exception e) {
+        logger.warn("Failed to close RocksDB instance", e);
+      }
+
+      dbGeneration.incrementAndGet();
+
+      try {
+        db = RocksDBProvider.reopenRocksDB(dbFile, conf);
+        logger.info("RocksDB instance recovered at {}", dbFile);
+      } catch (IOException e) {
+        logger.error("Safe reopen failed for RocksDB at {}. ", dbFile, e);
+      }
+    } finally {
+      rwLock.writeLock().unlock();
+    }
+  }
+
+  private void checkState() {
+    if (isClosed()) {
+      throw new IllegalStateException("DB is closed");
+    }
+  }
+
+  private boolean isClosed() {
+    return closed;
+  }
+
+  @FunctionalInterface
+  interface CheckedSupplier<T> {
+    T get() throws RocksDBException;
+  }
+
+  @FunctionalInterface
+  interface CheckedRunnable {
+    void run() throws RocksDBException;
+  }
+
+  private <T> T withRecovery(CheckedSupplier<T> operation) throws 
RocksDBException {
+    checkState();
+    long generation = 0;
+    try {
+      rwLock.readLock().lock();
+      try {
+        if (isClosed()) {
+          throw new IllegalStateException("DB is closed");
+        }
+        generation = dbGeneration.get();
+        return operation.get();
+      } finally {
+        rwLock.readLock().unlock();
+      }
+    } catch (RocksDBException e) {
+      if (autoRecoveryEnabled) {
+        tryRecoverDBInstance(generation);
+      }
+      throw e;
+    }
+  }
+
+  private void runWithRecovery(CheckedRunnable operation) throws 
RocksDBException {
+    withRecovery(
+        () -> {
+          operation.run();
+          return null;
+        });
   }
 
   @Override
   protected void putInternal(byte[] key, byte[] value) throws RocksDBException 
{
-    db.put(key, value);
+    runWithRecovery(() -> db.put(key, value));
   }
 
   @Override
   protected void putInternal(byte[] key, byte[] value, boolean sync) throws 
RocksDBException {
-    if (sync) {
-      db.put(SYNC_WRITE_OPTIONS, key, value);
-    } else {
-      db.put(key, value);
-    }
+    runWithRecovery(
+        () -> {
+          if (sync) {
+            db.put(SYNC_WRITE_OPTIONS, key, value);
+          } else {
+            db.put(key, value);
+          }
+        });
   }
 
   @Override
   protected byte[] getInternal(byte[] key) throws RocksDBException {
-    return db.get(key);
+    return withRecovery(() -> db.get(key));
   }
 
   @Override
   protected void deleteInternal(byte[] key) throws RocksDBException {
-    db.delete(key);
+    runWithRecovery(() -> db.delete(key));
   }
 
   @Override
   protected DBIterator newIterator(MetadataMetrics metrics) {
-    return new RocksDBIterator(db.newIterator(), metrics);
+    checkState();
+    rwLock.readLock().lock();
+    try {
+      if (isClosed()) {
+        throw new IllegalStateException("DB is closed");
+      }
+      long generation = dbGeneration.get();
+      return new RocksDBIterator(db.newIterator(), metrics, dbGeneration, 
generation);
+    } finally {
+      rwLock.readLock().unlock();
+    }
   }
 
   @Override
   public void close() throws IOException {
+    rwLock.writeLock().lock();
     try {
+      closed = true;
       db.close();
     } finally {
-      // WriteOptions is a native handle; release it even if db.close() throws.
+      rwLock.writeLock().unlock();
       SYNC_WRITE_OPTIONS.close();
     }
   }
+
+  // Visible for testing
+  long getDbGeneration() {
+    return dbGeneration.get();
+  }
+
+  // Visible for testing
+  void forceRecovery() {
+    tryRecoverDBInstance(dbGeneration.get());
+  }
+
+  // Visible for testing
+  void forceRecovery(long atGeneration) {
+    tryRecoverDBInstance(atGeneration);
+  }
 }
diff --git 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBIterator.java
 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBIterator.java
index cce181dce..c944fb3a8 100644
--- 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBIterator.java
+++ 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBIterator.java
@@ -21,6 +21,7 @@ import java.io.IOException;
 import java.util.AbstractMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.concurrent.atomic.AtomicLong;
 
 import org.rocksdb.RocksIterator;
 
@@ -33,6 +34,8 @@ public class RocksDBIterator implements DBIterator {
 
   private final RocksIterator it;
   private final MetadataMetrics metrics;
+  private final AtomicLong dbGeneration;
+  private final long creationGeneration;
 
   private boolean checkedNext;
 
@@ -40,13 +43,31 @@ public class RocksDBIterator implements DBIterator {
 
   private Map.Entry<byte[], byte[]> next;
 
-  public RocksDBIterator(RocksIterator it, MetadataMetrics metrics) {
+  public RocksDBIterator(
+      RocksIterator it, MetadataMetrics metrics, AtomicLong dbGeneration, long 
creationGeneration) {
     this.it = it;
     this.metrics = metrics;
+    this.dbGeneration = dbGeneration;
+    this.creationGeneration = creationGeneration;
+  }
+
+  // Fail fast on a stale iterator rather than transparently re-acquiring on 
the recovered DB:
+  // a mid-iteration recovery means the underlying data was unhealthy enough 
to force a reopen,
+  // and silently restarting iteration would hide that signal from the caller 
(and risk skipping
+  // or duplicating entries depending on what mutations were in flight).
+  private void checkGeneration() {
+    if (dbGeneration.get() != creationGeneration) {
+      if (!closed) {
+        it.close();
+        closed = true;
+      }
+      throw new IllegalStateException("DB instance was recreated, iterator is 
stale");
+    }
   }
 
   @Override
   public boolean hasNext() {
+    checkGeneration();
     if (!checkedNext && !closed) {
       next = loadNext();
       checkedNext = true;
@@ -63,6 +84,7 @@ public class RocksDBIterator implements DBIterator {
 
   @Override
   public Map.Entry<byte[], byte[]> next() {
+    checkGeneration();
     if (!hasNext()) {
       throw new NoSuchElementException();
     }
@@ -83,6 +105,7 @@ public class RocksDBIterator implements DBIterator {
 
   @Override
   public void seek(byte[] key) {
+    checkGeneration();
     metrics.onRead(
         () -> {
           it.seek(key);
diff --git 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBProvider.java
 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBProvider.java
index 03a53ec66..46e962f4d 100644
--- 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBProvider.java
+++ 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBProvider.java
@@ -33,6 +33,7 @@ import org.rocksdb.Status;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.celeborn.common.CelebornConf;
 import org.apache.celeborn.common.util.PbSerDeUtils;
 
 /**
@@ -48,26 +49,55 @@ public class RocksDBProvider {
 
   private static final Logger logger = 
LoggerFactory.getLogger(RocksDBProvider.class);
 
-  public static org.rocksdb.RocksDB initRockDB(File dbFile, StoreVersion 
version)
+  private static ManagedRocksDB createDBOptions(CelebornConf conf) {
+    BloomFilter fullFilter =
+        new BloomFilter(conf.workerRecoverDbRocksDBBloomFilterBitsPerKey(), 
false);
+    BlockBasedTableConfig tableFormatConfig =
+        new BlockBasedTableConfig()
+            .setFilterPolicy(fullFilter)
+            .setEnableIndexCompression(false)
+            .setIndexBlockRestartInterval(8)
+            .setFormatVersion(5);
+
+    Options dbOptions = new Options();
+    RocksDBLogger rocksDBLogger = new RocksDBLogger(dbOptions);
+
+    dbOptions.setCreateIfMissing(false);
+    dbOptions.setBottommostCompressionType(
+        
CompressionType.valueOf(conf.workerRecoverDbRocksDBBottommostCompression()));
+    
dbOptions.setCompressionType(CompressionType.valueOf(conf.workerRecoverDbRocksDBCompression()));
+    dbOptions.setTableFormatConfig(tableFormatConfig);
+    dbOptions.setLogger(rocksDBLogger);
+
+    return new ManagedRocksDB(dbOptions, fullFilter, rocksDBLogger);
+  }
+
+  /**
+   * Reopen an existing RocksDB without the delete-and-recreate fallback. Use 
this for recovery from
+   * transient errors. The returned {@link ManagedRocksDB} owns the DB plus 
its open-time native
+   * resources; the caller must close it to release them.
+   */
+  public static ManagedRocksDB reopenRocksDB(File dbFile, CelebornConf conf) 
throws IOException {
+    if (dbFile == null || !dbFile.exists()) {
+      throw new IOException("RocksDB path does not exist: " + dbFile);
+    }
+    ManagedRocksDB managedDb = createDBOptions(conf);
+    try {
+      managedDb.setDb(org.rocksdb.RocksDB.open(managedDb.options(), 
dbFile.toString()));
+      return managedDb;
+    } catch (RocksDBException e) {
+      managedDb.close();
+      throw new IOException("Failed to reopen RocksDB at " + dbFile, e);
+    }
+  }
+
+  public static ManagedRocksDB initRockDB(File dbFile, StoreVersion version, 
CelebornConf conf)
       throws IOException {
-    org.rocksdb.RocksDB tmpDb = null;
+    ManagedRocksDB managedDb = null;
     if (dbFile != null) {
-      BloomFilter fullFilter = new BloomFilter(10.0D /* 
BloomFilter.DEFAULT_BITS_PER_KEY */, false);
-      BlockBasedTableConfig tableFormatConfig =
-          new BlockBasedTableConfig()
-              .setFilterPolicy(fullFilter)
-              .setEnableIndexCompression(false)
-              .setIndexBlockRestartInterval(8)
-              .setFormatVersion(5);
-
-      Options dbOptions = new Options();
-      RocksDBLogger rocksDBLogger = new RocksDBLogger(dbOptions);
-
-      dbOptions.setCreateIfMissing(false);
-      dbOptions.setBottommostCompressionType(CompressionType.ZSTD_COMPRESSION);
-      dbOptions.setCompressionType(CompressionType.LZ4_COMPRESSION);
-      dbOptions.setTableFormatConfig(tableFormatConfig);
-      dbOptions.setLogger(rocksDBLogger);
+      managedDb = createDBOptions(conf);
+      Options dbOptions = managedDb.options();
+      org.rocksdb.RocksDB tmpDb = null;
 
       try {
         tmpDb = org.rocksdb.RocksDB.open(dbOptions, dbFile.toString());
@@ -78,6 +108,7 @@ public class RocksDBProvider {
           try {
             tmpDb = org.rocksdb.RocksDB.open(dbOptions, dbFile.toString());
           } catch (RocksDBException dbExc) {
+            managedDb.close();
             throw new IOException("Unable to create state store", dbExc);
           }
         } else {
@@ -102,23 +133,25 @@ public class RocksDBProvider {
           try {
             tmpDb = org.rocksdb.RocksDB.open(dbOptions, dbFile.toString());
           } catch (RocksDBException dbExc) {
+            managedDb.close();
             throw new IOException("Unable to create state store", dbExc);
           }
         }
       }
+      managedDb.setDb(tmpDb);
       try {
         // if there is a version mismatch, we throw an exception, which means 
the service
         // is unusable
         checkVersion(tmpDb, version);
       } catch (RocksDBException e) {
-        tmpDb.close();
+        managedDb.close();
         throw new IOException(e.getMessage(), e);
       } catch (IOException ioe) {
-        tmpDb.close();
+        managedDb.close();
         throw ioe;
       }
     }
-    return tmpDb;
+    return managedDb;
   }
 
   private static void createIfMissing(Options dbOptions, File dbFile) {
diff --git 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/storage/PartitionFilesSorter.java
 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/storage/PartitionFilesSorter.java
index 5979a8434..f0ac31454 100644
--- 
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/storage/PartitionFilesSorter.java
+++ 
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/storage/PartitionFilesSorter.java
@@ -124,7 +124,8 @@ public class PartitionFilesSorter extends 
ShuffleRecoverHelper {
         String recoverySortedFilesFileName =
             dbBackend.fileName(RECOVERY_SORTED_FILES_FILE_NAME_PREFIX);
         this.recoverFile = new File(recoverPath, recoverySortedFilesFileName);
-        this.sortedFilesDb = DBProvider.initDB(dbBackend, recoverFile, 
CURRENT_VERSION, source);
+        this.sortedFilesDb =
+            DBProvider.initDB(dbBackend, recoverFile, CURRENT_VERSION, source, 
conf);
         reloadAndCleanSortedShuffleFiles(this.sortedFilesDb);
       } catch (Exception e) {
         throw new IllegalStateException(
diff --git 
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/StorageManager.scala
 
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/StorageManager.scala
index 9a2d4a8a7..99f87cd18 100644
--- 
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/StorageManager.scala
+++ 
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/StorageManager.scala
@@ -301,7 +301,7 @@ final private[worker] class StorageManager(conf: 
CelebornConf, workerSource: Abs
       val dbBackend = 
DBBackend.byName(conf.workerGracefulShutdownRecoverDbBackend)
       RECOVERY_FILE_NAME = dbBackend.fileName(RECOVERY_FILE_NAME_PREFIX)
       val recoverFile = new File(conf.workerGracefulShutdownRecoverPath, 
RECOVERY_FILE_NAME)
-      this.db = DBProvider.initDB(dbBackend, recoverFile, CURRENT_VERSION, 
workerSource)
+      this.db = DBProvider.initDB(dbBackend, recoverFile, CURRENT_VERSION, 
workerSource, conf)
       reloadAndCleanFileInfos(this.db)
     } catch {
       case e: Exception =>
diff --git 
a/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/DBProviderSuiteJ.java
 
b/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/DBProviderSuiteJ.java
index d5208e5d1..6af300e4d 100644
--- 
a/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/DBProviderSuiteJ.java
+++ 
b/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/DBProviderSuiteJ.java
@@ -67,13 +67,15 @@ public class DBProviderSuiteJ {
             ? Utils.createDirectory(dbDir.getPath(), namePrefix)
             : new File(dbDir.getPath(), String.format("%s-%s", namePrefix, 
UUID.randomUUID()));
     WorkerSource workerSource = new WorkerSource(new CelebornConf());
+    CelebornConf conf = new CelebornConf();
     try {
       StoreVersion v1 = new StoreVersion(1, 0);
-      DBProvider.initDB(dbBackend, dbFile, v1, workerSource).close();
+      DBProvider.initDB(dbBackend, dbFile, v1, workerSource, conf).close();
       StoreVersion v2 = new StoreVersion(2, 0);
       IOException ioe =
           assertThrows(
-              IOException.class, () -> DBProvider.initDB(dbBackend, dbFile, 
v2, workerSource));
+              IOException.class,
+              () -> DBProvider.initDB(dbBackend, dbFile, v2, workerSource, 
conf));
       assertTrue(ioe.getMessage().contains("incompatible with current version 
StoreVersion[2.0]"));
     } finally {
       workerSource.destroy();
diff --git 
a/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/ManagedRocksDBSuiteJ.java
 
b/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/ManagedRocksDBSuiteJ.java
new file mode 100644
index 000000000..eb1762b16
--- /dev/null
+++ 
b/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/ManagedRocksDBSuiteJ.java
@@ -0,0 +1,224 @@
+/*
+ * 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.celeborn.service.deploy.worker.shuffledb;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.rocksdb.BloomFilter;
+import org.rocksdb.CompressionType;
+import org.rocksdb.InfoLogLevel;
+import org.rocksdb.Options;
+import org.rocksdb.RocksIterator;
+
+import org.apache.celeborn.common.CelebornConf;
+import org.apache.celeborn.common.util.JavaUtils;
+
+public class ManagedRocksDBSuiteJ {
+
+  static {
+    // Ensure the native library is loaded for the partial-init test below, 
which constructs
+    // RocksDB native resources directly without going through RocksDBProvider.
+    org.rocksdb.RocksDB.loadLibrary();
+  }
+
+  private File dbDir;
+  private File dbFile;
+  private StoreVersion version;
+  private CelebornConf conf;
+
+  @Before
+  public void setUp() throws IOException {
+    dbDir = Files.createTempDirectory("managed-rocksdb-test").toFile();
+    dbFile = new File(dbDir, "test-db");
+    version = new StoreVersion(1, 0);
+    conf = new CelebornConf();
+  }
+
+  @After
+  public void tearDown() throws IOException {
+    JavaUtils.deleteRecursively(dbDir);
+  }
+
+  @Test
+  public void testForwardingPutGetDeleteDelegateToUnderlyingDb() throws 
Exception {
+    ManagedRocksDB managedDb = RocksDBProvider.initRockDB(dbFile, version, 
conf);
+    try {
+      byte[] key = "key".getBytes(StandardCharsets.UTF_8);
+      byte[] value = "value".getBytes(StandardCharsets.UTF_8);
+
+      managedDb.put(key, value);
+      assertArrayEquals(value, managedDb.get(key));
+
+      managedDb.delete(key);
+      assertNull(managedDb.get(key));
+    } finally {
+      managedDb.close();
+    }
+  }
+
+  @Test
+  public void testNewIteratorReturnsUsableIterator() throws Exception {
+    ManagedRocksDB managedDb = RocksDBProvider.initRockDB(dbFile, version, 
conf);
+    try {
+      managedDb.put("a".getBytes(StandardCharsets.UTF_8), 
"1".getBytes(StandardCharsets.UTF_8));
+      managedDb.put("b".getBytes(StandardCharsets.UTF_8), 
"2".getBytes(StandardCharsets.UTF_8));
+
+      try (RocksIterator iter = managedDb.newIterator()) {
+        int seen = 0;
+        for (iter.seekToFirst(); iter.isValid(); iter.next()) {
+          seen++;
+        }
+        assertTrue("expected at least 2 entries from forwarded iterator, got " 
+ seen, seen >= 2);
+      }
+    } finally {
+      managedDb.close();
+    }
+  }
+
+  @Test
+  public void testCloseReleasesUnderlyingNativeHandles() throws Exception {
+    ManagedRocksDB managedDb = RocksDBProvider.initRockDB(dbFile, version, 
conf);
+    org.rocksdb.RocksDB underlyingDb = managedDb.db();
+    Options underlyingOptions = managedDb.options();
+    assertNotNull(underlyingDb);
+    assertNotNull(underlyingOptions);
+    assertTrue("underlying DB should own native handle pre-close", 
underlyingDb.isOwningHandle());
+    assertTrue(
+        "underlying Options should own native handle pre-close",
+        underlyingOptions.isOwningHandle());
+
+    managedDb.close();
+
+    assertFalse(
+        "underlying DB native handle must be released after close", 
underlyingDb.isOwningHandle());
+    assertFalse(
+        "underlying Options native handle must be released after close",
+        underlyingOptions.isOwningHandle());
+  }
+
+  @Test
+  public void testCloseIsIdempotent() throws Exception {
+    ManagedRocksDB managedDb = RocksDBProvider.initRockDB(dbFile, version, 
conf);
+    managedDb.close();
+    // A second close must not throw. The recovery path can race with explicit 
close in production,
+    // so this safety is load-bearing.
+    managedDb.close();
+  }
+
+  @Test
+  public void testCloseWithoutSetDbReleasesPartialInitResources() {
+    // Simulates the partial-init state inside RocksDBProvider when 
RocksDB.open(...) fails
+    // before setDb is ever called: the four open-time resources are 
allocated, but no DB
+    // handle has been attached. close() must still release them and must not 
NPE on the
+    // null db reference.
+    Options dbOptions = new Options();
+    BloomFilter bloomFilter = new BloomFilter(10.0D, false);
+    org.rocksdb.Logger rocksDBLogger =
+        new org.rocksdb.Logger(dbOptions.infoLogLevel()) {
+          @Override
+          protected void log(InfoLogLevel infoLogLevel, String logMsg) {}
+        };
+
+    assertTrue(dbOptions.isOwningHandle());
+    assertTrue(bloomFilter.isOwningHandle());
+    assertTrue(rocksDBLogger.isOwningHandle());
+
+    ManagedRocksDB managedDb = new ManagedRocksDB(dbOptions, bloomFilter, 
rocksDBLogger);
+    assertNull("db should be unset for the partial-init case", managedDb.db());
+
+    managedDb.close();
+
+    assertFalse(
+        "Options must be released even when db was never attached", 
dbOptions.isOwningHandle());
+    assertFalse(
+        "BloomFilter must be released even when db was never attached",
+        bloomFilter.isOwningHandle());
+    assertFalse(
+        "Logger must be released even when db was never attached", 
rocksDBLogger.isOwningHandle());
+  }
+
+  @Test
+  public void testDefaultCompressionMatchesPreviousHardcodedValues() throws 
Exception {
+    ManagedRocksDB managedDb = RocksDBProvider.initRockDB(dbFile, version, 
conf);
+    try {
+      assertEquals(CompressionType.LZ4_COMPRESSION, 
managedDb.options().compressionType());
+      assertEquals(
+          CompressionType.ZSTD_COMPRESSION, 
managedDb.options().bottommostCompressionType());
+    } finally {
+      managedDb.close();
+    }
+  }
+
+  @Test
+  public void testCustomCompressionTypeFromConf() throws Exception {
+    
conf.set("celeborn.worker.graceful.shutdown.recoverDb.rocksdb.compression", 
"NO_COMPRESSION");
+    conf.set(
+        
"celeborn.worker.graceful.shutdown.recoverDb.rocksdb.bottommostCompression",
+        "SNAPPY_COMPRESSION");
+    ManagedRocksDB managedDb = RocksDBProvider.initRockDB(dbFile, version, 
conf);
+    try {
+      assertEquals(CompressionType.NO_COMPRESSION, 
managedDb.options().compressionType());
+      assertEquals(
+          CompressionType.SNAPPY_COMPRESSION, 
managedDb.options().bottommostCompressionType());
+    } finally {
+      managedDb.close();
+    }
+  }
+
+  @Test
+  public void testCompressionTypeIsCaseInsensitive() throws Exception {
+    
conf.set("celeborn.worker.graceful.shutdown.recoverDb.rocksdb.compression", 
"zstd_compression");
+    ManagedRocksDB managedDb = RocksDBProvider.initRockDB(dbFile, version, 
conf);
+    try {
+      assertEquals(CompressionType.ZSTD_COMPRESSION, 
managedDb.options().compressionType());
+    } finally {
+      managedDb.close();
+    }
+  }
+
+  @Test
+  public void testCustomBloomFilterBitsPerKeyOpensCleanly() throws Exception {
+    // Bloom filter bits-per-key is wrapped inside BlockBasedTableConfig and 
not directly
+    // readable back via the RocksDB Java API, so the practical assertion is 
that the DB
+    // builds and opens without throwing when a non-default value is supplied.
+    
conf.set("celeborn.worker.graceful.shutdown.recoverDb.rocksdb.bloomFilter.bitsPerKey",
 "15.0");
+    ManagedRocksDB managedDb = RocksDBProvider.initRockDB(dbFile, version, 
conf);
+    try {
+      assertNotNull(managedDb.db());
+      byte[] key = "k".getBytes(StandardCharsets.UTF_8);
+      byte[] value = "v".getBytes(StandardCharsets.UTF_8);
+      managedDb.put(key, value);
+      assertArrayEquals(value, managedDb.get(key));
+    } finally {
+      managedDb.close();
+    }
+  }
+}
diff --git 
a/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBRecoverySuiteJ.java
 
b/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBRecoverySuiteJ.java
new file mode 100644
index 000000000..670063279
--- /dev/null
+++ 
b/worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBRecoverySuiteJ.java
@@ -0,0 +1,285 @@
+/*
+ * 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.celeborn.service.deploy.worker.shuffledb;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.rocksdb.RocksDBException;
+
+import org.apache.celeborn.common.CelebornConf;
+import org.apache.celeborn.common.util.JavaUtils;
+import org.apache.celeborn.common.util.ThreadUtils;
+import org.apache.celeborn.service.deploy.worker.WorkerSource;
+
+public class RocksDBRecoverySuiteJ {
+
+  private File dbDir;
+  private File dbFile;
+  private CelebornConf defaultConf;
+  private CelebornConf confWithRecovery;
+  private WorkerSource workerSource;
+  private StoreVersion version;
+
+  @Before
+  public void setUp() throws IOException {
+    dbDir = Files.createTempDirectory("rocksdb-recovery-test").toFile();
+    dbFile = new File(dbDir, "test-db");
+    defaultConf = new CelebornConf();
+    confWithRecovery = new CelebornConf();
+    confWithRecovery.set(
+        
"celeborn.worker.graceful.shutdown.recoverDb.rocksdb.autoRecovery.enabled", 
"true");
+    workerSource = new WorkerSource(defaultConf);
+    version = new StoreVersion(1, 0);
+  }
+
+  @After
+  public void tearDown() throws IOException {
+    workerSource.destroy();
+    JavaUtils.deleteRecursively(dbDir);
+  }
+
+  @Test
+  public void testRecoveryAfterCorruption() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSource, defaultConf);
+    assertNotNull(db);
+
+    byte[] key = "test-key".getBytes(StandardCharsets.UTF_8);
+    byte[] value = "test-value".getBytes(StandardCharsets.UTF_8);
+    db.put(key, value);
+
+    byte[] result = db.get(key);
+    assertNotNull(result);
+    assertEquals("test-value", new String(result, StandardCharsets.UTF_8));
+
+    db.close();
+
+    // Corrupt the DB by overwriting SST files
+    corruptDbFiles(dbFile);
+
+    // Reopen — initRockDB will wipe and recreate since files are corrupt
+    db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, workerSource, 
defaultConf);
+    assertNotNull(db);
+
+    // Data is gone after wipe-and-recreate, but DB is functional
+    byte[] newKey = "new-key".getBytes(StandardCharsets.UTF_8);
+    byte[] newValue = "new-value".getBytes(StandardCharsets.UTF_8);
+    db.put(newKey, newValue);
+
+    result = db.get(newKey);
+    assertNotNull(result);
+    assertEquals("new-value", new String(result, StandardCharsets.UTF_8));
+    db.close();
+  }
+
+  @Test
+  public void testConcurrentRecoveryOnlyReopensOnce() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSource, confWithRecovery);
+    assertNotNull(db);
+
+    byte[] key = "key".getBytes(StandardCharsets.UTF_8);
+    byte[] value = "value".getBytes(StandardCharsets.UTF_8);
+    db.put(key, value);
+
+    RocksDB rocksDB = (RocksDB) db;
+    assertEquals(0, rocksDB.getDbGeneration());
+
+    int threadCount = 8;
+    CyclicBarrier barrier = new CyclicBarrier(threadCount);
+    ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+    // Trigger recovery directly to verify concurrent recovery deduplication
+    rocksDB.forceRecovery();
+    long genAfterFirstRecovery = rocksDB.getDbGeneration();
+    assertEquals(1, genAfterFirstRecovery);
+
+    // Now launch concurrent threads that all try to trigger recovery at the 
same generation.
+    // genAfterFirstRecovery is captured once so every thread calls 
forceRecovery with the
+    // same stale-generation value; only one can win the write-lock check and 
actually reopen.
+    List<Future<?>> futures = new ArrayList<>();
+    for (int i = 0; i < threadCount; i++) {
+      futures.add(
+          executor.submit(
+              () -> {
+                try {
+                  barrier.await();
+                  rocksDB.forceRecovery(genAfterFirstRecovery);
+                } catch (Exception e) {
+                  throw new RuntimeException(e);
+                }
+              }));
+    }
+
+    try {
+      for (Future<?> f : futures) {
+        f.get();
+      }
+    } finally {
+      ThreadUtils.shutdown(executor);
+    }
+
+    // Generation should have incremented exactly once more (all threads saw 
the same generation
+    // and only one wins the write lock to perform the actual reopen; the rest 
observe the
+    // advanced generation and bail without reopening)
+    assertEquals(genAfterFirstRecovery + 1, rocksDB.getDbGeneration());
+
+    // DB should still be usable
+    db.put(key, value);
+    byte[] result = db.get(key);
+    assertNotNull(result);
+    assertEquals("value", new String(result, StandardCharsets.UTF_8));
+
+    db.close();
+  }
+
+  @Test
+  public void testOperationsAfterCloseDoNotResurrect() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSource, confWithRecovery);
+    assertNotNull(db);
+
+    byte[] key = "key".getBytes(StandardCharsets.UTF_8);
+    byte[] value = "value".getBytes(StandardCharsets.UTF_8);
+    db.put(key, value);
+    db.close();
+
+    // All operations after close should throw IllegalStateException
+    DB closedDb = db;
+    assertThrows(IllegalStateException.class, () -> 
closedDb.put("k".getBytes(), "v".getBytes()));
+
+    assertThrows(IllegalStateException.class, () -> 
closedDb.get("k".getBytes()));
+
+    assertThrows(IllegalStateException.class, () -> 
closedDb.delete("k".getBytes()));
+
+    assertThrows(IllegalStateException.class, closedDb::iterator);
+  }
+
+  @Test
+  public void testIteratorInvalidatedAfterRecovery() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSource, confWithRecovery);
+    assertNotNull(db);
+
+    byte[] key = "key".getBytes(StandardCharsets.UTF_8);
+    byte[] value = "value".getBytes(StandardCharsets.UTF_8);
+    db.put(key, value);
+
+    // Get an iterator at generation 0
+    DBIterator iter = db.iterator();
+    iter.seek(key);
+    assertTrue(iter.hasNext());
+
+    // Force a recovery so the generation increments
+    RocksDB rocksDB = (RocksDB) db;
+    assertEquals(0, rocksDB.getDbGeneration());
+    rocksDB.forceRecovery();
+    assertEquals(1, rocksDB.getDbGeneration());
+
+    // The stale iterator should throw on hasNext, next, and seek
+    assertThrows(IllegalStateException.class, iter::hasNext);
+    assertThrows(IllegalStateException.class, iter::next);
+    assertThrows(IllegalStateException.class, () -> iter.seek(key));
+
+    // A new iterator should work fine
+    DBIterator newIter = db.iterator();
+    newIter.seek(key);
+    assertTrue(newIter.hasNext());
+    newIter.close();
+
+    db.close();
+  }
+
+  @Test
+  public void testWithRecoveryTriggeredByRocksDBException() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSource, confWithRecovery);
+    assertNotNull(db);
+
+    byte[] key = "key".getBytes(StandardCharsets.UTF_8);
+    byte[] value = "value".getBytes(StandardCharsets.UTF_8);
+    db.put(key, value);
+    assertEquals("value", new String(db.get(key), StandardCharsets.UTF_8));
+
+    RocksDB rocksDB = (RocksDB) db;
+    assertEquals(0, rocksDB.getDbGeneration());
+
+    // Swap the internal ManagedRocksDB with a Mockito spy that throws on the 
first put.
+    // Recovery will replace this spy with a fresh instance, so subsequent 
puts succeed normally.
+    Field dbField = RocksDB.class.getDeclaredField("db");
+    dbField.setAccessible(true);
+    ManagedRocksDB original = (ManagedRocksDB) dbField.get(rocksDB);
+    ManagedRocksDB spied = spy(original);
+    doThrow(new RocksDBException("injected failure"))
+        .doCallRealMethod()
+        .when(spied)
+        .put(any(byte[].class), any(byte[].class));
+    dbField.set(rocksDB, spied);
+
+    RuntimeException thrown =
+        assertThrows(
+            RuntimeException.class,
+            () ->
+                db.put(
+                    "k2".getBytes(StandardCharsets.UTF_8), 
"v2".getBytes(StandardCharsets.UTF_8)));
+    assertTrue(thrown.getCause() instanceof RocksDBException);
+
+    // Recovery should have incremented the generation
+    assertEquals(1, rocksDB.getDbGeneration());
+
+    // Subsequent operations should succeed on the recovered DB
+    byte[] newKey = "after-recovery".getBytes(StandardCharsets.UTF_8);
+    byte[] newValue = "works".getBytes(StandardCharsets.UTF_8);
+    db.put(newKey, newValue);
+    assertEquals("works", new String(db.get(newKey), StandardCharsets.UTF_8));
+
+    db.close();
+  }
+
+  private void corruptDbFiles(File dir) throws IOException {
+    if (dir.isDirectory()) {
+      File[] files = dir.listFiles();
+      if (files != null) {
+        for (File f : files) {
+          if (f.isFile()
+              && (f.getName().endsWith(".sst")
+                  || f.getName().startsWith("MANIFEST-")
+                  || f.getName().equals("CURRENT"))) {
+            Files.write(f.toPath(), 
"corrupted-data".getBytes(StandardCharsets.UTF_8));
+          }
+        }
+      }
+    }
+  }
+}

Reply via email to