[Java] Assertion failure + JVM crash with rocksdbjni running under Tomcat
- 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
Assessment
This issue has not been assessed yet.