4

I have a couple of services (static site generators) that I want to trigger on a regular basis from the same systemd timer. I found this question/answer, which covers exactly what I want to do, and describes a setup whereby a .target file that Wants= multiple services is triggered by a corresponding timer. This sounds great, but I'm finding that when I actually set this up, it only ever triggers once, then disables itself!

I've prepared a minimal working example (this doesn't trigger multiple services, but demonstrates the same problem):

test-timer.timer:

[Unit]
Description=A test timer

[Timer]
OnCalendar=*-*-* *:*:30
Unit=test-timer.target

[Install]
WantedBy=timers.target

test-timer.target:

[Unit]
Description=Target unit
Wants=test-timer.service
After=test-timer.service

[Install]
WantedBy=timers.target

test-timer.service:

[Unit]
Description=Run test

[Service]
ExecStart=/usr/bin/bash -c "date --rfc-3339='seconds' >> /tmp/test-timer-output"

[Install]
Also=test-timer.target

Enable the timer:

$ sudo cp test-timer.* /etc/systemd/system/
$ sudo systemctl enable --now test-timer.timer
Created symlink /etc/systemd/system/timers.target.wants/test-timer.timer → /etc/systemd/system/test-timer.timer.

Then, when I look at the output of systemctl list-timers --all, prior to the first run I get (ignoring other timers):

NEXT                        LEFT       LAST     PASSED       UNIT                ACTIVATES
Fri 2021-10-08 10:38:30 EDT 21s left   n/a      n/a          test-timer.timer    test-timer.target

After the first run, NEXT and LEFT have been replaced with n/a:

NEXT     LEFT    LAST                        PASSED        UNIT                  ACTIVATES
n/a      n/a     Fri 2021-10-08 10:38:32 EDT 1min 5s ago   test-timer.timer      test-timer.target

I've also tried adding Persistent=true to the test-timer.target and explicitly enabling test-timer.target, but neither of these work. Any time I do systemctl restart test-timer.timer, it restarts, but only triggers one run, then never has another go.

If I remove the layer of indirection by changing the Unit= line of test-timer.timer to Unit=test-timer.service, the service happily triggers itself every minute as expected.

Am I missing some configuration or installation step?

Paul
  • 191
  • 7

1 Answers1

5

After getting some help on Twitter, I've managed to solve this issue. The problem is that a systemd timer will only activate services that are inactive, and the default behavior for a target is to activate and stay active unless something makes it go down (it is not tied to lifetime of the units it Wants=). To force the target unit to become inactive if any of the services it activates becomes inactive, use BindsTo= in place of Wants= in my example above. So, for this minimal example:

test-timer.target:

[Unit]
Description=Target unit
BindsTo=test-timer.service
After=test-timer.service

[Install]
WantedBy=timers.target

test-timer.service:

[Unit]
Description=Run test

[Service]
ExecStart=/usr/bin/bash -c "date --rfc-3339='seconds' >> /tmp/test-timer-output"

[Install]
Also=test-timer.target

You can then see that as soon as test-timer.service is finished running, test-timer.target will also become inactive (and thus the timer will be able to activate it again):

$ sudo systemctl list-units --all test-timer.target test-timer.service
  UNIT               LOAD   ACTIVE   SUB  DESCRIPTION
  test-timer.service loaded inactive dead Run test   
  test-timer.target  loaded inactive dead Target unit

Whereas prior to the change, the target was staying active after the service died:

$ sudo systemctl list-units --all test-timer.target test-timer.service
  UNIT               LOAD   ACTIVE   SUB    DESCRIPTION
  test-timer.service loaded inactive dead   Run test   
  test-timer.target  loaded active   active Target unit
Paul
  • 191
  • 7
  • Dagnabbit, you found the solution before I could post it :-) – Kevin P. Fleming Oct 11 '21 at 21:54
  • 3
    Especially when you have multiple services triggered by a target and they are all oneshots, you may use `StopWhenUnneeded=True`. This will cause the target to become inactive after the work has been done. – karlsebal Dec 14 '21 at 11:07
  • 1
    Seems `StopWhenUnneeded=` works better than `BindsTo=`. With the latter, I got an error `xxx.target: Bound to unit yyy.service, but unit isn't active` whenever the target is activated. – Chih-Hsuan Yen Jan 15 '22 at 17:47