Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Cage Sarin
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Cage Sarin

Monitoring ColdFusion Thread Activity And Status After Redirect

By
Published in Comments (21)

Yesterday, Bruno Soares asked me a very interesting question in the comments of my post on processing files with ColdFusion's CFThread tag. More or less, he asked me how one can keep track of both the status and the potential errors generated by a CFThread tag if the user is redirected away from a page while the threads are still being processed. When ColdFusion 8 first came out, I briefly explored the concept of cross-page thread references; but in that experiment, all of the processing logic was contained within the threads themselves - there was nothing really bringing them all together as a cohesive set of threads.

After Bruno asked me this question, I started to think about "Your Download Will Being Shortly" type pages. In those scenarios, custom headers (Refresh) are used to forward the user to the target file after they have viewed the intermediary confirmation page. This got me thinking about CFLocation and the type of headers it uses to perform 302 redirects (Temporarily Moved). A CFLocation tag sets the status code, status text, and location headers and then aborts the current page request such that nothing after the CFLocation tag will execute.

Part of what makes dealing with parallel threads difficult is that there is a conflict between providing immediate feedback to the user and allowing the current page enough time to properly monitor the thread execution. But, what if we used location headers without CFLocation? Would it be possible to redirect the user without terminating the current page such that the current page could still process the threads is a more meaningful and cohesive manner?

While launching a ColdFusion CFThread tag causes its execution to drift off into the ether somewhere, Joining a thread brings it back into context. Or rather, joining a thread forces the main page to pause processing until the given CFThread has finished executing and has updated its internal status. Joining a thread back to the primary page is a very powerful feature of ColdFusion because it gives the primary page the ability to examine the status and output of the thread to ensure that all processing has gone according to plan; and, if it has not gone according to plan - if the thread has raised an exception - it allows the primary page an opportunity to put appropriate response strategies into action (ala what Bruno is asking).

To be able to leverage the power of thread joining while still providing immediate feedback to the user, we need to redirect the user mid-page in a way that doesn't actually abort the current page processing. To do this, we have to manually set the location headers and flush them to the client:

<!--- Set the status code for the client. --->
<cfheader
	statuscode="302"
	statustext="Moved Temporarily"
	/>

<!--- Define the location to redirect to. --->
<cfheader
	name="location"
	value="./confirmation.cfm"
	/>

<!--- Flush the headers to the client. --->
<cfflush />

This will do exactly what the CFLocation tag does except, it won't Abort the page after the headers are set. As such, we'll be able to redirect the user immediately to the confirmation page while at the same time, still allow the current page to continue processing the threads cohesively.

Of course, it's not quite that simple! While the CFFlush tag is supposed to flush content to the browser, it doesn't necessarily do this right way. For some reason, the server won't commit the request (flush the headers) until enough content has been placed into the output buffer. So while we might execute a CFFlush tag, if the response doesn't build up sufficient content, the CFFlush won't be truly executed until the end of the entire request.

To get around this "limitation," we have to force the page content to surpass the minimum buffer size threshold. As such, we need to write 73,729 bytes of data to the output right before or right after the CFFlush tag is called. I am sure this is a server setting somewhere, but according to this thread, this seems to be right amount of data for ColdFusion 8 running on IIS.

Luckily, ColdFusion gives us the repeatString() method which makes generating a given amount of white space very easy. In the following demo, notice that after the CFThread tags are defined, I am using CFHeader to define the location and then CFFlush and repeatString() to flush those header values to the client. This allows us to redirect the user while still allowing the page to continue process on the server.

NOTE: In the following demo, I am storing thread information in the Application scope. This is just for demo purposes. Typically, you'd want to store user-specific information in the session scope. And, with multi-page information such as this, you'd probably want to use some sort of unique ID (UUID) that you pass along to the confirmation page.

<!--- Param the URL variables. --->
<cfparam name="url.launch" type="boolean" default="false" />

