Using JRuby with akka-http
Introduction & Why
At work we're going to start using Akka-http to write some new services. The language of choice was Scala, which is fine, but given than Scala runs on the JVM, and I've tried JRuby in the past with great success, I decided to try to convert the Akka-http quickstart tutorial to Ruby, mostly because Why Not (tm).
Setup
Creating new project with sbt
For this project, I'm going to use sbt and one of the Giter8 templates for scala. We're actually going to start with something really bare-bones.
sbt new scala/scala-seed.g8 \ --name='Ruby Test' \ --description='Running Akka with jruby'
That should create a new directory called ruby-test
with a very
basic Scala app inside it.
Adding dependencies
Next thing, we need to add our main dependencies:
- Akka-http
- Akka-stream (required by akka-http)
- JRuby
Using sbt this is fairly easy, just edit the build.sbt
file to look
something like mine:
import Dependencies._ lazy val root = (project in file(".")). settings( inThisBuild(List( organization := "com.example", scalaVersion := "2.12.7", version := "0.1.0-SNAPSHOT" )), name := "Ruby Test", libraryDependencies ++= Seq( scalaTest % Test, "com.typesafe.akka" %% "akka-http" % "10.1.5", "com.typesafe.akka" %% "akka-stream" % "2.5.18", "org.jruby" % "jruby" % "9.2.3.0" ) )
You can now try to build. This will fetch the dependencies and try to compile. It should just work, if it fails, try to check the dependencies.
cd ruby-test
mkdir -p src/main/ruby
sbt compile
The main ruby file
It's time to write some Ruby. We're going to create the main file
(src/main/ruby/index.rb
). Inside that, we're going to use HttpApp to
make our life easier.
require 'java' java_import 'akka.http.javadsl.server.HttpApp' class WebServer < HttpApp def routes path("hello", -> { get(-> { complete("<h1>Hello from JRuby</h1>") }) }) end end my_server = WebServer.new my_server.startServer("localhost", 8080)
That code is a straight translation from the sample code for the HttpApp API.
How does this work? It works because JRuby classes can inherit from Java classes, and you can pass them wherever the JVM (and whatever API) is expecting a java class. JRuby interop with java is really smooth (most of the time, I'll highlight some problems later on).
Loading and running ruby from Scala
Now, in order to make Scala run that ruby file, we'll modify the main
Scala file (src/main/scala/example/Hello.scala
). We'll use JRuby's
embedding facilities to run the script. In particular, we'll use
ScriptingContainer
.
package example import org.jruby.embed.ScriptingContainer import org.jruby.embed.PathType object Hello extends Greeting with App { val sc = new ScriptingContainer() sc.runScriptlet(PathType.RELATIVE, "src/main/ruby/index.rb") } trait Greeting { lazy val greeting: String = "hello" }
As you can see, loading and running Ruby inside Scala is extremely easy. You can run and test it now.
$ sbt run
And in another terminal:
$ curl http://localhost:8080/hello <h1>Hello from JRuby</h1>
Writing a more complete example (porting the Quickstart example)
That was all super easy, but now let's try to port the quick start example. This is a good example because it's a bit more real-like scenario, where you make use of the Actor system that's so popular in Akka.
The server
Let's begin with the server. You can replace the previous index.rb
with this new file.
require 'java' java_import 'akka.http.javadsl.server.AllDirectives' java_import 'akka.http.javadsl.Http' java_import 'akka.http.javadsl.ConnectHttp' java_import 'akka.actor.ActorSystem' java_import 'akka.stream.ActorMaterializer' require_relative 'user_registry_actor' require_relative 'user_routes' class WebServer < AllDirectives @user_routes = nil def initialize(system, registry_actor) @user_routes = UserRoutes.new(system, registry_actor) super() end def createRoute @user_routes.routes end def self.main system = ActorSystem.create("hello_akka_from_ruby") http = Http.get(system) materializer = ActorMaterializer.create(system) user_registry_actor = system.actorOf(UserRegistryActor.props, "userRegistryActor") app = WebServer.new(system, user_registry_actor) route_flow = app.createRoute.flow(system, materializer) http.bindAndHandle(route_flow, ConnectHttp.toHost("localhost", 8080), materializer) warn "Server online at http://localhost:8080" end end WebServer.main
As you can see, it's almost a 1-1 translation of the java sources. One
of the key differences is the explicit call to the no-arguments parent
constructor (self()
) due to the way Ruby inheritance works: if you
don't specify it, it will try to call the parent constructor with the
same arguments.
The Registry Actor
The next part is the Registry Actor. This piece of code is the one in charge of receiving messages and reacting to them.
Important things that changed from the Java code:
- We're using a ruby array (
[]
) to keep the list of users. - There is some extra work/boilerplate needed in order to call some methods because Ruby lacks types.
User
andUsers
are simpleStructs
that have two extra methods:to_json
andfrom_json
. We're going to use those in the routes to serialize them.- if you want to make your code even more ruby-like, you could replace
getSender
bysender
. JRuby automagically adds those aliases.
require 'java' java_import 'akka.actor.AbstractActor' java_import 'akka.actor.Props' java_import 'akka.japi.Creator' java_import 'akka.japi.pf.ReceiveBuilder' java_import 'java.io.Serializable' require_relative 'user_registry_messages' class Props java_alias :rcreate, :create, [Java::java.lang.Class, Java::akka.japi.Creator] end class ReceiveBuilder java_alias :rmatch, :match, [Java::java.lang.Class, Java::akka.japi.pf.FI::UnitApply] end class UserRegistryActor < AbstractActor class User < Struct.new(:name, :age, :country) def to_json(*args) to_h.to_json(*args) end def self.from_json(str) hh = JSON.parse(str) vals = self.members.map { |k| hh[k.to_s] } self.new(*vals) end end class Users < Struct.new(:users) def to_json(*args) to_h.to_json(*args) end def self.from_json(str) hh = JSON.parse(str) vals = self.members.map { |k| hh[k.to_s] } self.new(*vals) end end class Creator def create UserRegistryActor.new end end def self.props Props.rcreate(UserRegistryActor, Creator.new) end def createReceive @users = [] receiveBuilder .rmatch( UserRegistryMessages::GetUsers, ->(_) { getSender.tell(Users.new(@users), getSelf) }) .rmatch( UserRegistryMessages::CreateUser, ->(createUser) { user = createUser.user @users.push(user) getSender.tell(UserRegistryMessages::ActionPerformed.new("User #{user.name} created"), getSelf) }) .matchAny(->(o) {warn "received unknown message"}) .build end end
As stated above, this part has a few minor things to consider, mostly
due to the fact that ruby has no types. The call to self.props
,
which in Java is as simple as Props.create(ClassName.class)
, in
JRuby you have to use java_method
to fetch the method with the right
signature. In this particular case we're choosing the one that
receives a Java class and a Creator
, however, we had to provide our
own creator, because the default Creator used in Akka is a Generic
class. Generics can't be used in JRuby because the type info is lost
when the java code is compiled.
Luckily, JRuby provides java_alias and the ability to "open" the classes (methods added to the class after reopening will be only available to JRuby).
A similar scenario (dealing with type inference), is in the
createRecieve
method, where we have to be explicit about what method
we want to call. We can also reopen the ReceiveBuilder
class and
alias the methods we want to use.
The Message Registry
This part is really self explanatory, however there's one thing worth
mentioning if you're new to JRuby: in JRuby Java interfaces are mapped
to Modules, so implementing an interface means added them to your
mixin in your Ruby class, using include
🤯.
require 'java' java_import 'java.io.Serializable' module UserRegistryMessages # interfaces are mapped to modules in JRuby # https://github.com/jruby/jruby/wiki/CallingJavaFromJRuby#implementing-java-interfaces-in-jruby class GetUsers include Serializable end class ActionPerformed attr_reader :description include Serializable def initialize(description) @description = description end end class CreateUser attr_reader :user include Serializable def initialize(user) @user = user end end class GetUser attr_reader :name include Serializable def initialize(name) @name = name end end end
The routes
This was a bit more complicated to port, mostly because I couldn't get
the Jackson marshaller to work with Struct
. If you know how to make
it work, please drop me a line.
The way I got this working, was to create a very simple
JsonMarshaller
that's basically a simplification of the Jackson
marshaller in Akka, but I'm using the JSON
module in Ruby to do the
dirty work. Everything else should be mostly the same as the Java
version.
require 'java' require 'json' java_import 'akka.util.Timeout' java_import 'akka.pattern.PatternsCS' java_import 'akka.http.javadsl.server.AllDirectives' java_import 'akka.http.javadsl.model.StatusCode' java_import 'akka.http.javadsl.model.StatusCodes' java_import 'akka.http.javadsl.marshalling.Marshaller' java_import 'akka.http.javadsl.unmarshalling.Unmarshaller' java_import 'akka.http.javadsl.model.MediaTypes' java_import 'java.util.concurrent.TimeUnit' require_relative 'user_registry_actor' require_relative 'user_registry_messages' class JsonMarshaller def self.marshaller Marshaller.wrapEntity( -> (dispatcher, obj) { obj.to_json }, Marshaller.stringToEntity, MediaTypes::APPLICATION_JSON ) end def self.unmarshaller(obj_type) Unmarshaller .forMediaType(MediaTypes::APPLICATION_JSON, Unmarshaller.entityToString) .thenApply(-> (str) { obj_type.from_json(str) }) end end class UserRoutes < AllDirectives attr_reader :timeout def initialize(system, user_registry_actor) @user_registry_actor = user_registry_actor @timeout = Timeout.new(5, TimeUnit::SECONDS) super() end def routes route( pathPrefix( "users", -> { route(get_or_post_users) } ) ) end def get_or_post_users pathEnd( -> { route( get(-> { future_users = PatternsCS .ask(@user_registry_actor, UserRegistryMessages::GetUsers.new, timeout) .thenApply(-> (obj) { obj }) onSuccess(-> { future_users }, -> (users) { complete(StatusCodes::OK, users, JsonMarshaller.marshaller) }) }), post(-> { entity( JsonMarshaller.unmarshaller(UserRegistryActor::User), -> (user) { user_created = PatternsCS .ask(@user_registry_actor, UserRegistryMessages::CreateUser.new(user), timeout) .thenApply(-> (obj) { obj }) onSuccess(-> { user_created }, -> (performed) { complete(StatusCodes::CREATED, performed.description, JsonMarshaller.marshaller) }) }) }) ) } ) end end
Due to time constraints, I only implemented get
and post
on
/users
, but you can easily extend this to the rest of the
example.
Where to go from here
From here, you could rewrite Sinatra using akka-http if you want (only partially joking), or write a full-fledged app using ruby and powered by the Actor Model provided by Akka. After spending my weekend debugging and learning about the Actor system in Akka, I thing I would stay in either Java or Scala.
Some considerations:
- If I decided to use this to build a real app, I would probable invest some time building wrappers to make my life easier. The code in this little weekend project works, but it's far from being beautiful Ruby code.
- You will hit a bunch of bumps due to lack of typing in Ruby: things that could've been easily caught by the compile/IDE phase due to type mismatch, will translate into a lot of time debugging.
- To my eyes, the Java examples in the Akka website are much more understandable than the Scala code. This is probably a personal bias due to lack of exposure to the language.
- The java docs for Akka are mostly ok-ish. To get this mini-project working, I kept two tabs open, one with the akka docs and another one with the akka-http java docs.