This weekend I exchanged a few emails with a reader about how you can handle very slow processes in a Model-Glue application. Typically folks will handle slow processes using one of these methods:
- cfflush: Print out a 'Please Wait' type message, use cfflush to flush out the content, and then start the slow process
- cfthread: You can use cfthread to either run a bunch of parallel slow processes at once, or 'fire and forget' a slow process
- scheduler: Use the ColdFusion scheduler to run the slow process completely outside the view of the site visitor.
Of course the best way to handle a slow process is to ensure you've done everything possible to speed it up. As an example, I was convinced that a particular process on coldfusionbloggers.org was slow because it had to be. Turned out it was a stupid SQL mistake on my part. So before any attempt is made to mitigate or hide a slow process, you need to do everything possible to ensure you haven't missed something obvious.
Once you've done that, what next? If you ever tried to use cfflush within a Model-Glue view, you know what happens:
Message Unable to perform cfflush.
Detail You have called cfflush in an invalid location, such as inside a cfquery or cfthread or between a CFML custom tag start and end tag.
Because your view file ends up being run as a custom tag (behind the scenes) you can't use the cfflush tag. So what about cfthread?
I created a simple demo application (available as a zip to this blog entry) using Model-Glue 3. I began by creating a new event, page.slow, that would represent my slow process:
<event-handler name="page.slow">
<broadcasts>
<message name="doItSlow" />
</broadcasts>
<results>
<result do="template.main" />
</results>
<views>
<include name="body" template="pages/slow.cfm" />
</views>
</event-handler>
The doItSlow controller method is where I put my slow process:
<cffunction name="doItSlow" access="public" output="false">
<cfargument name="event" type="any" required="true">
<!--- First, am I running the slow process? --->
<cfif structKeyExists(application, "slowprocess")>
<cfset arguments.event.setValue("status", "ongoing")>
<cfset arguments.event.setValue("progress", application.slowprocess)>
<cfelse>
<cfset arguments.event.setValue("status", "began")>
<cfthread name="slowprocess" priority="low">
<cfset application.slowprocess = 0>
<!--- run 10 processes that take 1 minute each. --->
<cfloop index="x" from="1" to="10">
<cfset application.slowprocess++>
<cfset sleep(15000)>
</cfloop>
<cfset structDelete(application, "slowprocess")>
</cfthread>
</cfif>
</cffunction>
There are two main things happening in this method. If I see that a particular application variable doesn't exist, I begin the process within a cfthread block. I use an application variable, slowprocess, to both signify that I've begun the process and to record how far along I am. If the application variable does exist, note how I use the event object to record what's going on and how far along we are.
Now I don't think you would normally be running the slow process from the controller. This would typically be in the model with the controller simply firing it off and asking a service object (for example) for an update on what's going on. I only used the controller here for everything since I wanted a quick demo.
The view is interesting. I'm going to display the current status and do an automatic reload:
<cfset status = event.getValue("status")>
<cfset progress = event.getValue("progress")>
<cfset event.setValue("usemeta",true)>
<cfoutput>
<b>status=</b>#status#<br/>
<b>progress=</b>#progress#
</cfoutput>
The usemeta is simply a flag to my template view:
<cfset usemeta = event.getValue("usemeta", false)>
<html>
<head>
<link rel="stylesheet" type="text/css" href="css/stylesheet.css"></link>
<cfif isBoolean(usemeta) and usemeta>
<meta http-equiv="refresh" content="10">
</cfif>
</head>
<body>
<div id="banner">Demo</div>
<!--- Display the view named "body" --->
<cfoutput>#viewCollection.getView("body")#</cfoutput>
</body>
</html>
So the end result is - the person starts the process and can just sit back and watch as the page gives an updated status on the process.
Again - this is just a quick demo. It isn't best practice or anything. (In fact, it will continuously reload the process.)
So I was going to stop there. But why stop when you can try something cool? Many moons ago I blogged about XML/SWF Charts, a cheap, and very sexy, charting engine. One of the coolest feature is it's ability to point to an XML file to both configure the chart and create an auto-reload data set for the chart. What follows is video of a modified version (also in the zip) where the chart engine itself runs the request for the status. The resulting data is output in the XML format required for the chart.
I'm really impressed by this charting engine. It may not be as easy as cfchart, but it is certainly as pretty, and the auto-update for data is worth the price in itself.
So outside of the pretty charts - have folks done anything like the above code? (Again, ignoring the fact I used the controller.)