You know how ColdFusion is so robust that oftentimes there are those tags and functions that exist, yet you've never once had an occasion to use them? Well, today I used a tag for which I've previously not had a need: cfthread. I figured I'd share my use case and implementation in case it piques the curiosity of other CFTHREAD virgins.
The Scenario
Our company has a client who requested that we automate the portion of the process whereby a document originating from them is returned to them by us after we're finished with it. They've built some kind of "folder watching" process on their end, and so asked if we could sFTP the final file to their server. Digging in to the legacy code that performs the final processing, I identified the place where I could surgically place the code to perform this work. But, I didn't want to make the user to encounter any additional delays in page load time.
The Solution
Enter CFTHREAD, a sweet little tag that will allow me to very simply execute a chunk of code asynchronously and unattached from the page it resides within. By simply encompassing the autonomous bit of code within cfthread tags, CF will run it in parallel to the page itself. But, though I did want the upload itself to occur without hindering the execution of the remainder of the page, I DID want to give my user a final message indicating whether or not the upload was successful. In other, more relevant words, i wanted this autonomous process to join back up with the original request at the end, and update a message div on the page appropriately.
Since my standalone process was for sFTP purposes, I'm going to use that snippet of code in my example. Here is the section I needed to run on its own:
<!--- if this is a targeted client AND the estimate was approved, FTP it to the target site --->
<cfif thisCarrier IS targCarrier AND (PDFMAccRjt IS "A" OR PDFCAccRjt IS "A")>
<!--- fetch the ftp settings for client... --->
<cfquery name="qryGetSettings" datasource="#dsn#">
select ftpsite,ftpusername,ftppassword,ftpDestFolder
from clientTable
where carrier = <cfqueryparam value="#targCarrier#" cfsqltype="cf_sql_varchar" />
</cfquery>
<cfif qryGetSettings.recordcount eq 1 AND qryGetSettings.ftpsite IS NOT "">
<!--- get path to the PDF file --->
<cfset pathToPDF = "#expandpath("\PDF")#\#InvHolder#.pdf" />
<cfif fileexists(pathToPDF)>
<cfset ftpingfile = true /><!--- need this flag for evaluating things at the end of this template --->
<!--- create new file name... --->
<cfset newfilename = "#claimnum#-01--#dateformat(now(),"mmddyyyy")##timeformat(now(),"HHmmss")#.pdf" />
<cfthread action="run" name="FTPit">
<cfoutput>
<cfset objFTPProperties = {
Server = "#qryGetSettings.ftpsite#",
Username = "#qryGetSettings.ftpusername#",
Password = "#qryGetSettings.ftppassword#",
Secure = true
} />
</cfoutput>
<cfftp
action="open"
connection="objConnection"
attributeCollection="#objFTPProperties#"
/>
<cfftp
action="putfile"
connection="objConnection"
localfile="#pathToPDF#"
remotefile="#qryGetSettings.ftpDestFolder##newfilename#"
transfermode="auto"
/>
<cfftp
action="close"
connection="objConnection"
/>
</cfthread>
</cfif><!--- if the estimate pdf exists --->
</cfif><!--- if we found this carrier's ftp info --->
</cfif>
So, while the above code is executing (assuming we met our conditional criteria), the remainder of the page runs. At the end of the template, I use the following code to join the upload request back to the original request:
<!-- div to hold status message for parallel process... -->
<div id="uploading" name="uploading" style="color:red;">
<cfif ftpingfile>
<br><br>Please wait. Uploading file to #targCarrier#... <img src="images/spinner.gif" align="absmiddle" /><br><br>
<cfelse>
<br><br>NO PDF UPLOAD ATTEMPTED. EITHER NO PDF WAS FOUND TO UPLOAD, OR THE CLIENT HAS NO FTP SETTINGS IN THE SYSTEM<br><br>
</cfif>
</div>
<cfflush><!--- output the content of the page thus far to the browser so the user has something to see/do until our process finishes --->
<!--- if we attemptd to ftp a file... --->
<cfif ftpingfile>
<cfthread action="join" name="FTPit" />
<cfif cfthread.FTPit.status IS "Completed">
<script>
document.getElementById('uploading').innerHTML = "<br><br>Estimate Successfully Uploaded to <cfoutput>#targCarrier#</cfoutput>!<br><br>";
</script>
<cfelse>
<script>
document.getElementById('uploading').innerHTML = "<br><br><strong>There was a problem uploading the estimate. Here are the details:</strong><br><cfoutput>#JSStringFormat(cfthread.FTPit.error.detail)#</cfoutput><br><br>";
</script>
<!--- send details of failed attempt to Doug --->
<cfsavecontent variable="failedFTP">
<cfdump var="#cfthread#">
</cfsavecontent>
<cfmail to="dboude@adomain.com" from="administrator@adomain.com" type="html" subject="failed FTP attempt for invoice #InvHolder#">
FTP process failed. Here are the details:
<br>
<cfoutput>#failedFTP#</cfoutput>
</cfmail>
</cfif>
</cfif>
That's it! So while the user is looking at and potentially interacting with their page as usual, the status message div I placed will be updated appropriately as soon as the upload finishes. Just to clarify, the browser will still show its "loading" status until the threaded ftp process finishes, since we told CF to join it back to the original request. But, because we joined it at the end, and we cfflushed prior to that, the user will see their typical output in the meantime.

