jruby/warbler

Warbler doesn't appear to pull in all necessary dependencies from Jarfile

Taywee opened this issue · 8 comments

In this case, I'm using the h2 database. When I use jbundler directly, it appears to work, but after a run through bundler, it finds the h2 engine, but not the dependent h2-mvstore.

Full example:

$ pwd            
/tmp/warbletest

Gemfile

gem "warbler", "~> 2.0"
gem 'jbundler', '~> 0.9'

Jarfile

jar 'com.h2database:h2', '1.4.197'

bin/main.rb

#!/usr/bin/env ruby

begin
  require 'jbundler'
rescue LoadError
  # This will fail when wrapped in a jar
end

Java::org.h2.Driver

connection = Java::java.sql.DriverManager.get_connection('jdbc:h2:/tmp/h2-warbler-test.db')
connection.auto_commit = false

statement = connection.create_statement
statement.execute_update 'CREATE TABLE IF NOT EXISTS foo (a INTEGER, b CLOB, c BOOLEAN)'
statement.execute_update 'INSERT INTO foo (a, b, c) VALUES (5, \'test\', TRUE)'
connection.commit

Execution

$ ./bin/main.rb 

$ warble 
No executable matching config.jar_name found, using bin/main.rb
rm -f warbletest.jar
Creating warbletest.jar

$ java -jar warbletest.jar 
Exception in thread "Thread-4" java.lang.NoClassDefFoundError: org/h2/mvstore/MVMap$2
	at org.h2.mvstore.MVMap.entrySet(MVMap.java:827)
	at org.h2.store.LobStorageMap.removeAllForTable(LobStorageMap.java:301)
	at org.h2.engine.Database.removeOrphanedLobs(Database.java:1381)
	at org.h2.engine.Database.close(Database.java:1300)
	at org.h2.engine.DatabaseCloser.run(DatabaseCloser.java:63)
Caused by: java.lang.ClassNotFoundException: org.h2.mvstore.MVMap$2
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 5 more

Note that it still happens the same even when I explicitly put the h2-mvstore dependency in the Jarfile. I haven't been able to find a workaround so far.

kares commented

might be an edgy case, you could try "forcing" JRuby's class-loader to be the context-loader during boot.
require 'jruby'; JRuby.set_context_class_loader if works Warbler could/should? maybe set it for you

Where should that live? I put that in my main.rb at the top, and it still fails with the same error.

I've noticed that when I explicitly put h2-mvstore in the jarfile, then it does actually end up in ./META-INF/lib/h2-mvstore-1.4.197.jar in the jar, but otherwise it is absent. Even when it's in there, the class isn't found, so the jar doesn't appear to end up properly referenced.

I found some interesting things here. If I explicitly load in the classes in my ruby, it works:

...
require 'java'

Java::org.h2.Driver
Java::org.h2.mvstore.const_get('MVMap$2')
Java::org.h2.mvstore.const_get('MVMap$2$1')
Java::org.h2.mvstore.WriteBuffer

connection = Java::java.sql.DriverManager.get_connection('jdbc:h2:/tmp/h2-warbler-test.db')
...

This works correctly. If I don't reference those classes in the main.rb file, I get the NoClassDefFoundError. It works the same whether h2-mvstore is referenced in the Jarfile or not. I don't actually know a whole lot about Java. Might this be an issue with how h2 itself is packaged? Any idea why jbundler works around this, but warbler doesn't, and how to possibly make warbler handle this case?

It does look like warbler's JarMain always uses URLClassLoader, where the JRuby Main class uses it's own class loader. I assume that might have something to do with it.

Doesn't work for me, even if I do the set_context_class_loader as the very first statement. This is my main.rb file here:

JRuby.set_context_class_loader

connection = Java::java.sql.DriverManager.get_connection('jdbc:h2:/tmp/h2-warbler-test.db')
connection.auto_commit = false

statement = connection.create_statement
statement.execute_update 'CREATE TABLE IF NOT EXISTS foo (a INTEGER, b CLOB, c BOOLEAN)'
statement.execute_update 'INSERT INTO foo (a, b, c) VALUES (5, \'test\', TRUE)'
connection.commit

And when I run it:

$ warble
No executable matching config.jar_name found, using bin/main.rb
rm -f warbletest.jar
Creating warbletest.jar

$ java -jar warbletest.jar
Exception in thread "Thread-3" java.lang.NoClassDefFoundError: org/h2/mvstore/MVMap$2
	at org.h2.mvstore.MVMap.entrySet(MVMap.java:827)
	at org.h2.store.LobStorageMap.removeAllForTable(LobStorageMap.java:301)
	at org.h2.engine.Database.removeOrphanedLobs(Database.java:1381)
	at org.h2.engine.Database.close(Database.java:1300)
	at org.h2.engine.DatabaseCloser.run(DatabaseCloser.java:63)
Caused by: java.lang.ClassNotFoundException: org.h2.mvstore.MVMap$2
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 5 more

that is strange as I did see your problem and I looked at the classloader-hierarchy and this setup just worked for me.

then I see only your way to hard load these three classes

Maybe it's a JVM difference, I'm using:

$ java -version
openjdk version "1.8.0_171"
OpenJDK Runtime Environment (IcedTea 3.8.0) (Gentoo icedtea-3.8.0)
OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode)

Honestly, having to explicitly specify classes I need is an acceptable workaround for me here. It would be good to get to the root of what's causing the issue (whether in Warbler or H2), but despite needing some ugly-looking hacks to get it working, it's not holding me back.