Coldbox - Scheduled Tasks (A Couple of Suggestions)

I’ve spent the last couple of days learning and understanding the new Coldbox Scheduler.

One of the first things I found is that when you call runEvent from the defined task, like:

task( "process-logs" )
	.call( function(){
		runEvent(event:"scheduled.station.processLogs", eventArguments:{"scheduled":true});
	})
	.every(5, "minutes")
	.delay(1, "minutes")
	.withNoOverlaps();
}

that runEvent does NOT fire interceptors. We toss a few things in the prc like prc.ServiceFactory and prc.Context (homegrown). I had to create a pre-handler in my “scheduled/station.cfc” to create those things for me. Kind of a duplicate of my SetupInterceptor. That’s okay, I didn’t mind too much, but I would suggest the documentation reflect that.

My second suggestion is this, in a round-about way. You can see I have an argument there. (Which I now know is a true arguments scoped variable!) I am creating the scheduled task that way so that I can also run it manually, if need be, and differentiate between a scheduled run and a manual hit-the-event-in-the-url execution.

You have that great .withNoOverlaps() feature. I think that’s pretty smart. I would say, though, it would be nice to have access to the information that must be stored somewhere indicating the task is running. If you have an “every minute” event that takes two minutes, the second execution must know it’s already in progress.

Yet, when I run this code:

local.oScheduler = controller.getSchedulerService().getSchedulers()[appScheduler@coldbox];|
local.stTask = local.oScheduler.getTaskRecord(arguments.task);|

The TaskRecord has no indication of whether its running or not. (It also, at least in my testing, does not show the “nextRun” time, it’s just blank in the dump.) But, again, it must be somewhere to prevent the overlaps and if it were included in the TaskRecord lookup… that would be cool.

It would be great for me to get that task that way, during my manual run, to determine if Coldbox is firing it right now, so I can NOT do a second run manually.

Just a couple of suggestions. But I really like it, and I’m glad I’m forcing myself to learn my way through it.

that runEvent does NOT fire interceptors. We toss a few things in the prc like prc.ServiceFactory and prc.Context (homegrown). I had to create a pre-handler in my “scheduled/station.cfc” to create those things for me. Kind of a duplicate of my SetupInterceptor. That’s okay, I didn’t mind too much, but I would suggest the documentation reflect that.

Which interceptors would that be? runEvent() has never fired any external interception points. I think you are confusing it with a normal event execution and runEvent() has NEVER been the normal default event. It’s just a pure executable of the requested event. The only interceptions are the localized interception points.

The TaskRecord has no indication of whether its running or not. (It also, at least in my testing, does not show the “nextRun” time, it’s just blank in the dump.) But, again, it must be somewhere to prevent the overlaps and if it were included in the TaskRecord lookup… that would be cool.

The nextRun was removed, it was never done. Even though @DaveL has volunteered (cough cough) to implement it. The overlaps are stored in CacheBox so many servers in a cluster can discover it. I think it can be done where the task record can request the cache record as well. Could be.

Just a couple of suggestions. But I really like it, and I’m glad I’m forcing myself to learn my way through it.

Keep them coming. This is brand new territory and we keep improving it in order to make this core solid first. Then I want to produce a nice UI to produce reports and stats and much more.

You’re right, Luis, I guess I am confusing it with a normal lifecyle event. Again, not a big deal, I was able to compensate, but I do think it would make a good addition to the one-page documentation you have on it, just so someone isn’t beating their head against the wall wondering why something isn’t in the rc/prc they might expect.

NextRun is fine, just an observation. The key is still there, though. Certainly not critical for my needs. What I did is, when I run manually, I’m disabling the task, and if it was originally enabled, re-enabling it at the end of the run (and in a catch block in case of errors). That’ll work for now.

I’ll try to dig around in Cachebox for the info (or the code that uses it). I need to flip some code over to Cachebox anyway that I have in place and am just using variables scope for at the moment. (Quick and dirty, cleanup after it works!)

I’m using an extensive processing system for scheduled tasks, batches, data, exceptions, etc. and I’m investigating how I can replace a lot of that with the Scheduler stuff.

Great job guys!

I haven’t forgotten, Luis! :slight_smile:
I was pretty busy putting together some important PRs for Quick to allow for single-table inheritance (STI). I haven’t heard back from Eric yet, but hopefully it will make for a nice addition.

I will be traveling next week but I plan on running some experiments to see if I can get nextRun to work. Date math is always a PITA, but I am up for the challenge.

Luis, it’s not super-duper awesome, just some quick hacky code, but I threw this together.

I’ll be glad to share my quick and dirty code.

I did learn one thing, though, while writing this:

local.oTask = controller.getSchedulerService().getSchedulers().getTasks()[specific task name]
local.oTask.future.getDelay("seconds")

