Social Media
More About This Website

My name is Wayne Robinson and I'm a web applications developer from Queensland, Australia. In August 2005 I discovered Ruby on Rails and instantly fell in love. From that point forward, Ruby on Rails has been my language of choice for new projects however, I still use PHP to maintain some legacy applications.

Categories
Login

Entries in ruby on rails (1)

Thursday
Apr012010

A JRuby/Rails Message Driven Bean

A while ago I found this tutorial on getting an EJB message-driven bean up and running utilising JRuby and Rails at http://nodnol.org/blog/chris/entry/a_jruby_rails_message_driven. This website now appears to be down so I am reproducing it here for posterity (and my own reference).

I'm building a system which receives messages from a larger application platform and records the details in its local database. It's built with Rails and JRuby, with Apache ActiveMQ as the message broker. Using JRuby lets me use ActiveMQ's native JMS-based client, rather than speaking STOMP to the broker (as our larger platform does on the other end of the queue). My deployment platform is Glassfish, and I'm deploying the Rails app to it in the usual way with Warbler.

To begin with, I wrote a standalone script to subscribe to the JMS queue, based on the suggestion to use JMS rather than ActiveMessaging. This works well, and is very simple to deploy for development. As Shane points out, it's just a literal translation from Java to (J)Ruby. A problem is that even when configured to use a failover transport to a pair of message brokers, the script will frequently exit and need restarting. Not only that, but starting the script as a separate process means I've got two JVMs running - one for Glassfish and one for the JRuby script, and they can't share a common JDBC connection pool, so there's more to manage there.

Finally, I'd have to manage any concurrency required myself - the script is resolutely single-threaded, and if I need multiple threads to saturate the hardware, I'd need to write the thread management code as part of the script.

Since I'm already using an app server for the web part of the system, the answer seems to be to move the script into Glassfish as a message-driven Bean.

There are a few issues to be solved: I only want a single copy of the Rails environment, and I need to arrange for the relevant parts of our application to be packaged so it can be correctly deployed with the bean.

The upside is that with Rails 2.2, I can share that Rails environment among as many threads as I need, without having to synchronize access to the app - given a JDBC connection pool, Glassfish will start many bean instances to handle incoming messages.

Unfortunately there doesn't seem to be anything like Warbler to help out with packaging the app into an MDB, so it's all rather manual. Here's what I needed to do to set this up:

  • Provide an app entry point which would accept a message, and run the required business logic in ruby
  • Create a standard J2EE Message-driven Bean in Java, and hook it to an external ActiveMQ instance
  • Spin up a single Rails instance inside the container
  • Arrange for the message receipt handler to call the app entry point with the contents of the message

App entry point for messaging

This is a class method on the model the app will create as a result of receiving the message. My messages are simple YAML strings, and this method accepts that YAML directly.

That means I'm converting to a Ruby data structure from YAML in Ruby code, and it might be better to make sure it's done in Java - the messages are small enough that this isn't a big deal though.

Create an MDB

I'm using NetBeans for this, simply because of its integration with Glassfish. Everything is done with Java 5 annotations, bar the selection of activemq instead of the built in broker (which doesn't have native STOMP support).

All that's needed beyond the standard Bean that NetBeans sets up is this stanza in sun-ejb-jar.xml:

assuming the ActiveMQ RA is deployed in Glassfish as "activemq". This configuration is taken from this forum post

Rails Instance

I only want to start one Rails instance to be shared among all the bean instances. I'll create a RailsRuntime class, which loads JRuby, initialises Rails and provides a "string eval" method, and then another class, RailsRuntimeSingleton, to maintain that single instance.

Here's the RailsRuntime class:

There's a hardcoded path to the JRuby home directory here, which isn't ideal - really I should just include a jruby-complete jar but I don't have the infrastructure in place yet to also include all the required gems, so I'm relying on that fixed path for now.

I do bundle up the Rails app into the jar, and so there's a fixed path available to environment.rb. Production mode is also set here.

The singleton class is very simple. A static property is guaranteed to be initialised only once, so this avoids the need for any synchronization around the RailsRuntime setup.

Call the app when we receive a message

Now I've got a Rails instance spun up, and messages coming into the bean, it's time to plug it all together. Here's the bean's onMessage method and the hook into the Rails app:

Before Rails 2.2, I needed to synchronize around the eval() method, to serialise access to Rails. Now that's not necessary, and I can allow the container to spawn as many threads as I have connections available in the JDBC pool.

My test environment is a simple, single-threaded Perl STOMP sender running flat out to an ActiveMQ instance, plus the bean in Glassfish all on a Macbook Pro. When the bean runs serialized, the receiver isn't able to keep up and a backlog of messages builds up. Simply removing the synchronized block means the receiver can keep up with the sender easily.


Next steps

There's a lot to get right with this setup even leaving aside the dependency on the external JRuby.

  • The message queue configuration in Java in the Bean class
  • The external message broker configuration in sun-ejb-jar.xml
  • Packaging up Rails and the application as part of the Bean

More work is required to include jruby-complete.jar and remove that external dependency. Beyond that, making the Java parts generic, allowing configuration in Ruby and building outside of the IDE in the style of Warbler seems to be the way to go.