Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Joel Hill
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Joel Hill

Spawning Nested ColdFusion Threads With Synchronous Fallbacks

By
Published in Comments (4)

Super quick post on ColdFusion threads since I have so little time these days. A couple of weeks ago, I blogged about separating your threading logic from your business logic. In addition the points outlined in that post, I've found that this approach has another excellent and unexpected benefit - it makes it easy to spawn threads even if you are unsure as to the threading nature of the calling context.

In ColdFusion, you can easily spawn asynchronous threads using the CFThread tag. You cannot, however, spawn one thread from inside of another (ColdFusion consciously added that limit to prevent people from shooting themselves in the foot). This makes it somewhat problematic when you want to encapsulate threading logic.

What I have found, though, is that if you separate your synchronous methods from your asynchronous methods, it positions you perfectly to spawn asynchronous code with a synchronous fallback. By that, I mean that your encapsulated code can try, by default, to run logic asynchronously; however, if accidentally-nested CFThreads raises an error, your encapsulated code can then catch that error and fallback to running the logic synchronously (inside the existing CFThread context).

To demonstrate this, I put together a Logging component that logs messages to a text file. By default, the logging method will try to execute the logging asynchronously; however, if it fails to do so, it will fallback to a synchronous execution.

<cfscript>
component
	output = false
	hint = "I log message to the given text file."
	{

	// I initialize the logger to log to the given file.
	public any function init( required string aFilePath ) {

		filepath = aFilePath;

		return( this );

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I log the given message.
	public void function logMessage( required string newMessage ) {

		// ColdFUsion has a limit in that threads cannot spawn other
		// threads. As such, if we try to run this code asynchronously,
		// it's possible that it will fail, if the calling context is
		// already inside of a thread. As such, we can TRY to call the
		// code asynchronously; and, if it fails due to a nested-thread
		// error (the only possible point of failure), we can fall back
		// to executing the logging synchronously.
		try {

			logMessageAsync( newMessage );

		} catch ( any error ) {

			logMessageSync( "... Asynchronous logging failed." );
			logMessageSync( newMessage );

		}

	}


	// ---
	// PRIVATE METHODS.
	// ---


	// I execute the logging ASYNCHRONOUSLY.
	private void function logMessageAsync( required string newMessage ) {

		thread
			name = "Logger.logMessageAsync.#createUUID()#"
			action = "run"
			newmessage = newMessage
			{

			logMessageSync( newMessage );

		}

	}


	// I execute the logging SYNCHRONOUSLY.
	private void function logMessageSync( required string newMessage ) {

		lock
			name = filepath
			type = "exclusive"
			timeout = 5
			{

			var logFile = fileOpen( filepath, "append", "utf-8" );

			fileWrite( logFile, ( newMessage & chr( 10 ) ) );

			fileClose( logFile );

		} // END: Lock.

	}

}
</cfscript>

The nice thing about this approach is that the Try-Catch is not mysterious. At first glance, you may worry that this setup could cause duplicated logging, depending on what the error is; but, it can't. Since the asynchronous method does nothing more than spawn a thread, the threading mechanism is the only possible source of error; any error inside of the spawned CFThread will fail to bubble up to the Try-Catch block.

To see this in action, here's a quick test script that logs messages inside and outside of ColdFusion CFThread tags:

<cfscript>

	// Create our logger.
	logger = new Logger( expandPath( "./log.txt" ) );


	// Now, let's try to log serval message from both the currently
	// executing page as well as from asynchronous threads.

	logger.logMessage( "In parent page 1." );

	thread
		name = "async1"
		action = "run"
		{

		logger.logMessage ( "In thread 1." );

	}

	thread
		name = "async2"
		action = "run"
		{

		logger.logMessage ( "In thread 2." );

	}

	logger.logMessage( "In parent page 2." );

</cfscript>

When we run the above code, we get the following log output:

In parent page 1.
... Asynchronous logging failed.
... Asynchronous logging failed.
In parent page 2.
In thread 1.
In thread 2.

As you can see, all four messages were logged successfully; however, the two messages logged inside of the top-level CFThread tag caused the encapsulated asynchronous logging to fail (due to nested threading).

Often times, when you deal with I/O bound tasks like writing to the file system or communicating with a 3rd-party API, you don't necessarily need to wait for an action to complete. In such cases, running said actions inside of a CFThread makes a lot of sense. However, when you are unsure as to whether spawning a thread is "safe", separating the synchronous methods from the asynchronous methods can make for an optimistic plan with safe fallbacks.

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

Reader Comments

10 Comments

Thanks for this Ben - I was just wondering how to handle this the other day. As it turns out I've had to pass in to the function whether or not it should run in a threaded manner, but next time I will use your method. :-)

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