Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Todd Sharp
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Todd Sharp

Communicating With The Client Whilst Inside A ColdFusion Custom Tag

By
Published in Comments (13)

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

198 Comments

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.

13 Comments

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.

39 Comments

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.

49 Comments

I have to agree with Dan here - AJAX updates are the way to go. In particular, because whitespace management and content compression break cfflush.

1 Comments

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)

15,880 Comments

@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.

15,880 Comments

@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.

49 Comments

@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.

15,880 Comments

@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.

22 Comments

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

15,880 Comments

@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.

22 Comments

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel