Sunday, November 1, 2009

CFThread Must Die (Kill Bill) - Part 2

In Part 1 I mentioned the internal CF class coldfusion.thread.Task that seems to be used for running processes with cfthread. Tasks for a given thread can be retrieved from the FusionContext class using a method called getUserThreadTask(). So between the two, you should have enough information for makings of an experimental tracker.

Now I was particularly interested in the coldfusion.thread.Task's cancel() method and whether it could be used to kill a cfthread programatically. So I created a very simple cfthread. The thread itself does very little besides outputting a few messages. But I added a few sleep statements to simulate a thread that would take twenty (20) seconds to execute.

<cfset threadName = "CFZombieThread_Test" >

<!--- Start the new thread ---->
<cfthread action="run" name="#threadName#" myName="#threadName#">
<cfset var zombieThreadName = getPageContext().getFusionContext().getCurrentUserThreadName() />

<cfloop from="1" to="20" index="counter">

<cfoutput>
Mindless groan from zombie thread #zombieThreadName# at #now()# ...<br/>
</cfoutput>
<!---
Force the thread to sleep to simulate a long running task
--->
<cfset sleep(1 * 1000) />

</cfloop>

</cfthread>

Next, I used the ever handy getPageContext() function to grab the FusionContext. I am not extremely familiar with this class. But from what I have learned, it provides executing requests with access to context-sensitive information like session variables, application settings, etcetera. After grabbing the context, I called the getUserThreadTask() method to retrieve the Task associated with my newly created thread.

Note: When using this method be sure to convert the thread name to upper case. I discovered through trial and error that the thread names are stored in some type of case sensitive structure, and all the keys are in upper case. So if you use mixed, or lower case, the method will not be able to locate your thread.


<cfset context = getPageContext().getFusionContext() />
<cfset task = context.getUserThreadTask(UCASE(threadName)) />
<cfdump var="#cfthread[threadName]#" label="#threadName# BEFORE cancel() request " />


Next, I forced the current page to take a brief nap, to give my thread a chance to produce some output and prove it was running. Finally, I used the Task's cancel() method to kill the thread. Then forced the parent page to sleep again before checking the thread's final status. Just to give the thread a chance to catch its breath.

<!--- Sleep for a few seconds to give the thread a chance to produce some output ...--->
<cfset createObject("java", "java.lang.Thread").currentThread().sleep(2000) />

<!--- Now, try and kill the thread --->
<cfset task.cancel() />

<!--- Finally, give the thread a chance to catch up before we check its final status ...--->
<cfset createObject("java", "java.lang.Thread").currentThread().sleep(500) />
<cfdump var="#cfthread[threadName]#" label="#threadName# AFTER cancel() request " />

As you can see, the thread was successfully terminated.



The next step was storing the thread information in a shared scope. So it could be accessed as needed. Now I know there are a few ways you could approach it and different scopes you could use. But for the sake of simplicity, I decided to have the threads add and remove themselves from a tracker object stored the application scope. It is obviously not as elegant as a built in monitor, and could definitely use improvement. But it was good enough for some simple tests.


<!--- Start the new thread ---->
<cfthread action="run" name="#newThreadName#" >

<cfset var myThreadUUID = "" />
<cfset var myThreadContext = getPageContext().getFusionContext() />
<cfset var myThreadName = myThreadContext.getCurrentUserThreadName() />

<!---
First, the thread will add itself to the tracker object
--->
<cfset myThreadUUID = application.threadTracker.addThread(
threadName = myThreadName,
threadTask = myThreadContext.getUserThreadTask(myThreadName)
) />

<!---
DO THREAD STUFF HERE .....
--->


<!---
Finally, the finished thread will remove itself from
the application tracker object
--->
<cfset application.threadTracker.removeThread(
threadUUID = myThreadUUID
) />
</cfthread>


Now for the Usual Disclaimers....
As there is a bit too much code to post, I threw together a small example to demonstrate the concepts. It is rough and highly experimental. So use it at your own risk ;) Now I have only had a chance to test it on the Developer version. I am assuming it will work on the Standard edition too.

Anyway, for those interested in how it works, you can download the sample code from widget in the right menu. (Absolutely zero time went into window dressing. So if you have delicate eyes, you might want to leave the room now).

5 comments:

rad_g,  November 2, 2009 at 3:14 AM  

AFAIR killing threads forcibly is not the best idea. One should never do so. Threads should be asked to finish their job. Otherwise your app may leave the data in an intermediate state.

cfSearching November 2, 2009 at 3:45 AM  

@rad_g,

I wholeheartedly agree that whenever possible threads should be allowed to run their course. Killing threads is not something to take lightly. I believe I was very clear about my feelings on that in Part 1 ;) However, I would not go so far as to say they should never be killed. The key is to shut them down gracefully, properly releasing all resources. So that the death of one thread death does not have a dangerous cascade effect on the entire server.

Keeping in mind all of this was an experiment, one of the main reasons I started investigating alternatives to the low-level Thread.stop() is that it is _known_ to be unsafe. From everything I have read, it is very low level and can do a bit of hatchet job. So that led me to investigating, other more stable options.

After a lot of testing and reasearch, I am pretty certain that Task.cancel() is what CF uses internally to terminate threads. Both when you call cfthread terminate and most likely when you kill a task via the Server Monitor. If that is indeed the case I feel relatively certain the internals take great care to properly terminate the thread in question, in as clean and stable a manner as possible. If it did not, I highly doubt thread termination would be implmentated as a feature in CF. Of course, I have not actually seen the source code. So this is all supposition on my part. But given that this is an internal method, I feel relatively confident Task.cancel() does a lot more cleanup than the basic axe-murdering job of Thread.stop().

So while I agree that threads should not be terminated indescriminitely, sometimes it just needs to be done. Like the occaisional runaway thread that continues to chug along, consuming more and more resources. But I think the key is to do it cleanly, with a surgical instrument, not a rusty sledgehammer.

-Leigh

Ben Reid January 26, 2011 at 5:13 PM  

I notice that the download of the code link no longer works. I am attempting to write my own Server Monitor, Active ColdFusion Threads interface that allows admin users to kill any cfthread that was started on ColdFusion stanard. Obviously the Server Monitoring API is not available in ColdFusion standard, so some alternate ability to terminate cfthreads is required. Do you still have the code you had available or was it just limited to the code as posted in the blog?

cfSearching January 26, 2011 at 9:23 PM  

@Ben -

Thanks for letting me know about the broken link. Yes, there was more code. I believe I still have a copy of the experimental code somewhere. I will check tomorrow and post back.

-Leigh

cfSearching January 27, 2011 at 6:39 PM  

@Ben -

Done. Just keep in mind it is experimental code only. I have never used it on a production server. So caveat emptor ;)

-Leigh

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep