Nothing beats Ruby when it comes to rapid development, quick feedback, and delightful coding. The Ruby runtime and traditional ruby frameworks favor synchronous programming, which makes them easy to use and understand. But microservices and real-time apps require asynchronous programming and non-blocking IO to enable maximum throughput. That's where JRuby comes in.
You can build reactive microservices in Ruby using JRuby and frameworks like Ratpack. JRuby interprets Ruby code into Java Virtual Machine (JVM) bytecode to gain the performance and concurrency benefits of Java without writing any Java code or XML. But the performance benefits of the JVM are just the beginning. You can also use JRuby to leverage libraries like Ratpack, a micro-framework for building modern HTTP applications with reactive principles in mind.
Together, JRuby and Ratpack make an excellent platform for reactive systems. In a moment, you’ll learn how to build such an app, but we first need to define this “reactive” buzzword.
What does it mean to be reactive?
The term “reactive” implies many different characteristics, such as applying backpressure from a client. But another important characteristic is an ability to perform non-blocking communication. When a thread of execution in a reactive application waits for an external service (like a database or other microservice) the thread will release itself to go do other work instead of blocking and waiting for the response.
The difference between blocking and non-blocking looks like this:
A reactive system increases throughput by allowing the server thread to do other work while the database is processing its response. In this way, it can more fully saturate the CPU and improve performance. To implement this kind of architecture, you need a runtime and framework that are built from the ground up to support non-blocking IO.
JRuby supports Rails, Sinatra, Puma, Eventmachine, Sidekiq, and almost all of your favorite CRuby libraries and frameworks. But JRuby also opens the door to new technologies. Among those new technologies is Ratpack, which is loosely comparable to Sinatra. Unlike Sinatra, however, it leverages the powerful concurrency libraries of the JVM to provide an interface with first-class support for asynchronous programming and non-blocking IO. Ratpack is a JVM framework, but you can use it with your favorite Ruby tools.
Creating a JRuby and Ratpack app
To create a JRuby Ratpack app, you’ll need to install JRuby. You can download the binaries for your platform from the JRuby website or run rvm install jruby
if you’re using RVM.
Now create a directory for your app, move into it, and create a Gemfile
with these contents:
source "https://rubygems.org"
ruby '2.3.0', :engine => 'jruby', :engine_version => '9.1.1.0'
gem "jbundler", "0.9.2"
This adds JBundler, which installs Java dependencies as if they were Ruby Gems. Initialize Bundler and JBundler by running these commands:
$ jgem install bundler
$ jruby -S bundle install --binstubs
JBundler uses a Jarfile
to manage dependencies. Create this file alongside your Gemfile
, and put the following code in it:
jar 'io.ratpack:ratpack-core', '1.3.3'
jar 'org.slf4j:slf4j-simple', '1.7.10'
Then run JBundler to install the JAR dependencies:
$ bin/jbundle install
Now you’re ready to create an app. In the same directory as the Gemfile
and Jarfile
, create a server.rb
file and put the following code in it:
require 'java'
require 'jruby/core_ext'
require 'bundler/setup'
Bundler.require
java_import 'ratpack.server.RatpackServer'
RatpackServer.start do |b|
b.handlers do |chain|
chain.get do |ctx|
ctx.render("Hello from Ratpack+JRuby")
end
end
end
This creates a Ratpack server, and defines a single get
handler in the handler chain, which is a Ratpack concept used to facilitate asynchronous and non-blocking IO. This handler renders the string "Hello from Ratpack+JRuby" for the default /
route.
Save the file, and start the server by running this command:
$ jruby server.rb
Open a browser and point it to http://localhost:5050
, and you’ll see the output rendered by the handler.
This is a great start, but it doesn’t demonstrate the full power of Ratpack. Let’s add a streaming service.
Non-Blocking streaming
Open the server.rb
file again, and add these statements immediately after the first java_import
:
java_import 'ratpack.stream.Streams'
java_import 'ratpack.http.ResponseChunks'
java_import 'java.time.Duration'
Then add the following code to the b.handlers
block after the first get
handler:
chain.get("stream") do |ctx|
publisher = Streams.periodically(ctx, Duration.ofMillis(1000)) do |i|
i < 10 ? i.to_s : nil
end
ctx.render(ResponseChunks.stringChunks(publisher))
end
This creates another get
handler, but on the /stream
route. It will stream a series of numbers to the client without blocking the request thread. When the stream is paused (every 1000 milliseconds) it will release the thread so it can do other work. This is particularly beneficial when the stream needs to consume data from a remote resource.
Start the app again with the jruby server.rb
command. Then browse to http://localhost:5050/stream
and you’ll see the integers rendered every 1000 milliseconds.
You’re starting to unlock the power of a reactive system. Not only does this service periodically publish new items, it will also apply back pressure based heuristics. But there is much more Ratpack can do. Let’s add a database to the application to make use of some more reactive features.
Using a database
JRuby works with many popular Ruby database clients including ActiveRecord and Sequel. In this example, we’ll use Sequel to interact with a single table mapped to a Ruby class. Database queries and updates can be expensive so we’ll wrap them with Ratpack Promises to prevent the IO operations from blocking the request thread.
To set up Sequel you’ll need a database migration, a Rake task, a model class, and of course a database. Rather than write all of this from scratch, you can deploy the example app to Heroku by clicking this button:
The example app extends the application you’ve already created, so it will look familiar. After you’ve deployed it, get a local copy of the app by running these commands (but replace
$ git clone https://github.com/jkutner/ratpack-jruby-example
$ cd ratpack-jruby-example
$ heroku git:remote <app-name>
Open the project’s server.rb
file, and you’ll see some new handlers that look like this:
c1.get do |ctx|
Blocking.get do
DB[:widgets].all
end.then do |widgets|
ctx.render(JSON.dump(widgets))
end
end
The Blocking.get
method is part of the Ratpack framework. It returns a Promise, which executes a Ruby block asynchronously. A promise is a unit of work queued to be executed, The work is executed when some other code subscribes to the promise by calling the then
method. After the Promise is executed, the block passed to the then
method is executed.
This API is inspired by Node.js and it’s popular event-driven architecture. But unlike Node.js, Ratpack's execution model guarantees execution order. It can also detect when there is no more user code to execute, which means it can notify the user of some issue on the server side. Ratpack and JRuby also have the benefit of a multithreaded platform in the JVM. This characteristic makes JRuby and Ratpack a great platform for real-time apps. Building real-time apps Real-time web apps update users with new data as soon as it’s available, rather than requiring them to ask for it or refresh a page. The classic example is a chat application.
In many cases, real-time apps are implemented using Websockets. To demonstrate this with Ratpack, you could modify the steaming example you created earlier to use the WebSockets.websocketBroadcast()
method to broadcast the output. For example:
WebSockets.websocketBroadcast(ctx, publisher)
Then with the appropriate client-side code, you could consume this streaming service.
But Ratpack also includes a built-in module that uses Websockets to stream metrics data for your application's runtime. It uses Dropwizard Metrics, another JVM library to capture memory usage, response time, and other data. To use the module, switch your Git repo to the “metrics” branch by running this command:
$ git checkout -t origin/metrics
In the server.rb
file, you’ll see this code, which sets up the Dropwizard module:
b.server_config do |config|
config.base_dir(BaseDir.find)
config.props("application.properties")
config.require("/metrics", DropwizardMetricsConfig.java_class)
end
b.registry(Guice.registry { |s|
s.module(DropwizardMetricsModule.new)
})
In the handlers
block, you’ll see this code, which sets up the metrics view and the websocket handler:
chain.files do |f|
f.dir("public").indexFiles("metrics.html")
end
chain.get("metrics-report", MetricsWebsocketBroadcastHandler.new)
The client-side, in metrics.html
and metrics.js
, is fairly standard Websocket code.
Deploy the metrics
branch to Heroku by running this command:
$ git push -f heroku metrics:master
Then open the metrics view by running this command:
$ heroku open metrics.html
You’ll see the dynamically generated gauges and graphs that represent the real runtime data for your application. Make a few requests to the app, and you’ll notice that they change on their own.
Frank, Dean, Sammy and you
Ratpack opens the door for powerful and modern Ruby applications that are both pleasant to develop and reactive. Under the hood, Ratpack uses Netty, an asynchronous event-driven networking framework, which is the cutting edge of server technology.
Ratpack is a great way to take advantage of many modern architectural patterns without giving up the Ruby language you love.
Joe Kutner is the author of Deploying with JRuby 9k and the JVM Platform Owner at Heroku.