If CFThread truly had an Interval attribute, the body of the CFThread tag would provide the behavior which would be run at the end of every interval; since this is just a proof of concept, however, we have to invoke a little hackery to make things work. In the following code, I am using a beautiful idea that Elliott Sprehn outlined in his CFUNITED 2010 talk - "I Bet You Didn't Know You Could Do That With ColdFusion." Specifically, I am referring to defining a ColdFusion user defined function (UDF) as the "body" of a ColdFusion custom tag:
<cf_mytag iterator="myBody"> <cffunction name="myBody"> <!--- Tag body here. ---> </cffunction> </cf_mytag>
As you can see here, where we would normally define the custom tag body, we are, instead, defining a user defined function. Then, we are passing that UDF name (or reference if you prefer) to the custom tag such that it may manually invoke the UDF as the custom tag body (either once or in an iterative mannor). Using a UDF-as-body approach does create some complications as far as binding and execution context go; but, as you'll see in my proof of concept, these hurdles can be overcome with a little bit of hackery (different than the "Arguments" approach Elliott used in his presentation, although ironically, based on something else that Elliott taught me).
Now that you understand the use of UDFs as custom tag bodies, let's take a look at my CFThread-Interval proof of concept:
<!--- Set the counter that will be incremented by the interval callback method. ---> <cfset counter = 1 /> <!--- Start an asychronous thread and run it at intervals of 500 millisconds. Since I am not *truly* defining a CFThread here, I need provide a function callback in leu of the CFThread tag body.... yo, this is just a proof of concept! In reality, the CFThread body would be THE function that executed at intervals. ---> <cf_thread name="interval" action="run" interval="250" callback="intervalCallback"> <!--- ---> <cffunction name="intervalCallback"> <!--- With each interval, increment the counter. ---> <cfset counter++ /> </cffunction> <!--- ---> </cf_thread> <!--- Sleep the current thread - that should give our interval enough time to execute several interval durations (and update the counter variable). ---> <cfthread action="sleep" duration="2500" /> <!--- Kill our interval thread. We don't want this beast running indefinitely on the machine. ---> <cf_thread name="interval" action="terminate" /> <!--- Output the current counter variable. ---> <cfoutput> Counter: #counter# </cfoutput>
To mimic the CFThread tag, I've created a thread.cfm (cf_thread) ColdFusion custom tag. As I explained above, this custom tag uses a user defined function, "intervalCallback," in leu of an authentic CFThread tag body. While this concept might seem foreign, the rest of this demo should be easier to understand: we're creating a couter in the main page which our spawned thread will then increment at 250ms intervals. After we launch our asynchronous thread, I am sleeping the primary thread for a few seconds in order to allow enough time for the "interval" thread to execute a few times.
When we run the above demo, we get the following output:
As you can see, in the 2.5 seconds that the primary page went to sleep, our asynchronous thread, running at 250ms intervals, was able to execute 9 times, incrementing the counter once per execution.
Ok, so how does this cf_thread custom tag work? Typically, when you execute a user defined function (UDF), such as the one we are using for the custom tag body, the UDF executes in the context of the calling page, not the defining page. As such, we have to dip down into the Java layer to manually execute the tag-body UDF in the context of the parent page as defined by the custom tag's Caller scope. Once we can do that, the asynchronous behavior itself is simply defined as a CFThread that runs for an indefinite amount of time, sleeping for a duration defined by the custom tag's Interval attribute:
<!--- NOTE: This cf_thread.cfm custom tag is not meant to be a full replica of the CFThread functionality; I am only trying to create functionality as a proof of concept that is related to running threads on an interval. ---> <!--- Param the tag attributes. ---> <cfparam name="attributes.name" type="variablename" /> <cfparam name="attributes.action" type="string" default="run" /> <!--- We only need the callback name if the action is run - we will assume that the interval is non-zero, but we won't be validating for that in this proof of concept. ---> <cfif (attributes.action eq "run")> <cfparam name="attributes.interval" type="numeric" /> <cfparam name="attributes.callback" type="variablename" /> </cfif> <!--- ------------------------------------------------- ---> <!--- ------------------------------------------------- ---> <!--- Check to see if the action is terminate. ---> <cfif (attributes.action eq "terminate")> <!--- Kill the given thread. ---> <cfthread name="#attributes.name#" action="terminate" /> <!--- Exist out of this tag since it shouldn't do anything else after terminating a thread. ---> <cfexit method="exitTag" /> </cfif> <!--- ------------------------------------------------- ---> <!--- ------------------------------------------------- ---> <!--- If we've made it this far, the only other action that we are currently supporting in RUN with an interval. In order to get this proof of concept to work, we are going to have to execute the interval callback in the context of the CALLER scope. Since UDFs outside of ColdFusion components bind to their execution context, we have to jump through some black-magic hoops to get this to execute properly. NOTE: This is in heavy thanks to Elliott Sprehn who's Kung Fu is truly amazing. ---> <!--- Get the meta data for the caller page. This will give us the CLASS used for the caller scope; from this, we can use reflection to access the given field (page context) from a given instance of that class (caller). ---> <cfset callerMetaData = getMetaData( caller ) /> <!--- Get the class field that would hold a reference to the pageContext. By default, we cannot access this field so we'll have to toggle is access rights. ---> <cfset contextField = callerMetaData.getDeclaredField( "pageContext" ) /> <!--- This is a private field so we have to explicitly change its access (I don't fully understand things at this level - this is just what Elliott had... and, if you don't do it, it tells you that the field is private and throws an error). ---> <cfset contextField.setAccessible( true ) /> <!--- Now that we have the Field object that represents the PageContext property, use reflection to get that property from the CALLER instance. ---> <cfset callerPageContext = contextField.get( caller ) /> <!--- This method will simply echo whatever arguments have been passed to it. This is just an easy way to define an argument collection to be used when invoking the passed-in Greeting method. NOTE: To be used with NAMED attributes. Positional attributes have be executed using an Object array... (I think). ---> <cffunction name="getArgumentCollection" access="public" output="false" hint="I echo the arguments collection."> <cfreturn arguments /> </cffunction> <!--- At this point, we should have everything we need in order to execute the interval callback function in the context of the caller scope. Now, we have to start the CFThread that actually runs the interval function. You'll notice that we are NOT going to pass in any of the function stuff. Passing data to CFThread via its tag attributes performes deep-copy on the values; we need those refrences to be maintained. ---> <cfthread name="#attributes.name#" action="run"> <!--- Since this thread is meant to run at an interval, we need to enter an indefinite loop - remember, this is just a proof of concept. ---> <cfloop condition="true"> <!--- Sleep the tag for the interval. Notice that we are accessing this via the custom tag's Variables scope, not the attributes. If we tried to use the Attributes scope, it would have been conflicting with the CFThread's local attributes collection. ---> <cfthread action="sleep" duration="#variables.attributes.interval#" /> <!--- Invoke the callback function in the caller context. NOTE: To invoke the method, we are using the underlying, undocumented Java methods for invoking ColdFusion methods. ---> <cfset caller[ variables.attributes.callback ].invoke( caller.variables, javaCast( "string", variables.attributes.callback ), callerPageContext.getPage(), getArgumentCollection() ) /> </cfloop> </cfthread> <!--- NOTE: Since ColdFusion moves all UDF definitions to the top of a file when it compiles (for lack of a better metatphor), we don't need to worry about the END MODE of execution for this tag - the callback UDF, while defined inside the custom tag body, is actually available before the custom tag executes. ---> <cfexit method="exitTag" />
The bulk of this custom tag is concerned with getting the Caller scope's PageContext in order to invoke our UDF in the appropriate execution context. If you look past that, however, you'll see that there is little more to this custom tag than an encapsulated CFThread tag that uses a Sleep directive to pause the asynchronous thread for each interval. I assume that this ties up a valuable thread on the server; but, like I said, this is just a proof of concept.
When it comes to passing data into a ColdFusion CFThread, you have to be very careful; data passed to a new thread is passed by deep-copy. Since we need to maintain all of the referential integrity between the caller context and the custom tag body, you'll notice that I am not passing any data into the custom tag. Rather, I am relying on the implicit sharing of the custom tag's Variables scope with the CFThread execution context.
I know that you might be looking at this and thinking that using Scheduled Tasks is just going to be easier; but, I think there is something really nice about creating repetitive behavior within the context of the relevant code. Imagine that we had a ColdFusion component instance that needed to do something internal to the CFC on a regular basis (ex. cleaning cache, watching target directory, etc.); with a CFThread/Interval situation, we could simply launch a "timer" within the component and keep all of the logic completely encapsulated. If we had to go the scheduled task route, however, not only would we have to provide an unrelated landing page for the scheduled task, we'd also have to grant external access to a private piece of behavior.
Want to use code from this post? Check out the license.
Ah, very interesting. I had never actually thought about what thread is used once the execution of the callback actually takes place. Since you bring up WebWorkers, however, I believe they are truly asynchronous, correct?