That's simple but consider "run something 4x per day but randomize a delay by hour so all of the 200 servers doing that task won't run it all at once"
In cron, you basically have to either use your configuration management to generate those times, or have a random delay script running before the command
In systemd timers, it's just
OnCalendar=0/6:00:00
RandomizedOffsetSec=60m
and the offset generated will be stable for the job on a given machine (i.e. always same on this machine but different on others) so you will get nice uniform distribution of load.
If you add
Persist=true
the job will also be run once if there was one or more scheduled runs when the machine was down
> In cron, you basically have to either use your configuration management to generate those times, or have a random delay script running before the command
Nope. From crontab(5)
The RANDOM_DELAY variable allows delaying job startups by random amount
of minutes with upper limit specified by the variable. The random scal‐
ing factor is determined during the cron daemon startup so it remains
constant for the whole run time of the daemon.
That's from my cronie install, but it looks like this has been a feature of some crons for at least a decade. (Notice that the post date of [0] is in 2016.) Given that cronie is based on vixie-cron, and I think I was was using vixie-cron in 2002, I bet it's been a thing for at least twenty years.
It's a mystery to me why everyone tries to use OnCalendar here, when "n amount of times within a certain timeframe" can be done much more easily with OnActiveSec, in this case that'd be OnActiveSec=6h.
$ systemctl cat public-inbox-watch@.timer
# /etc/systemd/system/public-inbox-watch@.timer
[Unit]
Description=Periodic fetch of public mailing list
[Timer]
# twice a day
OnCalendar=*-*-* 5,17:35
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=multi-user.target
1) Your production equipment doesn't have its TZ set to UTC? Enjoy dealing with the intermittent and irregular hassle of DST changes, I guess.
2) From the crontab(5) on my system:
The CRON_TZ variable specifies the time zone specific for the cron ta‐
ble. The user should enter a time according to the specified time zone
into the table. The time used for writing into a log file is taken from
the local time zone, where the daemon is running.
If you have a job you need scheduled in a different timezone, dump a new file in /etc/cron.d, alter its CRON_TZ variable and go to town, as it were.
[1] This question is sarcasm. SystemD is often like this... dead simple things look dead simple, but complex things are -if they're possible at all- at least as complex as they are everywhere else.
I found the systemd time spec syntax you referenced to be logical and well thought out.
Cron syntax is simpler for the easy cases because cron tries to do less. It ignores years and seconds entirely, and doesn't try to adhere roughly to ISO8601 ordering and field separators, instead using space universally for field separation and euro-style least-to-most significant field ordering. I like ISO8601, so I get along with systemd's style better, despite it introducing slightly more cognitive load.
The only thing that threw me for a loop and seems like "special magic" was
> "Mon *-05~07/1" means "the last Monday in May."
But good luck doing that in one line in cron.
Some cron-style libraries seem to support L/W/# for last / nearest-weekday / nth of month, but I don't know if any system crons do. (cronie? dcron? I don't think so. fcron? bcron? I don't see it there either.) '#' is syntactic sugar for DOW + 7-day range, while L is covered by the above quoted syntax.
If your cron has that kind of syntax, then for a case like "weekday closest to 1st of month", "W" is more convenient than writing 3 systemd timer rules to cover the three cases (weekday day 1, monday day 2, friday last day of month), but that's a big if. Generally you'd have to write 3 rules in cron anyway.
> I found the systemd time spec syntax you referenced to be logical and well thought out.
I found this amusing when in combination with
> The only thing that threw me for a loop and seems like "special magic" was
but -regardless- a careful reader notes that I never said that the Timer scheduling syntax was illogical or poorly thought out. It's at least as complicated as crontab-style time syntax, which was my entire point.
Related: Not that it's ether part of the core scheduler syntax or necessarily as nice as having it in the core syntax, but my crontab(5) suggests that one can use things like date(1) to get more fine-grained control over the time of execution:
As noted above, skip values only operate within the time period they´re attached to.
For example, specifying "0/35" for the minute field of a crontab entry won´t cause
that entry to be executed every 35 minutes; instead, it will be executed twice every
hour, at 0 and 35 minutes past.
For more fine-grained control you can do something like this:
* * * * * if [ $(expr \( $(date +%s) / 60 \) % 58) = 0 ]; then echo this runs every 58 minutes; fi
0 * * * * if [ $(expr \( $(date +%s) / 3600 \) % 23) = 0 ]; then echo this runs every 23 hours on the hour; fi
Adjust as needed if your date(1) command does not accept "+%s" as the format string
specifier to output the current UNIX timestamp.
While I expect that you're not one of those people, I know that folks who are accustomed to working with extremely inflexible tools forget (or never learned) that these sorts of things are possible. I'm very aware that people sometimes cut off their own limbs with power tools, but that's not a good reason to ban their use.
Obviously you can do additional time & date checking in a shell wrapping the script or binary cron ultimately runs, just as you could do the time & date checking in the script or binary itself. That's far more complex.
Systemd enables cleaner, simpler syntax for common cases. Instead of "59 23 * * *", simply "23:59". Instead of "0 0 * * sun[day]", simply "sun[day]". 1st of the month and don't care exactly what time? Instead of "0 0 01 * *", simply "*-*-01".
Systemd started with the principle that they wanted to accept ISO8601 timestamps, then extended that with lists, increments, ranges. They developed that into a superset of basic cron capabilities, while maintaining similar syntax as best they could for increments and lists; they diverged for ranges and nth-from-last because those conflicted with the - date separator. They repurposed the little-used ~ cron randomization operator in the process. (systemd uses a separate RandomizedDelaySec line for that, which is also arguably superior because it randomizes per trigger, not on initial load of the timer)
The alternative is how AWS does it. They have separate cron() and at() syntax. at() takes timestamps. cron() takes cron+quartz syntax, mangled to remove seconds. They also have rate(value units), because apparently cron syntax is too complicated for most people. I'm sure in theory they did it to support "every 7 minutes" cases. I bet if you surveyed actual rate() AWS schedules, 99% would be cases that evenly divide a larger time unit, and could therefore be implemented with cron().
Cron syntax is more of a mess than I thought. While basic system crons don't support any advanced features, AWS and Cloudflare have adopted LW# capabilities, derived from quartz job scheduler syntax (Cloudflare references quartz explicitly). Quartz has extra fields for seconds and year (year optional, so it must go last, which breaks d m Y sequencing) so its syntax has 6 or 7 fields vs the standard 5. CF uses 5 fields, no seconds or years. AWS uses 6 fields, years but not seconds. Whenever you interact with cron-derived systems, you have to remember not just whether they support quartz-style syntax, but whether they support seconds and/or years. Is that really simpler?
The one feature I found interesting, "W", may not work the way I anticipated according to quartz's documentation. I thought "nearest weekday to the 1st of the month" might be useful because it maps to some billing cycles, but quartz (at least through 2.3) couldn't do that. The 2.3 docs say that 1W, if the 1st falls on a Saturday, triggers on the 3rd because it won't jump backwards over a month boundary. That was my only interesting use case for "W". Systemd does everything else, though it requires more verbosity in some cases, it's simpler in many others. 2.4/2.5 are from the last couple of years, and their docs are restructured and don't mention that, so maybe it was finally fixed? I'm not about to install java crud to test.
I didn't find the *-*~01 syntax that strange (last day of every month). The "fri *-*~07/1" syntax for "last friday of every month" is what I found magical, but it makes sense now that I've thought about it more.
What would you prefer for 3rd friday of the month? Cron (5-field, quartz syntax)
0 0 ? * fri#3 (special syntax is needed because cron uses logical-or to combine day and weekday fields)
or systemd
fri *-*-15..21 (works because systemd day and weekday are joined with logical-and)
> Systemd enables cleaner, simpler syntax for common cases.
I disagree. It's all a matter what you're accustomed to. On top of that, what's at least as important as clear syntax for simple cases is the establishing of a handful of rules that well serve both the simple and complex cases. Consistency that leads to complicated cases being made easy should not be avoided just to make the simple cases extremely welcoming to the newcomer. Note that I'm not saying that 'systemd-crond' falls into this trap, but I am saying that oh so many tools that aim to be "approachable by newcomers" enthusiastically throw themselves into it.
> Cron syntax is more of a mess than I thought.
Nah. It's just there are a very large number of cron implementations. If The Systemd Project were actually a thing that people could make an independent reimplementation of, we'd see at least as much inconsistency in 'systemd-cron' scheduling syntax as people extended it in their reimplementations to meet their needs.
> What would you prefer for 3rd friday of the month? Cron (5-field, quartz syntax)
I don't understand why the "day of month" column has a "?" instead of a "*", and I don't understand why "fri#3" isn't rendered as "Fri/3", but whatevz. I prefer that to the systemd syntax.
FWIW, I think you can directly translate your systemd scheduler line into this for most crons:
* * 15-21 * Fri
Edit: Oh, no, you cannot. From crontab(5)
Note: The day of a command’s execution can be specified in the following two fields — ’day of month’, and ’day of week’. If both
fields are restricted (i.e., do not contain the "*" character), the command will be run when either field matches the current time.
For example,
"30 4 1,15 * 5" would cause a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
Hm... Yeah, the handling of the "day of week" column is a terrible wart on the rules.
I think you're talking about syntactic simplicity. Systemd requires three field separators (space, :, -) because that's how ISO8601 dates are written. That means not all fields will be space separated, which may be more challenging to visually parse. It's not going to look as clean. Is that what you mean?
Everything else is simpler. You don't have to count fields to figure out whether you're dealing with a day vs a month, or an hour vs minute, no matter how the fields are aligned or how long or complex they are. You can scan left and right and look for space and dash and colon to figure out where you are. The formats for time and date are distinctive and ubiquitous. You don't have to guess what you're looking at when you just see one of them in isolation.
Very little follows from cron's format. You have to learn which fields are which for your particular implementation: does it include seconds? You have to learn the bizarre interaction between day and weekday, to do something non-trivial with them. You have to carefully keep track of which field you're in because you can't determine that from neighboring syntax. The visual clutter, if you want to call it that, of systemd's iso8601-based syntax only applies if you're doing complicated rules. In the vast majority of cases it would simply be an ISO8601 timestamp with various values replaced with *'s, and at most one /.
That first impression you had is what I thought too, before diving into it for my previous replies. But no, cron logic is that day and weekday are ORed. It lets you do clever things like run a script on the 1st and 15th of every month, AND on mondays, all in one line. I don't think I've ever seen such a thing in the wild. I'd go so far as to say it's stupid, because it's likely to be misinterpreted if anyone did use it that way. All the other fields are ANDed. I would say that systemd's AND logic is better... and more consistent, which is your own acceptance criterion.
The first one works in that specific case, but not more generically. For example, "Last Monday in February", or "last Monday of the month" for multiple months unless they're all 30 or all 31 days.
This was fun to cook up and may (or may not!) break if one's locale changes:
# Run a command on the last day of the month. Only starts checking at midnight towards the end of the month. Assumes GNU date, which is fair if we're discussing a Linux-only cron-alike.
0 0 28-31 * * if [ $(date +\%d) -eq $(date --date="$(date +\%m)/1 + 1 month - 1 day" +\%d) ]; then /usr/local/bin/runCommand.sh; fi
I bet one could do something similar to determine if we're at the "last $NAMED_WEEKDAY in the month" by counting ahead a week and seeing if the month name changes.
If I were doing this for real, I'd either switch to a more capable cron, or take a serious try at the date math and then wrap it up as a standalone helper. Or I guess I'd look to see if someone already built that helper. ...I guess...
I am curious whether there's a more capable system cron that supports that kind of thing. Quartz job scheduler (java), AWS, and CF obviously wouldn't qualify. I think this is only possible if you're using a heavyweight job scheduler like those. Or... systemd.
It ultimately doesn't matter "for real", because almost nobody without some horrific legacy system to integrate with would need to use anything more than lists, ranges, and increments. There's a reason nobody's added these features to system crons. Clever trigger times for events that don't really need to be triggered at clever times... sure, but lacking that capability wouldn't change anything, the trigger would just be a more boring one.
Events like "last Friday of the month" or "nearest weekday to the 1st of the month" or even "Friday the 13th" are more for business logic, not system crontabs.
Looking at the other examples on that page, I'm gonna say that it's only arguably easier to read for basic stuff... especially if you're familiar with the syntax. The complex stuff is -at best- just as difficult.
In cron, you basically have to either use your configuration management to generate those times, or have a random delay script running before the command
In systemd timers, it's just
and the offset generated will be stable for the job on a given machine (i.e. always same on this machine but different on others) so you will get nice uniform distribution of load.If you add
the job will also be run once if there was one or more scheduled runs when the machine was down