Summary
Yesterday's post was about "why". Today's is about "how".
Advertisement
This post outlines the development and deployment process for JRuby on Rails
based on notes from talks by Ola Bini of Thoughtworks, Nick Sieger
of Sun, and Anil Hemrajani of BigUniverse.com, as well as the
Jruby on Rails lab. (The steps can all be done in NetBeans, but it's
easiest to show them in print using the command line.)
Note:
My congratulations to the event organizers. Many of the
slides are already available online! That made it a world
easier to fill in the blanks and check my understanding.
You will of course have installed JRuby and the Rails gem:
jruby -S gem install rails
The "jruby -S" ensures that the JRuby script is excecuted, rather
than the equivalent Ruby script, in case you have both. (Having
both is a good idea. If you run into a problem, running the
same code in Ruby can identify a bug in JRuby--and Ruby's error
messages are more informative, at times.)
The next step is to configure Rails to use Java Database Connnectivity
(JDBC). You do that by installing the ActiveRecord adapter gem for
your database. For example, for MySQL:
You also need to ensure that your Rails app uses only pure-Ruby gems
and libraries, because those that have native code won't run. (You
get the Java class libraries when you run JRuby, but you lose Ruby
libraries that have code written in C.)
Nick Sieger provided this list of common replacements:
For RMagick or ImageScience, use ImageVoodoo
For OpenSSL, use the JRuby-OpenSSL gem
For Ruby/LDAP, use JRuby/LDAP
For json, use json_pure
Anil Hemrajani listed some other gems you'll probably want to use:
ferret -- for searching
crypt -- to store passwords
redcloth -- convert text to html
glassfish_rails -- to fire up glassfish and run the web app
capistrano -- command line admin commands for Mongrel, Apache, and others
(doesn't run on Windows)
To install the glassfish gem, for example:
jruby -S gem install glassfish
Ola Bini also makes use of a couple of Java libraries:
Hibernate package for extra database functionality
You'll fill in those values three times in the YAML file, once
for the development environment, once for the testing environment,
and once for the production environment. (Rails is pre-configured
to have three separate environments for those activities. The
designers wisely deciced that it wasn't a good idea to run tests
against the production database!)
Other configuration values:
Don't cache classes in the development environment. Thay way,
they always reload, so they reflect your latest changes.
(Important, because you generally don't need to restart the
server to test new code.)
Do cache them in the production environment, for speed.
(It makes sense to cache them in the testing environment, for
the same reason.)
For production, turn on "whiny-nils", so you get more
information when a null ptr error occurs. (In development,
you'll have a good idea where to look for the problem. But
in a production setting, it can be difficult to determine the
sequence of events that led to it.)
The development environment typically uses the built-in webrick
server, because it's small and starts fast. Production deployments
are starting to use glassfish as a regular thing.
The Rails script/generate utility can create all kinds of stuff,
including:
Models, views, controllers
Scaffolding
Tests
Things created by plugins
Start by using script/generate to create new application components. When
starting out, make use of "scaffold" construction, which creates
models, views, and the glue code to tie them together:
jruby script/generate scaffold Blog name:string
That command creates a Blog class which will be tied to a "blogs"
table in the database. The table will have two columns. The one
you've specified (name) will be of type string. The other will
be the row ID column, which Rails creates automatically for each
table.
A blog will have posts, so define those, as well:
jruby script/generate scaffold Post subject:string content:string ...
As part of the scaffolding, Rails creates classes like CreateBlog
and CreateComment. Those classes have setup and teardown methods
that are used when constructing and migrating the database.
Note:
When you add columns to a table, you'll create classes like
Add<someColumn>To<someTable>. The setup and teardown methods
in those classes will then add or remove the columns to uplevel
or downlevel the database.
If the database doesn't already exist, and the database server is
running, you can have Rails create it for you:
jruby -S rake db:create
Whenever you've made a change to the database (and immediately after
constructing it), you'll use the "migrate" command to uplevel or
downlevel it to a specified version. Usually, you'll be upleveling
to the latest version:
jruby -S rake db:migrate
Or you can downlevel to a previous version:
jruby -S rake db:migrate VERSION=0
You can also specify which environment to migrate:
This is where you write a few lines of code, so you get to feel
like you're actually doing something. (So far, Rails has been
doing pretty much everything!)
There's still not a whole lot to do, however. In the model
classes, you'll want to specify that a record isn't legal
unless there is value for the "name" column, for example:
class Post < ActiveRecord::Base
validates_presence_of :name
You'll also want to specify has_many (or has_one) and belongs_to
connections, to tie database tables together using their primary
keys:
class Post < ActiveRecord::Base
has_many :comments
classs Comment < ActiveRecord::Base
belongs_to :post
To create a form to add a comment to a post, you'll edit the
dynamic html+ruby (.erb) file views/posts/show.html.erb:
<% form :action => 'post_comment' do %>
<p><label for="comment_comment">Comment</label><br/>
<%= text_area 'comment', 'comment' %></p>
<%= submit_tag "Post" %>
<% end %>
To display the comments:
<ul>
<% @post_comments.each do |comment| %>
<li>
<%= h comment.comment %>
</li>
<% end %>
</ul>
Pretty simple, huh?
Note:
Your Action controllers can also use responds_to to implement
RESTful services. (For example, the app can provide XML in
response to a Javascript request.) For a good explanation of
that capability, see Jamis Buck's writeup in the Resources.
For production deployment, you can package up the web app in a WAR
and deploy it on any compliant app server. (For glassfish, you drop
the WAR in the autodeploy/ directory.)
Of course, the directory layout for a Rails app is very different
from the one required a WAR file, so you use a tool that move
the components around as it creates the archive. There are several
tools you can use to do the job:
GoldSpike -- older
Warbler -- newer
JRubyWorks -- mentioned in one talk, but not discusssed
The new and improved way of doing things is to use Warbler. It was
designed to improve on Goldspike's connection pooling, and to make
configuration more intuitive. (It is also reported to do a more
efficient job of packing.)
Note:
For now, Warbler requires a number of additional tuning steps,
described in the next section. But that requirement is in the
process of changing, so if you start developing your Rails app
today, the process should be simpler by the time you're ready
for a production deployment.
Nick Sieger hopes to eliminate most of these tuning steps in the
near future. But for the moment, there are a few things you'll need
to do to make sure everything runs optimally.
First, you'll want to set maximum and minimum values for size of
the runtimes pool in config/warble.rb:
Warbler::Config.new do |config]
config.webxml.jruby.min.runtimes = 2
config.webxml.jruby.max.runtimes = 4
end
Then, you'll want to set up logging so it works in a Java web
app container, as well as standard Rails containers:
if defined?(JRUBY_VERSION) && defined?($servlet_context)
# Logger expects an object that responds to #write and #close
device = Object.new
def device.write(message)
$servlet_context.log(message)
end
def device.close; end
# Make these accessible to wire in the log device
class << RAILS_DEFAULT_LOGGER
public :instance_variable_get, :instance_variable_set
end
old_device = RAILS_DEFAULT_LOGGER.instance_variable_get "@log"
old_device.close rescue nil
RAILS_DEFAULT_LOGGER.instance_variable_set "@log", device
end
Next, you need to turn off the Rails session handler, because the
the servlet/container will be managing them:
config.action_controller.session_store = :java_servlet_store
# ...
class CGI::Session::JavaServletStore
def initialize(session, options) end
def restore; end
def update; end
def close; end
end
The last major step is to use JNDI for connection pooling. A
small-scale app won't notice the difference, but a large-scale
app will.
To do that, configure the production database for JNDI in
database.yml:
But Rails doesn't close connections by default, so you need to
add a tiny bit of code to close them in
config/initializers/close_connections.rb:
if defined?($servlet_context)
require 'action_controller/dispatcher'
ActionController::Dispatcher.after_dispatch do
ActiveRecord::Base.clear_active_connections!
end
end
Finally, there are a few remaining things to do:
Ensure view caching is enabled (that's the default in Rails 2.0.2):
config.action_view.cache_template_loading = true
Avoid asset ID timestamp checks with RAILS_ASSET_ID:
ENV['RAILS_ASSET_ID'] = “r#{source_revision}”
Ensure full-page cache directory points to root of WAR:
config.action_controller.page_cache_directory
Note:
In Rails 2.1, use Rails.public_path for that last step.
That's a fair amount of work, of course, but in return you get
a highly scalable app running in a Java Web App server. And
Sieger's intention is to continue refining things so that there
will less and less to do over time.