Earlier today I blogged about ColdFusion's Asynchronous Gateway. I promised a more appropriate example to demonstrate the power of the gateway, and instead of watching the Steelers lose, I figured I'd write code instead.
One note: I don't mean to imply that using the Asynch Gateway for logging is bad, or not a "real" example. Michael Dinowitz wrote a good comment about he uses asynch logging on his own site. Be sure to read his remarks.
So - the new demo. A while ago I wrote a proof of concept for Adobe's new cfthread/cfjoin tags (which may or may not be added to the product). The code simply fetched my RSS feed, grabbed each URL, and then downloaded them via CFHTTP to the local file system. Here is an example of the code using nothing special at all, just simple CFML:
<cfhttp url="http://ray.camdenfamily.com/rss.cfm" result="result">
<cfset myrss = result.filecontent>
<cfset myrssParsed = xmlParse(myrss)>
<cfset myurls = xmlSearch(myrssParsed, "/rss/channel/item/link/text()")>
<cfset links = arrayNew(1)>
<cfloop index="x" from="1" to="#arrayLen(myurls)#">
<cfset arrayAppend(links, myurls[x].xmlvalue)>
</cfloop>
<cfloop index="loopcounter" from="1" to="#arrayLen(links)#">
<cfhttp url="#links[loopcounter]#" result="result">
<cfset filename = getDirectoryFromPath(getCurrentTemplatePath()) & "agtest/" & loopcounter & ".html">
<cffile action="write" file="#filename#" output="#result.filecontent#">
<cfoutput>Done with #links[loopcounter]#<br></cfoutput>
<cfflush>
</cfloop>
The first half of the code handles creating an array of links. (And will not be repeated in my next example, but it will still be used.) I then loop over each item in the array, fetch it, and store the result. On average this took about 8-9 seconds on my machine.
Now for the Asynch version. I created a new gateway instance named URLSucker. Obviously you want to use the Asynchronous Gateway type. I pointed it to a new CFC. Lets take a look at that code:
<cfcomponent output="false">
<cffunction name="onIncomingMessage" output="false" returnType="void">
<cfargument name="cfEvent" type="struct" required="yes">
<cfset var result = "">
<cfif structKeyExists(arguments.cfEvent.data, "url") and structKeyExists(arguments.cfEvent.data, "filename")>
<cfhttp url="#arguments.cfEvent.data.url#" result="result">
<cffile action="write" file="#arguments.cfEvent.data.filename#" output="#result.fileContent#">
</cfif>
</cffunction>
</cfcomponent>
This is rather short so it should be easy to explain. I mentioned in the last entry that your CFC has to have a method named onIncomingMessage. The data that I pass to the gateway will exist in the arguments.cfEvent.data value. For URLSucker, there are two values - a URL and a filename. I fetch the HTML and save it. Now let's go back to our CFM and see how the new calls work:
<cfset props = structNew()>
<cfloop index="loopcounter" from="1" to="#arrayLen(links)#">
<cfset filename = getDirectoryFromPath(getCurrentTemplatePath()) & "agtest/a_" & loopcounter & ".html">
<cfset props.url = links[loopcounter]>
<cfset props.filename = filename>
<cfset status = sendGatewayMessage("URLSucker", duplicate(props))>
<cfif status>
<cfoutput>Done with #links[loopcounter]#<br></cfoutput>
<cfflush>
</cfif>
</cfloop>
If you remember, the gateway expects a struct of data. I create it once before the loop since I will just be changing the values inside the loop. I create the URL and filename values I mentioned that my CFC will be using. I then use sendGatewayMessage, telling it to use URLSucker and passing in my structure. (Note the duplicate() call, I'll come back to it later.)
So - at this point, I've basically taken the slow part of my code out of the CFM and I'm letting the asych gateway handle running it. If you run the CFM again it will be incredibly faster. If you then monitor the agtest folder (that's where I stored the results), you will see the files pop up a few seconds after the CFM is run.
Pretty easy, eh? What's up with the duplicate() call then? Well, structs passed to functions are actually passed by referenced. I knew this - but I thought that there was enough of a disconnect between my CFM and the CFC that the duplicate wouldn't be needed. But without it, all your files end up using the filename and url of the last entry in the loop. Very surprising, but also very easy to fix.
p.s. As I wrote this, the Steelers have now taken the lead. Someone remind me to blog next time the Saints play.