dans.blog


The miscellaneous ramblings and thoughts of Dan G. Switzer, II

Safely restart the Application scope in ColdFusion 8 and above

I've been doing some major restructuring of a legacy application lately and was looking for the safest method to "reset" the Application scope. We wanted to be able to safely push out small bug fixes to CFCs stored in the Application scope without having to restart the ColdFusion service just in order for the changes to take affect. We were looking for a way that would do this that was safe across threads, without having to go through and put locks on every one of our Application scope reads.

I did some playing with various techniques, but really wanted something like the ApplicationStop() function that was introduced in ColdFusion 9. Fortunately Dave, over at Mister Dai, had already done the ground work and discovered that ColdFusion 7 & 8 both have a coldfusion.runtime.ApplicationScopeTracker object that manages the Application scope. Inside this object is a method called cleanUp() which appears to do exactly what the ApplicationStop() function does in ColdFusion 9.

After a couple of quick tests, Dave's claim seem to be accurate—calling the cleanUp() method really did seem to safely end an Application lifecycle. However, I wanted to go deeper and really put together some tests I could run under load to see if my simple tests we accurate. Fortunately, even through significant load at the code showed stability. I put together several tests all of which should that the cleanUp() method seems to safely end the Application scope and the next new page request will re-initialize the Application scope as if the application was initialized for the first time.

While I'm not going to bother post all my load test scripts, I will share some very simple code which shows that the cleanUp() method does safely clean up the application:

<cfparam name="url.thread" type="numeric" default="1" />

<cfset secretKey = "your-secret-url-variable-name" />
<cfset secretValue = "your-secret-url-value" />

<!---// create random key //--->
<cfset key = createUUID() />
<cfif not structKeyExists(Application, "_")>
  <cfset Application["_"] = {} />
</cfif>

<cfoutput>
  <!---// show an existing random App keys //--->
  <cfdump var="#duplicate(Application['_'])#" />

  <!---// store the new random key in the App scope //--->
  <cfset Application["_"][key] = key />

  <!---// output key //--->
  <div>
    #Application["_"][key]#
  </div>

  <!---// for the first thread, let's run a CFHTTP to reset the Application scope //--->
  <cfif url.thread eq 1>
    <cfthread action="run" name="threadReinitApp">
      <cfset sleep(1500) />
      <cfhttp url="#getPageContext().getRequest().getRequestURL().toString()#?#secretKey#=#urlEncodedFormat(secretValue)#" method="get" result="thread.cfhttp" redirect="false">
      </cfhttp>
    </cfthread>
  </cfif>

  <!---// delay for 5 seconds, to make sure the reinit request has completed //--->
  <cfset sleep(5000) />

  <!---// output key, if Application scope is completely killed, then this would error //--->
  <div>
    #Application["_"][key]#
  </div>

  <!----// show the results of re-initializing app //--->
  <cfif isDefined("threadReinitApp")>
    <cfthread action="join" name="threadReinitApp" timeout="600" />
    <cfdump var="#threadReinitApp.cfhttp#" />
  </cfif>
</cfoutput>

What this code does is:

  1. Creates a random key in the Application scope
  2. Spawns a thread to re-initialize your application using your secret reinit key/value
  3. While the other thread has been spawned, the main thread waits 5 seconds so we have enough time for the re-init script to run
  4. Attempt to output the variable in the Application scope.

If the Application scope was reset with something like structClear(Application) then the second time we tried to output the Application["_"][key] the script would have generated an error. However, in all my load testing, this code has never thrown an error.

To actually re-initialize my Application, I implemented 2 custom UDFs that I add to my Application.cfc that are based on Dave's original code. The two functions are ApplicationReset() and ApplicationKill(). The ApplicationReset() method takes in your URL secret key parameter name and calls the ApplicationKill() method and then re-load the current page without the secret URL key. The ApplicationKill() is based on Dave's code, but provides a cross-ColdFusion version of ColdFusion 9's ApplicationStop() method.

<cffunction name="ApplicationReset" access="public" returntype="void" output="false" hint="Resets the application scope">
  <cfargument name="secretKey" type="string" required="true" hint="Secret URL key used to reset the application" />

  <!---// declare local variables //--->
  <cfset var currentUrl = getPageContext().getRequest().getRequestURL().toString() />
  <!---// get the current query string, but remove the secret key //--->
  <cfset var queryString = reReplaceNoCase(cgi.query_string, "(^|&)" & arguments.secretKey & "=.*?(&|$)", "\2", "all") />

  <!---// reset the application (from Application.CFC) //--->
  <cfset ApplicationKill() />

  <!---// add the query string //--->
  <cfif len(queryString)>
    <cfset currentUrl = currentUrl & "?" & queryString />
  </cfif>

  <!---// reload the current page w/out the reinit token //--->
  <cflocation url="#currentUrl#" addtoken="false" />
  <cfabort />
