@ryudoawaru
at Kaigi on Rails 2024, 2024/10/25
Hello everyone, today I want to talk about running JRuby on Rails and what does it mean for us Rails developers.
I'm Mu-Fan Teng, but you can also call me Ryudo Owaru - my Japanese name.
I've been working with Ruby for over 17 years.
I'm a Ruby Evangelist in Taiwan and work in 5xRuby as CEO.
Let me share some Ruby events in Taiwan.
Last December, we had the first RubyConf Taiwan after the pandemic.
This August, we had a Ruby Community Room at COSCUP 2024 which is a general OSS conference.
We also have monthly meetup called Ruby Jam.
These show how active our Ruby community is.
You're all welcome to join us when you visiting Taiwan.
Let me share our growing partnership with Ruby City Matsue. We've had three significant events over the past year.
In November 2023, during Ruby World Conference, I visited Matsue City Hall to discuss Ruby community cooperation with Mayor うえさだ あきひと. The following month, we were honored to have Deputy Mayor やまね こうじ give a speech at RubyConf Taiwan 2023.
On this March, Mayor うえさだ and his team visited 5xRuby. During this visit, we had productive discussions about future collaboration in three key areas: community exchange, business cooperation, and talent development.
We're excited about step up the connection between our Ruby communities and look forward to more collaboration.
Today's agenda will start with a brief introduction to JRuby, followed by sharing my experience with JRuby and explaining the differences between JRuby and CRuby.
Next, I'll discuss my experience on how to migrate CRuby on Rails projects to JRuby, including potential challenges and solutions you might encounter during the process.
Then, I'll share benchmark results from converting some of my projects to JRuby. These results will help us Rails engineers better understand the benefits of using JRuby in Rails projects and when it's appropriate to use.
Finally, I'll briefly discuss the current state of the JRuby ecosystem based on my understanding over the past few months, followed by concluding remarks.
JRuby is one of many Ruby language implementations.
It runs on the Java Virtual Machine
This picture show the compiler pipeline of JRuby, first parsing Ruby scripts into Ruby IR, then compiling them into Java Byte Code for JVM execution.
I actually have no Java experience at all.
My first real encounter with JRuby was 11 years ago, when my manager assigned me to use a paid Java Library called Aspose in a Rails project to manipulate Word files.
def save_document(xml_data, xslt_data, output_format)
temp_xml_fn = "#{Time.current.to_i}-#{rand(1000)}.xml"
File.write(temp_xml_fn, compose_xslt(xslt_data, xml_data) )
aspose_doc = Rjb::import('com.aspose.words.Document').new(temp_xml_fn)
aspose_doc.save("output.#{output_format}")
end
Initially, I used the RJB RubyGem to access Java Libraries in Ruby.
This snippet of code is for rendering Word or PDF document by the Aspose Java library with XML data.
# config/initializers/aspose.rb
::Aspose = Java::ComAsposeWords
require File.join(Rails.root, '/vendor/lib/Aspose.Words.jdk16.jar')
java_import('com.aspose.words.License')
In subsequent projects, in order to use more complete features of Aspose, we decided to start new projects directly using JRuby.
This code is for requiring jar file into Ruby when initialize Rails.
From that period, I had no chance to use JRuby in work anymore.
For Rails developers like us, what are the practical differences between JRuby and CRuby in actual usage?
In terms of installation, unlike CRuby which needs to be compiled beforehand, we only need to install JDK first, then download the packaged files from the JRuby official website, extract it, and it can be executed directly.
There are also no annoying OpenSSL version issues.
The most significant advantage of using JRuby is being able to benefit from both the Java and Ruby language ecosystems simultaneously.
For example, if your CRuby project need to access Oracle database, you need to download the Oracle Instant Client corresponding to the operating system & hardware.
Then download and install it, which can be troublesome for platforms w/o installation packages, especially a huge pain for running in container or CI usage.
Then, when installing the ruby-oci8 gem, we need to point the location of Oracle Instant Client's library and header files.
Instead of the disturbing process of CRuby, in JRuby, all you need is just download and put the Oracle JDBC driver jar file into your lib directory, no need for both Instance Client and ruby-oci8 gem.
Aspose::DocumentBuilder.class_eval do
(1..9).each do |hx|
define_method "write_h#{hx}".to_sym do |*args|
style = eval("Aspose::StyleIdentifier::HEADING_#{hx}")
get_paragraph_format.set_style_identifier(style)
if block_given?
yield
else
writeln args.first
end
get_paragraph_format.set_style_identifier Aspose::StyleIdentifier::NORMAL
end
end
end
The most highlight of JRuby is that we can treat Java class like Ruby class.
For example, this code snippet injects original Java class Aspose::DocumentBuilder
by adding methods to insert outline level text into Word document, just like a helper method in Rails.
Almost Java methods in camelcase name can be accessible through underscore name.
time = Benchmark.realtime do
threads = 4.times.map do
Thread.new do
100.times { (1..10000).reduce(:*) }
end
end
threads.each(&:join)
end
puts "Total time: #{time.round(2)} seconds"
jruby p.rb # Total time: 3.02 seconds
ruby p.rb # Total time: 14.26 seconds
Since JRuby runs on top of the JVM, it can get true parallelism from native Java thread.
For example, in my MacBook Air, this simple program can run 4 times faster within JRuby compare to CRuby.
Next, I'll share my experience on how to migrate existing CRuby projects to JRuby.
It usually doesn't take much time, though you might need to modify some code, and there will be cases where migration isn't possible.
Here are the steps for migrating your project.
First, find the JRuby version that matches your CRuby project.
Second, adjust the Gemfile to specify which RubyGems to use for specific platform.
Then, if any RubyGems show incompatibility with JRuby, you'll need to identify and resolve these issues or find alternatives.
Let's talk about choosing the right JRuby version. It's quite simple - Ruby 3.0 is our reference point.
If your project uses CRuby 3.0 or newer, go with JRuby 9.4.
For older CRuby versions, use JRuby 9.3. Thanks to Ruby's backward compatibility, this matching rule works well.
# Gemfile
platform :ruby do
gem 'pg', '~> 1.1'
end
platform :jruby do
gem 'activerecord-jdbc-adapter', '~> 71.0'
gem 'activerecord-jdbcpostgresql-adapter', '~> 71.0'
end
Based on the platform option provided by Bundler and RubyGems, we can easily provide different Rubygems combinations for CRuby and JRuby in the Gemfile.
This could makes the project runnable under both CRuby and JRuby.
Handling RubyGem compatibility usually takes the most time during the migration process.
Case | Solution | Example |
The RubyGem has API level equivalent replacement / API互換の代替品がある | Use the API equivalent gem / API互換のgemを使用 | |
The RubyGem has functionally satiable replacement / 機能的に満足できる代替品がある | Use the replacement and change relative code / |
Since JRuby is based on Java, it basically cannot use any C-Binding Rubygems unless the Gem is built with FFI.
So for those Gems, we need to find API-level or functional-level alternatives, which may involve code modifications.
Beyond C-Binding RubyGems, there are many wrap-up RubyGems that aren't compatible with JRuby.
A common example is TailwindCSS-Rails, which packages platform-specific TailwindCSS standalone CLI individually.
For example, when execute the Ruby script tailwindcss
, the script does ask Gem::Platform
class to provide OS and CPU information then return the correct path of the CLI standalone within the Gem folder.
bundle exec bin/rails tailwindcss:watch
rails aborted!
Tailwindcss::Commands::UnsupportedPlatformException: tailwindcss-rails does not support the universal-java platform
Please install tailwindcss following instructions at https://tailwindcss.com/docs/installation
When running the same code in JRuby, it will only return "universal-java" regardless of your hardware.
As a result, it cannot find the platform-specific executable and raises an exception.
TAILWINDCSS_INSTALL_DIR
env variable / TAILWINDCSS_INSTALL_DIR環境変数でCLIを指定可能As far as the wrap-up script is Ruby code, you can prepare the binary executable and modify the script to make it work.
Some Gem like TailwindCSS-Rails has specific environment variable to point this out.
Another common issue with RubyGem compatibility stems from the GemSpec configuration.
Some RubyGems require different setups for platforms which describe in their GemSpec, and these versions should be packaged and uploaded individually.
However, Gem owners might not always be aware of this when publishing. In most cases, this can be resolved by pointing to the GitHub version of the Gem.
If that's not possible, you might need to fork it to your own repository and make the necessary modifications.
Next, let's look at benchmarks from a Rails project perspective, testing data that closely reflects real-world scenarios.
Project | Ruby / Rails / Database | Other Stack |
Taiwanese Dictionary Site / 台湾語辞書サイト | Ruby 3.2+Rails 7.1 + PostgreSQL | good_job |
Clinic reservation SAAS service / 診療予約SaaSサービス | Ruby 3.1+Rails 7.0 + PostgreSQL | sidekiq + Redis |
From several Rails projects I've been involved in developing and maintaining,
I selected these 2 modern projects as test targets. Let me introduce these test projects in detail.
The first project is a Taiwanese Dictionary Site. Taiwanese here refers to Hokkien, a dialect developed from Minnan language.
While not an official language, it's widely used in Taiwan.
This project is hosted by a Taiwanese government agency - the National Academy for Educational Research - and was developed by us.
It's a website where users can search for Taiwanese Hokkien language dictionaries or textbook examples using kanji characters or romanized spelling.
The relationship between Taiwanese romanization and Kanji characters is similar to the relationship between Japanese hiragana and kanji, though Taiwanese romanization combinations are much more complex than Japanese hiragana.
Therefore, this website's main functionality focuses on various search capabilities, without features like user uploads, and all functions are accessible without login requirements.
The next project is a clinic appointment booking service called Clinking.
It's a software as a service platform, provided in a multi-tenant format for different clinics to use.
For establish the stack for behchmark.
I set up separate Application Server, Database, and stress testing instances in the cloud, conducting tests within the internal network to eliminate network latency factors. During the testing process, I ensured that the database host's configuration and specifications would not become a bottleneck for application performance.
The default setup uses 4 cores and 8GB RAM for the both Application and DB instance , and the Tester Instance has only 2GB RAM with 2 cores.
To simulate real scenarios for testing, we specifically wrote custom K6 scripts for different projects.
Additionally, we adjust Virtual Users and Execution Duration and set a maximum request threshold; requests exceeding this threshold are considered failed.
Whether it's JRuby or CRuby, we use Puma to run the service and Nginx as a Reverse Proxy.
By default, for the CRuby group, we set to use Puma's Worker Mode, expecting the number of Workers to match the number of CPU cores, with each Worker opening 4 Threads, and enabling YJIT.
For the JRuby group, we directly set the THREAD Size equal to sum of CRuby group and enable the JVM's invokedynamic
option.
However, due to the nature of JVM, we have to initiate a 4-minute warm-up run to ensure the JVM JIT compiler can run and optimized.
For the dictionary project testing, we prepared five different Taiwanese romanization terms beforehand.
In each test iteration, the script randomly selects one of these terms and queries the corpus database.
The system then retrieves and displays the results, showing 100 items per page.
Item | VUs | CRuby | JRuby |
Total Request | 16 | 4806 | 4928 |
P95 Response Time | 16 | 455 | 417 |
Total Request | 24 | 6103 | 6390 |
P95 Response Time | 24 | 709 | 664 |
Ram Usage | 1.6GB | 3GB |
From the test results, we can see that while JRuby currently achieves nearly 5% advantage in throughput, it also consumes almost twice the RAM.
For the appointment service project, we simulate a typical patient entering the homepage, browsing different departments and dates, resting for 0.5 seconds, then repeating.
This is a backend heavy operation as it iterates through all time period and reservation slots for that department of the clinic for the week to check current reservation status.
Additionally, for departments currently seeing patients, it displays the current patient number.
Item | VUs | CRuby | JRuby |
Total Request | 8 | 2730 | 3090 |
P95 Response Time | 8 | 918 | 729 |
Total Request | 12 | 3072 | 3546 |
P95 Response Time | 12 | 1204 | 1027 |
Ram Usage | 0.8GB | 2.6GB |
Despite consuming more memory, JRuby achieves significantly meaningful performance improvements in this case.
When the number of users increases, JRuby can outperform CRuby by 15 percent or more.
On the other hand it also takes 3 times more ram then CRuby.
Summarizing the above tests, we can understand that:
For Rails Development
Next I'll talk about the state of JRuby's ecosystem for Rails development.
Currently, JRuby's core development is mainly carried out by Charles and Thomas and the JRuby 10 will come by end of the year.
Even after RedHat stopped their support, development hasn't been interrupted, so it's safe to use at present.
However, there are still some issues with key gems in the Rails ecosystem.
For us Rails developers, the most important active-record JDBC adapter doesn't yet officially support Rails 7.1 and 7.2, which is the most crucial issue to watch.
The main branch in GitHub can be built and run with Rails 7.1 but still not officially released yet.
While the Oracle Enhanced Adapter works with JRuby, you cannot install it directly from RubyGems.org due to GemSpec dependency constraints.
You'll need to install from its GitHub repository source code.
However, when using recent JRuby versions, some legacy compatibility patches in the adapter's source code need to be removed.
A pull request addressing this issue has been submitted, though the repository's GitHub Actions CI is currently experiencing issues.
Many RubyGems are lack of proper Gemspec updated for JRuby, also there are some fundamental Gems like rbtree or debug need to have Java version implemented.
About my motivation for this talk:
For us Rails developers, while JRuby seems to have many benefits, the most important question is probably "Is it really as fast as it appears?"
Having seen Charles discussing JRuby development and showing benchmarks in his presentations every year, as a Rails developer, I couldn't help but be very interested in exploring "What advantages can we take by JRuby on Rails?"
I also want to do something not directly related to my work, and this could be a starting point for my OSS contributions beyond organizing community events.
For Rails Projects, here are several scenarios where you might consider migrating your CRuby project to JRuby:
As for JRuby's future, as the most long-lived Ruby Implementation, it still depends on community member's effort to maintain the ecosystem.
Besides the core and documentation, I think the most important effort is to maintain RubyGems for JRuby compatibility.
Donation @headius
JRuby Consultantation
JRuby Matrix
If you want to support JRuby, there are following QR code links.
@headius
Thank you all for listening.
Also I want to thank to Charles advised me a lot for this slide.