Ylan Segal

Use Multiple Ruby Engines in the Same Project

One of the biggest pains of using jruby is the slow startup time.

For a trivial rails application, the startup is really painful:

1
2
3
4
5
$ rvm current
jruby-1.7.11
$ time rails runner "puts 'Hello'"
Hello
rails runner "puts 'Hello'"  24.82s user 0.83s system 223% cpu 11.457 total

Compare to the same project running MRI:

1
2
3
4
5
$ rvm current
ruby-2.1.1
$ time rails runner "puts 'Hello'"
Hello
rails runner "puts 'Hello'"  1.14s user 0.19s system 98% cpu 1.355 total

MRI is more than 20 times faster!

If You Can’t Ditch jRuby…

Because of deployment constraints and a small portion of the application depending on java classes, ditching jRuby altogether is not an option. However, having a fast TDD loop is essential, which is hard to do with startup times of more than 20 seconds. What if we could run individual specs in MRI and our integration specs and server in jRuby?

Platform specific gems in Bundler

Bundler supports specifying a platform when defining a gem, which allows to write a Gemfile like:

Gemfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
source 'https://rubygems.org'

gem "rails", "4.0.4"
gem 'sass-rails', '~> 4.0.0'

platform :ruby do
  gem "pg", "~> 0.17.1"
end

platform :jruby do
  gem 'activerecord-jdbc-adapter'
  gem 'activerecord-jdbcpostgresql-adapter'
  gem 'trinidad'
end

This syntax allows us to run bundle install on each platform and generate a Gemfile.lock file. For simple projects, this approach is sufficient and one can switch rubies with no adverse effects.

However, for more complex projects the generated Gemfile.lock for each platform is not the same, due to the fact that there are some platform specific gems being installed. In general, if you intend to add your Gemfile.lock file (and you should in a Rails application), then this will cause your repo to be dirty constantly and endlessly having to delete the existing Gemfile.lock when changing rubies. Hardly ideal.

Multiple Gemfiles, But Not Really

Bundler also has support for using an environment variable BUNDLE_GEMFILE for determining which Gemfile to use. In combination with some hackery, it make it possible to use the same Gemfile but have platform specific lock files.

Gemfile.mri
1
eval File.read('Gemfile')

In order to start using mri for our project (jruby is the default):

1
2
$ export BUNDLE_GEMFILE=Gemfile.mri
$ rvm use ruby

Now, that shell is ready to roar and there is no interference in the Gemfile.lock.

The difference between the lock files locks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ diff -h Gemfile.lock Gemfile.mri.lock
38a39
>     atomic (1.1.16)
45c46
<     jruby-rack (1.1.13.1)
---
>     jruby-rack (1.1.14)
51a53
>     pg (0.17.1)
85a88,89
>     thread_safe (0.3.1)
>       atomic (>= 1.1.7, < 2)
92,95c96,99
<     trinidad (1.4.4)
<       jruby-rack (>= 1.1.10)
<       trinidad_jars (>= 1.1.0)
<     trinidad_jars (1.3.0)
---
>     trinidad (1.4.6)
>       jruby-rack (~> 1.1.13)
>       trinidad_jars (>= 1.3.0, < 1.5.0)
>     trinidad_jars (1.4.0)
99a104
>   ruby