<!--- Check to see if the page has been submitted. --->
<cfif url.launch>


	<!---
		We are about to launch some threads that we want to
		keep track of, as such, let's set a flag in the Application
		scope.

		NOTE: We sould not typically store user-specific information
		in the "Application" scope, but this is just for ease of
		demonstration purposes.
	--->
	<cfset application.isProcessing = true />

	<!---
		We're going to keep track of the number of threads we will
		be processing with this request.
	--->
	<cfset application.threadCount = 10 />

	<!---
		We're going to keep track of the number of threads are
		actively being processed. This will start at the "thread
		count" and decrement as each thread completes.
	--->
	<cfset application.activeThreads = application.threadCount />

	<!---
		We going to keep track of the outcome of each thread after
		it has finished executing.
	--->
	<cfset application.threads = [] />


	<!---
		Iterate up to the total thread acount and launch as many new
		threads as needed.
	--->
	<cfloop
		index="threadIndex"
		from="1"
		to="#application.threadCount#"
		step="1">

		<!---
			Launch an asynchronous thread. This will execute in
			parallel with the primary page and will take almost no
			time do define.
		--->
		<cfthread
			name="thread#threadIndex#"
			action="run"
			index="#threadIndex#">

			<!---
				Randomly sleep the thread for several seconds to
				demonstrate that some heavy processing might be
				taking place.
			--->
			<cfthread
				action="sleep"
				duration="#randRange( 2000, 3000 )#"
				/>

			<!---
				At this point, the thread has finished performing
				whatever heavy processing it was gonna be doing.
				For tracking sake, let's decrement the active thread
				acount. Because this will create potential race
				conditions, we'll lock this variable.
			--->
			<cflock
				name="decrementActiveThreads"
				type="exclusive"
				timeout="3">

				<!--- Decrement thread count. --->
				<cfset application.activeThreads-- />

			</cflock>

		</cfthread>

	</cfloop>


	<!--- ------------------------------------------------- --->
	<!--- ------------------------------------------------- --->


	<!---
		Now that we have defined our threads, we want to provide
		immediate feedback to the user. We are going to do this
		in the form of a re-location. However, we cannot use a
		traditional CFLocation as this will abort the page request.
		Rather, we'll use CFHeader to define the CFLocation action
		without implying the abort.
	--->
	<cfheader
		statuscode="302"
		statustext="Moved Temporarily"
		/>

	<!--- Define the location to redirect to. --->
	<cfheader
		name="location"
		value="./confirmation.cfm"
		/>

	<!---
		Tell the browser to reset the content and define the
		mime-type for the client.
	--->
	<cfcontent type="text/html" />

	<!---
		Ok, now here's the really funky-ass part. We need to
		"convince" the server to actually flush data to the screen.
		Even if we call CFFlush, the server won't push content to
		the client unless it sees that it has enough. As such, we
		have to create enough white-space to convice the browser that
		it is prudent to flush the headers.

		NOTE: This was not an issue with earlier versions of
		ColdFusion, but seems to be popping up in CF8.
	--->
	<cfoutput>#repeatString( " ", 73729 )#</cfoutput>

	<!---
		Now that have set the headers and provided enough "contente,"
		we have to comit the response to the client. This will cause
		the headers to be passed back.
	--->
	<cfflush />


	<!--- ------------------------------------------------- --->
	<!--- ------------------------------------------------- --->


	<!---
		At this point, the User should have been redirected to the
		confirmation page; as such, the rest of the processing will
		not affect the user's experience. Let's now join all of the
		outstanding threads back to the page so we can use them.

		NOTE: We could have used the Name attribute to join a single
		thread; but, without the name attribute, this will cause all
		threads defined on this page to be joined.
	--->
	<cfthread action="join" />

	<!---
		Now that the threads have been joined, loop over the threads
		and add them to the array of processed threads.
	--->
	<cfloop
		item="threadName"
		collection="#cfthread#">

		<!--- Add this thread to the array. --->
		<cfset arrayAppend(
			application.threads,
			cfthread[ threadName ]
			) />

	</cfloop>

	<!--- Flag that the threads are no longer being processed. --->
	<cfset application.isProcessing = false />

	<!---
		At this point, we need to abort the page. Because we did a
		"soft" relocation, that page did not abort as it normally
		would have with a CFLocation. Now that we are done with all
		of our thread logic, however, we must manually abort.
	--->
	<cfabort />

</cfif>


<!DOCTYPE HTML>
<html>
<head>
	<title>ColdFusion CFThread Post-Redirect Monitoring</title>
</head>
<body>

	<h1>
		ColdFusion CFThread Post-Redirect Monitoring
	</h1>

	<p>
		<a href="./test.cfm?launch=1">Launch some threads</a>
		already!
	</p>

</body>
</html>

As you can see in the above demo, when the page starts to be processed, we are storing thread information in a central place (the Application scope). Then, the asynchronous CFThread tags are defined and the user is immediately redirected. Since we are manually setting the Location headers, however, the primary page will continue to process. This allows the current page to join all of the threads back into the main process where it can then examine their status and output values. While we are not doing it in this demo, the primary page could then easily respond to thread errors by sending out emails or launching subsequent threads or even logging data to the database.

To demonstrate the ease with which these threads are now managed, I set up a confirmation page that continually pings the server looking for feedback:

