Cached Closures And User Defined Functions (UDFs) In ColdFusion
The other day, in my post about creating a closure-based tunnel between a CFThread and the parent function, Dan G. Switzer, II mentioned that I should be careful about using cached closures in ColdFusion. After he mentioned that, I did some preliminary testing and couldn't reproduce any problems. So, I kept on trying new things. And, eventually, I was able to reproduce the problem with a cached closure that referenced a user defined function (UDF) that was a method on a cached ColdFusion component.
It looks like Dan already logged a bug about this, which is apparently marked as Fixed. However, even with the latest update to ColdFusion 11, I can still reproduce this problem. So, either it's not really fixed; the problem wasn't really understood; this is a different problem; or, the update simply hasn't been released yet. To see the issue in action, I've created a small Logger Factory that returns a struct with two methods that access and mutate a lexically-bound array of log messages:
component
output = false
hint = "I create the logger using both closures and component methods."
{
/**
* I create a logger factory.
*
* @output false
*/
public any function init() {
return( this );
}
// ---
// PUBLIC METHODS.
// ---
/**
* I create the logger object, which is just a struct with closure-based methods.
*
* @output false
*/
public struct function createLogger() {
var messages = [];
var logger = {
log: function( required string message ) {
// *** CAUTION: Here, we are referencing a UDF from within the closure.
arrayAppend( messages, formatLogMessage( message ) );
},
getMessages: function() {
return( messages );
}
};
return( logger );
}
/**
* I am here to generically test if a closure can call a user defined function.
*
* @message I am the message being formatted.
* @output false
*/
public string function testFormatter( required string message ) {
var myClosure = function() {
return( formatLogMessage( message ) );
};
return( myClosure( message ) );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I provide trivial formatting on the given log message. Really, I am here to test
* whether or not the cached closure can reference this UDF at a future point in time.
*
* @message I am the message being formatted.
* @output false
*/
private string function formatLogMessage( required string message ) {
return( "[#dateFormat( now(), 'yyyy-mm-dd' )#] #message#" );
}
}
Inside the log() method, you can see that I am referencing the ColdFusion component method, formatLogMessage(). And, here's where things get dicey. According to the ColdFusion documentation, you shouldn't do this
Note: A closure cannot call any user-defined function, because the function's context is not retained, though the closure's context is retained. It gives erroneous results. For example, when a closure is cached, it can be properly called for later use, while a function cannot.
However, on a Stack Overflow question (referenced in the bug ticket), this is apparently an issue that has been fixed. But, not that I can see.
Ok, now that we have the log factory, we're going to create an instance of the LogFactory.cfc and an instance of the logger and cache then both in the Application scope:
component
output = false
hint = "I define the application settings and event handlers."
{
// I am the application settings.
this.name = hash( getCurrentTemplatePath() );
this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 );
// ---
// PUBLIC METHODS.
// ---
/**
* I initialize the application, setting up persisted objects.
*
* @output false
*/
public boolean function onApplicationStart() {
// NOTE: We are going to create and cache the log factory itself in order
// to ensure that the CFC isn't garbage collected (just eliminating possible
// referential integrity errors).
var logFactory = application.logFactory = new LogFactory();
// Create an instance of our closure-based logger and cache it in the
// application scope. This way, it will be accessible to future requests
// contexts beyond the initial application request.
application.logger = logFactory.createLogger();
return( true );
}
}
Once the application boots-up, our index page is going to log a message in both the primary page thread as well as in a spawned CFThread body:
<cfscript>
// Check for an application re-initialization script.
if ( structKeyExists( url, "init" ) ) {
applicationStop();
location( cgi.script_name );
}
// Let's spawn an asynchronous thread that will invoke the logger beyond the
// life-span of the current request object.
thread name = "async-logging" {
// Ensure parent request finishes before we try to log anything.
sleep( 2 * 1000 );
application.logger.log( "Hello from THREAD." );
}
// Use the logger from within the current request.
application.logger.log( "Hello from MAIN request." );
// Test to see if a cache component method can be invoked by a UDF as long as the
// UDF was created as part of the current page request.
// --
// writeDump( application.logFactory.testFormatter( "hello world" ) );
// Output all of the messages. While the current Thread message won't be
// available at this time, we should be able to see messages logged in previous
// request threads.
writeDump(
label = "Messages",
var = application.logger.getMessages()
);
</cfscript>
At first, this appears to work. At least for a few seconds (and a few page refreshes). Then, at about the same time that the CFThread completes (although not always consistently), the page starts breaking with the following ColdFusion error:
Response cannot be null
Now, if I go in there and comment-out the call to the logger and un-comment the call to the testFormatter() method, things work as expected. Even though the underlying ColdFusion component method is cached, the function expression - myClosure() - can invoke it just fine.
So, it seems that there is no inherent problem with a ColdFusion closure calling a ColdFusion user defined function as long as the closure is created and invoked within the life-span of the current page request. We only appear to run into a problem when the ColdFusion closure is invoked after the current request has ended and that closure subsequently invokes a ColdFusion user defined function.
Want to use code from this post? Check out the license.
Reader Comments