Learning ColdFusion 9: A Bug With File-Based Object Caching
I found a weird bug in ColdFusion 9's new caching system. It seems that when you cache an object that is based on a file, if you delete the file while the object is still in the cache, the cache becomes corrupt. To demonstrate this, I have created a ColdFusion user defined function that simply grabs a reference to itself from the system cache (based on the method name) and tries to update its own meta data:
<cffunction name="clicker" access="public" returntype="numeric" output="false" hint="I keep track of how many times I was clicked."> <!--- Get the refernce to this method from the cache. ---> <cfset local.this = cacheGet( "clicker" ) /> <!--- Get the meta data for this method. ---> <cfset local.metaData = getMetaData( local.this ) /> <!--- Param the click count to be zero. ---> <cfparam name="local.metaData.clickCount" type="numeric" default="0" /> <!--- Increment the click count and return the current click count on this method. ---> <cfreturn ++local.metaData.clickCount /> </cffunction>
As you can see, this method, "clicker", assumes that its reference was cached in the system cache at the ID, "clicker". Using this ID, it then grabs a reference to itself, params a click count variable, increments the variable, and then returns the current click count. Since ColdFusion function objects are first-class citizens, there should be nothing questionable about this.
Then, I created another ColdFusion page that uses the above UDF. However, rather than including it directly, it reads in the above UDF file as plain text and writes it to ColdFusion 9's new virtual file system (RAM disk). Then, the UDF gets included from the RAM disk, making the Clicker() method available in the page's variables scope. This method instance is then cached in the system cache and the RAM disk is cleaned up:
<!--- Read in the clicker file. ---> <cfset clickerCode = fileRead( expandPath( "./clicker.cfm" ) ) /> <!--- Write the clicker ColdFusion function code to teh RAM disk so that we can then include it into the current file. ---> <cfset fileWrite( "ram://clicker.cfm", clickerCode ) /> <!--- Include the RAM-based clicker method into the current page. This will make the method "clicker" available to the variables scope of this page. NOTE: We have a mapping to the ram disk as "/ram". ---> <cfinclude template="/ram/clicker.cfm" /> <!--- Get a reference to the first-class method object. ---> <cfset clickerMethod = clicker /> <!--- Add the clicker method to system cache so that the clicker method can refer to itself internally. ---> <cfset cachePut( "clicker", clickerMethod, createTimeSpan( 0, 0, 5, 0 ), createTimeSpan( 0, 0, 5, 0 ) ) /> <!--- Now that we have included the clicker method and have a handle on the function object, we can delete the code from the RAM. ---> <cfset fileDelete( "ram://clicker.cfm" ) /> <!--- Execute the clicker method a few times to see that it is successfully working - retrieving itself from the system cache and updating its own meta data. ---> <cfoutput> Clicked: #clicker()#<br /> Clicked: #clicker()#<br /> Clicked: #clicker()#<br /> Clicked: #clicker()#<br /> Clicked: #clicker()#<br /> </cfoutput>
As you can see, once we have a handle on the clicker Function object, we store it for 5 minutes in the system cache and delete the scratch code file from the RAM disk. Since ColdFusion treats Function objects as first-class citizens, there should be nothing odd about this and nothing that requires the Function object to have a corresponding code file. However, when we run the code above, rather than seeing the click count output, we get the following ColdFusion error:
An error occurred when performing a file operation lastModified on file /clicker.cfm. The cause of this exception was: org.apache.commons.vfs.FileSystemException: Could not determine the last modified timestamp of "ram:///clicker.cfm" because it does not exist.
If I go back and comment out this line:
<cfset fileDelete( "ram://clicker.cfm" ) />
... then everything works fine and when we run the page, we get the following output:
It seems that there is a bug in the ColdFusion 9 cache system where the cached objects need to correspond to a physical files. Because Functions and Components are so dynamic at runtime and can be manipulated to be completely different than their original definitions, it seems wrong that any physical file would be required for simple caching.
Want to use code from this post? Check out the license.
I have logged this bug:
This "bug" appears in other places at least as early as ColdFusion 8 (the only version with which I am familiar).
One example I have run into sometimes. Suppose you have a <cfinterface> and a <cfcomponent> implementing that interface. Suppose you create an instance of the component and cache the instance somewhere, then alter the interface file so that the interface and component definitions are no longer in sync, then attempt to use that instance later on. Boom!
Is this a bug? Good question. I'm going to say - no. Certainly, you would like ColdFusion to work differently. So that makes this a feature request.
ColdFusion is not among these languages, and neither is Java. Neither ColdFusion nor Java has native support for first-class functions (although some other languages that run on the JVM do support first-class functions).
Functions-as-first-class-objects means "inner anonymous bound closures".
- "Inner": these can be defined within other functions.
- "Anonymous": these need not be named, but may instead simply be assigned to variables as values directly (or passed directly into function calls, or returned directly from the function call they were defined in). Note that this is not necessarily a requirement, but is extraordinarily convenient (Python lacks this).
- "Bound": if these are defined in instance methods, then these must as well be instance methods (applies to object-oriented languages only).
- "Closures": these inner functions must capture as much of the lexical scope (e.g. function-local variables) as will be required - and must still be able to access and modify the captured portion of the lexical scope even after the original function has stopped executing.
ColdFusion function objects, like C function objects, fail to meet all four of these categories. Therefore I would certainly not call ColdFusion functions "first-class."
Ben, what happens when you turn on trusted cache? What if the temp file contained a cfcomponent tag and you created and cached an instance of that component instead of just a reference to the UDF?
Yeah, it would be awesome to get lexical binding. That would make life very awesome. The rest of the points do, to a certain extend, work. You can define functions within functions (as long as they are included). You can assign a function to a variable, but its initial definition cannot be anonymous. And, methods are bound to CFCs only when they are called as methods of the CFC (not executing a reference to a method in a CFC).
Obviously, these are not perfect, but adding lexical binding would be the cats pajamas.
Also, Adobe has confirmed that this is a bug (or a feature request) and has alerted me that it has been fixed for production.
I am not sure what would happen with CFCs. I'll see if I can play around with it.
Undoubtedly you've put two and two together already but I'd guess that the source of this bug is the caching by reference bug (undocumented feature) that Rob Brooks-Bilson talks about here: http://www.brooks-bilson.com/blogs/rob/index.cfm/2010/8/9/Bug-with-Ehcache-and-ColdFusion-Query-Objects
That's an interesting behavior that Rob is pointing out. I am not sure if it related, however. I think that mine has more to do with the meta data of the file and the trusted cache (theory). When a function gets created, ColdFusion creates meta data that gets associated with that function. When I delete the file, it's probably deleting the meta data, which I think, is messing up the function invocation.
I just updated to CF11, i have a function that writes a dynamic cfm to ram://
then includes the cfm inside a savecontent then deletes the cfm
Strangely enough this worked fine in CF9 but not in CF11. I get the error
Could not determine the last modified timestamp of ram:///myfile.cfm
I have commented out the delete tag, but i still get the same error.