</cffunction>

<!---// stops an application, so that on next reque //--->
<cffunction name="ApplicationKill" access="public" returntype="boolean" output="false" hint="Implementation of ApplicationStop() that works with CF7 and CF8">
  <cfif isDefined("application")>
    <cftry>
      <cfif listFind("7,8", listFirst(server.coldfusion.productVersion))>
        <!---// this is just in case there's no app scope but variables.application //--->
        <cfset createObject("java", "coldfusion.runtime.ApplicationScopeTracker").cleanUp(application) />
      <cfelse>
        <!---// use CF9+'s thread safe method of restarting the application //--->
        <cfset applicationStop() />
      </cfif>

      <cfreturn true />
      <cfcatch type="any"></cfcatch>
    </cftry>
  </cfif>

  <cfreturn false />
</cffunction>

While you should always test stuff for yourself, I did spend a good amount of time running various load tests to ensure that these methods were safe. I was running 20 concurrent users simulating heavy load and would randomly cycle in a request to reset the Application scope at least 10% of the time. I ran dozens of these tests, resulting in 50,000+ page views without seeing any errors or unexpected results—and that was just with one specific load test, I actually ran a couple different tests and they all worked as expected.

So, if you're looking for a safe way to "restart" (or "reset") the Application scope in ColdFusion 8 it appears the safest method would be to use the ApplicationKill() method listed above.


Easy AJAX using ColdFusion, jQuery and CFCs

A recent post on CF-Talk asked whether ColdFusion could use AJAX to do a database lookup. This is actually extremely easy to do in ColdFusion 8+, because it natively supports returning data in the JSON format.

To show how easy this is to do, I decided to throw together a little demo. This took me about 10 minutes to write—most of which was writing the markup. However, the bulk of the work is going to be handled automatically by ColdFusion, which will handle converting your data to JSON and by the jQuery Field Plug-in (which I wrote) which will handle populating your form from the data it receives from ColdFusion.

To show just how easy this all is, here's the jQuery code required to make an AJAX call to a CFC:

$.ajax({
  // the location of the CFC to run
    url: "example.cfc"
  // send a GET HTTP operation
  , type: "get"
  // tell jQuery we're getting JSON back
  , dataType: "json"
  // send the data to the CFC
  , data: {
    // the method in the CFC to run
      method: "getUserById"
    /*
      send other arguments to CFC
    */
    // send the ID entered by the user
    , userId: $("#userId").val()
  }
  // this gets the data returned on success
  , success: function (data){
    // this uses the "jquery.field.min.js" library to easily populate your form with the data from the server
    $("#frmMain").formHash(data);
  }
  // this runs if an error
  , error: function (xhr, textStatus, errorThrown){
    // show error
    alert(errorThrown);
  }
});

Our CFC looks like this:

<cfcomponent output="false">
  <cffunction name="getUserById" access="remote" returnType="struct" returnFormat="json" output="false">
    <cfargument name="userId" type="numeric" required="false" />

    <cfset var user = structNew() />
    <!---//
      The only tricky part here is to use the bracketed notation
      to match the case in your HTML, JS is case sensentive.
      If you use the dot notation (user.name) then the keys will
      be returned in uppercase.
    //--->
    <cfset user["name"] = "User " & arguments.userId />
    <cfset user["email"] = "user_" & arguments.userId & "@example.com" />
    <cfif (arguments.userId mod 2) eq 0>
      <cfset user["gender"] = "f" />
    <cfelse>
      <cfset user["gender"] = "m" />
    </cfif>

    <cfreturn user />
  </cffunction>
</cfcomponent>

I've posted a working example so that you can see how this code looks. For the "User ID" just enter any number. If an error occurs, the error callback will handle displaying the error to the screen. I've also posted the source code as a zip file you can download.

NOTE:
If you're still on ColdFusion MX & 7, there's a little more work because non-string data automatically gets converted to WDDX. While there are JS libraries for converting WDDX to native JS objects, they can be hard to find now that the OpenWDDX site has been shut down. You can find tools at RIAForge though that can convert your data into a JS string--which jQuery will automatically evaluate when it's received.
UPDATE:
I've added some sample code to the download to show how you can use a proxy template in you're using ColdFusion 7 to get the same results. Just look at the example_cf7.cfm. The only difference is we call the example_cf7_proxy.cfm template instead of calling the example.cfc directly. The proxy template uses CFJSON to convert the results to JSON.

[UPDATED: Friday, March 04, 2011 at 9:59:11 AM]