Using systemd timers

This post appeared originally in our sysadvent series and has been moved here following the discontinuation of the sysadvent microsite

You might, like me, once have tried to get something to run on the first Monday of the month, or maybe the last Friday of the month, or something else that’s a combination of a weekday (Mon–Sun) and a «week» (i.e. a date range, like 01–07). The naive approach is to use cron as you would expect it to work:

0 0 1-7 * 1 /usr/bin/foo

The surprise comes when this makes foo run every day of the first week every month, plus every Monday. Because while cron takes the intersection of restrictions at all other times, it takes the union in this one special case. It’s documented in crontab(5), at the very end of the DESCRIPTION section:

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.

Can this be fixed so it works like everything else? My instincts tell me it’s as entrenched as an old PHP API, so probably not. In any case, you want to get your cronjob running, not spend your time patching cron. You could setup a cronjob for Mondays and use a little script which checks the date. Or you could use systemd timers.

How to use timers

Say you write a little mondays.service

[Unit]
Description=Is it the first monday of the month

[Service]
Type=oneshot
ExecStart=/usr/bin/foo

and then pair it up with mondays.timer. systemd will automagically pair up $foo.{timer,service}, but you can also specify what the timer activates with Unit=.

[Unit]
Description=Is it the first monday of the month

[Timer]
OnCalendar=Mon *-*-1..7 00:00:00

[Install]
WantedBy=timers.target

Now all you have to do is systemctl enable --now mondays.timer and wait until the first Monday of the month! The time format of systemd timers is different from crontab format, and specified in systemd.time(7). The options for timers are specified in systemd.timer(5). The power of systemd timers is greater than this simple cron-like use.

A small table of differences

In this case I’ve used a calendar specification in pseudo-ISO8601 format (no T), with * wildcards for year and month, and a range for days. You can also use relative specifications, e.g. if you want something to run every hour, just write «hourly». The more magical examples in systemd.time(7) don’t work with calendar specifications, but they do work as sleep times after e.g. unit activation, boot. To rework your existing cronjobs into systemd format:

  cron systemd.time
Free * *
Group 1,3,5 1,3,5
Range 1-3 1..3
Step size */3 */3
Argument ordering Space-separated reverse of ISO8601 with weekday tacked on at the end ISO8601 (ish)

As you can see, the main difference is that the order is reversed, and systemd timers can handle more fine-grained time. This goes beyond the seconds in the date format: you can try to make something run every 0.1 seconds and see systemd complain about start requests repeated too quickly.

The other change is that the dash has been replaced with two dots as the range specifier. This lets you use dashes for the ISO8601 date format, and double dots seems to be a popular range specifier (e.g. it’s used in git, ★modern★ languages like haskell, perl6, ruby, rust, …).

How can I keep track of these timers?

systemctl list-timers will print active timers, the next time they’ll trigger, how long until that time, when they last ran, etc. As an example, I’ve got something running that’ll trigger on the first Monday of every month, and test.timer which runs every three minutes after it triggered test.service

NEXT                         LEFT                LAST                         PASSED   UNIT          ACTIVATES
Thu 2016-11-24 15:11:38 CET  2min 52s left       Thu 2016-11-24 15:08:38 CET  7s ago test.timer    test.service
Mon 2016-12-05 07:00:00 CET  1 weeks 3 days left n/a                          n/a    mondays.timer mondays.service

I’m sold!

Great! If you want to take the full plunge, there are some parsers floating around on the internet that can generate systemd units from crontabs. Otherwise, you can just keep your cron service and use systemd timers when they’re a better fit, or when you want a new job/timer. The added power does come with some added complexity, like if you like getting cronmails.

Finally, if you want to use these for your personal account rather than the system, everything works with the --user flag.


¹ How to write services is beyond the scope of this post, but if you’re stuck, try systemctl edit --full --force mondays.service and likewise for the timer. Editing with FULL FORCE isn’t as cool as it sounds: --full specifies you want to edit the whole service file; --force creates one if necessary.

² Rather than OnCalendar=, this has OnUnitActiveSec=3m

Emil Snorre Alnæs

Systems Consultant at Redpill Linpro

Emil started in Repill Linpro in 2015. He is our container platform specialist, but is always up for taking on a challenge outside of his own comfort zone.

Just-Make-toolbox

make is a utility for automating builds. You specify the source and the build file and make will determine which file(s) have to be re-built. Using this functionality in make as an all-round tool for command running as well, is considered common practice. Yes, you could write Shell scripts for this instead and they would be probably equally good. But using make has its own charm (and gets you karma points).

Even this ... [continue reading]

Containerized Development Environment

Published on February 28, 2024

Ansible-runner

Published on February 27, 2024