Copilot commented on code in PR #3695:
URL: https://github.com/apache/celeborn/pull/3695#discussion_r3270908449


##########
common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala:
##########
@@ -4520,6 +4522,17 @@ object CelebornConf extends Logging {
       .checkValues(Set("LEVELDB", "ROCKSDB"))
       .createWithDefault("ROCKSDB")
 
+  val WORKER_RECOVER_DB_AUTO_RECOVERY: ConfigEntry[Boolean] =
+    buildConf("celeborn.metadata.autoRecovery.enabled")
+      .categories("worker")
+      .doc("If true, the metadata DB will automatically attempt to recover 
from RocksDBException " +
+        "errors during put/get/delete operations. Recovery first tries a safe 
reopen; if that " +
+        "fails, recreates the DB." +

Review Comment:
   `WORKER_RECOVER_DB_AUTO_RECOVERY` doc string concatenation is missing 
whitespace between sentences ("recreates the DB." + "If false...") which will 
render as "DB.If" in generated docs. Add a space either at the end of the first 
string or the beginning of the next one.
   



##########
worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBProvider.java:
##########
@@ -48,26 +48,48 @@ public class RocksDBProvider {
 
   private static final Logger logger = 
LoggerFactory.getLogger(RocksDBProvider.class);
 
+  private static Options createDBOptions() {
+    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);
+
+    return dbOptions;
+  }
+
+  /**
+   * Reopen an existing RocksDB without the delete-and-recreate fallback. Use 
this for recovery from
+   * transient errors.
+   */
+  public static org.rocksdb.RocksDB reopenRocksDB(File dbFile) throws 
IOException {
+    if (dbFile == null || !dbFile.exists()) {
+      throw new IOException("RocksDB path does not exist: " + dbFile);
+    }
+    Options dbOptions = createDBOptions();
+    try {
+      return org.rocksdb.RocksDB.open(dbOptions, dbFile.toString());
+    } catch (RocksDBException e) {
+      throw new IOException("Failed to reopen RocksDB at " + dbFile, e);
+    }

Review Comment:
   `reopenRocksDB` allocates RocksDB native resources (`Options`, 
`BloomFilter`, `BlockBasedTableConfig`, logger) but never closes them on 
failure, which can leak native memory when recovery repeatedly attempts reopen. 
Ensure these handles are closed at least on the exception path (and consider a 
longer-term ownership model so they’re closed on success as well).



##########
worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBIterator.java:
##########
@@ -33,20 +34,32 @@ public class RocksDBIterator implements DBIterator {
 
   private final RocksIterator it;
   private final MetadataMetrics metrics;
+  private final AtomicLong dbGeneration;
+  private final long creationGeneration;
 
   private boolean checkedNext;
 
   private boolean closed;
 
   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;
+  }
+
+  private void checkGeneration() {
+    if (dbGeneration.get() != creationGeneration) {
+      throw new IllegalStateException("DB instance was recreated, iterator is 
stale");

Review Comment:
   `checkGeneration()` throws on stale iterators without closing the underlying 
`RocksIterator`. Since `RocksIterator` holds native resources, this can leak if 
callers hit the exception and skip `close()`. Consider closing `it` (or marking 
closed) before throwing when generation mismatches.
   



##########
worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDB.java:
##########
@@ -17,63 +17,213 @@
 
 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.metrics.source.AbstractSource;
+import org.apache.celeborn.service.deploy.worker.WorkerSource;
 
 /**
  * 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. Recovery first attempts a safe 
reopen; if that fails, it
+ * falls back to recreating the DB. When {@code autoRecoveryEnabled} is {@code 
false} (the default),
+ * 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 org.rocksdb.RocksDB 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 StoreVersion version;
+  private final boolean autoRecoveryEnabled;
+  private volatile boolean closed = false;
 
-  public RocksDB(org.rocksdb.RocksDB db, AbstractSource source, DBBackend 
dbBackend) {
+  public RocksDB(
+      org.rocksdb.RocksDB db,
+      AbstractSource source,
+      DBBackend dbBackend,
+      File dbFile,
+      StoreVersion version) {
     super(source, dbBackend);
     this.db = db;
+    this.dbFile = dbFile;
+    this.version = version;
+    this.autoRecoveryEnabled =
+        (source instanceof WorkerSource) && ((WorkerSource) 
source).metadataAutoRecoveryEnabled();
+  }
+
+  private void recreateDBInstance(long failedGeneration) {
+    if (isClosed()) {
+      return;
+    }
+
+    rwLock.writeLock().lock();
+    try {
+      if (dbGeneration.get() != failedGeneration) {
+        logger.info(
+            "RocksDB instance already recovered by another thread (generation 
{} -> {})",
+            failedGeneration,
+            dbGeneration.get());
+        return;
+      }
+
+      if (isClosed()) {
+        return;
+      }
+
+      try {
+        if (db != null) {
+          db.close();
+        }
+      } catch (Exception e) {
+        logger.warn("Failed to close RocksDB instance", e);
+      }
+
+      // Phase 1: try safe reopen
+      try {
+        db = RocksDBProvider.reopenRocksDB(dbFile);
+        dbGeneration.incrementAndGet();
+        logger.info("RocksDB instance recovered {}", dbFile);
+        return;
+      } catch (IOException e) {
+        logger.warn("Safe reopen failed for RocksDB at {}", dbFile, e);
+      }
+
+      // Phase 2: recreate
+      try {
+        db = RocksDBProvider.initRockDB(dbFile, version);
+        dbGeneration.incrementAndGet();
+        logger.error("RocksDB {} was recreated.", dbFile);
+      } catch (IOException e) {
+        dbGeneration.incrementAndGet();
+        logger.error("Failed to recreate RocksDB instance 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();
+    rwLock.readLock().lock();
+    boolean unlocked = false;
+    long generation = dbGeneration.get();
+    try {
+      return operation.get();
+    } catch (RocksDBException e) {

Review Comment:
   `checkState()` runs before acquiring the read lock in `withRecovery`. A 
concurrent `close()` can set `closed=true` and close the native DB after 
another thread passes `checkState()` but before it acquires the read lock, 
leading to post-close operations throwing `RocksDBException` (or worse) instead 
of consistently throwing `IllegalStateException`. Consider checking `closed` 
under the lock (e.g., re-check after acquiring the read lock) to make the 
closed-state behavior thread-safe.



##########
worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDB.java:
##########
@@ -17,63 +17,213 @@
 
 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.metrics.source.AbstractSource;
+import org.apache.celeborn.service.deploy.worker.WorkerSource;
 
 /**
  * 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. Recovery first attempts a safe 
reopen; if that fails, it
+ * falls back to recreating the DB. When {@code autoRecoveryEnabled} is {@code 
false} (the default),
+ * 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 org.rocksdb.RocksDB 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 StoreVersion version;
+  private final boolean autoRecoveryEnabled;
+  private volatile boolean closed = false;
 
-  public RocksDB(org.rocksdb.RocksDB db, AbstractSource source, DBBackend 
dbBackend) {
+  public RocksDB(
+      org.rocksdb.RocksDB db,
+      AbstractSource source,
+      DBBackend dbBackend,
+      File dbFile,
+      StoreVersion version) {
     super(source, dbBackend);
     this.db = db;
+    this.dbFile = dbFile;
+    this.version = version;
+    this.autoRecoveryEnabled =
+        (source instanceof WorkerSource) && ((WorkerSource) 
source).metadataAutoRecoveryEnabled();
+  }
+
+  private void recreateDBInstance(long failedGeneration) {
+    if (isClosed()) {
+      return;
+    }
+
+    rwLock.writeLock().lock();
+    try {
+      if (dbGeneration.get() != failedGeneration) {
+        logger.info(
+            "RocksDB instance already recovered by another thread (generation 
{} -> {})",
+            failedGeneration,
+            dbGeneration.get());
+        return;
+      }
+
+      if (isClosed()) {
+        return;
+      }
+
+      try {
+        if (db != null) {
+          db.close();
+        }
+      } catch (Exception e) {
+        logger.warn("Failed to close RocksDB instance", e);
+      }
+
+      // Phase 1: try safe reopen
+      try {
+        db = RocksDBProvider.reopenRocksDB(dbFile);
+        dbGeneration.incrementAndGet();
+        logger.info("RocksDB instance recovered {}", dbFile);
+        return;
+      } catch (IOException e) {
+        logger.warn("Safe reopen failed for RocksDB at {}", dbFile, e);
+      }
+
+      // Phase 2: recreate
+      try {
+        db = RocksDBProvider.initRockDB(dbFile, version);
+        dbGeneration.incrementAndGet();
+        logger.error("RocksDB {} was recreated.", dbFile);
+      } catch (IOException e) {
+        dbGeneration.incrementAndGet();
+        logger.error("Failed to recreate RocksDB instance 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();
+    rwLock.readLock().lock();
+    boolean unlocked = false;
+    long generation = dbGeneration.get();
+    try {
+      return operation.get();
+    } catch (RocksDBException e) {
+      rwLock.readLock().unlock();
+      unlocked = true;
+      if (autoRecoveryEnabled) {
+        recreateDBInstance(generation);
+      }
+      throw e;
+    } finally {
+      if (!unlocked) {
+        rwLock.readLock().unlock();
+      }
+    }

Review Comment:
   `withRecovery` recreates the RocksDB instance but always rethrows the 
original `RocksDBException` without retrying the failed operation. If the 
intent is to make metadata operations recover transparently, consider retrying 
the operation once after a successful recovery (or update the config/docs/tests 
to explicitly state recovery only applies to subsequent operations).
   



##########
worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDB.java:
##########
@@ -17,63 +17,213 @@
 
 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.metrics.source.AbstractSource;
+import org.apache.celeborn.service.deploy.worker.WorkerSource;
 
 /**
  * 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. Recovery first attempts a safe 
reopen; if that fails, it
+ * falls back to recreating the DB. When {@code autoRecoveryEnabled} is {@code 
false} (the default),
+ * 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 org.rocksdb.RocksDB 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 StoreVersion version;
+  private final boolean autoRecoveryEnabled;
+  private volatile boolean closed = false;
 
-  public RocksDB(org.rocksdb.RocksDB db, AbstractSource source, DBBackend 
dbBackend) {
+  public RocksDB(
+      org.rocksdb.RocksDB db,
+      AbstractSource source,
+      DBBackend dbBackend,
+      File dbFile,
+      StoreVersion version) {
     super(source, dbBackend);
     this.db = db;
+    this.dbFile = dbFile;
+    this.version = version;
+    this.autoRecoveryEnabled =
+        (source instanceof WorkerSource) && ((WorkerSource) 
source).metadataAutoRecoveryEnabled();
+  }
+
+  private void recreateDBInstance(long failedGeneration) {
+    if (isClosed()) {
+      return;
+    }
+
+    rwLock.writeLock().lock();
+    try {
+      if (dbGeneration.get() != failedGeneration) {
+        logger.info(
+            "RocksDB instance already recovered by another thread (generation 
{} -> {})",
+            failedGeneration,
+            dbGeneration.get());
+        return;
+      }
+
+      if (isClosed()) {
+        return;
+      }
+
+      try {
+        if (db != null) {
+          db.close();
+        }
+      } catch (Exception e) {
+        logger.warn("Failed to close RocksDB instance", e);
+      }
+
+      // Phase 1: try safe reopen
+      try {
+        db = RocksDBProvider.reopenRocksDB(dbFile);
+        dbGeneration.incrementAndGet();
+        logger.info("RocksDB instance recovered {}", dbFile);
+        return;
+      } catch (IOException e) {
+        logger.warn("Safe reopen failed for RocksDB at {}", dbFile, e);
+      }
+
+      // Phase 2: recreate
+      try {
+        db = RocksDBProvider.initRockDB(dbFile, version);
+        dbGeneration.incrementAndGet();
+        logger.error("RocksDB {} was recreated.", dbFile);
+      } catch (IOException e) {
+        dbGeneration.incrementAndGet();
+        logger.error("Failed to recreate RocksDB instance at {}. ", dbFile, e);
+      }

Review Comment:
   `logger.error("RocksDB {} was recreated")` is used for a normal recovery 
path. Logging this at ERROR can create noisy alerts even though the system 
self-healed. Consider WARN/INFO for successful recovery and reserve ERROR for 
unrecoverable failures.



##########
worker/src/main/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDB.java:
##########
@@ -17,63 +17,213 @@
 
 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.metrics.source.AbstractSource;
+import org.apache.celeborn.service.deploy.worker.WorkerSource;
 
 /**
  * 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. Recovery first attempts a safe 
reopen; if that fails, it
+ * falls back to recreating the DB. When {@code autoRecoveryEnabled} is {@code 
false} (the default),
+ * 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 org.rocksdb.RocksDB 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 StoreVersion version;
+  private final boolean autoRecoveryEnabled;
+  private volatile boolean closed = false;
 
-  public RocksDB(org.rocksdb.RocksDB db, AbstractSource source, DBBackend 
dbBackend) {
+  public RocksDB(
+      org.rocksdb.RocksDB db,
+      AbstractSource source,
+      DBBackend dbBackend,
+      File dbFile,
+      StoreVersion version) {
     super(source, dbBackend);
     this.db = db;
+    this.dbFile = dbFile;
+    this.version = version;
+    this.autoRecoveryEnabled =
+        (source instanceof WorkerSource) && ((WorkerSource) 
source).metadataAutoRecoveryEnabled();
+  }
+
+  private void recreateDBInstance(long failedGeneration) {
+    if (isClosed()) {
+      return;
+    }
+
+    rwLock.writeLock().lock();
+    try {
+      if (dbGeneration.get() != failedGeneration) {
+        logger.info(
+            "RocksDB instance already recovered by another thread (generation 
{} -> {})",
+            failedGeneration,
+            dbGeneration.get());
+        return;
+      }
+
+      if (isClosed()) {
+        return;
+      }
+
+      try {
+        if (db != null) {
+          db.close();
+        }
+      } catch (Exception e) {
+        logger.warn("Failed to close RocksDB instance", e);
+      }
+
+      // Phase 1: try safe reopen
+      try {
+        db = RocksDBProvider.reopenRocksDB(dbFile);
+        dbGeneration.incrementAndGet();
+        logger.info("RocksDB instance recovered {}", dbFile);
+        return;
+      } catch (IOException e) {
+        logger.warn("Safe reopen failed for RocksDB at {}", dbFile, e);
+      }
+
+      // Phase 2: recreate
+      try {
+        db = RocksDBProvider.initRockDB(dbFile, version);
+        dbGeneration.incrementAndGet();
+        logger.error("RocksDB {} was recreated.", dbFile);
+      } catch (IOException e) {

Review Comment:
   Recovery fallback calls `RocksDBProvider.initRockDB`, which (on non-NotFound 
open errors) deletes the existing DB files and recreates the store. As 
implemented, any `RocksDBException` can therefore trigger destructive 
recreation when safe reopen fails. Consider limiting the recreate path to 
specific corruption/read-only status codes, or gating destructive recreation 
behind a separate explicit config to avoid unexpected data loss.



##########
worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBRecoverySuiteJ.java:
##########
@@ -0,0 +1,181 @@
+/*
+ * 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 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.apache.celeborn.common.CelebornConf;
+import org.apache.celeborn.common.util.JavaUtils;
+import org.apache.celeborn.service.deploy.worker.WorkerSource;
+
+public class RocksDBRecoverySuiteJ {
+
+  private File dbDir;
+  private File dbFile;
+  private WorkerSource workerSource;
+  private WorkerSource workerSourceWithRecovery;
+  private StoreVersion version;
+
+  @Before
+  public void setUp() throws IOException {
+    dbDir = Files.createTempDirectory("rocksdb-recovery-test").toFile();
+    dbFile = new File(dbDir, "test-db");
+    workerSource = new WorkerSource(new CelebornConf());
+
+    CelebornConf confWithRecovery = new CelebornConf();
+    confWithRecovery.set("celeborn.metadata.autoRecovery.enabled", "true");
+    workerSourceWithRecovery = new WorkerSource(confWithRecovery);
+
+    version = new StoreVersion(1, 0);
+  }
+
+  @After
+  public void tearDown() throws IOException {
+    workerSource.destroy();
+    workerSourceWithRecovery.destroy();
+    JavaUtils.deleteRecursively(dbDir);
+  }
+
+  @Test
+  public void testRecoveryAfterCorruption() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSource);
+    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);
+    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 testConcurrentRecoveryOnlyRecreatesOnce() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSource);
+    assertNotNull(db);
+
+    byte[] key = "key".getBytes(StandardCharsets.UTF_8);
+    byte[] value = "value".getBytes(StandardCharsets.UTF_8);
+    db.put(key, value);
+
+    RocksDB rocksDB = (RocksDB) db;
+
+    // Close and reopen to get a working DB, then verify generation tracking
+    db.close();
+    db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, workerSource);
+    assertNotNull(db);
+    rocksDB = (RocksDB) db;
+
+    assertEquals(0, rocksDB.getDbGeneration());
+
+    db.put(key, value);
+    byte[] result = db.get(key);
+    assertNotNull(result);
+
+    db.close();
+  }
+
+  @Test
+  public void testOperationsAfterCloseDoNotResurrect() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSourceWithRecovery);
+    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, 
workerSourceWithRecovery);
+    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);
+
+    // Verify iterator works before any recovery
+    assertTrue(iter.hasNext());
+    iter.close();
+

Review Comment:
   `testIteratorInvalidatedAfterRecovery` never triggers a recovery event and 
therefore does not assert the new stale-iterator behavior. To cover the 
iterator invalidation logic, force a recovery (e.g., make an operation throw 
`RocksDBException` so generation increments), then assert that the previously 
created iterator throws `IllegalStateException` on `hasNext/next/seek`.
   



##########
worker/src/test/java/org/apache/celeborn/service/deploy/worker/shuffledb/RocksDBRecoverySuiteJ.java:
##########
@@ -0,0 +1,181 @@
+/*
+ * 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 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.apache.celeborn.common.CelebornConf;
+import org.apache.celeborn.common.util.JavaUtils;
+import org.apache.celeborn.service.deploy.worker.WorkerSource;
+
+public class RocksDBRecoverySuiteJ {
+
+  private File dbDir;
+  private File dbFile;
+  private WorkerSource workerSource;
+  private WorkerSource workerSourceWithRecovery;
+  private StoreVersion version;
+
+  @Before
+  public void setUp() throws IOException {
+    dbDir = Files.createTempDirectory("rocksdb-recovery-test").toFile();
+    dbFile = new File(dbDir, "test-db");
+    workerSource = new WorkerSource(new CelebornConf());
+
+    CelebornConf confWithRecovery = new CelebornConf();
+    confWithRecovery.set("celeborn.metadata.autoRecovery.enabled", "true");
+    workerSourceWithRecovery = new WorkerSource(confWithRecovery);
+
+    version = new StoreVersion(1, 0);
+  }
+
+  @After
+  public void tearDown() throws IOException {
+    workerSource.destroy();
+    workerSourceWithRecovery.destroy();
+    JavaUtils.deleteRecursively(dbDir);
+  }
+
+  @Test
+  public void testRecoveryAfterCorruption() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSource);
+    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);
+    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 testConcurrentRecoveryOnlyRecreatesOnce() throws Exception {
+    DB db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, 
workerSource);
+    assertNotNull(db);
+
+    byte[] key = "key".getBytes(StandardCharsets.UTF_8);
+    byte[] value = "value".getBytes(StandardCharsets.UTF_8);
+    db.put(key, value);
+
+    RocksDB rocksDB = (RocksDB) db;
+
+    // Close and reopen to get a working DB, then verify generation tracking
+    db.close();
+    db = DBProvider.initDB(DBBackend.ROCKSDB, dbFile, version, workerSource);
+    assertNotNull(db);
+    rocksDB = (RocksDB) db;
+
+    assertEquals(0, rocksDB.getDbGeneration());
+

Review Comment:
   `testConcurrentRecoveryOnlyRecreatesOnce` does not exercise recovery or 
concurrency (it only closes and reopens a new DB instance and checks 
generation==0). To validate the new generation-based single-recreate behavior, 
this test should trigger a `RocksDBException` and run concurrent operations 
against the *same* `RocksDB` instance, then assert generation increments 
exactly once and the DB remains usable afterward.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to