Making today worse so tomorrow seems better.

JRuby Service in Rails

on

I have two worlds that need to collide. Rails solves the decade long problem of abusive Java web stacks and Java provides mature codebase to the Rails paradigm. JRuby has brought me the spark, now if I can only get the fire burning.

The problem: I need a Spring context to startup with Rails and use the config/database.yml.

The solution: A JRuby Service.

I want to expose tons of existing Java code and libs into Rails, but do not want to deal with a segmented world. Everything should be configured and started from one place, this is were a JRuby Service helps. The JRuby runtime will automatically load a class implementing a JRuby service interface (such as org.jruby.runtime.load.BasicLibraryService) when it is packaged in a specifically named jar at a specifically named placed.

Originally I was jumping though hoops trying to call static singletons, but ended up scratching my head how to get access to the Rails runtime. After finding hints from Ola Bini, with some searching and source diving, I found the javadoc for org.jruby.runtime.load.LoadService has the key. (An aside, it would be nice if the JRuby folks published the JavaDocs, even if it changed with every release).

Except from LoadService.java JavaDoc:

How to make a class that can get required by JRuby

First, decide on what name should be used to require the extension. In this purely hypothetical example, this name will be ‘active_record/connection_adapters/jdbc_adapter’. Then create the class name for this require-name, by looking at the guidelines above. Our class should be named active_record.connection_adapters.JdbcAdapterService, and implement one of the library-interfaces. The easiest one is BasicLibraryService, where you define the basicLoad-method, which will get called when your library should be loaded.

The next step is to either put your compiled class on JRuby’s classpath, or package the class/es inside a jar-file. To package into a jar-file, we first create the file, then rename it to jdbc_adapter.jar. Then we put this jar-file in the directory active_record/connection_adapters somewhere in JRuby’s load path.

The short and skinny of this? The package defines the directory and the class name determines the jar name. The package period separators are converted to slashes, so the package com.slackworks would be the directory com/slackworks. For brevity, this example uses the simple package slackworks, for a directory of slackworks. The class name, sans Service and converted from CamelCase to under scores determines the jar name. So the RailsSpringService class translates to rails_spring.jar

Building a JRuby Service

Step 0: Have a working JRuby setup.

Already done the JRuby installation dance with a rails instance with JDBC for MySQL to monkey with. All of the following source can be pulled from git://sprocket.slackworks.com/srv/git/samples.git

Step 1: Implementing BasicLibraryService.

For org.jruby.runtime.load.BasicLibraryService, this is fairly straight forward. All you need is to implement the basicLoad method. In addition, the org.jruby.Ruby has a static ThreadLocal reference for the Ruby runtime using methods getCurrentInstance and setCurrentInstance. This will be used as a place for the beans being created in the Spring Context to access the Ruby runtime. Just be careful of the ThreadLocal restrictions in multi-threaded environments. In this scenario of using the Ruby runtime to help build the Spring context, it is fine.
    /**
     * JRuby Service to load a Spring Context
     */
    public class RailsSpringService implements BasicLibraryService {
        private static ApplicationContext applicationContext;

        /**
         * Executed by JRuby
         */
        public boolean basicLoad(final Ruby runtime) throws IOException {
            // Set the Ruby runtime into the TheadLocal static reference
            Ruby.setCurrentInstance( runtime );    

            // Load the Spring context
            applicationContext = new ClassPathXmlApplicationContext( new String[] { "applicationContext.xml" } );        

            return true;
        }

        /**
         * Get access to the Spring Context
         */
        public static ApplicationContext getApplicationContext() {        
            return applicationContext;
        }
    }

Complete source at RailsSpringService.java which includes the package declaration that deteremines the directory

Step 2: Setting up the Spring context.

I am not going to get into the knitty gritty about Spring, only showing how Spring can load the rails’ config/database.yml to setup a Datasource. The juicy part is the following:
    // Get the RAILS_ENV from the JRuby runtime
    Ruby jruby = Ruby.getCurrentInstance();        
    RubyModule kernel = jruby.getKernel();
    String rails_env = kernel.getConstant("RAILS_ENV").asJavaString();
This is called by the YamlConfig.java constructor to correctly parse the database.yml and use the configuration for the running RAILS_ENV.

The DatabaseConfig.java is an extension of YamlConfig specific for handling the database.yml. Lastly, DataSource.java is an extension of Apache Commons DBCP BasicDataSource.java that is constructed using DatabaseConfig.

This allows for a applicationContext.xml of:
    <?xml version="1.0" encoding="UTF-8"?>
    <beans>
        <!--  Rails Database Config -->
        <bean id="databaseConfig" class="slackworks.rails.DatabaseConfig">
            <constructor-arg value="config/database.yml" />        
        </bean>

        <!--  Datasource  -->
        <bean id="dataSource" class="slackworks.rails.DataSource" destroy-method="close">
            <constructor-arg ref="databaseConfig" />        
            <property name="initialSize"  value="2" />
            <property name="maxActive"    value="15" />
            <property name="maxIdle"      value="2" />        
            <property name="testOnBorrow" value="true" />        
        </bean>
    </beans>

Where the databaseConfig bean loads from the config/database.yml, using the Rails’ RAILS_ENV, which gets passed to the dataSource bean. Now we have a DataSource that is in synch with Rails.

Step 3: Packaging Rails Spring Service.

Using the powers of Maven, the pom.xml has been tweaked so that ‘mvn package’ will create lib/slackworks with the rails_spring.jar and all its dependencies. The lib/slackworks directory need to be copied to the RAILS_ROOT

Step 4: Set the classpath.

JRuby is on the road to having jars automatically be loaded into the JVM, but I do not know of a smart way to automatically load the rails_spring.jar dependencies. To alleviate at, the classpath needs to be manually set to include everything in lib/slackworks, i.e. for Java 6, export CLASSPATH=/rails-app/lib/slackworks/*.

Step 5: Testing with console.

Now that everything is in place, the classpath has been set, time to fire it up for a test run. Simply call from the RAILS_ROOT,
     jruby script/console
You will be greet with the normal
    Loading development environment
Now load the RailsSpringService, via
    require 'slackworks/rails_spring'

and you will be spammed with logging output as Spring starts up and reads the database.yml

Step 6: Load RailsSpringService in Rails.

In the config/environment.rb, at the bottom at
    require 'slackworks/rails_spring'

Now when Rails starts up, you will be greeted by the same output as Spring starts up, the worlds have collided. Accessing the Spring context from Rails:

    include_class 'slackworks.RailsSpringService'
    dataSource = RailsSpringService.getApplicationContext().getBean( 'dataSource' )

Next Steps

Instead of ActiveRecord directly creating JDBC connections, create a ActiveRecord adapter that builds a DataSource to dole out connections. The DataSource can be crammed into JNDI, providing a easy access for the Spring Context. This removes the wasteful need of having to start a separate set of database connections for the Spring Context.

Caveat.

The world of JRuby is still a moving target, which should be no suprise to anyone, considering the JRuby team has sprinkled the web and the source with tidbits stating such. Thusly meaning, your mileage may vary.