The getDelay() return… it’s negative if the task is currently running. So that helps me a bit. Now I just need to throw moment.js and some countdown code so I can have a more real-time screen. (Like if it’s running (negative delay), I would block the enable/disable buttons for that entry. (And have the ajax function also ignore it when looping over Disable All / Enable All. At least until I create interruptable code.)

Let me know, I’ll email the code over to you if you want it as a starting place. I’m sure it’ll be improved over the next couple of days. I needed it so that I could edit code, reinit and prevent those scheduled items rom running.

Well, seemed great… until I realized there might be a bug.

I’m doing task.disable() and task.enable() and my UI (and the task properties) are showing it correctly, but it’s not keeping it from running.

Hmmm. My remote server is not working when I disable() a task, but my local is. Local is on Commandbox, remote server is on Lucee… (some version). I’ll let you know more when I know more.

Nope. Well, yes, but not sure how. Turns out that somehow there were TWO instances of the scheduled task running. The one I was working with in my UI (and the only one I could find in the SchedulerService data) was indeed disabled. But there was another one, somewhere that was running. When I re-enabled the one I could see, my logs would show TWO sets of log entries. And just one when it was disabled. Something was lost somewhere, still running, that I couldn’t access. Unsure how it happened, but I know there were reinits in there. Might be worth looking into. Sorry I can’t tell you more about how it happened.

I’ll probably need to create some method of getting all the tasks, their current disabled states, then disable all of them, let the reinit proceed, then after initialization, re-enable those that weren’t disabled prior to the reinit. Obviously I’ll have to persist something to disk, and the entire idea sounds… fragile at best. At least now I can see what tasks are up and which aren’t. Maybe I should configure them ALL to be disabled by config, and upon any reinit I’ll just have to go re-enable them. Hmmm. Things to consider.

@lmajano I have made some progress on calculating nextRun, but could use a little assistance with getting the proper future object. More information here: [COLDBOX-1068] - Welcome

Ignore the “Success: 0”, sure that’s not accurate. But my little control panel for the Scheduled Tasks is pretty nice.

@DaveL do you want my code for determining the Next Run value? I’m sure it’s convoluted how I’m getting it, but… it seems to be fairly accurate. Well, technically that’s a countdown, and Javascript is refreshing it every second.

Also, the BIG RED HEADER is just so I know it’s prod vs. dev. Good trick when you sometimes get in a hurry. (I do the same with the Microsoft SQL Studio, I color code dev, test and prod different colors. Red is Prod, green is Dev, usually.

@CaptainPalapa, yes! Since we seem stuck on not being able to calculate nextRun server-side, having a javascript solution in place would be very helpful. Thank you!

Well, the initial calculation isn’t Javascript, but here’s my Ajax handler function that returns the data. Maybe it’ll give you a starting place. Gotta send rc.scheduler and rc.task(name). You could just default rc.scheduler, too, to the native one, but I was trying to plan ahead a bit.

<cffunction name="getColdboxTaskData" access="public" returntype="any" hint="Ajax to change CB Task Status">
	<cfargument name="event">
	<cfargument name="rc">
	<cfargument name="prc">

	<cfset local.oScheduler = controller.getSchedulerService().getSchedulers()[rc.scheduler] />
	<cfset local.stTask = local.oScheduler.getTaskRecord(rc.task) />

	<cfset local.bEnabled = !local.stTask.task.isDisabled() />
	<cfset local.nNextRunSeconds = 0 />
	<cfif structKeyExists(local.stTask, "future") && isObject(local.stTask.future)>
		<cfset local.nNextRunSeconds = local.stTask.future.getDelay("seconds") />
	</cfif>

	<cfset local.dtUTCNow = "" />
	<cfset local.dtNextRunISO = "" />
	<cfif local.bEnabled>
		<cfset local.utcNow = dateConvert("local2utc", now()) />
		<cfset local.utcThen = dateAdd("s", local.nNextRunSeconds, local.utcNow) />
		<!--- Doing the math here minimizes time loss in http transmission, the JS will pick up the "destination" time --->
		<!--- Sending back the current UTC date will let us calculate run-times in the front end --->
		<cfset local.dtUTCNow = "#dateFormat( local.utcNow, 'yyyy-mm-dd' )#T#timeFormat( local.utcNow, 'HH:mm:ss' )#Z" />
		<cfset local.dtNextRunISO = "#dateFormat( local.utcThen, 'yyyy-mm-dd' )#T#timeFormat( local.utcThen, 'HH:mm:ss' )#Z" />
	</cfif>

	<cfset local.stReturn = {
		"scheduler":rc.scheduler
		,"task":rc.task
		,"enabled":local.bEnabled
		,"nextRunSeconds" = local.bEnabled ? local.nNextRunSeconds : -9999
		,"nextRunISO" = local.dtNextRunISO
		,"nowISO" = local.dtUTCNow
		,"running": local.nNextRunSeconds LT 0 && local.nNextRunSeconds NEQ -9999
		,"taskHash": hash(rc.scheduler & rc.task, "QUICK")
		,"stats": local.stTask.task.getStats()
		,"additionalArgs": structKeyExists(local.stTask.task, "additionalArgs") ? local.stTask.task.additionalArgs : {}
	} />
	<cfreturn local.stReturn />
</cffunction>
1 Like

Thanks for sharing the code!
This is the magic line right here:

nNextRunSeconds = local.stTask.future.getDelay("seconds")

Your code also confirms the problem I ran into when trying to solve the nextRun calculation when calling getStats() for a given task. The full details are in my latest comment here: [COLDBOX-1068] - Welcome

The TLDR is that the future that exists within the Task never gets updated, so when you call getDelay() it returns a negative number. The future object that exists in the controller, is updated and can be used for determining the nextRun.

I brought this up to @lmajano at ITB2022. If there was some way to make sure the Task had access to the right future object, the stats could be accurately generated on demand.

Once the issue gets fixed, you will be be able to use the following in your code example, to get nextRun:

local.nNextRunSeconds = local.stTask.getStats().nextRun
1 Like