XML.com: XML From the Inside Out
oreilly.comSafari Bookshelf.Conferences.

advertisement

Fun with Amazon's Simple Queue Service
by Jason Levitt | Pages: 1, 2

Placing an Entry on a Queue

In this next example, I place two entries on the queue named "Jason." The first entry is the string "Dude, what are your doing?" and the second entry is the string "sheer poetry." The operation is called Enqueue.

http://webservices.amazon.com/onca/xml
?Service=AWSSimpleQueueService&Version=2004-10-14
&SubscriptionId=001VHHVC74XFD88KCY82
&Operation=Enqueue&QueueName=Jason
&QueueEntryBody.1=Dude,%20what%20are%20you%20doing?
&QueueEntryBody.2=sheer%20poetry

If this request succeeds, then you'll see a response like this:

....
   ....
  <EnqueueResult>
    <Request>
      <IsValid>True</IsValid>
      <EnqueueRequest>
        <QueueName>Jason</QueueName>
        <QueueEntryBodies>
          <QueueEntryBody>Dude, what are you doing?</QueueEntryBody>
          <QueueEntryBody>sheer poetry</QueueEntryBody>
        </QueueEntryBodies>
      </EnqueueRequest>
    </Request>
    <Status>SUCCESS</Status>
  </EnqueueResult>

The actual response from Amazon, assuming the request is valid and has no logic errors, is a single Status tag with a value of "SUCCESS." If the request failed for some reason, then an Error block would appear with the appropriate error code.

Reading Queue Entries

To read entries from a queue, we only need to know the name of the queue we created. Alternatively, we could use the QueueId that was returned by our CreateQueue call. You can specify up to 25 entries to read from a queue in one request by using the ReadCount parameter. Here, we just want to read the two entries we put on the queue.

http://webservices.amazon.com/onca/xml
  ?Service=AWSSimpleQueueService&Version=2004-10-14
  &SubscriptionId=001VHHVC74XFD88KCY82&Operation=Read
  &QueueName=Jason&ReadCount=2
....
  ....
  <ReadResult>
    <Request>
      <IsValid>True</IsValid>
      <ReadRequest>
        <QueueName>Jason</QueueName>
        <ReadCount>2</ReadCount>
      </ReadRequest>
    </Request>
    <QueueEntries>
      <QueueEntry>
        <QueueEntryId>
        0CSZ9B047F84K9G1SW7S|0QY2R0SBDXWVW87QG8ZD|100T0A28JX6EW1W4MYYF
        </QueueEntryId>
        <QueueEntryBody>Dude, what are you doing?</QueueEntryBody>
      </QueueEntry>
      <QueueEntry>
        <QueueEntryId>
        0ESP6SZGBPJEHNZ2V649|0QY2R0SBDXWVW87QG8ZD|100T0A28JX6EW1W4MYYF
        </QueueEntryId>
        <QueueEntryBody>sheer poetry</QueueEntryBody>
      </QueueEntry>
    </QueueEntries>
  </ReadResult>

Note that the entries come back in the same order they were put on the queue, e.g. FIFO, and that each entry has a corresponding QueueEntryId. Because I created the queue with the default Read Lock of 60 seconds, refreshing my browser window, and thus executing this again, should yield an Error block that indicates no results (typically, though, because of caching effects on the Amazon server side, the same entries are sometimes returned).

<SimpleQueueServiceError>
 <ErrorCode>AWS.SimpleQueueService.NoData</ErrorCode> 
 <ReasonText>Success, but no data was found in the queue.</ReasonText> 
</SimpleQueueServiceError>

Deleting Queue Entries

In order to delete an entry from a queue, we need the QueueEntryIds that are returned from the previous Read operation. The operation is called Dequeue:


http://webservices.amazon.com/onca/xml
?Service=AWSSimpleQueueService&Version=2004-10-14
&SubscriptionId=001VHHVC74XFD88KCY82&Operation=Dequeue&QueueName=Jason
&QueueEntryId.1=0CSZ9B047F84K9G1SW7S|0QY2R0SBDXWVW87QG8ZD|100T0A28JX6EW1W4MYYF
&QueueEntryId.2=0ESP6SZGBPJEHNZ2V649|0QY2R0SBDXWVW87QG8ZD|100T0A28JX6EW1W4MYYF

Like the Enqueue operation, a successful result just yields "SUCCESS":

....
....
<DequeueResult>
    <Request>
      <IsValid>True</IsValid>
      <DequeueRequest>
        <QueueName>Jason</QueueName>
        <QueueEntryIds>
          <QueueEntryId>
          0CSZ9B047F84K9G1SW7S|0QY2R0SBDXWVW87QG8ZD|100T0A28JX6EW1W4MYYF
          </QueueEntryId>
          <QueueEntryId>
          0ESP6SZGBPJEHNZ2V649|0QY2R0SBDXWVW87QG8ZD|100T0A28JX6EW1W4MYYF
          </QueueEntryId>
        </QueueEntryIds>
      </DequeueRequest>
    </Request>
    <Status>SUCCESS</Status>
  </DequeueResult>

A Chat Application Using Simple Queue Services

I tried to think "out of the box" for my sample SQS application. Why not a chat application? It makes perfect sense, or no sense at all, depending on your perspective. I'm also a big fan of Google's Gmail application, which is one of the coolest cross-platform Javascript applications ever created. I decided to use the XmlHTTPRequest object and the DOM Level 3 capabilities of Mozilla and Internet Explorer to craft my application. My chat application, which I call sqschat, actually runs off a file from your hard disk and doesn't require a web server at all (IE users may have to right-click that link and save to local disk). It does, however, require the following:

  • Either Internet Explorer 6 on Windows or a recent Mozilla browser (Firefox 1.0 or Netscape 7.2 works fine). Sorry, Safari 1.24 doesn't have DOM Level 3 (yet).
  • The files sarissa.js and sarissa_ieemu_xpath.js from the open source Javascript library Sarissa. Sarissa provides a Javascript compatability layer to smooth over the differences between Microsoft's XmlHTTPRequest and DOM implementations and the ones used by Mozilla and other DOM Level 3 compliant browsers. Get Sarissa here. (I used Sarissa version 0.9.4.2).

You should be able to run this application from your hard drive with no problem. You may have to dumb down the security settings on Internet Explorer, and Mozilla browsers require that you respond to a dialog box that warns you of accesses over the internet. You cannot run this file from a web server under Mozilla without digitally signing it first.

Application Design

My chat application takes advantage of the fact that Amazon SQS queues must have unique names (under a particular Subscription ID), and that you can search for queues using a prefix of the queue name. All users must be using the same Subscription ID so that they can access each others' queues. Each person in the chat has a single queue from which they read messages. The name of the queue is the name of the chat room appended to the name the user chooses to chat with. So, if Simon and Jason are chatting in the "lounge," Simon has a queue named "loungeSimon," and Jason has a queue named "loungeJason." When Simon says something in the chat room, the text is echoed to his screen and also placed on Jason's queue. Meanwhile, an endless loop keeps reading from Simon's queue in order to receive messages from Jason. After Simon reads a message from his queue, he deletes it. When he exits chat, his queue is deleted.

To find all the users currently chatting in the lounge, I do a ListMyQueues call with Prefix=lounge. This call will return all queues with a name starting with the string "lounge." I can then use this list of queue names to send my messages to everyone in the chat room.

Code Details

All of the heavy lifting in this application is done by a single function that I call makeHTTPrequest:


function makeHTTPrequest(targetURL) {
   // Set security -- only needed for Mozilla
   if(!_SARISSA_IS_IE){
     netscape.security.PrivilegeManager.enablePrivilege('UniversalBrowserRead');
   }
   // Get the HTTP request object
   var reqObj = Sarissa.getXmlHttpRequest();
   // Open the REST request, flag it as a synchronous request
   reqObj.open("GET", targetURL, false);
   // Send a header to force IE not to cache
   reqObj.setRequestHeader('If-Modified-Since','Sun, 31 Dec 2000 20:20:20 CST');
   // Make a synchronous request
   reqObj.send(null);
   // Return the results
   return reqObj;
}

The function takes an operation URL as an argument and returns the completed request object. The first thing I do is set Mozilla's UniversalBrowserRead privilege which lets us make HTTP requests outside of our machine. The constant _SARISSA_IS_IE is created by Sarissa and is a convenient way to test if we're using (or not using) IE. The actual HTTP request is made using Sarissa's cross-platform XmlHTTPRequest object. For simplicity sake, I use the synchronous version (the "false" in reqObj.open) rather than the asynchronous version. To make this more robust, choose the asynchronous version and catch any exceptions that occur.

The If-Modified-Since header is needed to force IE to not cache GET results. Without this header, IE often returns the same results over and over. Mozilla ignores the header and doesn't cache the requests.

A few important constants I use over and over are the Subscription ID, the name of the chat room, and the version of SQS that we're using:


var subscriptionId = "001VHHVC74XFD88KCY82"; // You subscription id
var chatroom = "poolhall";                   // The name of the chat room
var version = "2004-10-14";                  // The version of Amazon Queue 
                                                Service to use

Some pre-defined Xpath paths also come in handy for pulling data out of the responses from Amazon:


  // path to queue names
  var whoPath = "//aws:ListMyQueuesResult/aws:Queues/aws:Queue[*]/aws:QueueName";
  // path to Error code
  var errorPath = "//aws:ReadResult/aws:SimpleQueueServiceError/aws:ErrorCode";
  // path to queue entries
  var entryPath = "//aws:ReadResult/aws:QueueEntries/aws:QueueEntry[*]/aws:
     QueueEntryBody";
  // path to queue entry ids
  var idPath = "//aws:ReadResult/aws:QueueEntries/aws:QueueEntry[*]/aws:
     QueueEntryId";

The paths use a default namespace that I made up, called "aws". I define the default namespace for all my DOM documents using the targetNamespace from the Amazon WSDL. The version must match the version used in the REST requests.

// the generic DOM document
var domDoc = Sarissa.getDomDocument();
// set the default namespace to Amazon's default
Sarissa.setXpathNamespaces
  (domDoc, "xmlns:aws='http://webservices.amazon.com/AWSSimpleQueueService/" + 
   version + "'");

I start the script by getting the user's chat name from them and then creating their queue. I create the queue with a Read Lock timeout of 0 seconds, since there will be no one reading from my queue except for me and I always want queue entries returned immediately.


// Get the user's chat handle
var d = new Date();
var handle= prompt("Enter your chat name: ", " ");   
if ( (handle == ' ') || ( handle == null) )  {     
   // Anonymous chatters are Noname + time in seconds      
   handle="Noname" + d.getTime();
}
   
// Create the user's chat queue
var initUrl = "http://webservices.amazon.com/onca/xml?
  Service=AWSSimpleQueueService&Version=" + version + "&SubscriptionId=" + 
  subscriptionId + "&Operation=CreateQueue&ReadLockTimeoutSeconds=0&QueueName=" + 
  chatroom + handle;
var initcall = Sarissa.getXmlHttpRequest();
makeHTTPrequest(initUrl);

I then get a list of all the queues which have a prefix starting with the chat room name, which is the same as getting a list of all the people who are currently chatting in that chat room. I then create a DOM document out of the results and apply the whoPath Xpath query to get the list.


// Get the initial list of people in the chat
var inchatUrl ="http://webservices.amazon.com/onca/xml
  ?Service=AWSSimpleQueueService&Version=" + version + 
  "&SubscriptionId=" + subscriptionId + "&Operation=ListMyQueues
  &QueueNamePrefix=" + chatroom;
whocall=makeHTTPrequest(inchatUrl);

// Find the online users by their QueueName
// domDoc is parsed in the HTML at the bottom of the script
domDoc.loadXML(whocall.responseText);
nameList = domDoc.selectNodes(whoPath);

I cycle through the nameList later in the HTML in order to display the initial list of people chatting in the chat room.


div = document.getElementById("online");
div.innerHTML = "<b>Currently talking in chat room " + chatroom +": </b><br />";
for(i=0;i < nameList.length;i++) {
   n = Sarissa.getText(nameList[i]);
   div.innerHTML = div.innerHTML + "<b> " + n.substring(clen) + " </b>";
}

