Sunday, January 29, 2012

Fun with Rails, JRuby, and JEE

Background

So a little while ago we were looking at a possible complete rewrite of our customer's web site and we started evaluating what we could do. My first thought was to do a Rails app. I've done a few small Rails projects but none for a public site for an actual customer that expects results. Now our customer's data center only supports .Net and JEE applications so this posed an interesting integration issue. I had heard that you could get Rails applications to run in a JEE application server using JRuby but I had never done it. So I took some time to experiment with it and this is what I found...

First try

So first off I just installed JRuby. I'm on Ubuntu 11.10 so I just did what I do for installing anything:

apt-get install jruby
This got me a JRuby 1.5.1 installed. I then proceeded to install warbler (a Rails to JEE war file packager).
gem install warbler
Then I created an empty Rails app to experiment with:
rails new testapp --database mysql
This created a standard rails app directory. I then created a simple scaffold just to get something in there that would actually interact with the database.
rails g scaffold Person string:first_name string:last_name
I made sure that the databases existed:
mysql -u root mysql -e 'create database testapp...'
I then migrated the databases:
rake db:migrate RAILS_ENV=production
I then proceeded to package up the app in a war file by executing the warble command that the warble gem provides:
warble
And it churned out a testapp.war file. I then deployed the war file to my local Tomcat directory, started it up and hit the app with my browser. All the static content was served up just fine and all the dynamic content that actually touched ruby code did not. In fact, when trying to reach the dynamic portions, the request timed out. Not a 500 error message or anything, just nothing. And nothing showed up in the Tomcat logs either. Which made trying to guess the issue a nasty nightmare.

Resulting war file

So of course I took a look at the generated war file and looked to see what was in there. And surprisingly enough, it looked much like my Rails directory. All the public content (static html, style sheet, javascript files) was in the top level so it could be served up by regular requests to the files. In the WEB-INF folder you get the app, config, gems, and vendor directories with what you would expect in them. The web.xml is real simple: a few context parameters and a request filter pointing all traffic to a RackFilter:

<!DOCTYPE web-app PUBLIC
  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
  <context-param>
    <param-name>public.root</param-name>
    <param-value>/</param-value>
  </context-param>

  <context-param>
    <param-name>rails.env</param-name>
    <param-value>production</param-value>
  </context-param>

  <filter>
    <filter-name>RackFilter</filter-name>
    <filter-class>org.jruby.rack.RackFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>RackFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <listener>
    <listener-class>org.jruby.rack.rails.RailsServletContextListener</listener-class>
  </listener>
</web-app>
And of course in the lib directory you get the 3 jar files that do all the black magic: jruby-core-XXX.jar, jruby-rack-XXX.jar, jruby-stdlib-XXX.jar These files hand off all servlet requests to the Ruby code that's hidden in the WEB-INF folders. Quite remarkable really. Simple. Noninvasive. Just wish it worked.

Logging and debugging

In general, I like the debugging that you get with Rails. Development logs are very verbose and you get lots of good information. However, there's a disconnect between what JRuby/Rails would log (actual errors in the Ruby code) and what Tomcat would log (war deployment issues and lifecycle errors). After a little googling I found that you can set Rails' logging to go to the STDOUT. That way Tomcat would pick it up in the regular Tomcat logging. Great! So I went into my Rails application and in the config/application.rb I added a line:

config.logger = Logger.new(STDOUT)
So I re-warbled (I'm liking that as a verb) and redeployed to Tomcat. And when I went to access the dynamic portion of the site, I got a stack trace in the log. Wonderful! Now I could try to fix something. Well at the top I noticed something rather odd. It said that I was using Ruby 1.8. Well when I got all the Rails stuff working I was working with a RVM install of Ruby 1.9.2. And I thought my JRuby was new enough that it'd be using 1.9 as well.

Version confusion

So I found out that you can get JRuby via RVM as well. So I installed the latest JRuby from RVM (1.6.5). And I found out that when you use JRuby with RVM your standard Ruby becomes JRuby.

rvm use jruby-1.6.5
ruby -v
jruby 1.6.5 (ruby-1.8.7-p330) (2011-10-25 9dcd388) (Java HotSpot(TM) Server VM 1.6.0_22) [linux-i386-java]
Note that in parenthesis it shows what Ruby version JRuby will be emulating. So even the latest JRuby was still going to be running as Ruby 1.8. After more googling I found that you can specify to use 1.9 mode as an argument to JRuby or you can set an environment variable to always use the 1.9 mode.
jruby --1.9 -v
jruby 1.6.5 (ruby-1.9.2-p136) (2011-10-25 9dcd388) (Java HotSpot(TM) Server VM 1.6.0_22) [linux-i386-java]

export JRUBY_OPTS=--1.9
jruby -v
jruby 1.6.5 (ruby-1.9.2-p136) (2011-10-25 9dcd388) (Java HotSpot(TM) Server VM 1.6.0_22) [linux-i386-java]
Later I was informed by @brianthesmith via @headius that just recently, JRuby master now uses Ruby 1.9 as the default. So hopefully this little version issue will go away with the next release.

The next issue I had was kinda a stupid on my part but again I'm not too familiar with the workings of Ruby/JRuby. When I install a gem using Ruby, either by doing a gem install or a bundler install, it doesn't install as a gem system wide that JRuby would be able to use. It's only available to that Ruby install. So JRuby knows nothing of the gems installed in your standard Ruby. Seems a little obvious now, but at the time it was a bit frustrating. And, JRuby cannot use native gems. Meaning that if something uses native code (like a database driver), JRuby will not be able to use it. For example, in your Rails app you might have ActiveRecord use the mysql2 adapter gem. In JRuby you would need the activerecord-jdbcmysql-adapter gem.

Also fun, is that if you execute:

gem install rails
You will get the latest greatest Rails (3.2 as of writing this). If you want to use an earlier version, good luck! You have to change your Gemfile to have an earlier version. But there are several gems listed in the automatically generated Gemfile that are dependent on a the initial version of Rails. In fact, there are several gems specified in the Gemfile that you don't need right away. Such as sass-rails and coffee-rails which are tied to the Rails version. The standard auto-gen Gemfile looks like this:
source 'https://rubygems.org'

gem 'rails', '3.2.1'

# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'

gem 'activerecord-jdbcmysql-adapter'

gem 'jruby-openssl'

# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'sass-rails',   '~> 3.2.3'
  gem 'coffee-rails', '~> 3.2.1'

  # See https://github.com/sstephenson/execjs#readme for more supported runtimes
  gem 'therubyrhino'

  gem 'uglifier', '>= 1.0.3'
end

First off, I noticed that when you execute bundle install it took forever or timed out. But if you change the source to point to http://rubygems.org instead of https://rubygems.org, or better yet, set the source to :rubygems, you'll actually be able to do a bundle install.

Another fun thing is that if you're using JRuby whilst executing rails new appname, it will give you JDBC database adapters in the Gemfile, but not in the config/database.yml file. But if you re-warble and create a war file with the non-JDBC adapters, it won't work in your JEE deployment. Fun.

What I wish the Gemfile defaulted to was this:

source :rubygems

gem 'rails', '3.2.1'
gem 'activerecord-jdbcmysql-adapter'
gem 'jruby-openssl'
Then add other gems as you need them. And if you want to use an earlier version of Rails, you could by just specifying a different version (ie. gem 'rails', '3.0.7').

Warble configuration

Finally, there's some adjustments I needed to make to the warble packager for the specifics of my app. To configure warble, you need to execute the following at the top directory level of your application:

warble config
This will generate a config/warble.rb file where you can make changes to the warble configuration.

So one change I needed to make was to provide a list of all the gems that the webapp needed when it is deployed. This will bundle them up in the war file. It'd be really nice if warble could read through your Gemfile and update this setting itself. But for now, it's pretty simple to explicitly specify the needed gems. To do this, go in and uncomment the config.gems line in config/warble.rb:

config.gems += ["activerecord-jdbcmysql-adapter", "jruby-openssl"]

One last configuration in the warble config file that I had to do was to set the JRuby compatibility version:

config.webxml.jruby.compat.version = "1.9"
This will set a context parameter in your web.xml file that will tell JRuby to be in the 1.9 mode.

Finally it works!

At this point, I was good to go. Re-warbled my app to get a new war file then deploy to my Tomcat server. And everything seemed to function as expected.

It seems that these war files are self-contained such that you don't need JRuby or any gems in a location outside of the server installed on the deployment server. So any standard JEE server should be able to deploy the JRuby/Rails app without any knowledge of Ruby or JRuby. That's pretty cool. So if your datacenter does not support Ruby on their production servers but will support JEE servers, this may be an alternative for you and your team.

Last thoughts

Well this was my first experience with JRuby and I gotta admit that these little hiccups were a bit frustrating. I'm not quite sure how things could be changed to help out complete noobs like me to setup an initial deployment, but I hope that this will help someone who's possibly struggling with the same setup issues. And if I got something terribly wrong in the above description, please correct me and I'll edit the post. Thanks!

2 comments:

  1. Good note from JRuby: so if you install jruby-openssl before running bundle install, then the https url for rubygems.org will work.

    ReplyDelete
  2. Nice and interesting information.

    ReplyDelete