A buddy and I were chatting yesterday about a particular piece of logic he needed to implement. He wanted to have some particular code run on a page request, but no more than once every five minutes. Because it needed to happen on a page request, cfschedule would not have fit the bill. Therefore it needed to be bound to a particular request. I suggested the following simple bit of code.
<cfcomponent output="false">
<cfset this.name = "demo">
<cffunction name="onApplicationStart" returnType="boolean" output="false">
<cfset application.lastprocess = now()>
<cfreturn true>
</cffunction>
<cffunction name="onRequestStart" returnType="boolean" output="false">
<cfargument name="thePage" type="string" required="true">
<cfif datediff("n", application.lastprocess, now()) gt 3>
<cflog file="application" text="I'm doing that thing you asked me to do every few minutes.">
<cfset application.lastprocess = now()>
</cfif>
<cfif structKeyExists(url, "init") >
<cfset onApplicationStart()>
</cfif>
<cfreturn true>
</cffunction>
</cfcomponent>
Not exactly rocket science, but the basic idea is to simply record the last time the process was run. This defaults to the application start up time. If more than 3 minutes (not 5 as I said earlier, wanted to make it easier to test) have passed, we run the process again and update the application variable. In this example the "process" is a cflog command, but it could really be anything. If your process is more than one line of code though you would want to abstract it out into it's own method.
So what's the big thing missing from this? Locking. The onRequestStart method is not single threaded. It is possible that two, or more, requests may end up running my process. So why didn't I lock it? I think it is important to remember that just because something can be run more than once, it doesn't alway simply that it is important enough to lock. I'll probably get some push back on that (bring it on, baby!) but if your process is something not impacted by multiple requests, then do you need to bother worrying about it?
That being said, if you did want to ensure that only one request ran the process, it wouldn't necessarily be a lot more work. Here is the modified version (just the onRequestStart method).
<cffunction name="onRequestStart" returnType="boolean" output="false">
<cfargument name="thePage" type="string" required="true">
<cfset var needToRunProcess = false>
<cflock scope="application" type="readonly" timeout="30">
<cfif datediff("n", application.lastprocess, now()) gt 3>
<cfset needToRunProcess = true>
</cfif>
</cflock>
<cfif needToRunProcess>
<cflock scope="application" type="exclusive" timeout="30">
<cfif datediff("n", application.lastprocess, now()) gt 3>
<cflog file="application" text="I'm doing that thing you asked me to do every few minutes.">
<cfset application.lastprocess = now()>
</cfif>
</cflock>
</cfif>
<cfif structKeyExists(url, "init") >
<cfset onApplicationStart()>
</cfif>
<cfreturn true>
</cffunction>
Certainly a bit more verbose, eh? The readOnly lock is a "gentler" lock that won't slow down the application quite as much. The exclusive lock is where the magic happens. Why do I duplicate the CFIF condition? It's possible that two threads can come in and set needToRunProcess to true. Only one thread can access the inside of the exclusive lock. I check it again because an earlier thread may have finished the update.
Archived Comments
I think this might just help with refreshing my Application variable for my round robin display :)
Thanks yet again Ray.
Ok, I thought you have to have a named lock for this to properly avoid race conditions because if you had a another "scope" lock running at the same it would take precedence?
@Spills: When working with a scoped variable, you make use of the scoped lock. When dealing with other things, like files for example that you want to single thread access to, you would use a named lock.
Thanks for the quick explanation Ray. One more question about nested locks, I thought when mixing the cflock type i.e. "readonly or exclusive" the next lock can be overriden by the first lock type in some cases?
I'm not mixing them. I'm using a readOnly lock to see if I _need_ to do the work. The exclusive lock is where the work is actually done. So you have a 'gentler' lock up front, and the harder, single thread only lock, is run only when we THINK we need to do stuff.
Sorry, I realize that you are not nesting your locks, just more curious as I recently inherited an application that has a ton of cflock code and lots of it is nested. I suspect the great majority of it is not even needed but just trying to get a handle on it.
Thanks for your great blog!
In the "old days" (CF5 and earlier), you had to go lock crazy or your server would (probably) crash and burn. I still see a lot of 'over locking' - hence my intentional example w/o locks. :)
How do you stop it?
You don't. :)