The rest of the application has two parts. One part is this loop that continually runs checkForever(), a routine that checks for new people (queues) that are entering the chat room and also grabs any incoming messages.


var ourInterval = window.setInterval("checkForever()", checkinterval);

The other part gets executed when you enter text into the text form field that I provide. When you enter a message into the chat, I put it on all the other chat members queues by cycling through the nameList:


for(i=0;i < nameList.length;i++) {
      n = Sarissa.getText(nameList[i]);
      if (n.substring(clen) == handle) continue;
      entry = encodeURIComponent("" + handle + "" + " > " + tin + "<br />");
      enqueueUrl="http://webservices.amazon.com/onca/xml 
        ?Service=AWSSimpleQueueService&Version=" + version + 
        "&SubscriptionId=" + subscriptionId + "&Operation=Enqueue
        &QueueName=" + n + "&QueueEntryBody.1=" + entry;
     makeHTTPrequest(enqueueUrl);      
   }

Retrieving incoming messages requires two calls. In the first call, I retrieve as many entries as possible from my queue. The SQS API lets you retrieve up to 25 messages per request by specifying ReadCount=25.


// Get any msgs from my queue
   myqueueUrl="http://webservices.amazon.com/onca/xml
     ?Service=AWSSimpleQueueService&Version=" + version + 
     "&SubscriptionId=" + subscriptionId + "&Operation=Read
     &QueueName=" + chatroom + handle + "&ReadCount=25";
   enqcall=makeHTTPrequest(myqueueUrl);

I then create a DOM document with the response and use Xpath requests to collect all the queue entries and all the queue entry IDs.

  
   domDoc.loadXML(enqcall.responseText);
   entryList = domDoc.selectNodes(entryPath);
   idList = domDoc.selectNodes(idPath);

Displaying the incoming messages is just a matter of cycling through the entryList and referencing the appropriate div tag:


// Output other people's noise
   div = document.getElementById("thechat");
   for(i=0;i < entryList.length;i++) {
      e = Sarissa.getText(entryList[i]);
      f = decodeURIComponent(e);
      div.innerHTML = div.innerHTML + f;
   }

Finally, I go through the idList and use the queue IDs to build the parameter list for the REST request that deletes the entries from my queue.I delete them so that subsequent read requests do not return them and also so that I can delete the user's queue when they exit (queues cannot be deleted unless they are empty).


// Remove msgs from my queue
   urlSuffix = '';
   for(i=0;i < idList.length;i++) {
      count = i + 1;
      urlSuffix = urlSuffix + '&QueueEntryId.'+ count + '=' + 
        Sarissa.getText(idList[i]);
   } 
   if (urlSuffix != '') {
      myqueueUrl="http://webservices.amazon.com/onca/xml
        ?Service=AWSSimpleQueueService&Version=" + version + 
        "&SubscriptionId=" + subscriptionId + "&Operation=Dequeue
        &QueueName=" + chatroom + handle + urlSuffix;
      makeHTTPrequest(myqueueUrl);
   }

I do an onUnload event on the body tag to try to delete the user's queue when they exit. Unfortunately, it only works if the user keeps the window open and navigates to another web page. If the user closes the window, it doesn't get executed, and the user's queue persists. Having a special 'exit chat' button is a better idea.


function Cleanup() {   
   var cleanupUrl = "http://webservices.amazon.com/onca/xml
     ?Service=AWSSimpleQueueService&Version=" + version + 
     "&SubscriptionId=" + subscriptionId + "&Operation=DeleteQueue
     &QueueName=" + chatroom + handle;
   makeHTTPrequest(cleanupUrl);
}

Redux

Amazon Simple Queue Services is indeed simple, and this application hardly stresses the full capabilities of it. But it does prove the general-purpose nature of SQS--how it can be applied to applications other than the usual business middleware. For more SQS code samples, visit Amazon's code samples page.



1 to 2 of 2
  1. Alleged DOM Level 3 capabilities of Mozilla and Internet Explorer
    2005-02-08 10:48:30 Martin_Honnen
  2. What a cool idea...
    2005-01-06 06:21:46 BrendanT
1 to 2 of 2