Running JRuby in an OSGi container

Posted on July 24, 2009 by Tommy McGuire
Labels: eclipse virgo, software development, ruby, osgi, java
On the JRuby user mailing list, Hasan posted a link to a workaround for a problem running JRuby scripts using JSR 223. The problem is that a bundle which uses the JRubyScriptingEngineFactory to create an engine to run a JRuby script fails because it cannot see the org.jruby.javasupport.Java class. The workaround is to set the current thread's context classloader to null before creating the engine, thereby forcing the JRuby engine to use RubyInstanceConfig.class' classloader, which comes from the JRuby bundle and can see the org.jruby.javasupport package.

I pointed out in a subsequent email that, after doing that, the JRuby script would not be able to locate Java classes in the script's own bundle. Yoko Harada then suggested adding "DynamicImport-Package: *" to the headers (see JRUBY-3792) for the jruby-complete bundle as a way to work around the problem with the first workaround.

That is not something I would want to do.

In this case, you have three OSGi bundles: the jruby-complete bundle, the JSR223 engine bundle, and your script's bundle. In OSGi, each bundle has it's own classloader which either loads things or delegates to other bundles' classloaders based on the import and export directives in the bundles' MANIFEST.MF. The JSR223 engine bundle imports the packages it needs from the jruby-complete bundle, and the script bundle imports the packages it needs to start up the JSR223 engine from the JSR223 engine bundle.

In the original problem, the script bundle's classloader could not see the org.jruby.javasupport package (containing the Java class) because there was nothing imported into the script bundle from the jruby-complete bundle. When the code in the script bundle tried to initialize the jruby engine, the script bundle raised the ClassNotFound exception.

