Thursday, July 26, 2007

Moist Tests: Production Code Needs to be DRY, But Tests Don't

I've spoken a bit about this before when discussing our standard_params notation. I think we've taken the DRY principle too far. And I think this happens a lot in the software industry. We latch onto a good principle, and our meticulous (some would say anal) natures lead us to want to apply it everywhere. And we do it with DRY. Don't Repeat Yourself. Removing duplication in code removes bugs. Re-use promotes efficient development. It's a fantastic principle, first outlined by Dave Thomas and Andy Hunt in The Pragmatic Programmer. It's probably still underused in most code bases. But one area where I see it abused is in tests. Tests don't need to by DRY, they can be Moist*. Take these two tests for example:


def test_formatted_telephone_number
telephone_number = TelephoneNumber.new(:area_code => "123", :telephone_number_base => "5556811")
assert_equal "(123) 555-6811", telephone_number.formatted
end

def test_unformatted_telephone_number
telephone_number = TelephoneNumber.new(:area_code => "123", :telephone_number_base => "5556811")
assert_equal "1235556811", telephone_number.unformatted
end


DRY would see the duplicate creation of the same TelephoneNumber object, and say "Extract Method" to remove the duplication. Or perhaps we could stick an instance variable in our setup, and assign the TelephoneNumber object to it there:



def setup
@telephone_number = TelephoneNumber.new(:area_code => "123", :telephone_number_base => "5556811")
end

def test_unformatted_telephone_number
assert_equal "1235556811", @telephone_number.unformatted
end



And if this wasn't a test (if it was actual code that will be executed in production), I would definitely agree. If you remove the duplication, you only have one place to change the code when that time arises, and bugs are less likely to result. But in tests it is different. When a test fails, I want to fix it quickly. And when I have to search around a large test class for extracted methods, or instance variables that may be modified throughout the class, this costs me time. And I'm willing to accept the risk of the duplication causing a bug (after all, it is only test code) if I reduce the time wasted.

So for me, private methods in test are a smell. So too are setup methods. The only time I use them is when I can get the abstraction such that I very very rarely have to look at what is happening beneath the abstraction. A good example is rails' Controller tests. They have a setup which initializes the controller, and a request and response object. I think I can count on one hand the number of times that I've had to care about these variables, so in that case, it's ok.

*I believe zak came up with this term, though I'm not sure. It may have been Jay Fields. Either way, I'm certainly not taking credit.

Thursday, July 5, 2007

Performance of the JRuby JMS messaging solution

Please note: This performance testing method was flawed as it was undertaken in the 'development' environment. There are more accurate results here.

Now that we have a JRuby JMS implementation, how does it perform? I did a quick test to compare the ActiveMessaging solution with the JRuby JMS messaging solution. The Customer application, Orders application, ActiveMQ server and the message handler (ActiveMessaging poller/JRuby JMS process) were all running on my laptop. As such, the absolute timings presented here are not relevant, but the comparison between the two solutions is relevant, and somewhat interesting.

Testing Method


I sent 100 messages from the Customer application and measured the elapsed time between when the message was sent, and when processing had finished in the Orders application. With each solution, I captured the current time immediately before the message was sent in the Customer application, and captured the current time immediately after it was processed in the Orders application. These were the results:



As you can see, the elapsed time increases at a constant rate for the ActiveMessaging solution, as the 100 messages pile up in the queue. The last message took just under 14 seconds to be processed (with a significant amount of that time being spent in the queue). Contrast this with the Java JMS solution, where the 100th message took only 2.5 seconds to be processed. Even more interesting than the difference between the two times is the flattening of the Java JMS curve. The elapsed time does start to grow as the messages pile up in the queue, but after around the 60th message, the elapsed time remains relatively constant. This represents a great advantage when trying to ensure predictable performance of your messaging solution.

Conclusion


We are going to look seriously at using JRuby and JMS on our current project in place of ActiveMessaging. I mentioned in my last post that although I deployed the Orders application using JRuby, you can actually leave the web-app itself in MRI (Matz Ruby Implementation) and just deploy the message handler in JRuby. The message handler is running as a separate process to the web application. It needs to load the rails application to have access to the Customer model so that it can save the message contents to the database, but that's it. The web requests can still be handled by an MRI-deployed rails app. So that's probably what we will end up doing. Thoughtworks Studios recently released a project management tool called Mingle that is deployed on JRuby, so it's definitely possible to deploy the whole web-app in JRuby, but as an interim step we will probably stick with MRI.

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.