Friday, March 19, 2010

JMS Synchronous Request/Reply

So far I've introduced some basics around JMS 1.1, including the Common Interface and Security/Concurrency. Continuing through the JMS 1.1 spec, my next drill-down will be around the Request/Reply idiom. Just so you know, these articles track my own learning curve around JMS, so you are invited to correct me and comment as needed.

Section 2.10 of the spec describes Request/Reply as an arrangement where a client sends a message to a remote service of some kind, expecting a reply. The request message header specifies a destination to which the service can (optionally!) respond with some information, or a confirmation that a requested action has been performed, or...etc. A well-behaved service not only replies, but the reply includes the ID of the request message (typically) so the requestor can correlate the reply with the previous request. JMS additionally provides basic helper implementations (QueueRequestor and TopicRequestor) that encapsulate some of the details involved here, including creating the temporaryqueue or temporarytopic to which the reply is sent. In this series of posts, I'll trace my own prototyping, starting with the QueueRequestor (which is a synchronous messaging style) and evolving with some asynchronous alternatives, adding functionality in stages.

I'll use increasingly sophisticated levels of remote service, with various flavors of requestors associated with each. The remote services include one that simply listens on a request queue; another that does that plus notifies requestors about exceptions or invalid messages; and a third that does that plus uses message filtering on an asynchronous shared queue so that only the original requestor receives the reply. This post will describe the basic remote service and a basic requestor.

The remote service listens asynchronously on a system-wide queue and replies to requests at the reply-to destination specified in the JMSReplyTo header field. It is not terribly robust - it assumes the requestor will be sending non-null text messages; if a given message is either null or non-text, an exception is thrown. The service constructs a reply with the correlation ID (JMSCorrelationID header field) set to the request message ID (JMSMessageID header field), sends the reply and acknowledges the request.

This class provides a hook for subclasses to further decorate the message (e.g. setting properties, etc.), because we just happen to know we'll be using that in a later prototype extending this remote service.

The requestor connects to the service queue, sends a request using the QueueRequestor (which blocks on a dedicated temporary channel), and verifies that the reply has a correlation ID that matches the request message ID.

As an aside, the QueueRequestor and TopicRequestor classes are, interestingly, the only concrete classes in the JMS 1.1 distro - in other words, JMS providers need not implement anything here. Though, it wouldn't be surprising if provider-specific request/reply classes are available; it's depends on your needs as to whether proprietary mechanisms are appropriate, as always.

And, here's the requestor code. The comments explain how to demo things (assuming the ActiveMQ provider is already running), and describes what we might think about to improve the functionality:

package com.mybiz.jms.activemq.server.requestreply.requestor.sync;
import com.mybiz.jms.activemq.server.requestreply.connection.SyncRequestorConnectionStuff;
import com.mybiz.jms.activemq.server.requestreply.replier.RemoteService;
import com.mybiz.jms.activemq.server.requestreply.util.MessageUtil;
import javax.jms.JMSException;
import javax.jms.TextMessage;
public class VanillaSyncRequestor {
// Demo:
//
// Start the remote service, then run the requestor - console printouts
// reflect request and reply on both sides of messaging provider.
//
// Now kill the remote service, then run the requestor - request is made,
// but no reply just yet. Start up the service "after a while", and the
// reply is received.
//
// What does it need:
//
// This is a simple request-reply demonstration. It will not know if
// the remote service throws an exception (which can happen, as noted
// above, if the request is either null or is not a text message). Though
// it has a dedicated temporary channel, it can possibly block for indefinite
// periods of time - e.g. if the service goes down, it will block until that
// service is back online; worse, if the service throws an exception, it can
// block "forever". Blocking until the service comes back online can be
// considered a good thing, i.e. the request will "eventually" be handled -
// and, it can be considered a bad thing, e.g. if many clients are blocking
// on a wait for an offline service, this consumes system resources.
/**
* Send a text message using the given connection stuff, wait synchronously
* for a reply.
*/
public void sendRequest(SyncRequestorConnectionStuff stuff) throws JMSException {
TextMessage requestMessage = stuff.getSession().createTextMessage(
"Do this as soon as you're online!");
// this convenience method sends the request, at which point the
// JMSMessageID is set in the header of that message; it acknowledges
// the reply and then confirms that the JMSCorrelationID in the
// reply matches the JMSMessageID, throwing an exception if that is
// not the case.
MessageUtil.processRequestAndReply(stuff.getRequestor(), requestMessage);
}
public static void main(String[] args) throws JMSException {
VanillaSyncRequestor requestor = new VanillaSyncRequestor();
// get the connection, session and QueueRequestor - on return,
// the connection will have been started
SyncRequestorConnectionStuff serviceQueueStuff =
new SyncRequestorConnectionStuff(
RemoteService.url, RemoteService.serviceQueueName);
requestor.sendRequest(serviceQueueStuff);
// requestor is done - release connection. No need to release any
// other objects, that's done automatically when releasing the connection.
serviceQueueStuff.getConnection().close();
}
}

I'm omitting the code for MessageUtil and ConnectionStuff - these can be regarded as black-boxes that "just work" as noted, for purposes of this article. But I will offer the printouts that occur when the suggested demo steps are taken:

1 - Start up the ActiveMQ provider (you'll of course need to download and install this first):

4 - Stop the service; run the requestor again - note that it sends the request but then appears to stop - in fact, it's blocking, waiting for the service to come back online:

INFO: Successfully connected to tcp://localhost:61616
12:30:07.875 Sending request for service 'Do this as soon as you're online!'...

5 - Start up the service - the printout there looks the same as above, since it grabs the message waiting in the queue and replies. At that point, thankfully, the requestor now completes, albeit "some time later", as can be seen by the timestamps:

As the comments in VanillaSyncRequestor indicate, there is room for improvement. Next, I'll demonstrate a client that blocks "forever", and take some steps to at least partially mitigate that. Then we'll move on to use asynchronous messaging in a request/reply approach.

1 comment:

Welcome to the Perimeter Sweep Blog

My blog is largely intended to be a placeholder for topics involving software development - architecture, technology drill-downs, best practices, various solutions, workarounds, gotchas and the like - things that will remind me what I've learned over time. If it helps you out also - all the better.

Subscribe To This Blog

About Me

I'm a Senior Software Engineer, an avid runner, and formerly a professional musician...currently the proud father of a super-tyke, raising two Siberian Huskies and married to my best friend. Life is good.