Performing Disaster Recovery After A ColdFusion Page Timeout Exception
Previously, I blogged about how CFSetting updates the page timeout setting and does not actually start a new timeout. I had written about this in terms of disaster recovery. I can't find the conversation at this moment, but someone had raised a good point that in order to add time to the request timeout you would need to know how long the page had already been running (or you might set a value in seconds that has already passed).
I had suggested that you could use GetTickCount() in the Application.cfm/cfc file to get the start time of the page, then use that to get the run time of the page when it times out. This is lame because it requires modifying the Application.cfm/cfc file, which just doesn't feel right. Luckily, I just found out how to get the processing time of the page without modifying any other templates.
Using that technique, we can create a slightly more elegant disaster recovery model for ColdFusion request timeout exceptions:
<cffunction name="KillTime" access="public" returntype="void" output="false" hint="I kill time for the given miliseconds."> <!--- Define arguments. ---> <cfargument name="MS" type="numeric" required="true" /> <!--- Get start and end tick out values. ---> <cfset var intStart = GetTickCount() /> <cfset var intEnd = (intStart + ARGUMENTS.MS ) /> <!--- Loop until this time is killed. ---> <cfloop condition="(GetTickCount() LT intEnd)"> <!--- Just try to kill some processing time. ---> <cfset intStart = (intStart * Pi()) /> </cfloop> <!--- Return out. ---> <cfreturn /> </cffunction> <!--- Set the current time out to be 2 seconds. We are setting this intentionally low because we want the page request to timeout. ---> <cfsetting requesttimeout="2" /> <!--- Try to kill some time. ---> <cftry> <!--- Here, we are killing time - 4 seconds to be approximate. This will exceed the request time out set above (2 seconds) and will throw an error. ---> <cfset KillTime( 4000 ) /> <!--- The KillTime() method call has timed out (just as we expected it to). ---> <cfcatch> <!--- Since we just caught an page timeout error, let's set a new request time out that is high enough to definitely let the page continue running. We HAVE to do this because the line of code after this requires too much processing time to run in the 15-30 miliseconds that we have left to work with. ---> <cfsetting requesttimeout="10" /> <!--- Now that we have some cushion time, let's use the page and fusion context objects to get the date/time stamp at which this page started processing. Then, get the number of seconds that that start date is smaller than the current date/time stamp value. ---> <cfset intRunTimeInSeconds = DateDiff( "s", GetPageContext().GetFusionContext().GetStartTime(), Now() ) /> <!--- Now that we know how long the page has been running, let's create a new timeout setting with that time plus whatever additional time we feel is necessary to perform our disaster recovery. In this case, we are going to add an additional 5 seconds. This will override the "Safe" CFsetting we ran above. ---> <cfsetting requesttimeout="#(intRunTimeInSeconds + 5)#" /> <!---------------------------------------> <!--- Perform disaster recovery here. ---> <!---------------------------------------> </cfcatch> </cftry> <!--- Get the new run time in seconds for the page. ---> <cfset intRunTimeInSeconds = DateDiff( "s", GetPageContext().GetFusionContext().GetStartTime(), Now() ) /> <!--- Output that run time. ---> <p> The page has been processing for: #intRunTimeInSeconds# Seconds. </p>
Running the above code, we get:
The page has been processing for: 2 Seconds.
As you can see, even though we were caught in the middle of a ColdFusion request timeout, we were able to recover from it quite nicely.
The KillTime() method is used just to force a page timeout after a period of time. Once that happens, notice that we are immediately setting a static CFSetting request timeout setting. I don't like that we have to do this, but unfortunately, the DateDiff() that we are performing is too slow to execute fully in what little "wiggle" room we have (around 16 ms before the page actually dies). The idea of that first CFSetting tag is to have a timeout that we definitely will NOT exceed. I made it 10 seconds, but heck, you could make it 600 seconds. It won't matter since we then quickly override it with something more appropriate.
Once we know that we are safe to run, we can then easily figure out how long the page has been running and set a new timeout that is equal to the existing page run time plus whatever time we feel we are going to need to do CFLog or CFMail or what ever you need to do.
Want to use code from this post? Check out the license.
This is a very good idea, but doesn't onError usually run after a timeout? So couldn't you get yourself an email that way, too? What are the other instances where this could be of help?
I suppose that you could use this method to redirect the user to another page when certain operations time out, but I'd be interested in hearing about specific applications for this.
The OnError() application even is policed by the same CFSettings as that the rest of the page processing. Therefore, if a CFQuery tag times out, OnError() will indeed fire, but if you try to do anything complex in the OnError(), it will fail.
This code above applies to all timeout exceptions as far as I know.
Just an aside regarding your killtime function....you could also do a java sleep like so:
<cfset thread = CreateObject("java", "java.lang.Thread")>
As seen on
Good thought, but as it turns out, sleeping a thread actually pauses the timeout:
I agree. Through a thread in java simpler
I agree that a Java thread would be simpler...... if it worked ;)
You may want to wrap cfoutput tags around your final output on line 112 though or it won't output. :)
Why do I even bother googling problems like this? I should know just to come to your site. :D
This looks great. I am looping over a list and pulling data from another site and for whatever reason it sometimes hangs up. I was just catching the error and continuing the loop. But I'm thinking now is to limit the timeout so it doesn't hang for such a long time on those problem items and just moves on to the next one faster.
Always happy to be a second pair of eyes and inspiration in any problem :)
And what should happen now? A major client with a HUGE application that was originally "written" in 4.5 and I'm rewriting in 8 started throwing ugly timeout errors. It has worked fine all these years. I found that the original "coder" *cough cough* used cfsetting liberally throughout the application based on how long HE thought the query or whatever should take. So in one place he set the timeout to 3 seconds. That wasn't bad but he used Fusebox to do this in so there are layers of pages and on each call he put a different timeout so I had to drill down changing them all.
But what really irked me is that he just let it show the ugly error page. Now with what you showed here, they will fail nicely. Actually what I did was set it so it will reset the timeout and retry the query at the longer timeout. It does this until it reaches the absolute max timeout at which time it says "AW S**T" and stops trying and displays an error.
He set the request timeout to 3 seconds sometimes?? Ha ha ha, that just seems silly. Talk about getting anal with your timeout settings.