cron expression support for background tasks in Um...
# contributing
j
Going to nerd snipe @Andrew McKaskill and @Sebastiaan for this 😁 I'm looking at getting an alternative to
IRecurringBackgroundJob
up and running that supports cron expressions (human time) rather than delay/period timespans (machine time). Do either of you know if anyone's working on cron support? I can see there's already a cron parser library in there but it's only used for first delay internally.
Unfortunately I think it needs a whole different approach to the
RecurringHostedServiceBase
so may need to be a separate thing anyway.
h
Hangfire maybe?
s
I couldn't find cron support for background tasks recently. But other than a known syntax, does it solve anything? The problem I had recently was that the task timer would restart each time the app restarted. This is not solved by cron right? Thaf would only be solved by storing the last execution time of a task in persistent storage.
Oh maybe I misread, absolutely Hangfire is a 3rd party option that does this correctly with cron support.
j
I can't easily add a dependency like hangfire to this project - and it kind of seems like overkill when Umbraco has all the right moving parts to support cron expressions. The benefit of cron is being able to logically target things like the "first Sunday of the month at 10:30", "every Monday, Wednesday and Friday at 04:00" which usually better corresponds to business logic than a simple delay + periodical repeat. It's also format that IT/Infrastructure people understand and can configure easily via environment variables.
s
That's fair enough! That said.. as you know, no new features are planned for v13 so you'd probably be looking at swapping out the built-in implementation, which I think should be possible since @Andrew McKaskill's changes made it it.
j
Yeah, though looking at it, none of the Umbraco APIs for this make much sense for cron based approach anyway and only adds a thin layer with some Umbraco specific concerns on top of the built in APIs in system.threading.
It may be better to just roll my own (and package it if necessary).
a
You are right in that there is no cron support currently. But you could extend ( 😉 ) the existing interface and hostedServiceRunner to have a cron pattern as well as a timer, and then choose one or the other depending on which one is filled in for a given background job.
There's nothing really special about the timer - it only starts and stops a recurring timer that triggers the "Execute" function
So you could make a "CronJobRunner" that instead of using a timer evaluates the cron and sets a "NextExecuteTime"
How soon do you need this? Happy to sit down and work out the concept with you if it's urgent.
Ohh - proper nerd sniping - you've got me thinking about this now to the point where I cracked the code open.
Ok - here's how I would approach it: We still need the
Timer
object, because that's doing the multi-threading locking that causes the task to come back into context. If we get rid of that we'd need a new hosted service that was running on a short timer and constantly checking to see if there are any jobs that need doing. So... instead, we can convert the calls that re-enable the timer to calculate a next run date/time and just set the timer for the delay between now and that time. Because the timer is disabled and re-enabled on every execution (to ensure we don't have two copies running at once if it's a long running task) it should be easy enough to get the "next" run time every time. We should also be able to add a new property to the common interface with the Cron pattern and pick the delay or the cron pattern based on it. We can't change the constructor - it's really there for legacy purposes anyway incase anyone subclassed it - but we can add a new overload with cron patterns in it.
j
This sounds good, I've started on a PoC in the project. > to ensure we don't have two copies running at once if it's a long running task I noticed this. Which is an interesting decision. > it should be easy enough to get the "next" run time every time. Yes, which works well for the C# cron parsers.
a
> I noticed this. Which is an interesting decision. > Not mine - I just refactored it
the next user story I'm on for my current project requires me to do some background job stuff. How urgent is your need? I might be able to knock something up for you that you can inject into your DI container by the end of the week
j
I've got to hack something together pretty quick, but I've already got half-implemented cron support that I'm migrating from a v8 site v8 anyway. I'm more interested to see if we can get either a good package or something in core that would make cron support more starightforward OoTB.
a
Ok, no worries. I'll take a look this week anyway and ping you if I work anything nice out.
j
Amazing, thank you!
s
I think I created an issue for that some years ago, and probably also a PR.
a
Ok @Jason how about this: https://gist.github.com/andrewmckaskill/da80c65a92e0e12dd7ebc9c34b3dc44d Basically, just change the period using the
ChangePeriod
method after each execution based on the next occurence from the cron expression. When I get a chance I'll extend the actual base class to do this properly, but this should work in the mean time. (Note - I really hacked this together quick. It compiles, but I haven't actually run it yet 😁 )
j
Thanks @Andrew McKaskill this is great and makes a lot of sense. I've ended up moving further away from the base service & interface to add other things like concurrency. Thinking ahead to getting something like this into core. I think adding an abstraction for an
IBackgroundJob
with basically just the task, then having a separate
IRecurringBackgroundJob
and
ICronJob
would make it cleaner.
a
That would make sense to be able to incorporate tasks that were on-demand. Would it be better to have an ISchedule property that was part of the task and there could be multiple implementations (with cron and defined recurring period being two options)? I've got a branch somewhere that is more focused on the surrounding aspects (rather than the core execution needs) - all of which can be derived from the notification handling: logging of starts/stops/errors to persistent storage including potentially database, as well as a dashboard to see and/or trigger individual jobs.
j
I'm not sure. It will be easier to interact with the jobs in the DI container if they have their own interface type. I also think it will be easier to refactor the BackgroundHostedService and separate concerns as necessary if the tasks themselves have discreet interfaces. Is there any common property that would make sense to be shared between different types of
ISchedule
? When the current API uses delay + period timespans (and would need to remain in place) and a cron expression would be a string.
a
I was thinking more like a method you could call to give you the next scheduled run time. Then any ISchedule object could implement it however they wanted.
j
A bigger problem, it turns out, is that the concept of recurring jobs in Umbraco is closely coupled to System.Threading.Timer which means we can't do things like support tasks scheduled to run more than 49.7 days in the future... https://github.com/umbraco/Umbraco-CMS/issues/16936
128 Views