A Quick Look At The ITaskEventHandler Mechanics In Adobe ColdFusion
For years, I've been using the URL-based mechanics to invoke scheduled tasks in both Adobe ColdFusion and Lucee CFML. But, I recently learned that there's an alternate means of invoking a scheduled task: the eventHandler. In the CFSchedule tag, you can use the eventHandler attribute to point ColdFusion to a component rather than a URL.
What's Wrong With the URL-Based Mechanics
There's nothing wrong with the URL-based mechanics. I've been using them for years with great success. The only stumbling block, for me, has been the fact that I have to define a hosts entry to make sure that the underlying CFHttp requests don't leave the server and then route-back. For example, with BigSexyPoems, I have to put this in my server's hosts file:
127.0.0.1 bigsexypoems.com
127.0.0.1 app.bigsexypoems.com
127.0.0.1 www.bigsexypoems.com
This way, the task URL(s) never leave the server; but, can still route to the proper IIS website using the URL domain.
Aside: if you use container-based deployments, and you're only running a single ColdFusion "service" inside of your container, you can skip the
hostsentry and just target127.0.0.1in your scheduled task URL. I have to use a domain name since I have multiple websites running on my VPS (Virtual Private Server) and IIS needs to know which one I'm targeting within each request.If the
CFScheduletag could add ahostheader option, this would become a non-issue since I'd be able to target the right website with an HTTP Header instead of a domain.
The only reason that I'm curious about this CFC-based invocation of scheduled tasks is because there's a slight impedance mismatch between the mechanics of the URL-based invocation and the goal of a task. Meaning, the task is meant to implement some scheduled logic - it really has no need to understand how it was invoked.
CFC-Based Task Execution Seems Fundamentally Broken
This is a hot take; but, after playing around with CFC-based scheduled task execution for a few mornings, the mechanics of it seem fundamentally broken to me. By that, I mean that I ran into enough "gotchas" and deviations away from the way in which I see the "world of ColdFusion" that there appears to be no value-add in me changing to this style of task orchestration.
As you read this overview, know that my thoughts here are very subjective. If you use and love CFC-based task execution, I'm not here to "yuck your yumm". This approach just doesn't dovetail with my personal approach.
A Quick Look
To use the CFC-based task orchestration, your CFSchedule tag needs to define the eventHandler attribute instead of the url attribute:
<cfscript>
cfschedule(
action = "update",
task = "Experimenting with ITaskEventHandler",
group = "BigSexyPoems",
mode = "application",
// This:
eventHandler = "wwwroot.ApplicationTaskRunner",
// Not this:
// operation = "HTTPRequest",
// url = config.scheduledTasks.url,
startDate = "1970-01-01",
startTime = "00:00 AM",
interval = 60 // Every 60-seconds.
);
</cfscript>
This eventHandler attribute contains a dot-delimited path to a ColdFusion component that implements the cfide.scheduler.ITaskEventHandler interface. Getting this CFC to "work" took a tremendous amount of trial-and-error because the mechanics of this component are barely documented at all. The best I could find was:
When I Google for cfide.scheduler.ITaskEventHandler, I literally get 2-pages of results. When's the last time you did a Google search that only yielded 2-pages — just wild!
Since BigSexyPoems already has a centralized task runner, I figured the easiest point of experimentation would be to turn around and invoke this runner directly instead of invoking it via a traditional URL-request. This turned out to be a good strategy as it quickly surfaced many of the issues that make this approach incompatible with my view of the world.
With that said, here's my ITaskEventHandler implementation that turns-around and invokes my existing TaskService:
component
implements = "cfide.scheduler.ITaskEventHandler"
hint = "I do nothing but provide a non-request-based ingress to scheduled tasks."
{
// PSEUDO CONSTRUCTOR: the task runner is freshly instantiated for each execution. As
// such, the pseudo constructor runs on each execution. HOWEVER, the `application`
// scope is not yet defined within the pseudo constructor. Attempting to reference
// globally-cached values will result in an error.
// ---
// PUBLIC METHODS.
// ---
/**
* I provide a way to short-circuit task execution.
*/
public boolean function onTaskStart( struct context = {} ) {
// CAUTION: while this might seem like a place to put component initialization, it
// appears that `variables` values assigned in this method WILL NOT be present in
// the `variables` scope within subsequent method invocation (ex, execute()).
// --
// variables.logger = !!! this will not work as expected !!!
// variables.taskService = !!! this will not work as expected !!!
// Note: returning `false` will prevent the rest of the methods from executing.
return true;
}
/**
* This execute the scheduled task logic as long as onTaskStart() returned `true`.
*/
public void function execute( struct context = {} ) {
// Note: since the onRequestStart() event handler isn't executed for these task
// components, the Application.cfc never has a chance copy `ioc` into the
// `request` scope. As such, I'm using the `ioc` as defined on the shared
// `application` scope to manage server look-up.
var taskService = application.ioc.get( "core.lib.service.system.task.TaskService" );
var logger = application.ioc.get( "core.lib.util.Logger" );
try {
taskService.executeOverdueTasks();
} catch ( any error ) {
logger.logException( error );
}
}
/**
* This fires after the task is executed. This is true even if there's an error in the
* task execution (see caveat notes in the onError() method below).
*/
public void function onTaskEnd( struct context = {} ) {
// ...
}
/**
* This only fires when the task runner is invoked at an unexpected time. For example,
* it's always fired for me when the task runner is first configured (and is then
* invoked immediately); or, when the task is paused for too long (relative to the task
* interval) and is then restarted.
* --
* Source: https://www.isummation.com/blog/day-14-coldfusion-10-schedule-enhancement-task-handler/
*/
public void function onMisfire( struct context = {} ) {
// ...
}
/**
* This only seems to fire when there's no `onError()` event handler in the core
* `Application.cfc` framework component. In such a scenario, the `context` looks
* to contain one additional property, `exceptionMessage`. If `Application.cfc` has an
* `onError()` event handler, errors within the task seems to get swallowed silently;
* and, only the `onTaskEnd()` method is invoked.
*/
public void function onError( struct context = {} ) {
// ... [context.exceptionMessage] ...
}
}
I tried to outline a number of my grievances in the code comments above, but let me articulate the extend of them more clearly:
The
applicationscope doesn't appear to be available within the component's pseudo-constructor. As such, I can't setvariableshere that I would normally define using theCFPropertytag and dependency-injection (DI). This is mostly a superficial issue, as I would like the "look and feel" of all my CFCs to be consistent.Since I can't define
variablesin the pseudo-constructor, I tried to define them in theonTaskStart()life-cycle method. However, it seems that anyvariablesvalues that I set in theonTaskStart()method are not available within theexecute()method. I don't even know how that's possible — I must be making an error somewhere in my code, I just can see it!The
contextpassed into each method doesn't persist changes. Meaning, if I add a key to thecontextobject within theonTaskStart()method, that key is not present within thecontextobject passed into theexecute()method. This isn't a big deal, I was just trying to find ways to initialize some state since thevariablesscope appears to be a no-go (see above).Since this CFC is invoked outside of the normal URL-based request/response model, the
Application.cfclife-cycle methodonRequestStart()never executes. As such, I don't have access to my normalrequest-based variables. This isn't a huge deal, I can still useapplication-scoped variables; but it requires a mental adjustment.The
formscope doesn't exist. You might wonder why this matters since we're not using URL-based task invocation. It matter (to me) because I'm logging errors. And myLogger.cfcexpects theformscope to always exist because this is, after all, a web application. I had to go into myLogger.cfcand update theformreferences to all have fall-backs. Which ran me into another bug in Adobe ColdFusion 2025 that can't handle implicit object syntax in a Elvis expression without leaking memory.As an aside, the
sever,application,url,cookie, andcgiscopes all exist. The only meaningful scope that doesn't exist isform. I might consider this a "bug"; but, Adobe has a history of making theformscope undefined in some scenarios.The
onError()life-cycle method doesn't ever get called if you have anonError()event-handler defined inApplication.cfc. Unless that's explicitly defined somewhere, that behavior seems surprising.
None of these are "bugs" per se; neither are they implicitly "deal breaks"; but, they are all enough of a detractor to make the mechanics of CFC-based scheduled tasks seem less attractive than the URL-based mechanics. The URL-based mechanics aren't perfect; but at least I don't have to shift my mindset when moving from one part of the ColdFusion application to another. It might not make "academic sense"; but treating everything as a "web request" makes a lot of "pragmatic sense".
Your mileage may vary!
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →