HomeAboutGitlab

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 and Users are simple Structs that have two extra methods: to_json and from_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 by sender. 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.

Copyright © 2012-2018 Rolando Abarca - Powered by orgmode

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License unless otherwise noted.