rocksdb/java/rocksjni/comparatorjnicallback.cc
Alan Paxton d47126875b Fix Java API ComparatorOptions use after delete error (#11176)
Summary:
The problem
-------------
ComparatorOptions is AutoCloseable.

AbstractComparator does not hold a reference to its ComparatorOptions, but the native C++ ComparatorJniCallback holds a reference to the ComparatorOptions’ native C++ options structure. This gets deleted when the ComparatorOptions is closed, either explicitly, or as part of try-with-resources.

Later, the deleted C++ options structure gets used by the callback and the comparator options are effectively random.

The original bug report https://github.com/facebook/rocksdb/issues/8715 was caused by a GC-initiated finalization closing the still-in-use ComparatorOptions. As of 7.0, finalization of RocksDB objects no longer closes them, which worked round the reported bug, but still left ComparatorOptions with a potentially broken lifetime.

In any case, we encourage API clients to use the try-with-resources model, and so we need it to work. And if they don't use it, they leak resources.

The solution
-------------
The solution implemented here is to make a copy of the native C++ options object into the ComparatorJniCallback, rather than a reference. Then the deletion of the native object held by ComparatorOptions is *correctly* deleted when its scope is closed in try/finally.

Testing
-------
We added a regression unit test based on the original test for the reported ticket.

This checkin closes https://github.com/facebook/rocksdb/issues/8715

We expect that there are more instances of "lifecycle" bugs in the Java API. They are a major source of support time/cost, and we note that they could be addressed as a whole using the model proposed/prototyped in https://github.com/facebook/rocksdb/pull/10736

Pull Request resolved: https://github.com/facebook/rocksdb/pull/11176

Reviewed By: cbi42

Differential Revision: D43160885

Pulled By: pdillinger

fbshipit-source-id: 60b54215a02ad9abb17363319650328c00a9ad62
2023-02-17 13:03:41 -08:00

648 lines
21 KiB
C++

// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
//
// This file implements the callback "bridge" between Java and C++ for
// ROCKSDB_NAMESPACE::Comparator.
#include "rocksjni/comparatorjnicallback.h"
#include "rocksjni/portal.h"
namespace ROCKSDB_NAMESPACE {
ComparatorJniCallback::ComparatorJniCallback(
JNIEnv* env, jobject jcomparator,
const ComparatorJniCallbackOptions* options)
: JniCallback(env, jcomparator),
m_options(std::make_unique<ComparatorJniCallbackOptions>(*options)) {
// cache the AbstractComparatorJniBridge class as we will reuse it many times
// for each callback
m_abstract_comparator_jni_bridge_clazz = static_cast<jclass>(
env->NewGlobalRef(AbstractComparatorJniBridge::getJClass(env)));
// Note: The name of a Comparator will not change during it's lifetime,
// so we cache it in a global var
jmethodID jname_mid = AbstractComparatorJni::getNameMethodId(env);
if (jname_mid == nullptr) {
// exception thrown: NoSuchMethodException or OutOfMemoryError
return;
}
jstring js_name = (jstring)env->CallObjectMethod(m_jcallback_obj, jname_mid);
if (env->ExceptionCheck()) {
// exception thrown
return;
}
jboolean has_exception = JNI_FALSE;
m_name = JniUtil::copyString(env, js_name,
&has_exception); // also releases jsName
if (has_exception == JNI_TRUE) {
// exception thrown
return;
}
// cache the ByteBuffer class as we will reuse it many times for each callback
m_jbytebuffer_clazz =
static_cast<jclass>(env->NewGlobalRef(ByteBufferJni::getJClass(env)));
m_jcompare_mid = AbstractComparatorJniBridge::getCompareInternalMethodId(
env, m_abstract_comparator_jni_bridge_clazz);
if (m_jcompare_mid == nullptr) {
// exception thrown: NoSuchMethodException or OutOfMemoryError
return;
}
m_jshortest_mid =
AbstractComparatorJniBridge::getFindShortestSeparatorInternalMethodId(
env, m_abstract_comparator_jni_bridge_clazz);
if (m_jshortest_mid == nullptr) {
// exception thrown: NoSuchMethodException or OutOfMemoryError
return;
}
m_jshort_mid =
AbstractComparatorJniBridge::getFindShortSuccessorInternalMethodId(
env, m_abstract_comparator_jni_bridge_clazz);
if (m_jshort_mid == nullptr) {
// exception thrown: NoSuchMethodException or OutOfMemoryError
return;
}
// do we need reusable buffers?
if (m_options->max_reused_buffer_size > -1) {
if (m_options->reused_synchronisation_type ==
ReusedSynchronisationType::THREAD_LOCAL) {
// buffers reused per thread
UnrefHandler unref = [](void* ptr) {
ThreadLocalBuf* tlb = reinterpret_cast<ThreadLocalBuf*>(ptr);
jboolean attached_thread = JNI_FALSE;
JNIEnv* _env = JniUtil::getJniEnv(tlb->jvm, &attached_thread);
if (_env != nullptr) {
if (tlb->direct_buffer) {
void* buf = _env->GetDirectBufferAddress(tlb->jbuf);
delete[] static_cast<char*>(buf);
}
_env->DeleteGlobalRef(tlb->jbuf);
JniUtil::releaseJniEnv(tlb->jvm, attached_thread);
}
};
m_tl_buf_a = new ThreadLocalPtr(unref);
m_tl_buf_b = new ThreadLocalPtr(unref);
m_jcompare_buf_a = nullptr;
m_jcompare_buf_b = nullptr;
m_jshortest_buf_start = nullptr;
m_jshortest_buf_limit = nullptr;
m_jshort_buf_key = nullptr;
} else {
// buffers reused and shared across threads
const bool adaptive = m_options->reused_synchronisation_type ==
ReusedSynchronisationType::ADAPTIVE_MUTEX;
mtx_compare = std::unique_ptr<port::Mutex>(new port::Mutex(adaptive));
mtx_shortest = std::unique_ptr<port::Mutex>(new port::Mutex(adaptive));
mtx_short = std::unique_ptr<port::Mutex>(new port::Mutex(adaptive));
m_jcompare_buf_a = env->NewGlobalRef(ByteBufferJni::construct(
env, m_options->direct_buffer, m_options->max_reused_buffer_size,
m_jbytebuffer_clazz));
if (m_jcompare_buf_a == nullptr) {
// exception thrown: OutOfMemoryError
return;
}
m_jcompare_buf_b = env->NewGlobalRef(ByteBufferJni::construct(
env, m_options->direct_buffer, m_options->max_reused_buffer_size,
m_jbytebuffer_clazz));
if (m_jcompare_buf_b == nullptr) {
// exception thrown: OutOfMemoryError
return;
}
m_jshortest_buf_start = env->NewGlobalRef(ByteBufferJni::construct(
env, m_options->direct_buffer, m_options->max_reused_buffer_size,
m_jbytebuffer_clazz));
if (m_jshortest_buf_start == nullptr) {
// exception thrown: OutOfMemoryError
return;
}
m_jshortest_buf_limit = env->NewGlobalRef(ByteBufferJni::construct(
env, m_options->direct_buffer, m_options->max_reused_buffer_size,
m_jbytebuffer_clazz));
if (m_jshortest_buf_limit == nullptr) {
// exception thrown: OutOfMemoryError
return;
}
m_jshort_buf_key = env->NewGlobalRef(ByteBufferJni::construct(
env, m_options->direct_buffer, m_options->max_reused_buffer_size,
m_jbytebuffer_clazz));
if (m_jshort_buf_key == nullptr) {
// exception thrown: OutOfMemoryError
return;
}
m_tl_buf_a = nullptr;
m_tl_buf_b = nullptr;
}
} else {
m_jcompare_buf_a = nullptr;
m_jcompare_buf_b = nullptr;
m_jshortest_buf_start = nullptr;
m_jshortest_buf_limit = nullptr;
m_jshort_buf_key = nullptr;
m_tl_buf_a = nullptr;
m_tl_buf_b = nullptr;
}
}
ComparatorJniCallback::~ComparatorJniCallback() {
jboolean attached_thread = JNI_FALSE;
JNIEnv* env = getJniEnv(&attached_thread);
assert(env != nullptr);
env->DeleteGlobalRef(m_abstract_comparator_jni_bridge_clazz);
env->DeleteGlobalRef(m_jbytebuffer_clazz);
if (m_jcompare_buf_a != nullptr) {
if (m_options->direct_buffer) {
void* buf = env->GetDirectBufferAddress(m_jcompare_buf_a);
delete[] static_cast<char*>(buf);
}
env->DeleteGlobalRef(m_jcompare_buf_a);
}
if (m_jcompare_buf_b != nullptr) {
if (m_options->direct_buffer) {
void* buf = env->GetDirectBufferAddress(m_jcompare_buf_b);
delete[] static_cast<char*>(buf);
}
env->DeleteGlobalRef(m_jcompare_buf_b);
}
if (m_jshortest_buf_start != nullptr) {
if (m_options->direct_buffer) {
void* buf = env->GetDirectBufferAddress(m_jshortest_buf_start);
delete[] static_cast<char*>(buf);
}
env->DeleteGlobalRef(m_jshortest_buf_start);
}
if (m_jshortest_buf_limit != nullptr) {
if (m_options->direct_buffer) {
void* buf = env->GetDirectBufferAddress(m_jshortest_buf_limit);
delete[] static_cast<char*>(buf);
}
env->DeleteGlobalRef(m_jshortest_buf_limit);
}
if (m_jshort_buf_key != nullptr) {
if (m_options->direct_buffer) {
void* buf = env->GetDirectBufferAddress(m_jshort_buf_key);
delete[] static_cast<char*>(buf);
}
env->DeleteGlobalRef(m_jshort_buf_key);
}
if (m_tl_buf_a != nullptr) {
delete m_tl_buf_a;
}
if (m_tl_buf_b != nullptr) {
delete m_tl_buf_b;
}
releaseJniEnv(attached_thread);
}
const char* ComparatorJniCallback::Name() const { return m_name.get(); }
int ComparatorJniCallback::Compare(const Slice& a, const Slice& b) const {
jboolean attached_thread = JNI_FALSE;
JNIEnv* env = getJniEnv(&attached_thread);
assert(env != nullptr);
const bool reuse_jbuf_a =
static_cast<int64_t>(a.size()) <= m_options->max_reused_buffer_size;
const bool reuse_jbuf_b =
static_cast<int64_t>(b.size()) <= m_options->max_reused_buffer_size;
MaybeLockForReuse(mtx_compare, reuse_jbuf_a || reuse_jbuf_b);
jobject jcompare_buf_a =
GetBuffer(env, a, reuse_jbuf_a, m_tl_buf_a, m_jcompare_buf_a);
if (jcompare_buf_a == nullptr) {
// exception occurred
MaybeUnlockForReuse(mtx_compare, reuse_jbuf_a || reuse_jbuf_b);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return 0;
}
jobject jcompare_buf_b =
GetBuffer(env, b, reuse_jbuf_b, m_tl_buf_b, m_jcompare_buf_b);
if (jcompare_buf_b == nullptr) {
// exception occurred
if (!reuse_jbuf_a) {
DeleteBuffer(env, jcompare_buf_a);
}
MaybeUnlockForReuse(mtx_compare, reuse_jbuf_a || reuse_jbuf_b);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return 0;
}
jint result = env->CallStaticIntMethod(
m_abstract_comparator_jni_bridge_clazz, m_jcompare_mid, m_jcallback_obj,
jcompare_buf_a, reuse_jbuf_a ? a.size() : -1, jcompare_buf_b,
reuse_jbuf_b ? b.size() : -1);
if (env->ExceptionCheck()) {
// exception thrown from CallIntMethod
env->ExceptionDescribe(); // print out exception to stderr
result = 0; // we could not get a result from java callback so use 0
}
if (!reuse_jbuf_a) {
DeleteBuffer(env, jcompare_buf_a);
}
if (!reuse_jbuf_b) {
DeleteBuffer(env, jcompare_buf_b);
}
MaybeUnlockForReuse(mtx_compare, reuse_jbuf_a || reuse_jbuf_b);
releaseJniEnv(attached_thread);
return result;
}
void ComparatorJniCallback::FindShortestSeparator(std::string* start,
const Slice& limit) const {
if (start == nullptr) {
return;
}
jboolean attached_thread = JNI_FALSE;
JNIEnv* env = getJniEnv(&attached_thread);
assert(env != nullptr);
const bool reuse_jbuf_start = static_cast<int64_t>(start->length()) <=
m_options->max_reused_buffer_size;
const bool reuse_jbuf_limit =
static_cast<int64_t>(limit.size()) <= m_options->max_reused_buffer_size;
MaybeLockForReuse(mtx_shortest, reuse_jbuf_start || reuse_jbuf_limit);
Slice sstart(start->data(), start->length());
jobject j_start_buf = GetBuffer(env, sstart, reuse_jbuf_start, m_tl_buf_a,
m_jshortest_buf_start);
if (j_start_buf == nullptr) {
// exception occurred
MaybeUnlockForReuse(mtx_shortest, reuse_jbuf_start || reuse_jbuf_limit);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return;
}
jobject j_limit_buf = GetBuffer(env, limit, reuse_jbuf_limit, m_tl_buf_b,
m_jshortest_buf_limit);
if (j_limit_buf == nullptr) {
// exception occurred
if (!reuse_jbuf_start) {
DeleteBuffer(env, j_start_buf);
}
MaybeUnlockForReuse(mtx_shortest, reuse_jbuf_start || reuse_jbuf_limit);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return;
}
jint jstart_len = env->CallStaticIntMethod(
m_abstract_comparator_jni_bridge_clazz, m_jshortest_mid, m_jcallback_obj,
j_start_buf, reuse_jbuf_start ? start->length() : -1, j_limit_buf,
reuse_jbuf_limit ? limit.size() : -1);
if (env->ExceptionCheck()) {
// exception thrown from CallIntMethod
env->ExceptionDescribe(); // print out exception to stderr
} else if (static_cast<size_t>(jstart_len) != start->length()) {
// start buffer has changed in Java, so update `start` with the result
bool copy_from_non_direct = false;
if (reuse_jbuf_start) {
// reused a buffer
if (m_options->direct_buffer) {
// reused direct buffer
void* start_buf = env->GetDirectBufferAddress(j_start_buf);
if (start_buf == nullptr) {
if (!reuse_jbuf_start) {
DeleteBuffer(env, j_start_buf);
}
if (!reuse_jbuf_limit) {
DeleteBuffer(env, j_limit_buf);
}
MaybeUnlockForReuse(mtx_shortest,
reuse_jbuf_start || reuse_jbuf_limit);
ROCKSDB_NAMESPACE::RocksDBExceptionJni::ThrowNew(
env, "Unable to get Direct Buffer Address");
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return;
}
start->assign(static_cast<const char*>(start_buf), jstart_len);
} else {
// reused non-direct buffer
copy_from_non_direct = true;
}
} else {
// there was a new buffer
if (m_options->direct_buffer) {
// it was direct... don't forget to potentially truncate the `start`
// string
start->resize(jstart_len);
} else {
// it was non-direct
copy_from_non_direct = true;
}
}
if (copy_from_non_direct) {
jbyteArray jarray =
ByteBufferJni::array(env, j_start_buf, m_jbytebuffer_clazz);
if (jarray == nullptr) {
if (!reuse_jbuf_start) {
DeleteBuffer(env, j_start_buf);
}
if (!reuse_jbuf_limit) {
DeleteBuffer(env, j_limit_buf);
}
MaybeUnlockForReuse(mtx_shortest, reuse_jbuf_start || reuse_jbuf_limit);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return;
}
jboolean has_exception = JNI_FALSE;
JniUtil::byteString<std::string>(
env, jarray,
[start, jstart_len](const char* data, const size_t) {
return start->assign(data, static_cast<size_t>(jstart_len));
},
&has_exception);
env->DeleteLocalRef(jarray);
if (has_exception == JNI_TRUE) {
if (!reuse_jbuf_start) {
DeleteBuffer(env, j_start_buf);
}
if (!reuse_jbuf_limit) {
DeleteBuffer(env, j_limit_buf);
}
env->ExceptionDescribe(); // print out exception to stderr
MaybeUnlockForReuse(mtx_shortest, reuse_jbuf_start || reuse_jbuf_limit);
releaseJniEnv(attached_thread);
return;
}
}
}
if (!reuse_jbuf_start) {
DeleteBuffer(env, j_start_buf);
}
if (!reuse_jbuf_limit) {
DeleteBuffer(env, j_limit_buf);
}
MaybeUnlockForReuse(mtx_shortest, reuse_jbuf_start || reuse_jbuf_limit);
releaseJniEnv(attached_thread);
}
void ComparatorJniCallback::FindShortSuccessor(std::string* key) const {
if (key == nullptr) {
return;
}
jboolean attached_thread = JNI_FALSE;
JNIEnv* env = getJniEnv(&attached_thread);
assert(env != nullptr);
const bool reuse_jbuf_key =
static_cast<int64_t>(key->length()) <= m_options->max_reused_buffer_size;
MaybeLockForReuse(mtx_short, reuse_jbuf_key);
Slice skey(key->data(), key->length());
jobject j_key_buf =
GetBuffer(env, skey, reuse_jbuf_key, m_tl_buf_a, m_jshort_buf_key);
if (j_key_buf == nullptr) {
// exception occurred
MaybeUnlockForReuse(mtx_short, reuse_jbuf_key);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return;
}
jint jkey_len = env->CallStaticIntMethod(
m_abstract_comparator_jni_bridge_clazz, m_jshort_mid, m_jcallback_obj,
j_key_buf, reuse_jbuf_key ? key->length() : -1);
if (env->ExceptionCheck()) {
// exception thrown from CallObjectMethod
if (!reuse_jbuf_key) {
DeleteBuffer(env, j_key_buf);
}
MaybeUnlockForReuse(mtx_short, reuse_jbuf_key);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return;
}
if (static_cast<size_t>(jkey_len) != key->length()) {
// key buffer has changed in Java, so update `key` with the result
bool copy_from_non_direct = false;
if (reuse_jbuf_key) {
// reused a buffer
if (m_options->direct_buffer) {
// reused direct buffer
void* key_buf = env->GetDirectBufferAddress(j_key_buf);
if (key_buf == nullptr) {
ROCKSDB_NAMESPACE::RocksDBExceptionJni::ThrowNew(
env, "Unable to get Direct Buffer Address");
if (!reuse_jbuf_key) {
DeleteBuffer(env, j_key_buf);
}
MaybeUnlockForReuse(mtx_short, reuse_jbuf_key);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return;
}
key->assign(static_cast<const char*>(key_buf), jkey_len);
} else {
// reused non-direct buffer
copy_from_non_direct = true;
}
} else {
// there was a new buffer
if (m_options->direct_buffer) {
// it was direct... don't forget to potentially truncate the `key`
// string
key->resize(jkey_len);
} else {
// it was non-direct
copy_from_non_direct = true;
}
}
if (copy_from_non_direct) {
jbyteArray jarray =
ByteBufferJni::array(env, j_key_buf, m_jbytebuffer_clazz);
if (jarray == nullptr) {
if (!reuse_jbuf_key) {
DeleteBuffer(env, j_key_buf);
}
MaybeUnlockForReuse(mtx_short, reuse_jbuf_key);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return;
}
jboolean has_exception = JNI_FALSE;
JniUtil::byteString<std::string>(
env, jarray,
[key, jkey_len](const char* data, const size_t) {
return key->assign(data, static_cast<size_t>(jkey_len));
},
&has_exception);
env->DeleteLocalRef(jarray);
if (has_exception == JNI_TRUE) {
if (!reuse_jbuf_key) {
DeleteBuffer(env, j_key_buf);
}
MaybeUnlockForReuse(mtx_short, reuse_jbuf_key);
env->ExceptionDescribe(); // print out exception to stderr
releaseJniEnv(attached_thread);
return;
}
}
}
if (!reuse_jbuf_key) {
DeleteBuffer(env, j_key_buf);
}
MaybeUnlockForReuse(mtx_short, reuse_jbuf_key);
releaseJniEnv(attached_thread);
}
inline void ComparatorJniCallback::MaybeLockForReuse(
const std::unique_ptr<port::Mutex>& mutex, const bool cond) const {
// no need to lock if using thread_local
if (m_options->reused_synchronisation_type !=
ReusedSynchronisationType::THREAD_LOCAL &&
cond) {
mutex.get()->Lock();
}
}
inline void ComparatorJniCallback::MaybeUnlockForReuse(
const std::unique_ptr<port::Mutex>& mutex, const bool cond) const {
// no need to unlock if using thread_local
if (m_options->reused_synchronisation_type !=
ReusedSynchronisationType::THREAD_LOCAL &&
cond) {
mutex.get()->Unlock();
}
}
jobject ComparatorJniCallback::GetBuffer(JNIEnv* env, const Slice& src,
bool reuse_buffer,
ThreadLocalPtr* tl_buf,
jobject jreuse_buffer) const {
if (reuse_buffer) {
if (m_options->reused_synchronisation_type ==
ReusedSynchronisationType::THREAD_LOCAL) {
// reuse thread-local bufffer
ThreadLocalBuf* tlb = reinterpret_cast<ThreadLocalBuf*>(tl_buf->Get());
if (tlb == nullptr) {
// thread-local buffer has not yet been created, so create it
jobject jtl_buf = env->NewGlobalRef(ByteBufferJni::construct(
env, m_options->direct_buffer, m_options->max_reused_buffer_size,
m_jbytebuffer_clazz));
if (jtl_buf == nullptr) {
// exception thrown: OutOfMemoryError
return nullptr;
}
tlb = new ThreadLocalBuf(m_jvm, m_options->direct_buffer, jtl_buf);
tl_buf->Reset(tlb);
}
return ReuseBuffer(env, src, tlb->jbuf);
} else {
// reuse class member buffer
return ReuseBuffer(env, src, jreuse_buffer);
}
} else {
// new buffer
return NewBuffer(env, src);
}
}
jobject ComparatorJniCallback::ReuseBuffer(JNIEnv* env, const Slice& src,
jobject jreuse_buffer) const {
// we can reuse the buffer
if (m_options->direct_buffer) {
// copy into direct buffer
void* buf = env->GetDirectBufferAddress(jreuse_buffer);
if (buf == nullptr) {
// either memory region is undefined, given object is not a direct
// java.nio.Buffer, or JNI access to direct buffers is not supported by
// this virtual machine.
ROCKSDB_NAMESPACE::RocksDBExceptionJni::ThrowNew(
env, "Unable to get Direct Buffer Address");
return nullptr;
}
memcpy(buf, src.data(), src.size());
} else {
// copy into non-direct buffer
const jbyteArray jarray =
ByteBufferJni::array(env, jreuse_buffer, m_jbytebuffer_clazz);
if (jarray == nullptr) {
// exception occurred
return nullptr;
}
env->SetByteArrayRegion(
jarray, 0, static_cast<jsize>(src.size()),
const_cast<jbyte*>(reinterpret_cast<const jbyte*>(src.data())));
if (env->ExceptionCheck()) {
// exception occurred
env->DeleteLocalRef(jarray);
return nullptr;
}
env->DeleteLocalRef(jarray);
}
return jreuse_buffer;
}
jobject ComparatorJniCallback::NewBuffer(JNIEnv* env, const Slice& src) const {
// we need a new buffer
jobject jbuf =
ByteBufferJni::constructWith(env, m_options->direct_buffer, src.data(),
src.size(), m_jbytebuffer_clazz);
if (jbuf == nullptr) {
// exception occurred
return nullptr;
}
return jbuf;
}
void ComparatorJniCallback::DeleteBuffer(JNIEnv* env, jobject jbuffer) const {
env->DeleteLocalRef(jbuffer);
}
} // namespace ROCKSDB_NAMESPACE