Tagged in: systemd

systemd.timer(5) has replaced crontab(5) on most Linux systems by now. While some advances are really cool (e.g. systemctl list-timers or RandomizedDelaySec), the amount of boilerplate is atrocious.

For instance, this is the default weekly timer for the fstrim service.

% systemctl cat fstrim.timer
# /usr/lib/systemd/system/fstrim.timer
[Unit]
Description=Discard unused blocks once a week
Documentation=man:fstrim
ConditionVirtualization=!container
ConditionPathExists=!/etc/initrd-release

[Timer]
OnCalendar=weekly
AccuracySec=1h
Persistent=true
RandomizedDelaySec=6000

[Install]
WantedBy=timers.target

In crontab(5), it would look more or less like:

0 3 * * Sun /usr/bin/fstrim --listed-in /etc/fstab:/proc/self/mountinfo --verbose --quiet-unsupported

§ Service Templates

Enter the @ symbol. We can write a systemd.service(5) whose name ends with @ and pass an argument to it. See the Specifiers’ syntax.

% systemctl cat btrfs-scrub@.service 
# /usr/lib/systemd/system/btrfs-scrub@.service
[Unit]
Description=Btrfs scrub on %f
ConditionPathIsMountPoint=%f
RequiresMountsFor=%f

[Service]
Nice=19
IOSchedulingClass=idle
KillSignal=SIGINT
ExecStart=/usr/bin/btrfs scrub start -B %f

§ Using @

What happens if we define a “template” monthly timer?

% systemctl --user cat monthly@.timer
# /usr/lib/systemd/user/monthly@.timer
[Unit]
Description=Monthly Timer for %i service

[Timer]
OnCalendar=*-*-1 02:00:00
AccuracySec=6h
Persistent=true
Unit=%i.service

[Install]
WantedBy=default.target

And I now use monthly@.timer instead of creating a specific timer for my simple unit that I need to run monthly.

% systemctl --user enable --now monthly@gpg--update-keys.timer
% systemctl --user list-timers
NEXT                         LEFT                LAST                        PASSED            UNIT                                ACTIVATES
Sat 2023-04-01 02:00:00 CEST 3 weeks 0 days left -                           -                 monthly@gpg--update-keys.timer      gpg--update-keys.service

And we can ensure it works:

% journalctl --user -u gpg--update-keys.service
Mar 01 02:00:13 toxoplasmosis systemd[1116]: Started Update local GPG keys with remote data.
Mar 01 ...

§ Abusing @

There’s nothing saying there should be only one @, right? (the example below is stupid, don’t scrub your disk daily…)

# systemctl start daily@btrfs-scrub@mnt-$(systemd-escape bbb76c63-e4ac-4e39-8897-a120c5d30686).timer
# systemctl list-timers
NEXT                         LEFT                LAST                        PASSED        UNIT                                                                           ACTIVATES
Sun 2023-03-12 00:00:00 CET  1h 34min left       Sat 2023-03-11 00:00:12 CET 22h ago       daily@btrfs-scrub@mnt-bbb76c63\x2de4ac\x2d4e39\x2d8897\x2da120c5d30686.timer   btrfs-scrub@mnt-bbb76c63\x2de4ac\x2d4e39\x2d8897\x2da120c5d30686.service
# systemctl status btrfs-scrub@mnt-bbb76c63\\x2de4ac\\x2d4e39\\x2d8897\\x2da120c5d30686.service
○ btrfs-scrub@mnt-bbb76c63\x2de4ac\x2d4e39\x2d8897\x2da120c5d30686.service - Btrfs scrub on /mnt/bbb76c63-e4ac-4e39-8897-a120c5d30686
     Loaded: loaded (/usr/lib/systemd/system/btrfs-scrub@.service; static)
     Active: inactive (dead) since Sat 2023-03-11 00:23:27 CET; 22h ago
   Duration: 23min 14.482s
TriggeredBy: ● btrfs-scrub@mnt-bbb76c63\x2de4ac\x2d4e39\x2d8897\x2da120c5d30686.timer
    Process: 2124568 ExecStart=/usr/bin/btrfs scrub start -B /mnt/bbb76c63-e4ac-4e39-8897-a120c5d30686 (code=exited, status=0/SUCCESS)
   Main PID: 2124568 (code=exited, status=0/SUCCESS)
        CPU: 1min 38.009s

