Using A Closure To "Terminate" CFThread Tags Across Page Requests In Lucee CFML 5.3.6.61
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