Communicating With The Client Whilst Inside A ColdFusion Custom Tag
Last week, I was having a conversation with Matt about the way in which ColdFusion prevents any content from being flushed to the browser during the execution of a ColdFusion custom tag. Matt wanted to be able to report updates to the client during the creation of an Excel document being generated with my POI custom tags project. I explained that this wasn't possible unless the custom tag was running in the context of a different output buffer.
Recently, I demonstrated that you could use Pusher's HTML5 WebSockets to push updates from the ColdFusion server to the client while an intense process was running on the server. This same approach can also be applied to ColdFusion custom tags since the update "content" is not coming from the ColdFusion server - it's coming from the Pusher server. Of course, this requieres you to have a Pusher application configured, include Pusher's Javascript in your page, and make additional HTTP requests.
To keep things simple, I wanted to explore a way in which a ColdFusion custom tag could communicate with the client using nothing but the current request. Because ColdFusion prevents the flushing of the content buffer associated with the custom tag, the key to this approach is run the custom tag in a secondary output buffer. In ColdFusion, the way to achieve this is to run the custom tag in a different thread.
In ColdFusion, each CFThread tag gets its own, independent output buffer. Output generated within a given CFThread tag body does not affect the generated output of the parent page; nor does the output generated in the parent page affect the output of any given CFThread. As such, running a custom tag within a CFThread tag has no bearing on the ability to use CFFlush within the parent page. This output divergence is what will allow our custom tags to send updates to the client in the midst of execution.
In the following demo, I will be executing a custom tag inside an asynchronous CFThread body. When I execute the CFThread, however, I will pass in a Function reference that will act as the communication bridge between the custom tag and the parent page. When the custom tag wants to report an update, it will invoke this function, passing in the update message.
This communication-bridge (function) stores the update message in a request-scoped variable. Although the CFThread tag executes in parellel, it shares the same Request scope as its parent page. The parent page then waits for changes to this variable and reports them to the client when noticed. Let's take a look at the main request:
<!---
Since we are waiting for our thread in parallel, let's increase
the page timeout to ensure we can process the response.
--->
<cfsetting requesttimeout="60" />
<!--- I am the name of the thread we are going to execute. --->
<cfset request.threadName = "testThread#randRange( 1111, 9999 )#" />
<!---
I am the status variable (this is what the thread is going to
update as it processes).
--->
<cfset request[ "#request.threadName#_status" ] = "-" />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Define a function that we will pass into the thread to allow it
to report progress without having to know much about how this is
being echoed to the client.
--->
<cffunction
name="reportProgress"
access="public"
returntype="string"
output="false"
hint="I report the progress of the given thread.">
<!--- Define arguments. --->
<cfargument
name="status"
type="string"
required="true"
hint="I am the status of the calling context."
/>
<!--- Update the progress status in the request. --->
<cfset request[ "#request.threadName#_status" ] = arguments.status />
<!--- Return out. --->
<cfreturn />
</cffunction>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Launch our async thread. Notice that we are passing a reference
to our status update method into thread for it to use.
NOTE: We are also passing-in the file name that we want the given
thread to output it's content to.
--->
<cfthread
name="#request.threadName#"
action="run"
reportprogress="#reportProgress#"
filename="#expandPath( './#createUUID()#.txt' )#">
<!---
Pass controll off to the custom tag. Notice that with a
custom tag, we cannot typically FLUSH output to the client
during its execution. However, since this is executing inside
a CFThread, it is now in a differnt output buffer.
--->
<cf_generatedocument
filename="#attributes.fileName#"
reportprogress="#attributes.reportProgress#"
/>
<!---
Once the document has been gernated, let's just store the
file name back in the thread result so the parent page can
reference it.
--->
<cfset thread.fileName = attributes.fileName />
</cfthread>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<cfoutput>
<!DOCTYPE html>
<html>
<head>
<title>Your Document Is Being Generated</title>
</head>
<body>
<h1>
Your Document Is Being Generated
</h1>
<p>
Progress:
<span id="documentProgrress">
<!--- This is where we will output updates. --->
</span>
</p>
<!--- --------------------------------------------- --->
<!--- --------------------------------------------- --->
<!--- --------------------------------------------- --->
<!--- --------------------------------------------- --->
<!--- Flush the current output to the client. --->
<cfflush />
<!--- Get the current status. --->
<cfset currentStatus = "" />
<!--- Keep looping until the thread is finished. --->
<cfloop condition="(cfthread[ request.threadName ].status neq 'COMPLETED')">
<!--- Check to see if the status has been updated. --->
<cfif (currentStatus neq request[ "#request.threadName#_status" ])>
<!--- Update the document using the status variable. --->
<script type="text/javascript">
document
.getElementById( "documentProgrress" )
.innerHTML = "#request[ '#request.threadName#_status' ]#"
;
</script>
<!---
Save the current status so we don't get duplicate
output (in the source code).
--->
<cfset currentStatus = request[ "#request.threadName#_status" ] />
<!---
Flush the update to the client so the document
actually updates.
--->
<cfflush />
<!---
Sleep for a few milliseconds to let the document
have a change to update.
--->
<cfthread
action="sleep"
duration="100"
/>
</cfif>
</cfloop>
<!---
Now that the thread has completed, forward user to the
generated file.
--->
<script type="text/javascript">
location.href = "#getFileFromPath( cfthread[ request.threadName ].fileName )#";
</script>
</body>
</html>
</cfoutput>
As you can see, we are launching an asynchronous CFThread tag which, in turn, executes the GenerateDocument.cfm ColdFusion custom tag. While the custom tag is executing, the parent page is flushing its display markup to the client. At the end of the resultant HTML page, the parent page is holding the connection to the client open while it checks for updates to the request-scoped status variable. When those updates are detected, it alerts the client by way of a Script (Javascript) tag.
When the custom tag has finished executing, allowing the CFThread tag to finish executing (which changes its status to "COMPLETED"), the parent page redirects the client to the generated file. You could handle this is a number of ways; however, since the current request has already flushed content to the client, there is no way to also serve up the generated file within the same request.
Now that you have seen the parent page, let's take a look at the custom tag:
GenerateDocument.cfm (ColdFusion Custom Tag)
<!--- Param the tag attributes. --->
<cfparam
name="attributes.fileName"
type="string"
/>
<cfparam
name="attributes.reportProgress"
type="any"
/>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Create a buffer to hold our file content. --->
<cfset buffer = [] />
<!--- Create 1000 lines in our file. --->
<cfloop
index="lineIndex"
from="1"
to="1000"
step="1">
<!--- Append the line to the buffer. --->
<cfset arrayAppend(
buffer,
"Hey this is line #lineIndex# of my file!"
) />
<!--- Report progress. --->
<cfset attributes.reportProgress(
"#lineIndex# lines have been written to this file."
) />
<!---
To mimic something that is actually processing intensive,
sleep this thread for a bit.
--->
<cfthread
action="sleep"
duration="5"
/>
</cfloop>
<!--- Report file completion. --->
<cfset attributes.reportProgress(
"Your file has been successfully generated!"
) />
<!--- Write the content to file. --->
<cfset fileWrite(
attributes.fileName,
arrayToList( buffer, (chr( 13 ) & chr( 10 )) )
) />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Exist out of tag. --->
<cfexit method="exitTag" />
As you can see, the custom tag builds up the content for an output file and sleeps the current thread (our asynchronous thread) in order to mimic a truly intensive process. With each iteration, the custom tag uses the passed-in reportProgress() function to communicate with the parent page. At first, I was tempted to just communicate with the parent page using the Thread scope; however, this would require the custom tag to know something about its context. The beauty behind the reportProgress() function is that it completely encapsulates the way in which the communication is taking place between the two, independent threads.
In order for this to work, your ColdFusion custom tag obviously has to know that it is going to report its progress as it executes; however, assuming that you work that concept into your custom tag logic, I think this approach creates a mostly straightforward solution to pushing updates from a ColdFusion custom tag to the client within a single request.
Want to use code from this post? Check out the license.
Reader Comments
This is so hella tight.
I think you're better off using AJAX polling instead of the <cfloop>. These types of loops can really eat up CPU cycles, plus if you get an error in your thread, the loops going to run forever.
I think you're better off having a companion AJAX script that handles reporting progress back to the main template.
Ben,
Great Post, great experiment. The biggest take away here was that you can pass functions my reference. I had no idea you could do that in CF.
It makes me wonder if you could, sorta, replicate the anonymous functions which I enjoy in Javascript.
The way I've done this in the past was having the process constantly updating a session variable. Something like Session.ProcessStatus["UID"].message, then just use ajax to poll a simple page that returned that status variable.
I have to agree with Dan here - AJAX updates are the way to go. In particular, because whitespace management and content compression break cfflush.
Ben,
to overcome this we rely on session polling. It is simpler to implement and has proven to be fairly scalable and also works with other areas of CF that prevent flushing, i.e. function with output directive set to "No".
We determine a variable in the session scope that is used to hold messages for the user, e.g.
Session.UserMessage
The solution, then, is two fold:
a) Set the content of the session variable in the Customtag or function with progress information.
b)On the user page, initialize the poll zone which will retrieve the content of the session variable by reloading either a pop-up or inline-frame on the page on a scheduled basis (10-15 seconds).
Overall three advantages that we found valuable:
a) simple to do
b) CFFLUSH issues with Virtual desktop infrastructure (VDI). There is quite some delay before the flushed content will show.
c) Client-side polling will stop if user moves away from the page while cfflush will still attempt to push content
Nonetheless it is pretty cool to play with tcp/ip sockets ;o)
@Adam,
Thanks my man - I thought it was an interesting approach.
@Dan,
Perhaps if the parent thread was slept for longer per "wait" loop, then it wouldn't be a CPU problem? As for as a forever loop, that shouldn't be a problem. If the CFThread fails, the status will cease to be "RUNNING" and the parent-page loop will break out.
AJAX might be the more "safe" in terms of processing, but I just think it adds a lot of overhead to the whole relationship - now you need a way to persist information regarding the tag across requests.
Honestly, I think the "push" approach might be the best since those only communicate information as needed. And, since push allows you to create / subscribe to channels on demand, then it's just easy and can really remain in the context of a single page. But, again, a bit more overhead.
@Peter,
Oh yeah, big time. Passing functions around in ColdFusion is a super awesome feature. It allows for highly dynamic nature of ColdFusion components.
@Tim, @Dan, @Roland,
I think there are definitely some good ways to go about using an AJAX approach. In the past, I've played around with using an application-scoped for each CFThread... then each CFThread clears its own value when it is finished executing:
www.bennadel.com/blog/752-Learning-ColdFusion-8-CFThread-Part-IV-Cross-Page-Thread-References.htm
I suppose one thing that we can take away, however, is that no matter what you do, single-page or cross-page approach, I think the Function approach is nice because it allows the custom tag to interact with your "messaging system" without having to couple it to any particular implementation.
@Roland,
I am not sure I understand your comment about white space management? Can you expand on that?
@Bilal,
You make a really good point about the ColdFusion page continuing to flush content even after the client leaves (assuming the page didn't finish executing). I had not even considered that aspect.
@All,
I've had a night sleep to digest the comments made before. I think I'm beginning to agree - some sort of asynchronous communication with the main page is probably better than creating a Conditional Loop at the end of the parent page. I kept saying that AJAX adds overhead; but clearly, so does this.
That said, I think I am still liking the idea of Push over Pull as far as getting updates to the parent page.
In either case - push (realtime) or pull (ajax) - I think the implementation should be encapsulated through an intermediary so the custom tags don't have to know much about what's going on.
@Ben - sorry for the confusion - I didn't mean CF's built-in whitespace management. I meant any post-processing done by the web server or a load balancer, such as compression or whitespace management.
@Roland,
Ah, gotcha. I am not too familiar with that kind of stuff. But all in all, I'm agreeing that a parallel approach might be more preferable in the long run. I'd like to come up with a nice application architecture for push notifications.
Yes!! You did it... nice one. For our purposes of creating an Excel file in a popup or modal, using this cfthread method to use the poi custom tag while outputting which piece of information in the process we are on, will work great! Less hits on the server than polling and less overhead than a pusher service... at least it seems for now!
I'll let you know how it integrates once I plug it into one of the pages.
Thanks again,
Matt
@Matt,
I'm definitely interested to hear about how the integration goes. Also, there's been some good back and forth in the comment regarding some performance concerns, so just be sure to read them. Good luck.
Retraction of my last comment. Once I got deep into this and tested big excel files mapping to big new poi:document generated excel files in the cfthread with the loop, things got wierd.
I like the session variable, ajax polling method described above and may implement that with some of the poi usage in our app.
Thanks again for the proof of concept.
Matt