Mar 11 00:00:12 toxoplasmosis systemd[1]: Started Btrfs scrub on /mnt/bbb76c63-e4ac-4e39-8897-a120c5d30686.
Mar 11 00:23:27 toxoplasmosis btrfs[2124568]: scrub done for bbb76c63-e4ac-4e39-8897-a120c5d30686
Mar 11 00:23:27 toxoplasmosis btrfs[2124568]: Scrub started:    Sat Mar 11 00:00:12 2023
Mar 11 00:23:27 toxoplasmosis btrfs[2124568]: Status:           finished
Mar 11 00:23:27 toxoplasmosis btrfs[2124568]: Duration:         0:23:15
Mar 11 00:23:27 toxoplasmosis btrfs[2124568]: Total to scrub:   1.07TiB
Mar 11 00:23:27 toxoplasmosis btrfs[2124568]: Rate:             774.40MiB/s
Mar 11 00:23:27 toxoplasmosis btrfs[2124568]: Error summary:    no errors found
Mar 11 00:23:27 toxoplasmosis systemd[1]: btrfs-scrub@mnt-bbb76c63\x2de4ac\x2d4e39\x2d8897\x2da120c5d30686.service: Deactivated successfully.
Mar 11 00:23:27 toxoplasmosis systemd[1]: btrfs-scrub@mnt-bbb76c63\x2de4ac\x2d4e39\x2d8897\x2da120c5d30686.service: Consumed 1min 38.009s CPU time.

§ Bonus: dealing with TTLs

Some software will refuse to update if they find out their database has not reached their TTL. This is all fine and good, unless said TTL is exactly 1 day, and downloading the database is slow: in that case, I would really prefer that my machine take care of downloading that database when necessary so that I can do what I want with a fresh database whenever I choose to. This cannot be achieved by precise timers (getting the database takes time; and that database then is marked with a 1 day TTL: if I try to update 1 day after I last launched the update, the TTL will not have elapsed), but we can do it with OnUnitInactiveSeconds.

% systemctl --user cat daily-inactive@.timer
# /home/moviuro/.local/share/systemd/user/daily-inactive@.timer
[Unit]
Description=Launch %i service 24hours after it deactivated

[Timer]
OnUnitInactiveSec=86401sec
Unit=%i.service
Persistent=true

[Install]
WantedBy=default.target
% systemctl --user list-timers
NEXT                         LEFT                LAST                        PASSED            UNIT                                ACTIVATES
Sun 2023-03-12 10:01:17 CET  11h left            Sat 2023-03-11 10:01:13 CET 12h ago           daily-inactive@flatpak-update.timer flatpak-update.service
% 
Mar 10 10:00:32 toxoplasmosis systemd[1116]: Starting Pull flatpak updates without installing them...
[...]
Mar 10 10:00:41 toxoplasmosis flatpak[1961605]: F: flathub:x86_64 appstream age 86411 is greater than ttl 86400
Mar 10 10:00:41 toxoplasmosis flatpak[1961605]: F: Updating appstream data for user remote flathub
[...]
Mar 10 10:00:42 toxoplasmosis systemd[1116]: Finished Pull flatpak updates without installing them.
Mar 10 10:00:42 toxoplasmosis systemd[1116]: flatpak-update.service: Consumed 1.127s CPU time.
Mar 11 10:01:13 toxoplasmosis systemd[1116]: Starting Pull flatpak updates without installing them...
[...]
Mar 11 10:01:15 toxoplasmosis flatpak[2420594]: F: flathub:x86_64 appstream age 86433 is greater than ttl 86400
[...]
Mar 11 10:01:16 toxoplasmosis systemd[1116]: Finished Pull flatpak updates without installing them.
Mar 11 10:01:16 toxoplasmosis systemd[1116]: flatpak-update.service: Consumed 1.132s CPU time.

Additionally, with Restart=on-failure along with RestartSec=, it’s possible to have a unit rerun after failure and success according to different schedules. It is now no longer needed to run a job every hour (or every minute…) so that once in a while, the job “happens” to actually do stuff when it’s needed.

§ See also