Thursday, July 5, 2007

JRuby JMS as a replacement for ActiveMessaging

I was talking to JRuby committer Ola Bini at RailsConf a few weeks ago about our ActiveMessaging solution, and he suggested we try using JRuby and JMS instead. By using JMS, we can eliminate the poller and have a truly "Event Driven" solution. So I've been playing around with JRuby and JMS in my spare time, with some really interesting results. In JRuby, we can implement the Java JMS interface MessageListener:

require "java"

include_class "org.apache.activemq.ActiveMQConnectionFactory"
include_class "org.apache.activemq.util.ByteSequence"
include_class "org.apache.activemq.command.ActiveMQBytesMessage"
include_class "javax.jms.MessageListener"

ENV['RAILS_ENV'] = 'development'
RAILS_ROOT=File.expand_path(File.join(File.dirname(__FILE__), '..','..'))
load File.join(RAILS_ROOT, 'config', 'environment.rb')

class MessageHandler
include javax.jms.MessageListener

def onMessage(serialized_message)
message_body = serialized_messageed_message.get_content.get_data.inject("") { |body, byte| body << byte }
customer_payload = YAML.load(message_body)
customer = Customer.new(:name => customer_payload.name)
customer.id = customer_payload.id
customer.save!
end

def run
factory = ActiveMQConnectionFactory.new("tcp://localhost:61616")
connection = factory.create_connection();
session = connection.create_session(false, Session::AUTO_ACKNOWLEDGE);
queue = session.create_queue("Customer");

consumer = session.create_consumer(queue);
consumer.set_message_listener(self);

connection.start();
puts "Listening..."
end
end

handler = MessageHandler.new
handler.run


Contrast this with similar Java code:

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.util.ByteSequence;
import org.apache.activemq.command.ActiveMQBytesMessage;

import javax.jms.*;


public class MessageHandler implements MessageListener {

private Connection connection;
private Session session;
private Queue queue;

public void onMessage(Message serializedMessage) {
String message = "";
ActiveMQBytesMessage bytes_message = (ActiveMQBytesMessage)serializedMessage;
ByteSequence sequence = bytes_message.getContent();

for(int i=0; i < sequence.getData().length; i++) {
message += (char)sequence.getData()[i];
}

// Here is where you would use the message to create the Customer object
// For now I will just print the YAML

System.out.println(message);
}

public static void main(String[] args) throws JMSException {
MessageHandler handler = new MessageHandler();
handler.run();
}

private void run() throws JMSException {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
connection = factory.createConnection();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
queue = session.createQueue("Customer");

MessageConsumer consumer = session.createConsumer(queue);
consumer.setMessageListener(this);

connection.start();
System.out.println("Listening...");
}
}


You'll notice that the 'run' methods are identical, save for the "rubyisation" of the Java code (underscores and the like). Java requires you to implement the onMessage method when inheriting from the MessageListener class, so we have done that in JRuby.

The Java class definition:

public class MessageHandler implements MessageListener


is replaced with the JRuby equivalent:

class MessageHandler
include javax.jms.MessageListener


The rest of the JRuby code is very similar to its Java equivalent, though it was tough going back to the type-safety gymnastics of Java, I must admit.

I was surprised that with the version of JRuby I am running (ruby 1.8.5 (2007-06-02 rev 3812) [i386-jruby1.0.0RC3]), I had to use camel case for the onMessage method definition rather then ruby-like underscores, but it's no big deal.

We'll save this file under the processors directory of our Orders application as message_handler.rb

Then we can deploy the Orders application in JRuby. Note that this isn't strictly necessary (but more on that later).

From the instructions on the JRuby wiki:

Install the Goldspike plugin:
~/orders]>script/plugin install svn://rubyforge.org/var/svn/jruby-extras/trunk/rails-integration/plugins/goldspike

Install the ActiveRecord-JDBC gem:
~/orders]>gem install activerecord-jdbc --no-rdoc --no-ri

The plugin provides the following rake task:
war:standalone:create - which packages up a web archive containing your application along with JRuby and the rails libraries

Run the task, and deploy the war to your app server (I'm using Tomcat)

We can then make the apache-activemq-4.1.1.jar available to our script for the jms-related code by setting the CLASSPATH environment variable:

export CLASSPATH=/path_to_jar/apache-activemqvemq-4.1.1.jar:$CLASSPATH

Then all we need to do is run the JRuby code with the following command:

jruby /path_to_tomcat/webapps/orders/processors/message_handler.rb

And that's it. Now we can process Customer messages using our JRuby JMS implementation.

3 comments:

tirsen said...

What about deployment? Running a separate JVM is quite expensive, significantly more expensive than running a separate Ruby process.

In Mingle we use the RailsTaskServlet to start processes inside the app server itself. You would need to configure it manually inside web.xml and point it to what script to start.

If you retrieve the JMS connection factory from the ap server through JNDI you also benefit from the resource management (pooling and what have you) of the app server itself.

In Da Club said...

Hi! I've been trying this piece of code with ActiveMQ 5.0 . Is there anything special for us to do specify the wireFormat as stomp? I'm trying to send messages to a JRuby consumer [using the code you have given] and the Ruby Stomp gem to send messages to it.

But somehow, the onMessage method does not get called. I am however able to receive messages on the ruby stomp gem.

Thanks a lot for all the great work!

Arun

railsmama said...

I'm having the same problem as Arun. Any help?