facebook / facebook/rocksdb

[Java] Assertion failure + JVM crash with rocksdbjni running under Tomcat

Open
#691 11 comments 0 reactions 1 assignee Claimed by @adamretter View on GitHub
java-api
Dominant language
C++
Stars
32.1k
Forks
6.9k
Avg merge
32m
Merged PRs (30d)
1

Description

Using rocksjni with a Java web application running in Tomcat, the JVM crashed with this log message:

```
Exception in thread "Thread-10" java.lang.NoClassDefFoundError: org/rocksdb/InfoLogLevel
Caused by: java.lang.ClassNotFoundException: org.rocksdb.InfoLogLevel
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
java: ./java/./rocksjni/portal.h:743: static _jclass* rocksdb::InfoLogLevelJni::getJClass(JNIEnv*): Assertion `jclazz != nullptr' failed.
```

Here's the relevant code:

```
// Get the java class id of org.rocksdb.WBWIRocksIterator.WriteType.
static jclass getJClass(JNIEnv* env) {
jclass jclazz = env->FindClass("org/rocksdb/InfoLogLevel");
if (jclazz == nullptr)
env->ExceptionDescribe();
assert(jclazz != nullptr);
return jclazz;
}
```

The class `org/rocksdb/InfoLogLevel` is certainly present, inside the rocksdbjni JAR file under `WEB-INF/lib`, so what's going on?

Here's my theory: as we all know Tomcat creates separate `ClassLoader`s for each web application. A web application's Java classes and library classes are not available from the system class loader; instead, you must find them using the loader that Tomcat has created for that web application.

Unfortunately, when rocksdb is setup by rocksdbjni and it wants to log something, it uses `LoggerJniCallback::Logv()`, which looks like this:

```
/**
* Get JNIEnv for current native thread
*/
JNIEnv* LoggerJniCallback::getJniEnv() const {
JNIEnv *env;
jint rs = m_jvm->AttachCurrentThread(reinterpret_cast(&env), NULL);
assert(rs == JNI_OK);
return env;
}

...

void LoggerJniCallback::Logv(const InfoLogLevel log_level,
const char* format, va_list ap) {
if (GetInfoLogLevel() <= log_level) {
JNIEnv* env = getJniEnv();

....

}
m_jvm->DetachCurrentThread();
}
}
```

Note that the current thread is not associated with the JVM at all, which is why it has to put the call back into Java-land inside a `AttachCurrentThread()`/`DetachCurrentThread()` pair. But that means that the corresponding Java thread no longer has a same context class loader. So it defaults to the system class loader and so the attempt to load `org/rocksdb/InfoLogLevel` fails.

Suggested solution:
1. Upon creation of a new RocksDB instance, store a reference to the current thread's context class loader inside the native code.
2. Whenever the native code needs to load a class, invoke `ClassLoader.findClass()` directly using the saved loader reference.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.