confirmation.cfm

<!DOCTYPE HTML>
<html>
<head>
	<title>ColdFusion CFThread Post-Redirect Monitoring</title>
	<script type="text/javascript" src="jquery-1.4.2.min.js"></script>
	<script type="text/javascript">

		// When the DOM is ready, initialize.
		jQuery(function( $ ){

			// Get the updates element.
			var updates = $( "#updates" );

			// Define a function to get the updates.
			getUpdates = function(){

				// Get the thread-processing updates from the server.
				$.ajax({
					type: "get",
					url: "updates.cfm",
					dataType: "json",
					success: function( results ){
						// Output the updates.
						updates.html( results.HTML );

						// Check to see if the updates are still
						// processing.
						if (results.ISPROCESSING){

							// Make another request to get updates.
							// Give the server a tiny rest.
							setTimeout(
								getUpdates,
								500
							);

						}
					}
				});

			};

			// Get the updates.
			getUpdates();

		});

	</script>
</head>
<body>

	<h1>
		Your Threads Are Being Proccessed.
	</h1>

	<p id="updates">
		<!--- Processing updates will go here. --->
	</p>

</body>
</html>

This confirmation page gets a processing flag and some HTML from the server. If the threads are still being processed, the confirmation keeps asking for updates. Here is the ColdFusion page that provides these AJAX-driven updates:

updates.cfm

<!--- Create the return struct. --->
<cfset response = {
	isProcessing = application.isProcessing,
	html = ""
	} />


<!---
	Check to see if the if the threads are still processing so we
	can see what kind of output to build.
--->
<cfif application.isProcessing>

	<!--- Save the processing output. --->
	<cfsavecontent variable="response.html">
		<cfoutput>

			#application.activeThreads# thread(s) still need to
			be processed.

		</cfoutput>
	</cfsavecontent>

<cfelse>

	<!--- Save the results output. --->
	<cfsavecontent variable="response.html">
		<cfoutput>

			<!--- Loop over the threads to output data. --->
			<cfloop
				index="thread"
				array="#application.threads#">

				#thread.name#:
				#thread.status#
				(#numberFormat( thread.elapsedTime )# milliseconds)<br />

			</cfloop>

		</cfoutput>
	</cfsavecontent>

</cfif>


<!--- Convert the response to a binary JSON response. --->
<cfset binaryResponse = toBinary(
	toBase64(
		serializeJSON( response )
		)
	) />

<!--- Stream the response back to the client. --->
<cfcontent
	type="text/json"
	variable="#binaryResponse#"
	/>

As you can see here, the updates page is checking to see if the isProcessing flag is true or false when defining the response HTML. In my previous exploration of cross-page thread references, something like this would not have been possible because there was no logic tying all of the threads together. However, since we are allowing the primary page to manage all of the threads through the Join action, the primary page can quite easily update the isProcessing flag once all of the threads have finished executing.

When you take this approach, you get the benefit of allowing the entire page to act as a cohesive unit. And, since we can do this without negatively impacting the user experience, it really makes the processing a lot easier. One thing to keep in mind, however, is that while CFThread tags never timeout, a standard ColdFusion request will. As such, if the primary page is going to be joining all of the threads back into the page, you will have to give it an appropriate requestTimeout setting such that it does not timeout immediatly after the threads have finished executing.

Want to use code from this post? Check out the license.

Reader Comments

4 Comments

I got to look more into this CFTHREAD. Since my shared hosting request timeout is at 30 seconds. When I need to process a ton of records after a file upload I always get a request timeout error. I assume I can chop the process into threads and bypass the timeout all together.

15,841 Comments

@Ernest,

Thanks my man. If you have any thoughts about CFThread or would like to see some more experimentation on anything specific, just let me know.

@Dmitry,

Yeah, I think pre-CF8, I used to be able to perform a CFFlush no problem, regardless of how much data I used in the buffer. An internal settings probably changed somewhere in the upgrades.

@Dusan,

No problem my man :) Glad to have stellar timing!

@Scot,

Yeah, CFThread never times out, at least not that I know of.

6 Comments

First off, let me say this stuff is amazing. I've implemented this for moving large files around in the backend. I even threw in a nifty percent complete progress bar.

However, I just ran into a problem. We are running CF9 so the cfflush should work by itself but I left the repeatString in just in case. I would think this would be browser independent but it only seems to work when I test it in FF. IE will not return until the thread finishes rejoining. In some cases, it will time out completely and I end up showing a nasty error to the user.

Do you know what might be causing this issue? The only thing I can think of is that the header for IE is different than FF. If it is smaller, it might just require more data than the 73729 to get it to flush. I tried doubling it to 147458 but I get the same result.

6 Comments

Ok now what really has me going is that I have pretty much the same exact code in another page and it works as expected. Both are called through ajax. The only difference is that the page that works is a form submission ajax function. It also passes the form data in the ajax call. The page that doesn't work just calls an ajax function on load. I tried adding "data: ''" to the ajax call and that didn't seem to make a difference.

15,841 Comments

@Evan,

I have definitely found that the CFFlush response in different browsers is, as you found, different. I am not sure why this is. One thing that we might try doing is adding a content-length header to the response headers and tell it that the content is really short. Not sure if that would help. Also, we could try adding an "interval" to the CFFlush:

<cfflush interval="1" />

... which should force the CFFlush tag to take place faster? I think?

But, definitely, I have seen differences in CFFlush behavior and it is irksome.

6 Comments

Adding <cfheader name="Content-Length" value="1"> did the trick. The interval="1" actually broke it. Also, since the you tell the browser there is only 1 byte of date, you don't need the full 73729 any more. I just put in straight text instead of more CF. This is my final code that seems to work across all browsers I've tried.

<cfheader statuscode="302" statustext="Moved Temporarily" />
<cfheader name="location" value="file_watch.cfm" />
<cfheader name="Content-Length" value="1">
<cfcontent type="text/html" />
hello
<cfflush />

Thanks for pointing me in the right direction.

15,841 Comments

@Evan,

Awesome - I'm glad that content-length trick worked :D That's great to know, especially if you no longer need to front-load the white-space. Actually, I'd say that's proof-positive that the flushing delay is a client-side rendering issue and not an actual ColdFusion-side flush concern.

15,841 Comments

@Evan,

Hmm, I'm playing around with this and actually finding the exact opposite. When I use a CFFlush[interval=1], it works great; however, when I add a CFHeader[content-length=1], my page just sits there and waits.

I have a CFThread[action=sleep] after the CFFlush to pause the page (so I can see if the re-location is taking place). It looks like it sits and wait for the thread to unsleep.

If I remove the content-length header, this works fine. Very odd. Very frustrating.

6 Comments

@Ben,

Very strange. I was using a sleep directly after the join as well. Easiest way to test it out. Content-header without interval=1 works for me in FF,IE, and Chrome. Without either, it works in FF (I think Chrome as well but haven't tested it). With both, it broke FF and IE (again didn't test Chrome). What version of each did you try it in. I used FF 3.6.7 Mobile (will try a full install), IE 7, and Chrome 8.

1 Comments

CF9 seems to have broken CFFLUSH. We have a large amount of code and this has always worked in FF and Chrome:

<cfoutput>#repeatString(" ", 73729)#</cfoutput>
<cfflush>

Now we are testing with CF9 and every one of the CFFLUSH commands no longer work. I even tried the demo from adobe on the CFFLUSH page and it does not work in FF or Chrome. If there are any new ideas we would certainly like to hear them as this may be the CF9 show stopper for us.

6 Comments

@Tim,

Use the code I posted above. We are using CF 9 and the code above works for IE7 and 8, FF, and Chrome. Let me know if you have any problems. I would love to find out how to make this sort of functionality more reliable.

2 Comments

Great posts Ben, I spend more time on your site finding out how to do things than I do with the Adobe Docs.

I like the Ajax calls to keep the user updated on the status, and maintaining a session is a great way of keeping track of threads launched by the user.

There is always more than one way to skin a cat in the programming world I was thinking of the following process maanged by one index.cfm page:

  1. Check if the user has a thread process launched in the session scope. If yes execute 2 else execute 3.
  2. If no thread process exists call a CFX which will launch the threads and populate session scope with thread status
  3. Output the current thread status from the session scope

This way the thread logic is separated from the presentation page, and no need for cfflush, cflocation, or cfaborts.

You would basically end up with index.cfm (the presentation page) and x_thread_launch.cfm (the cfx to launch the threads).

Although not as lightweight as Ajax we could use the refresh meta tag in the head section of the HTML for browser compatibility:

<meta http-equiv="refresh" content="600">
1 Comments

Ben,

my batch included retrieving db data and using it to do http requests, then finally saving results into db. however, only the first thread completed successfully, and the rest were terminated. I still can't figure what exactly was happening, but I could see that the first thread was completed using data from the 3rd or 4th thread.

then, I removed the cfthread code and I did the soft redirect before all the queries and http requests.

the updates page still displayed the count correctly but ofcourse without the end displaying the elapsedtime for each thread.

well, thanks to you i learned how to do this soft redirect which requires enough whitespace and a flush.

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