Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Patrick Trüssel
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Patrick Trüssel

Using A Closure To "Terminate" CFThread Tags Across Page Requests In Lucee CFML 5.3.6.61

By
Published in

While the CFThread tag has a "terminate" action; and Lucee has a threadTerminate() built-in function (BIF); these two approaches only work within a single page-request - any attempt to terminate a CFThread reference spawned from another page will result in a ColdFusion error. Over the weekend, however, as I was experimenting with task threads in Lucee CFML, I came up with an approach to cross-page thread termination that I thought was kind of interesting: using a ColdFusion Closure to communicate the intent to terminate in Lucee CFML 5.3.6.61.

To be clear, this approach doesn't "terminate" a CFThread tag in the same way that the native threadTerminate() function does. Meaning, it doesn't "interrupt" the thread. Instead, it merely provides a way to tell the CFThread tag that it should stop; and then, it's up to the CFThread logic to take that suggestion into account when executing its own internal control-flow.

Years ago, I looked at using Closures to create a CFThread tunnel. But, in that post, I was using the Closure to expose external data to the CFThread. In this post, I'm doing the opposite - I'm going to use the Closure to expose (so to speak) internal thread data to the outside world.

Specifically, I'm using a Closure to allow the outside world to change a thread-local variable, isRunning. By way of the thread scope, I'm defining a .forceQuit() method which will flip the isRunning Boolean to false. Then, my internal CFThread control-flow will see this change and break out of its own execution:

<cfscript>

	// Keep track of the spawned threads so that we can "force quit" them from another
	// page reference.
	if ( isNull( application.threadRefs ) ) {

		application.threadRefs = [];

	}

	// The execution of the CFThread tag is limited by the request-timeout of the server
	// and application. As such, we have to bump up the request-timeout to make sure that
	// our threads aren't terminated prematurely.
	setting
		requestTimeout = 120
	;

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	uniqueName = "Test Thread #createUniqueId()#";

	thread
		name = "testThread"
		type = "daemon"
		uniqueName = uniqueName
		{

		// I determine if the internal workings of the thread should continue running.
		var isRunning = true;

		// The THREAD scope acts as a "tunnel" by which the CFThread tag can expose data
		// to the outside world. In this case, we're going to expose a CLOSURE that sets
		// and THREAD-LOCAL VARIABLE to FALSE indicating that the CFThread should stop
		// executing its internal loop.
		thread.forceQuit = () => {

			systemOutput( "[ #uniqueName# ]: About to force quit thread." );
			isRunning = false;

		};

		// Keep "doing stuff" until we are told to stop.
		for ( var i = 1 ; isRunning && i <= 120 ; i++ ) {

			systemOutput( "[ #uniqueName# ]: Running iteration #i#.", true, false );
			sleep( 1000 );

		}

		systemOutput( "[ #uniqueName# ]: CFThread exiting.", true, false );

	} // END: Thread.

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	// Keep a reference to the thread so we can terminate it from another page.
	application.threadRefs.append( cfthread.testThread );
	
</cfscript>

As you can see, inside the CFThread tag, I have a for loop that will continue executing as long as the isRunning flag is set to true. This CFThread tag doesn't really have any additional logic; but, you can assume that the "loop" would be processing some sort of long-running asynchronous task.

Once the CFThread tag is spawned, I store a reference to it in the application scope, which allows me to reference that CFThread instance across pages. Which means, in my "stop" page, I can loop over these references and call the exposed .forceQuit() method:

<cfscript>

	if ( isNull( application.threadRefs ) || ! application.threadRefs.len() ) {

		echo( "No threads to kill." );

	}

	// Let's Loop over all of the cache CFThread instances and try to force-quit them.
	for ( threadRef in application.threadRefs ) {

		// NOTE: This IS NOT terminating the thread - it is calling a Thread-local
		// function that has logic that will, in turn, allow the thread to complete.
		threadRef.forceQuit();

	}

	application.threadRefs = [];
	
</cfscript>

Now, one point of CAUTION: the .forceQuit() function isn't actually defined and exposed on the thread scope until the CFThread tag has started executing. Which means, if the CFThread is queued-up for execution, this method reference will not yet exist. Of course, in this demo, I'm waiting until the threads are running before I try to stop them.

That said, if we start a CFThread, wait a few seconds, and then call the stop page, we get the following terminal output:

[ Test Thread 4b ]: Running iteration 1.
[ Test Thread 4b ]: Running iteration 2.
[ Test Thread 4b ]: Running iteration 3.
[ Test Thread 4b ]: Running iteration 4.
[ Test Thread 4b ]: About to force quit thread.
[ Test Thread 4b ]: CFThread exiting.

As you can see, we were able to start the CFThread tag on one page; and then, terminate the CFThread tag from an entirely different page.

It's not a perfect solution; but, it seems like it at least provides a viable way to influence CFThread tags across different pages within a Lucee CFML application. And, since CFThread tag attributes are passed by-reference in Lucee, we could probably come up with an even more robust way to accomplish.

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

Reader Comments

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