if (v->base == TIMER_CALENDAR) {
bool rebase_after_boot_time = false;
usec_t b, random_offset = 0;
+ usec_t boot_monotonic = UNIT(t)->manager->timestamps[MANAGER_TIMESTAMP_USERSPACE].monotonic;
if (t->random_offset_usec != 0)
random_offset = timer_get_fixed_delay_hash(t) % t->random_offset_usec;
t->last_trigger.realtime);
else
b = trigger->inactive_enter_timestamp.realtime;
- } else if (dual_timestamp_is_set(&t->last_trigger))
+ } else if (dual_timestamp_is_set(&t->last_trigger)) {
b = t->last_trigger.realtime;
- else if (dual_timestamp_is_set(&UNIT(t)->inactive_exit_timestamp))
+
+ /* Check if the last_trigger timestamp is older than the current machine
+ * boot. If so, this means the timestamp came from a stamp file of a
+ * persistent timer and we need to rebase it to make RandomizedDelaySec=
+ * work (see below). */
+ if (t->last_trigger.monotonic < boot_monotonic)
+ rebase_after_boot_time = true;
+ } else if (dual_timestamp_is_set(&UNIT(t)->inactive_exit_timestamp))
b = UNIT(t)->inactive_exit_timestamp.realtime - random_offset;
else {
b = ts.realtime - random_offset;
* time has already passed, set the time when systemd first started as the scheduled
* time. Note that we base this on the monotonic timestamp of the boot, not the
* realtime one, since the wallclock might have been off during boot. */
- usec_t rebased = map_clock_usec(UNIT(t)->manager->timestamps[MANAGER_TIMESTAMP_USERSPACE].monotonic,
- CLOCK_MONOTONIC, CLOCK_REALTIME);
+ usec_t rebased = map_clock_usec(boot_monotonic, CLOCK_MONOTONIC, CLOCK_REALTIME);
if (v->next_elapse < rebased)
v->next_elapse = rebased;
}
--- /dev/null
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# Persistent timers (i.e. timers with Persitent=yes) save their last trigger timestamp to a persistent
+# storage (a stamp file), which is loaded during subsequent boots. As mentioned in the man page, such timers
+# should be still affected by RandomizedDelaySec= during boot even if they already elapsed and would be then
+# triggered immediately.
+#
+# This behavior was, however, broken by [0], which stopped rebasing the to-be next elapse timestamps
+# unconditionally and left that only for timers that have neither last trigger nor inactive exit timestamps
+# set, since rebasing is needed only during boot. This holds for regular timers during boot, but not for
+# persistent ones, since the last trigger timestamp is loaded from a persistent storage.
+#
+# Provides coverage for:
+# - https://github.com/systemd/systemd/issues/39739
+#
+# [0] bdb8e584f4509de0daebbe2357d23156160c3a90
+#
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/test-control.sh
+. "$(dirname "$0")"/util.sh
+
+UNIT_NAME="timer-RandomizedDelaySec-persistent-$RANDOM"
+STAMP_FILE="/var/lib/systemd/timers/stamp-$UNIT_NAME.timer"
+
+# Setup
+cat >"/run/systemd/system/$UNIT_NAME.timer" <<EOF
+[Timer]
+OnCalendar=daily
+Persistent=true
+RandomizedDelaySec=12h
+EOF
+
+cat >"/run/systemd/system/$UNIT_NAME.service" <<\EOF
+[Service]
+ExecStart=echo "Service ran at $(date)"
+EOF
+
+systemctl daemon-reload
+
+# Create timer's state file with an old-enough timestamp (~2 days ago), so it'd definitely elapse if the next
+# elapse timestamp wouldn't get rebased
+mkdir -p "$(dirname "$STAMP_FILE")"
+touch -d "2 days ago" "$STAMP_FILE"
+stat "$STAMP_FILE"
+SAVED_LAST_TRIGGER_S="$(stat --format="%Y" "$STAMP_FILE")"
+
+# Start the timer and verify that its last trigger timestamp didn't change
+#
+# The last trigger timestamp should get rebased before it gets used as a base for the next elapse timestamp
+# (since it pre-dates the machine boot time). This should then add a RandomizedDelaySec= to the rebased
+# timestamp and the timer unit should not get triggered immediately after starting.
+systemctl start "$UNIT_NAME.timer"
+systemctl status "$UNIT_NAME.timer"
+
+TIMER_LAST_TRIGGER="$(systemctl show --property=LastTriggerUSec --value "$UNIT_NAME.timer")"
+TIMER_LAST_TRIGGER_S="$(date --date="$TIMER_LAST_TRIGGER" "+%s")"
+: "The timer should not be triggered immediately, hence the last trigger timestamp should not change"
+assert_eq "$SAVED_LAST_TRIGGER_S" "$TIMER_LAST_TRIGGER_S"
+
+# Cleanup
+systemctl stop "$UNIT_NAME".{timer,service}
+systemctl clean --what=state "$UNIT_NAME.timer"
+rm -f "/run/systemd/system/$UNIT_NAME".{timer,service}
+systemctl daemon-reload