diff --git a/java/CMakeLists.txt b/java/CMakeLists.txt index a60847ead37..d8fee41fce3 100644 --- a/java/CMakeLists.txt +++ b/java/CMakeLists.txt @@ -359,6 +359,7 @@ set(JAVA_TEST_CLASSES src/test/java/org/rocksdb/MergeVariantsTest.java src/test/java/org/rocksdb/MixedOptionsTest.java src/test/java/org/rocksdb/MultiColumnRegressionTest.java + src/test/java/org/rocksdb/MultiGetCorruptionTest.java src/test/java/org/rocksdb/MultiGetManyKeysTest.java src/test/java/org/rocksdb/MultiGetTest.java src/test/java/org/rocksdb/MutableColumnFamilyOptionsTest.java @@ -477,6 +478,7 @@ set(JAVA_TEST_RUNNING_CLASSES org.rocksdb.MergeVariantsTest org.rocksdb.MixedOptionsTest org.rocksdb.MultiColumnRegressionTest + org.rocksdb.MultiGetCorruptionTest org.rocksdb.MultiGetManyKeysTest org.rocksdb.MultiGetTest org.rocksdb.MutableColumnFamilyOptionsTest diff --git a/java/rocksjni/jni_multiget_helpers.cc b/java/rocksjni/jni_multiget_helpers.cc index 99994ac7cce..2909170e92e 100644 --- a/java/rocksjni/jni_multiget_helpers.cc +++ b/java/rocksjni/jni_multiget_helpers.cc @@ -158,6 +158,9 @@ jobjectArray MultiGetJNIValues::byteArrays( } env->DeleteLocalRef(jentry_value); + } else if (s[i].code() == ROCKSDB_NAMESPACE::Status::Code::kCorruption) { + ROCKSDB_NAMESPACE::RocksDBExceptionJni::ThrowNew(env, s[i]); + return nullptr; } else if (s[i].code() != ROCKSDB_NAMESPACE::Status::Code::kNotFound) { // The only way to return an error for a single key is to exception the // entire multiGet() Previous behaviour was to return a nullptr value for diff --git a/java/src/test/java/org/rocksdb/MultiGetCorruptionTest.java b/java/src/test/java/org/rocksdb/MultiGetCorruptionTest.java new file mode 100644 index 00000000000..0a47550dcab --- /dev/null +++ b/java/src/test/java/org/rocksdb/MultiGetCorruptionTest.java @@ -0,0 +1,92 @@ +package org.rocksdb; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.List; +import org.hamcrest.CustomTypeSafeMatcher; +import org.hamcrest.Description; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +public class MultiGetCorruptionTest { + private static final byte[] KEY = "key".getBytes(UTF_8); + private static final byte[] VALUE; + + static { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 100; i++) { + sb.append("value" + i + "\n"); + } + VALUE = sb.toString().getBytes(UTF_8); + } + + @ClassRule + public static final RocksNativeLibraryResource ROCKS_NATIVE_LIBRARY_RESOURCE = + new RocksNativeLibraryResource(); + + @Rule public TemporaryFolder dbFolder = new TemporaryFolder(); + + @Rule public ExpectedException exception = ExpectedException.none(); + + @Test + public void getKeyException() throws RocksDBException, IOException { + createCorruptedDatabase(); + try (Options options = new Options().setCreateIfMissing(true).setParanoidChecks(true); + RocksDB db = RocksDB.openReadOnly(options, dbFolder.getRoot().getAbsolutePath())) { + exception.expect(RocksDBException.class); // We need to be sure, exception is thrown only + db.get(KEY); // on GET operation. Require careful data corruption. + } + } + + @Test + public void multiGetKeyException() throws RocksDBException, IOException { + createCorruptedDatabase(); + try (Options options = new Options().setCreateIfMissing(true).setParanoidChecks(true); + RocksDB db = RocksDB.openReadOnly(options, dbFolder.getRoot().getAbsolutePath())) { + exception.expect(RocksDBException.class); + exception.expect(new CustomTypeSafeMatcher( + "Status.Code equal to Corruption") { + @Override + protected boolean matchesSafely(RocksDBException e) { + return e.getStatus().getCode() == Status.Code.Corruption; + } + + @Override + protected void describeMismatchSafely(RocksDBException e, Description mismatchDescription) { + mismatchDescription.appendText( + "was " + e.getStatus().getCodeString() + " " + e.getMessage()); + } + }); + + List keys = new ArrayList<>(); + keys.add(KEY); + db.multiGetAsList(keys); + } + } + + private void createCorruptedDatabase() throws RocksDBException, IOException { + try (Options options = new Options().setCreateIfMissing(true).setParanoidChecks(true); + RocksDB db = RocksDB.open(options, dbFolder.getRoot().getAbsolutePath())) { + db.put(KEY, VALUE); + try (FlushOptions flushOptions = new FlushOptions().setWaitForFlush(true)) { + db.flush(flushOptions); + } + } + + File[] files = dbFolder.getRoot().listFiles((dir, name) -> name.endsWith("sst")); + assertThat(files).hasSize(1); + + try (RandomAccessFile file = new RandomAccessFile(files[0], "rw")) { + file.seek(30); + file.write("corrupted".getBytes(UTF_8)); + } + } +}