I'm not entirely sure how the thread context classloaders work into the situation, but in this case the script's current thread classloader is the script bundle's classloader; when you set the current thread's classloader to null, you seem to disable the classloader for the script bundle and only use the classloader for the jruby-complete bundle for your script. (See Hasan's page for the reason why; it appears to have to do with the way JRuby is identifying the classloader it wants to use.) As a result, the engine is effectively running in the context of the jruby-complete bundle (I think), which contains org.jruby.javasupport, so the JRuby engine starts. However, packages and classes from the script bundle are not available because there is no dependency from the jruby-complete bundle on the script bundle. The problem is shown by the following code:

ScriptEngineFactory factory = new JRubyScriptEngineFactory();
System.out.println("Getting engine");

final ClassLoader loader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(null);
ScriptEngine engine = factory.getScriptEngine();
Thread.currentThread().setContextClassLoader(loader);

if (engine == null)
{
System.out.println("Cannot get engine");
}
else
{
System.out.println("Evaluating script");
engine.eval(
"include Java\nJava::test.jrubyscript.Printer.print"
);
}

where Printer.print is a Java class and method that simply prints something
and that lives in the same bundle as the bundle activator that runs this code. This produces

org.jruby.exceptions.RaiseException: cannot load Java class \
test.jrubyscript.Printer


Adding "DynamicImport-Package: *" to the jruby-complete bundle by itself does not help unless you also export the gov.nasa.test.jrubyscript package that contains the Printer class from the script bundle. (That is smell #1, if you are into refactoring: exporting a package from a module that is only used within the module.) Then, the jruby-complete bundle establishes a runtime-only dependency on the script bundle when it tries to load the Printer class.

The problem is that the DynamicImport-Package establishes a permanent link to the script bundle. (There is smell #2: the jruby-complete bundle is wired as a client to the script bundle, rather than the other way around.) Presumably, if you had two apps running as script bundles in the same OSGi container, instead of each using their own Printer class they would both use the Printer class from the first app bundle to get installed. We are currently using the SpringSource dm Server to serve two JRuby on Rails apps, as well as a number of non-JRuby applications, so this kind of linkage will eventually cause me some headaches.

The better solution, IMHO, is for the script bundle to actually declare its dependency on the jruby-complete bundle either by using Import-Package to bring in all of the packages it needs, or using Import-Bundle to pull in everything exported from the jruby-complete bundle. The first is pretty clearly a non-starter, since there is no way for me to tell which packages from jruby-complete the script bundle is going to need. The second works fine, though, and even allows the script bundle to as for a particular version of the jruby-complete bundle, which is one of the weaknesses of DynamicImport-Package.

It might be better for the JSR223 bundle to import all of the jruby-complete packages and then re-export them, so bundles like the script bundle would just have to specify a dependency on the JSR223 engine. But that is a completely different question.

The bottom line here is that the best solution is to play by the rules when using a container like OSGi.

Appendix



The MANIFEST.MF file for my script bundle looks like:

Manifest-Version: 1.0
Bundle-Classpath: .
Bundle-Version: 1.0.0
Bundle-Name: JRubyScriptTest
Bundle-ManifestVersion: 2
Bundle-Activator: gov.nasa.test.jrubyscript.Activator
Import-Package: com.sun.script.jruby,javax.script,org.osgi.framework
Bundle-SymbolicName: JRubyScriptTest
Import-Bundle: org.jruby.jruby


The bundle activator looks like:

package gov.nasa.test.jrubyscript;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

import com.sun.script.jruby.JRubyScriptEngineFactory;

public class Activator implements BundleActivator
{

@Override
public void start(BundleContext context) throws Exception
{
ScriptEngineFactory factory = new JRubyScriptEngineFactory();
System.out.println("Getting engine");
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("Loader: " + loader);
// Hasan's workaround: set the context classloader to null
// Thread.currentThread().setContextClassLoader(null);
ScriptEngine engine = factory.getScriptEngine();
System.out.println("Engine classloader: " + engine.getClass().getClassLoader());
// Thread.currentThread().setContextClassLoader(loader);
if (engine == null)
{
System.out.println("Cannot get engine");
}
else
{
System.out.println("Evaluating script");
engine.eval("puts \"hello\"");
engine.eval("include Java\nJava::gov.nasa.test.jrubyscript.Printer.print");
}
}

@Override
public void stop(BundleContext context) throws Exception
{
}

}


And my Printer class is:

package gov.nasa.test.jrubyscript;

public class Printer
{
public static void print()
{
System.out.println("Printer.print");
System.out.println("Printer classloader: " + Printer.class.getClassLoader());
}
}


To run this, no changes to the jruby-complete bundle or the JRuby JSR223 engine bundle.

To get Hasan's workaround to run, add

DynamicImport-Package: *

to the jruby-complete bundle,

Export-Package: gov.nasa.test.jrubyscript

to the script bundle, remove the Import-Bundle, and uncomment the classloader manipulations in the script bundle's activator.

[Edit: Welcome InfoQ readers!] Some links to previous JRuby/OSGi posts, including my incomplete series on running JRuby on Rails applications on the SpringSource dm Server as well as some other OSGi-related platitudes, falsehoods, and misunderstandings:

Comments


Hi,

Thank you very much for your post, I read all of your jRbuby and OSGi blog. What about gems included in rails war file under WEB-INF/gems? Is there a way to share these gems from multiple rails apps?

Thanks again
pado

Anonymous
2011-05-13

Pado, not as far as I know. There are two problems that I can see: First, the things under WEB-INF are not generally accessible outside the war/bundle and I do not know of a way to export Ruby resources (like gems) in the OSGi Java environment. (You might check with the JRuby mailing list, though.) Second, the JRuby runtime is set up by the Warbler interface between the Java Servlet API and Rails, so that is contained in the web application. That would make sharing the JRuby-generated classes hard, I would think.

Good luck!

Tommy McGuire
2011-05-14
active directory applied formal logic ashurbanipal authentication books c c++ comics conference continuations coq data structure digital humanities Dijkstra eclipse virgo electronics emacs goodreads haskell http java job Knuth ldap link linux lisp math naming nimrod notation OpenAM osgi parsing pony programming language protocols python quote R random REST ruby rust SAML scala scheme shell software development system administration theory tip toy problems unix vmware yeti
Member of The Internet Defense League
Site proudly generated by Hakyll.