Inspired by Mike Schierberl's use of Java to programmatically delete ColdFusion scheduled tasks, I decided to try and to accomplish the same thing with plain old ColdFusion. I have tried to do this in the past, but always failed (and ended up creating a phantom task on the server - yikes!). I would have never been able to do this without Mike's absolutely critical explanation:
The Adobe techNote is somewhat misleading, it turns out that delete does work through the cfschedule tag, but the problem arises when the scheduled task tries to delete itself.
That is a really, really important note indeed. But, I would like to take that one step further: A scheduled task cannot be deleted while it is still running. Well, at least not programmatically I think.
So, the trick here, is to be able to get a scheduled task to call another page that then deletes the scheduled task, AND this has to be done only after the scheduled task has stopped running. Sounds complicated? It's actually very simple to do and it all comes down to page timeouts.
Let's take a quick look at the code. Here is the page getting called by the scheduled task:
<!--- Make sure the page can only run for 5 seconds. ---> <cfsetting requesttimeout="5" /> <!--- Get this directory. ---> <cfset strDirectoryUrl = GetDirectoryFromPath( "http://#CGI.server_name##CGI.script_name#" ) /> <!--- Call execute task page. This is the page that will delete the currently running task. Put in a CFTry / CFCatch blocks because you never know how CFHttp might mess up. Tell the CFHttp tag to only allow ONE second of timeout time before it gives up waiting and continues on with the page processing. ---> <cftry> <cfhttp url="#strDirectoryUrl#execute_task.cfm" method="GET" useragent="Mozilla/5.0" timeout="1" /> <cfcatch> <!--- An error occurred. ---> </cfcatch> </cftry> <!--- Trace the execution to a file just so we can make sure we know exactly when the task is running. ---> <cffile action="APPEND" file="#ExpandPath( './trace.txt' )#" output="BT2 - Executed: #TimeFormat( Now(), 'hh:mm:ss:l' )# addnewline="true" />
Now, let's take a look at the execute task page that deletes the scheduled task:
<!--- Get the currently executing thread and force it to sleep for 10 seconds. ---> <cfset CreateObject( "java", "java.lang.Thread" ).CurrentThread().Sleep( JavaCast( "long", 10000 ) ) /> <!--- Kill task. ---> <cfschedule action="DELETE" task="BenTestTask2" />
This all works quite nicely. And, as I said before, it's all thanks to page timeouts.
Take a look at the execute task page. Notice that the first thing the page does is sleep (thanks Mark Mandel's code example for asyncHttp) for 10 seconds. Then, after sleeping, it deletes the task. Now, look at the original task page. Notice that it only allows 5 seconds for page execution and the MINIMAL amount of timeout for the CFHttp request (one second) to wait for a response. This means that the calling page (the original scheduled task) should either finish executing OR timeout in some way before the Sleep() on the target page ever finishes. The bottom line: we can be sure that by the time the CFSchedule tag is executing, the original page has is NO LONGER RUNNING.
Nice work Ben, nice to finally understand what the root of the problem was. Hopefully this will be fixed in Scorpio (although I don't have my hopes up considering they knew about the issue in CF6)
Couple of things I would add...
1) may want to consider adding throwonerror=no to the cfhttp tag. You won't see the error because it is in a scheduled task, but I think it would throw an exception every time
2) may want to think about implementing a lock to make sure that the scheduled task isn't running. Think of a scenario with a scheduled task running every 60 seconds. If your first request takes 58 seconds to process, it may start a new thread 2 seconds later. At this point the delete task is waiting 10 seconds, and when it tries to delete, the task will be running again. Unlikely, but it may cause some strange behavior.
For starters, never could have done without your dynamite insights first. As for the throwonerror, I think it defaults to not throwing an error, however, it would be nice to see it explicitly written out.
As for the lock... I follow, that, would be cool. I figure I could put a NAMED lock on the entire scheduled task, then put a lock on th execute_taks.cfm page as well.... hmmm, but then one could lock the other and vice versa. I will play around.
Very helpful post. Bit thanks to both you and Mike. Did you ever have time to investigate locking ideas?
I have not looked into the locking issue yet, but I have an idea for something even better. Hopefully I can post soon.
I'll stay tuned.. I look forward to reading about your new idea.
My new idea didn't work. I thought maybe I could do a page transfer (GetPageContext().Transfer()) to the page that would be deleting the task, but it did not work. The Transfer() method should kill the current page request, but I guess from ColdFusion's standpoint it's still the same thread or something. I don't know enough about what is going on underneath to understand why any of it doesn't work.
This may be a dumb question but what is getPageContext().Transfer()? I only know about getPageContext().forward() and include(). Is transfer() like ASP's Server.Transfer()?
I forgot to say thanks for the followup. I appreciate it.
Sorry, that was my mistake. I did mean GetPageContext().Forward(). I don't know what Transfer is... i think I had something else on my mind at the time.
No problem. You know I was reading Mike's blog entry and got to wondering about his comment: what happens if the scheduled task continues processing after the asynch http call.
I ran a few tests using CurrentThread().sleep() to force the scheduled task to continue executing for 30 seconds after the http call. The results I observed were that it created a phantom task.
Not that surprising I guess but it got me thinking about adobe's tech note 18361. Can you really delete a task through the CF Administrator if the task is still executing? So I created a recurring task that ran every 2 minutes. Then tried deleting the task in the ColdFusion Administrator (while the task was executing). The results were the same. It created a phantom task.
I'm curious if anyone else sees the same results or flaws in the test logic.
That's very interesting. I guess a task cannot be deleted through any method so long as the task is still running. Doesn't that seem strange? You would think that the thread running it would be totally separate from the definition of it... why should the two overlap?
I ran some more tests. Stranger still is that "updates" seem to have no effect either, if you apply the changes while the task is running. I'm wondering if the thread executing the task is blocking changes or maybe the updating thread isn't communicating the changes to the running task. Without knowing about the internal workings this is all just guesswork of course. But if my tests are correct then you cannot even modify a task through the CF Admin, if the task is currently running. Thats a bit disturbing..
Nice testing! Yeah hopefully this is something that they can fix in the upcoming versions. It has been known about (to a degree) for some time now.
Yes its definitely been around for a while. I am bit surprised that I haven't read more about the issue as it relates to any attempts to change a running task. The technote only mentions a problem with deletes. The neocron.xml file problem seems to have been fixed but the rogue task behavior still persists.
Like you said, hopefully it will get fixed in newer versions. It would also be nice to see some extended task information like "last run on date" or the "next scheduled date"... and of course a "kill" task feature might be nice ;)
This blog has been very informative for me.
You can also write a coldfusion template to edit the neo-cron.xml file and delete the scheduled tasks. This example:
Demonstrates how to delete expired and disabled scheduled tasks but you could modify it to add/edit/delete the scheduled tasks based on other factors as well.
That is a cool idea. The only thing worries me about it is that the path the XML file is hard coded. Other than that, very cool.
that's a good point. I wouldn't normally hard code something like that either, but it was just a simple proof of concept for a blog entry, so I try to keep them as straightforward as possible. If I were really in the need for something like that, I would probably make a custom tag, or more likely these days a CFC, that could add, edit, and delete the scheduled tasks and I would use the #server.coldfusion.rootdir# to get the coldfusion root folder in my cffile tags. And now that I'm thinking about it I would put named cflocks around the process to protect the integrity of the file. Perhaps I should add a little note about that on my entry for people that don't yet know the potential risks with editing these kinds of files.
Yeah, that sounds like a good solution. I forgot about the SERVER-scoped settings. I need to take another look at those to see what googies can be leveraged.
There's not too much that's usefull, rootdir is about the only one I ever really use. here's all of the server variables as of CF8: