Using A Closure To Encapsulate CFThread Execution And Error Handling In ColdFusion
In ColdFusion, I'm a huge fan of using Closures to create a clean separation of concerns between the business logic and the low-level mechanics required to execute a given algorithm. I've used closures for things like managing temp directories, pulling resources out of a connection pool, and implementing distributed locks. And, when it comes to executing CFThread
tags, I almost always split my asynchronous code from my business logic. However, it wasn't until the other day that it occurred to me that I could probably use Closures to simplify the execution of asynchronous CFThread
tags in ColdFusion.
The basic idea behind using a ColdFusion Closure to create a separation of concerns is that you pass the closure out of scope and into another function. This other function then handles the low-level, non-business-logic code and invokes the passed-in closure as part of the execution. The pattern looks something like this (pseudo-code):
<cfscript>
runMyClosure(
() => {
// The business logic here inside my closure.
}
);
// ---
// The low-level mechanics are handled here in this other method.
// ---
private void function runMyClosure( required function operator ) {
// Low-level mechanics here.
// Low-level mechanics here.
operator(); // Execute the passed-in operator.
// Low-level mechanics here.
// Low-level mechanics here.
}
</cfscript>
Normally, when using this technique, I'm executing synchronous code, like file I/O; and, the closure is really just there to separate the logic from the mechanics. A decade ago, when ColdFusion 10 came out, I did a lot of experimenting with passing closures into a CFThread
tag; however, until now, it never occurred to me that I might use closures to completely encapsulate the CFThraed
tag logic itself.
To explore this idea, I've created a mock ColdFusion component for creating users. It has one public method, createUser()
, that needs to perform some asynchronous logging as part of its workflow. This logging is going to be defined inside a ColdFusion closure which will be invoked inside an asynchronous CFThread
tag. However, the CFThread
mechanics will be encapsulated inside their own method, runSafelyInThread()
.
component
output = false
hint = "I provide service methods for users."
{
/**
* I create a user record with the given credentials.
*/
public numeric function createUser(
required string email,
required string password
) {
var user = {
id: randRange( 1, 999999 ),
email: email,
password: password,
createdAt: now()
};
// The given closure will be executed inside a CFThread body. Any errors that
// occur during the spawning / execution will be safely caught and logged.
runSafelyInThread(
() => {
cfdump( var = "User [#user.id#] created.", output = "console" );
}
);
return( user.id );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I encapsulate the asynchronous spawning and execution of the given operator.
*
* When it comes to running code asynchronously inside a CFThread tag, there are two
* things that can go wrong: first, the JVM might be out of threads (very rare) and
* can't actually spawn a new thread for you to run. And second, once the thread has
* been spawned and is running, the code executing inside the CFThread body might throw
* an error that is lost into the ether. To encapsulate the error handling, all the
* business logic will be contained inside the passed-in Operator; and, all of the low-
* level mechanics of safely running a thread will be handled inside this method.
* --
* NOTE: There is also the possibility of "sudden thread death"; but, I don't believe
* we can handle that sort of error here.
*/
private void function runSafelyInThread( required function operator ) {
try {
thread
name = "UserService.runSafelyInThread.#createUuid()#"
action = "run"
operator = operator
{
try {
operator();
} catch ( any error ) {
logError( error, "Operator error inside runSafelyInThread operator." );
}
}
// There's a tiny chance that the JVM is out of threads due to high load. Catch
// those errors so that the parent request doesn't blow up.
} catch ( any error ) {
logError( error, "CFThread could not be spawned in runSafelyInThread." );
}
}
/**
* I log the given error and message to the console.
*/
private void function logError(
required any error,
required string message
) {
cfdump( var = message, output = "console" );
cfdump( var = error, output = "console" );
}
}
As you can see, the runSafelyInThread()
method takes care of error handling, both inside and outside of the CFThread
tag. And, this method handles the execution of the passed-in ColdFusion closure once the the asynchronous thread has been successfully spawned. This allows the passed-in closure to worry-not about the threading and focus solely on the logging.
Notice also that the closure is referencing the user
object with is part of the local scope of the lexical context!
To try this out, I created a simple test page that creates a single user:
<cfscript>
id = new UserService()
.createUser( "skroob@spaceballs.movie", "12345" )
;
writeOutput( "Woot woot, #id# created." );
</cfscript>
If we run this ColdFusion code - in the latest Adobe ColdFusion or Lucee CFML - and we look at the logs, we get the following output:
[INFO] string User [990685] created.
This is the logging that was generated by our ColdFusion closure that was passed into the CFThread
tag.
Not only did the ColdFusion closure maintain the bindings to its lexical scope after it was passed into the CFThread
tag, we were able to create a clean separation of concerns between the business logic (the logging) and the low-level mechanics (the thread spawning). It's kind of astonishing how easy ColdFusion makes it to run code asynchronously.
runSafelyInThread()
vs runAsync()
You might look at my runSafelyInThread()
method and wonder how it is any different from ColdFusion's native, built-in function, runAsync()
. The runAsync()
function takes a closure and it returns a Future
, which is somewhat akin to JavaScript's Promise
object.
In theory, the runAsync()
functionality is a great concept. However, in the past, when I've played around with the runAsync()
function, I've stumbled over many obstacles ranging from when it blocks to how it handles (or rather doesn't handle) errors. It's very possible that in the years since I've looked at the initial implementation of runAsync()
, all of these bumps have been smoothed out. However, for the time being, I find the CFThread
tag so much easier to reason about.
Want to use code from this post? Check out the license.
Reader Comments
@Ben,
Things may have changed in ACF, so I might be wrong, but I think you have some potential issues that would make this not thread safe.
I know that in ACF, the closure is used to hold references to the current request, so if any of the variables your closure is relying on die after the request, then if the thread lives longer than the request, then things break.
For example, if the closure relied on data from the Request scope or the closure itself was something cached in a scope (like the Application or Server scope).
I think the safer way to implement this would be to implement the
runSafelyInThread
method to take two arguments—the closure and a data argument which would be passed to the closure. This way you can safely pass in the data as an argument cfthread, making sure the data is thread-safe, and then you pass that into your closure.I'd probably also have the function return the thread name, so you could use that in a join if needed.
@Dan,
I think we actually had this very same conversation back in 2015 🤣 I'm not sure how much of that is still an issue. And, if you're spawning your threads inside a ColdFusion component, then I think most of the values referenced will either be in the local scope of the calling function or in the
variables
of the component (ie, the other methods). Though, your mileage may vary depending on personal preference and techniques.I am curious, though, if the issues you mention apply to
runAsync()
as well, since it's also using a closure to provide the meat of the functionality. 🤔 I've not used that function very much at all.That said, always good to know there might be caveats!
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →