nosewheelie

Technology, mountain biking, politics & music.

Archive for May, 2008

Running a Rails app under Tomcat using JRuby

with one comment

A new project has me looking at running a Rails app under Tomcat using JRuby [1]. A quick google revels many conflicting guides, which seem to stem from different versions of JRuby and tools (e.g. GoldSpike vs. Warbler). I ended up using this guide for getting JRuby installed, Warbler for packaging of the WAR file and this guide as a basis for setting up a project to run under Tomcat.

The issues I encountered were mainly around these conflicting guides (I recommend you read the JRuby wiki), figuring out which gems to install, which gems need to be in the WAR file and how to configure the database. Don’t take this as the canonical way to do things, it worked for a simple MySQL-backed application based on our requirements below and is accurate as of 20/05/2008 (i.e. things will probably have moved on when you read this).

I built a simple single-table (MySQL) database-backed application and managed to successfully deploy it under Tomcat as a standard WAR file. This work was intended as a spike to determine whether this was possible using the current state of JRuby and associated tools. Our main focus was to determine if the application could support CSS bundling, action/page/fragment caching, correct cache HTTP headers (ETag, expires, cache-control, etc.), compression and a high YSlow score. We’ve done this work recently for Scoodi and know Rails is capable of supporting these under Mongrel.

The good news is it seems to work as advertised.

The process

  1. Install JRuby (download the zip & extract into /opt), be sure to add it to your PATH.

    Install the following gems (the DB ones are MySQL specific):

    $ jruby -S gem install -r jruby-openssl   # required for correct OpenSSL support, removes annoying nag
    $ jruby -S gem install -r warbler         # creates WAR files
    $ jruby -S gem install -r rails jdbc-mysql activerecord-jdbcmysql-adapter
    

    Your installed Gems should look like:

    $ jruby -S gem list
    
    *** LOCAL GEMS ***
    
    actionmailer (2.0.2)
    actionpack (2.0.2)
    activerecord (2.0.2)
    activerecord-jdbc-adapter (0.8)
    activerecord-jdbcmysql-adapter (0.8)
    activeresource (2.0.2)
    activesupport (2.0.2)
    jdbc-mysql (5.0.4)
    jruby-openssl (0.2.3)
    rails (2.0.2)
    rake (0.8.1)
    rspec (1.1.3)
    sources (0.0.1)
    warbler (0.9.9)
    

    This step actually took quite some time, as there’s many different gems available that look similar, for example if you look at the list on RubyForge with an eye towards MySQL, there’s ActiveRecord-JDBC, activerecord-jdbc-adapter, activerecord-jdbcmysql-adapter and jdbc-mysql. After you install them (or ask via gem) it’s pretty clear that some are dependencies of others, but this isn’t helpful at first.

  2. Create a Rails app. Based on the naming of Warbler, I chose to call it whipbird.
    $ jruby -S rails whipbird
    $ cd whipbird
    
  3. Warble-ise the app (this doesn’t seem to do anything to the application itself, it just creates the WAR contents in tmp, other warbler commands – see below – do allow you to make configuration changes):

    $ jruby -S warble
    
  4. Install Tomcat 6.

    Copy the generated WAR file to <TOMCAT_HOME>/webapps. You should get the following in your /opt/tomcat/logs/catalina.out.

    May 19, 2008 2:56:30 PM org.apache.catalina.startup.HostConfig deployWAR
    INFO: Deploying web application archive whipbird.war
    May 19, 2008 2:56:32 PM org.apache.catalina.core.StandardContext addApplicationListener
    INFO: The listener "org.jruby.rack.rails.RailsServletContextListener" is already configured for this context. The duplicate definition has been ignored.
    

    I haven’t looked into what the last warning means.

  5. Hit http://localhost:8080/whipbird/. You will see the default Rails page.

  6. Open up the project in IntelliJ (create the project, select JRuby as the SDK). If you ask it to, IntelliJ will install and configure RSpec and RSpec on Rails as plugins in the project.

  7. Configure Warbler to pull in the required gems:

    $ jruby -S warble config
    

    Add the following to config/warbler.rb:

    # Gems to be packaged in the webapp.  Note that Rails gems are added to this
    # list if vendor/rails is not present, so be sure to include rails if you
    # overwrite the value
    config.gems = ["actionmailer", "actionpack", "activerecord", "activerecord-jdbc-adapter", "activerecord-jdbcmysql-adapter", "activeresource", "activesupport", "jdbc-mysql", "jruby-openssl", "rails"]
    config.gems["rails"] = "2.0.2"
    
  8. Configure the databases in config/database.yml, the ActiveRecord-JDBC page has more details (there are other ways to do this using a standard JDBC URL, but this is the easiest):

    development:
      adapter: jdbcmysql
      username: root
      password:
      hostname: localhost
      database: whipbird_development
    
    test:
      adapter: jdbcmysql
      username: root
      password:
      hostname: localhost
      database: whipbird_test
    
    production:
      adapter: jdbcmysql
      username: root
      password:
      hostname: localhost
      database: whipbird_production
    
  9. Create the databases:

    $ mysql -u root
    mysql> create database whipbird_development;
    mysql> create database whipbird_test;
    mysql> create database whipbird_production;
    
  10. Point logging at the Tomcat logs, add the following to config/environment.rb:

    # Make Rails logging appear in Tomcat logs.
    config.logger = Logger.new(STDOUT)
    
  11. Go ahead and create some code. I used the scaffold generator, then added migrations, modified the views, etc.:

    $ jruby ./script/generate scaffold Bird
    
  12. I also hacked together theses Rake tasks to simplify deployment of the application into Tomcat, they should really be using Capistrano.

    TOMCAT_HOME="/opt/tomcat"
    WEBAPPS_HOME="#{TOMCAT_HOME}/webapps"
    APPLICATION_NAME = RAILS_ROOT.split(File::SEPARATOR).last unless defined?(APPLICATION_NAME)
    
    desc "Deploys the application, stopping and starting Tomcat"
    task "deploy" => ["deploy:stop", "deploy:app", "deploy:start"]
    
    namespace "deploy" do
      desc "Packages and deploys a WAR file to Tomcat"
      task "app" do
        puts "Creating WAR file"
        puts `jruby -S warble war:clean war`
        puts "Deploying #{APPLICATION_NAME} to #{WEBAPPS_HOME}"
        puts `rm -rf #{WEBAPPS_HOME}/ROOT*`
        puts `mv #{APPLICATION_NAME}.war #{WEBAPPS_HOME}/ROOT.war`
      end
    
      desc "Stop Tomcat"
      task "stop" do
        puts "Stopping Tomcat"
        puts `#{TOMCAT_HOME}/bin/shutdown.sh`
      end
    
      desc "Start Tomcat"
      task "start" do
        puts "Starting Tomcat"
        puts `#{TOMCAT_HOME}/bin/startup.sh`
      end
    end
    

    Note that we’re deploying the application as the Root webapp, see below for details.

  13. Deploy the application:

    $ jruby -S rake deploy
    
  14. Hit http://localhost:8080/. Create some data.

  15. Some other things I did to get prototype working:

    • Made whipbird the default webapp by calling it ROOT.war (there are better ways to do this) and removing the old root webapp.
    • Turned on caching of CSS & JS, in application.erb.html, in the standard Rails way.
    • Turned on GZIP compression in Tomcat.
    • In config/environments/production.rb set
      config.action_controller.page_cache_directory = "#{RAILS_ROOT}/cache/"
      config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache/"
      

      Added caches_page :index to the top of app/controllers/birds_controller.rb.

  16. Findings

    My findings (based on our initial requirements) are as follows:

    • CSS Bundling – This works, but due to rules inside asset_tag_helper.rb, the rails app needs to run as the root webapp in Tomcat. There may be a way to fix this via config, or, you can always patch the rails source. This is not a Tomcat/JRuby problem, the same thing crops up when running mongrel with a prefix.
    • JavaScript bundling – Works, with the same caveats as CSS bundling (needs to be the root webapp).
    • Page Caching – Works fine. RAILS_ROOT is set to /opt/tomcat/webapps/ROOT/WEB-INF/ by default, so the default cache lives in /opt/tomcat/webapps/ROOT/WEB-INF/public/.
    • Cache headers – All pages/assets contain ETags and can be compressed. They do not have expiry headers, this is usually set up in the front-end web server (e.g. Apache).
    • Cache IDs – As with Mongrel-Rails, cache IDs are based on modification times of files so change when the file changes (usually on re-deployment).
    • YSlow score – A (94)

    Stay tuned for performance stats…

    [1] Rails for (initial) speed of development [2] and feature set. Tomcat as it’s our client’s current deployment container.

    [2] Based on my experience with Rails to date, I don’t believe the Rails story is really as rosy as usually claimed, even for a moderately complex application, i.e. the sweet spot for a Rails app is limited.

Written by Tom Adams

May 21st, 2008 at 9:17 am

Posted in Java,Ruby

Tagged with ,

Serving Javascript using Rails’ asset hosts

without comments

I’ve not seen this anywhere in my various googling, so thought I’d post it in case it’s useful to anyone.

As a graphically rich site, Scoodi serves a lot of static content to a client browser; images, CSS and Javascript. We recently moved to using asset servers in order to speed up the total download time, however this had an unexpected side-effect in some versions of Internet Explorer, we got security warnings from our rich-text widget (TinyMCE), which meant members could not create new items or edit existing ones in IE.

We managed to resolve this by serving Javascript files without a host, meaning that they’d be resolved relative to the page (so inherit its scheme, host, port, etc.), however this involved a change to some internal Rails code, not something we’d like to maintain.

However fixing it in a generic way proved to be quite easy. Rails 2 lets you pass a function as an asset host, which gets invoked when an asset host is needed. This function gets passed the source (the images, etc. requested) and in the HEAD of the trunk of asset_tag_helper.rb it also (optionally) gets passed the request.

So, here’s the quick solution, added to the appropriate environment file. We only serve non-Javascript files from asset servers, and we also don’t use an asset server for SSL requests (avoiding mixed content warnings in some browsers).

config.action_controller.asset_host = Proc.new do |source, request|
  request.ssl? || source =~ /javascripts/ ? "#{request.protocol}#{request.host_with_port}" : "http://asset#{source.hash % 4}.scoodi.com"
end

There is another to give Rails the no-asset server host, by replacing "#{request.protocol}#{request.host_with_port}" with "" (the empty string). This however has a number of problems, 1) you don’t explicitly know which host is being used, which may cause problems over SSL (we haven’t reached an SSL-aware environment at the time of posting, so I can’t confirm this) and 2) it relies on the internals of asset_tag_helper.rb, where "" is treated as “don’t use the host provided”.

config.action_controller.asset_host = Proc.new do |source, request|
  request.ssl? || source =~ /javascripts/ ? "" : "http://asset#{source.hash % 4}.scoodi.com"
end

For both these reasons we originally decided to go with explicitly listing the scheme, host & port to use. However, we’ve had some issues when connecting to the site using an alternate address (say the machine’s IP address), the address that gets returned (request.host_with_port) returns a hostname that makes sense on the machine (running Rails) but may not be resolvable by the client (browser). Not using the hostname (returning the empty string) will cause Rails to not append a host (e.g. src="/javascripts/foo.js"), causing the browser to resolve the assets relative to the page, which will take into account the (correct) address of the server as requested by the browser. Good stuff.

Written by Tom Adams

May 7th, 2008 at 8:32 am

Posted in Ruby

Tagged with , ,