Making today worse so tomorrow seems better.

JNDI With Rails

on

I like to explain the relationship between Rails and Java is that Rails makes the things I hate doing easy and Java makes the things I like doing possible.

In this mind set, when using Rails on JRuby I want to do as little configuration in Java (XML sit-ups are bad) and leverage Rails as much as possible (a few yml stretches are good). The most common configuration cross over is the database connection. This is where JNDI helps out, now both sides can use the same connection pool.

Putting your ducks in a row

For the following examples to work, you will need the follow depedencies:

  • Sun’s JNDI File System Service Provider. Follow “Download JNDI 1.2.1 & More” to download File System Service Provider. You will need the fscontext and the providerutil jars both in you classpath. I would like to take this moment and say that Sun needs to get their head out of the mud and join the Maven party, instead of forcing people to navigate their cumbersome site.
  • Maven will make you hate less. Manually managing the cadre of jars that Java demands will just chalk you full of spite. While Maven does not alleviate the problem, it does put a purdy bow on it
  • A given, you must already have gone through the steps of setting up a JRuby runtime. Steps might be to strong of a word, basically it is unpackage and go.

Configuring Rails to create its own JNDI and Connection Pool

Now it is time to get the show on the road. The following setups will show how to startup a JNDI instance, register a Connection Pool, and then use the same Connection Pool in Rails and in a Spring

Setting up the database.yml

The latest version of activerecord-jdbc already supports using JNDI for connections. By setting additional information in database.yml, jndi_factory_initial and jndi_provider_url, a connection pool can be built and registered into JNDI. The jndi_factory_initial relates to the JNDI property java.naming.factory.initial, which boils down to the JNDI service that is going to be created. The jndi_provider_url relates to the JNDI property java.naming.provider.url, which is the URL to connect to the JNDI service. The standard setting, jndi, is going to be used as the location to store and retrieve the Connection Pool.

Extended database.yml

    development:
      adapter: jdbc
      jndi: jdbc/shared
      jndi_factory_initial: com.sun.jndi.fscontext.RefFSContextFactory
      jndi_provider_url: file:tmp
      driver: mysql
      url: jdbc:mysql://localhost/deployer_devel
      username: the_username
      password: a_password
    

Firing up the JNDI instance

The following copied into the environment.rb will create a JNDI instance and register a MySQL Datasource to create a Connection Pool. I would like to point out that the mentioned fscontext and the providerutil jars need to be in your classpath for this to work.

    # include Sun JNDI classes
    include_class "java.lang.System" 
    include_class "javax.naming.Context" 
    include_class "javax.naming.InitialContext" 
    include_class "javax.naming.Reference" 
    include_class "javax.naming.StringRefAddr" 
    include_class "javax.sql.DataSource" 

    # include Mysql datasource class
    include_class "com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource" 

    # Create JNDI dir where it stores binding information, if it does not exist
    # WARNING: This is hardcoded to match JNDI definition in database.yml!
    if !File.exist? "tmp/jdbc" 
      Dir.mkdir( 'tmp/jdbc' )
    end

    # ActiveRecord config
    active_record_config = ActiveRecord::Base.establish_connection.config

    # Register JNDI properties from ActiveRecord config
    System.setProperty(Context::INITIAL_CONTEXT_FACTORY, active_record_config[:jndi_context_factory]);
    System.setProperty(Context::PROVIDER_URL, active_record_config[:jndi_provider_url]);

    # JNDI context
    intial_context = InitialContext.new();

    # Create MySQL datasource
    data_source = MysqlConnectionPoolDataSource.new();
    data_source.setUser( active_record_config[:username] );
    data_source.setPassword( active_record_config[:password] );
    data_source.setUrl( active_record_config[:url] )
    intial_context.rebind("jdbc/datasource", data_source);

    # Construct DBCP SharedPoolDataSource reference
    ref = Reference.new(
        "org.apache.commons.dbcp.datasources.SharedPoolDataSource",
        "org.apache.commons.dbcp.datasources.SharedPoolDataSourceFactory",
        nil);

    # Some exciting Connection Pool options
    ref.add(StringRefAddr.new("dataSourceName", "jdbc/datasource"));
    ref.add(StringRefAddr.new("initialSize", "2" ));
    ref.add(StringRefAddr.new("maxActive"  , "30" ));
    ref.add(StringRefAddr.new("maxIdle"    , "2" ));
    ref.add(StringRefAddr.new("testOnBorrow", "true" ));
    ref.add(StringRefAddr.new("validationQuery", "SELECT 1" ));
    ref.add(StringRefAddr.new("connectionProperties", "autoReconnect=true;" ));
    intial_context.rebind( active_record_config[:jndi], ref);
    

Why a Mysql datasource and a DBCP Connection Pool you ask? The Mysql datasource is bare bones, only interested in data connection properties such as login, url, etc. The DBCP datasouce is tricked out as a connection pool, monitoring the number of active and idle connections to expand the connection pool as needed (among other neat features).

Hooking up Java to the Rails JNDI

Here is where the magic happens, the Java equivalent of ActiveRecord::Base.establish_connection.config.


    IRubyObject arBase = runtime.getModule( "ActiveRecord" ).getConstant( "Base" );        
    IRubyObject establishConnection = arBase.callMethod( runtime.getCurrentContext(), "establish_connection" );
    RubyHash arConfig = establishConnection.callMethod( runtime.getCurrentContext(), "config" ).convertToHash();

