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.