I've fiddled with my blog template because I decided I wanted more horizontal viewing space, given that it was using less than a third of my 1920 horizontal pixels. If it feels too spread out for you, I added a drag-and-drop handle over to the left to let you resize the main content column. The javascript is pretty primitive. If it breaks, drop me a comment.

Thursday, October 2, 2008

An Unexpected Chapter

I certainly didn't expect to be awake now, but a coworker is out of the country and working from a very different time zone, and he IM'd me with a question at about midnight, my time. The question was related to a problem we'd been seeing related to Apache Camel 1.4. There was a NullPointerException coming from inside Camel somewhere. This is the exception and stacktrace:
Execution of JMS message listener failed                                                                                 WARN  DefaultMessageListenerContainer(634) 01:25:03,534 DefaultMessageListenerContainer-10
org.apache.camel.RuntimeCamelException: java.lang.NullPointerException
at org.apache.camel.component.jms.EndpointMessageListener.onMessage(EndpointMessageListener.java:71)
at org.springframework.jms.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:531)
at org.springframework.jms.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:466)
at org.springframework.jms.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:435)
at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.doReceiveAndExecute(AbstractPollingMessageListenerContainer.java:316)
at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.receiveAndExecute(AbstractPollingMessageListenerContainer.java:255)
at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.invokeListener(DefaultMessageListenerContainer.java:887)
at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.run(DefaultMessageListenerContainer.java:822)
at java.lang.Thread.run(Thread.java:619)
Caused by: java.lang.NullPointerException
at org.apache.camel.component.jms.JmsMessage.createMessageId(JmsMessage.java:203)
at org.apache.camel.impl.MessageSupport.getMessageId(MessageSupport.java:127)
at org.apache.camel.impl.MessageSupport.copyFrom(MessageSupport.java:95)
at org.apache.camel.impl.DefaultExchange.safeCopy(DefaultExchange.java:98)
at org.apache.camel.impl.DefaultExchange.copyFrom(DefaultExchange.java:81)
at org.apache.camel.impl.DefaultEndpoint.createExchange(DefaultEndpoint.java:145)
at org.apache.camel.component.file.FileProducer.process(FileProducer.java:54)
at org.apache.camel.impl.converter.AsyncProcessorTypeConverter$ProcessorToAsyncProcessorBridge.process(AsyncProcessorTypeConverter.java:43)
at org.apache.camel.processor.SendProcessor.process(SendProcessor.java:75)
at org.apache.camel.management.InstrumentationProcessor.process(InstrumentationProcessor.java:57)
at org.apache.camel.processor.DeadLetterChannel.process(DeadLetterChannel.java:155)
at org.apache.camel.processor.DeadLetterChannel.process(DeadLetterChannel.java:91)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:101)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:85)
at org.apache.camel.management.InstrumentationProcessor.process(InstrumentationProcessor.java:57)
at org.apache.camel.processor.UnitOfWorkProcessor.process(UnitOfWorkProcessor.java:39)
at org.apache.camel.util.AsyncProcessorHelper.process(AsyncProcessorHelper.java:41)
at org.apache.camel.processor.DelegateAsyncProcessor.process(DelegateAsyncProcessor.java:66)
at org.apache.camel.component.jms.EndpointMessageListener.onMessage(EndpointMessageListener.java:68)
... 8 more
He IM'd me to say he'd discovered that trying to use two file endpoints in the same camel route caused this exception. After some testing, I narrowed his theory to "using a file endpoint anywhere but the end of a route". I should say that this is in a route that processes JMS messages. Here's the problem: Camel pulls a message from a JMS desintation and sends it along the route as an org.apache.camel.component.jms.JmsMessage packaged inside an org.apache.camel.component.jms.JmsExchange. When you get to a file endpoint, it does this:
1 public void process(Exchange exchange) throws Exception {
2     FileExchange fileExchange =
3     process(fileExchange);
4     ExchangeHelper.copyResults(exchange, fileExchange);
5 }
That's in org.apache.camel.component.file.FileProducer, for those following along. This is what creates the file from the incoming message/exchange. Line 2 there creates a FileExchange containing FileMessages based on the incoming JmsExchange, which contains JmsMessages. Line 3 is what writes the file using the contents of the newly created exchange. Then, for whatever reason, Camel copies the state of the FileExchange back onto the incoming exchange (yes, that method is copyResults(destination, source)), and that's the exchange that gets passed on down the route. In ExchangeHelper.copyResults, the message from the source exchange (the out message if it exists, the in message if out doesn't exist) is copied to the destination's out message. The destination doesn't have an out yet, so one is created, and then the state of the source message is copied to it. The problem is that JmsMessage has a property of type javax.jms.Message where it stores the original JMS message that it pulled from the JMS broker. FileMessage obviously does not have this. Basically what the above code does is copy from JmsMessage -> new FileMessage -> new JmsMessage. Plainly, then, any state in the original JmsMessage that doesn't fit in FileMessage will be lost in the new JmsMessage. One of those is the javax.jms.Message property. Unfortunately, this property is an integral piece of Camel's JmsMessage. One place it's used is in generating a message id in the createMessageId method, which you'll notice is the top line in the stack trace for the NullPointerException above, and the message id is used all over the place. What this boils down to is that you may not use a file endpoint in a route where the message comes from a JMS server except as the very last endpoint.* However, you can fudge this a little using Camel's implementation of the multicast pattern. This will break:
But this works:
Each file endpoint still has to be at the very end, but multicast() effectively lets you have many "ends" to a single route. *This may be a problem with mixing other message and exchange types as well, but JMS/File is the only one I've seen break myself.


Claus Ibsen said...

This should be improved in Camel 2.0 where we will do some refactorings of the API and remove usage of generics for Exchange. This removes all the copying currently needed when you pass exchanges between endpoints.

Glad the workaround works. I have created a ticket in our bug tracer CAMEL-996 so we wont forget this (eg creating a unit test for Camel 2.0 to ensure it works)

/Claus, a Camel rider

Willem said...

I think I fixed this issue 2 month ago in CAMEL-785[1]. Can you try the least Camel 1.5 snapshot[2] to verify it ?

[1] https://issues.apache.org/activemq/browse/CAMEL-785
[2] http://people.apache.org/repo/m2-snapshot-repository/org/apache/camel/apache-camel/1.5-SNAPSHOT/

Claus Ibsen said...

Also mind that:

Is using the pipes-and-filters EIP pattern. So there is a consumer between the two files, eg. reading from file://foo and saving to file://bar.

What you probably want is as you write to send the *same* jms message to multiple endpoints and hence you should use the static recipient EIP pattern, and that is the multicast() in Camel.

Ryan said...

Wow, you guys really watch the web for stuff related to Camel, huh?

No, this doesn't happen in Camel 1.5. It looks like CAMEL-785 removes the createMessageId() method's dependency on having a populated JmsMessage, which is what I was observing to break.

I'm glad to hear the message copying won't be needed in the future. What you said about sending the same message to multiple endpoints is accurate, so semantically, we probably should be using multicast anyway. However, it seems that a FileEndpoint shouldn't modify a message, and that, as a result, using pipes-and-filters should yield the same result in this case.

I have a unit test that tests this specific scenario if you'd like to have it. Just let me know where to send it.

Claus Ibsen said...

Ryan you can post it on the Camel user forum or as a mail to me at davsclaus at XXX apache dot org - without the XXX of course ;).