Oh my, that is a mouth full. JRuby provides the loosey goosey ways of Ruby by wrapping everything in IRubyObject interfaces and indirectly executing operations. This Java block allows access to the configuration of ActiveRecord, so all the jndi properties set in the database.yml can be plucked out. Here is a slimmed down version of RailsJndiService for JNDI.


    package slackworks;

    // Sun JSE
    import java.io.IOException;
    import java.sql.Connection;
    import java.sql.SQLException;
    import java.util.Properties;

    import javax.naming.InitialContext;
    import javax.naming.NamingException;
    import javax.sql.DataSource;

    // JRuby
    import org.jruby.Ruby;
    import org.jruby.RubyHash;
    import org.jruby.RubySymbol;
    import org.jruby.runtime.builtin.IRubyObject;
    import org.jruby.runtime.load.BasicLibraryService;

    public class RailsSpringService implements BasicLibraryService {
        protected Ruby runtime;
        private static Properties jndiProperties;
        private static String     jndiDataSourceName;

        public boolean basicLoad(final Ruby runtime) throws IOException {                    

            // JNDI properties
            Properties properties = new Properties();

            // ActiveRecord::Base.establish_connection.config[:jndi]
            IRubyObject arBase = runtime.getModule( "ActiveRecord" ).getConstant( "Base" );        
            IRubyObject establishConnection = arBase.callMethod( runtime.getCurrentContext(), "establish_connection" );
            RubyHash arConfig = establishConnection.callMethod( runtime.getCurrentContext(), "config" ).convertToHash();

            String jndi = arConfig.get( RubySymbol.newSymbol( runtime, "jndi" ) ).toString();

            // Get initial context factory from ActiveRecord 
            String context_factory = arConfig.get( RubySymbol.newSymbol( runtime, "jndi_factory_initial" ) ).toString();
            properties.setProperty( "java.naming.factory.initial", context_factory );

            // Get provider url from ActiveRecord 
            String provider_url = arConfig.get( RubySymbol.newSymbol( runtime, "jndi_provider_url" ) ).toString();
            properties.setProperty( "java.naming.provider.url", provider_url );

            return true;
        }

        public static Properties getJndiProperties() {
            return jndiProperties;
        }

        public static void setJndiProperties(Properties jndiProperties) {
            DeployerService.jndiProperties = jndiProperties;
        }

        public static String getJndiDataSourceName() {
            return jndiDataSourceName;
        }

        public static void setJndiDataSourceName(String jndiDataSourceName) {
            DeployerService.jndiDataSourceName = jndiDataSourceName;
        }

        public static boolean testOpenConnection() throws SQLException, NamingException {
            InitialContext initialContext = new InitialContext( jndiProperties );
            DataSource ds = (DataSource) initialContext.lookup( getJndiDataSourceName() );
            Connection conn = ds.getConnection();

            return !conn.isClosed();
        }        
    }

Using the powers of Maven, the pom.xml has been tweaked so that the task ‘mvn package’ will create lib/slackworks with the rails_jndi.jar and all its dependencies. For the lazy, I suggest simply using rails_jndi in git://sprocket.slackworks.com/srv/git/samples.git. The lib/slackworks directory need to be copied to the RAILS_ROOT and should contain:

  • avalon-framework-4.1.3.jar
  • log4j-1.2.12.jar
  • servlet-api-2.3.jar
  • commons-dbcp-1.2.2.jar
  • commons-logging-1.1.jar
  • commons-pool-1.3.jar
  • logkit-1.0.1.jar
  • mysql-connector-java-5.1.6.jar
  • jruby-1.1RC3.jar
  • rails_jndi.jar

Why so many jars? I have no idea. I think a common-dbcp is married to commons-pool and she does not let him leave the house alone. I bet logkit is log4j runty little brother and someone must be friends with servlet-api and snuck him into the party.

The classpath needs to be manually set to include everything in lib/slackworks, i.e. for Java 6, export CLASSPATH=/path/to/rails/lib/slackworks/*.

Testing with console

Now that everything is in place- the environment.rb setup, the RailsJndiService has been built, everything cozied up in lib/slackworks, and the classpath has been set. Time to fire up the console for a test run. Simply call from the RAILS_ROOT:
     jruby script/console
You will be greeted with
    Loading development environment    
Now load the RailsJndiService, via
    require 'slackworks/rails_jndi'
and you will be see the RailsJndiService retrieve the Connection Pool from JNDI
    Loading Rails JNDI Service
    Using ActiveRecord datasource at JNDI url: jdbc/shared
    Using ActiveRecord JNDI Intial Context Factory: com.sun.jndi.fscontext.RefFSContextFactory
    Using ActiveRecord property JNDI Provider URL: file:tmp
Just to be sure, run the test method on RailsJndiService, via:
    include_class 'slackworks.RailsJndiService'
    RailsJndiService.testOpenConnection()      
and you will be treated to a true return, meaning a valid connection is open from the datasource. Now to be super sure, run an a simple ActiveRecord test:
    ActiveRecord::Base.connected?

and you will be treated to another true return. Hooray, both Java and Rails are both using the same Connection Pool!

Where is the Beef?

So the examples are not that exciting on their own, unless you are in the face paced business of testing database connections. What it does show is how to unifiy database access in JRuby in Rails. Using JNDI also opens up a whole spectrum of integration with JEE applications.