Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Mark Drew and Reto Aeberli
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Mark Drew Reto Aeberli

A Quick Look At The ITaskEventHandler Mechanics In Adobe ColdFusion

By
Published in

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 hosts entry and just target 127.0.0.1 in 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 CFSchedule tag could add a host header 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 application scope doesn't appear to be available within the component's pseudo-constructor. As such, I can't set variables here that I would normally define using the CFProperty tag 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 variables in the pseudo-constructor, I tried to define them in the onTaskStart() life-cycle method. However, it seems that any variables values that I set in the onTaskStart() method are not available within the execute() 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 context passed into each method doesn't persist changes. Meaning, if I add a key to the context object within the onTaskStart() method, that key is not present within the context object passed into the execute() method. This isn't a big deal, I was just trying to find ways to initialize some state since the variables scope appears to be a no-go (see above).

  • Since this CFC is invoked outside of the normal URL-based request/response model, the Application.cfc life-cycle method onRequestStart() never executes. As such, I don't have access to my normal request-based variables. This isn't a huge deal, I can still use application-scoped variables; but it requires a mental adjustment.

  • The form scope 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 my Logger.cfc expects the form scope to always exist because this is, after all, a web application. I had to go into my Logger.cfc and update the form references 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, and cgi scopes all exist. The only meaningful scope that doesn't exist is form. I might consider this a "bug"; but, Adobe has a history of making the form scope undefined in some scenarios.

  • The onError() life-cycle method doesn't ever get called if you have an onError() event-handler defined in Application.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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel
Managed hosting services provided by:
xByte Cloud Logo