Thread-safe JRuby on Rails HOW-TO with the help of Warbler, Apache Tomcat and JNDI

  • Download Gist
JRuby on Rails thread safe HOW-TO.md
Markdown

Thread-safe JRuby on Rails HOW-TO

With the help of Warbler, Apache Tomcat and JNDI Connection Pool

Read those posts before anything else otherwise you'll find yourself in a very lonely place:

Prerequisites

A JVM must be installed on your system.

Install JRuby (RVM is recommended rvm install jruby).

If you have installed RVM, run this command in a terminal rvm use jruby

You need Rails 3.1 app with a database like MySQL, PostgreSQL, Oracle ... forget SQLite.

If you don't have one, run these commands in a terminal:

gem install rails
rails new your_rails_app
cd your_rails_app
bundle exec rails generate scaffold Post title:string body:text

In this HOW-TO we will use a PostgreSQL database (because I like it ;) ) and Apache Tomcat 6 or upper, because it's the most popular.

Add ActiveRecord JDBC PostgreSQL Adapter to your Gemfile, like this:

if defined?(JRUBY_VERSION)
  gem 'activerecord-jdbcpostgresql-adapter'
  gem 'jruby-openssl'
else
  gem 'pg'
end

Add Warbler to your Gemfile, like this:

if defined?(JRUBY_VERSION)
  group :development do
    gem 'warbler'
  end
end

Run this command in a terminal: bundle install

Then run this one: bundle exec warble config

Config Rails in thread safe mode

In your RAILS_ROOT, open the file config\environments\production.rb and uncomment this line: # config.threadsafe!

Then add those lines to ensure you can use rake tasks in production env:

  # Allow rake tasks to autoload models in thread safe mode, more info at http://stackoverflow.com/a/4880253
  config.dependency_loading = true if $rails_rake_task

Finally in your RAILS_ROOT open the file config\warble.rb And replace these 2 lines:

# config.webxml.jruby.min.runtimes = 2
# config.webxml.jruby.max.runtimes = 4

With these lines:

config.webxml.jruby.min.runtimes = 1
config.webxml.jruby.max.runtimes = 1

Migrate database, create the JNDI connection and configure Rails

Ensure your database production environment is configured, then run these commands in a terminal:

bundle exec rake db:create RAILS_ENV=production
bundle exec rake db:migrate RAILS_ENV=production

In your Apache Tomcat directory, open the file conf\context.xml.

Add the Resource tag inside the Context tag, like this:

<Context>
      <!-- ... -->
      <Resource name="jdbc/your_jndi_name" auth="Container" type="javax.sql.DataSource"
         maxActive="100" maxIdle="30" maxWait="10000"
         username="your_username" password="your_password" driverClassName="org.postgresql.Driver"
         url="jdbc:postgresql://your_hostname:5432/your_database_name"/>

</Context>

Copy your JDBC driver into the Apache Tomcat lib folder.

For Ubuntu: sudo cp ~/.rvm/gems/jruby-1.6.5/gems/jdbc-postgres-9.*/lib/*.jar /usr/share/tomcat6/lib

In your RAILS_ROOT open the file config\database.yml.

Rename the production entry in production_jdbc and add this one:

production:
  adapter: jdbc
  jndi: java:comp/env/jdbc/your_jndi_name
  driver: postgresql
  encoding: utf8
  wait_timeout: 5
  pool: 5

In your RAILS_ROOT open the file config\warble.rb.

And replace this line: # config.webxml.jndi = 'jdbc/rails'

With this one: config.webxml.jndi = 'jdbc/your_jndi_name'

Finally create a config/initializers/connection_pool_fix.rb file with this content:

# Monkey patch ConnectionPool#checkout to avoid database connection timeouts
# Source: https://github.com/rails/rails/issues/2547
# For Rails 3.2.0 and upper, You need to check if the pool error still occurs
if Rails.version < "3.2.0"
  class ActiveRecord::ConnectionAdapters::ConnectionPool
    def checkout
      # Checkout an available connection
      @connection_mutex.synchronize do
        loop do
          conn = if @checked_out.size < @connections.size
                   checkout_existing_connection
                 elsif @connections.size < @size
                   checkout_new_connection
                 end
          return conn if conn

          # No connections available; wait for one
          if @queue.wait(@timeout)
            next
          else
            # try looting dead threads
            clear_stale_cached_connections!
            if @size == @checked_out.size
              raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}.  The max pool size is currently #{@size}; consider increasing it."
            end
          end

        end
      end
    end
  end
end

Create the War file and restart Apache Tomcat

Run these commands in a terminal:

bundle exec rake assets:precompile
bundle exec warble

Stop the Apache Tomcat service: sudo service tomcat6 stop

In the Apache Tomcat directory, copy your your_rails_app.war file inside the webapps folder.

For Ubuntu: sudo cp your_rails_app.war /var/lib/tomcat6/webapps

Restart the Apache Tomcat service: sudo service tomcat6 start

Open this URL with your Web Browser: http://localhost:8080/your_rails_app/posts

Enjoy !

Special thanks to Ritchie Young who corrected my spelling mistakes.

It is better to use Gemfile builtin platform support like this:

gem 'pg', '~> 0.13', platforms: :mri
gem 'activerecord-jdbcpostgresql-adapter', platform: :jruby
gem 'jruby-openssl', platforms: :jruby

instead of

if defined?(JRUBY_VERSION)
  gem 'activerecord-jdbcpostgresql-adapter'
  gem 'jruby-openssl'
else
  gem 'pg'
end

Verry thanks samkiller!
I was very saved.

Is that jndi setup only for one database?
What if I run different applications under tomcat connecting with different databases?
For example:
under tomcat I'm running app1, app2 and app3.
app1 use database app1, app2 use app2 and app3 use app3.
How jndi can configured to use these three databases?

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.