]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
test: check the next elapse timer timestamp after deserialization
authorFrantisek Sumsal <frantisek@sumsal.cz>
Tue, 23 Sep 2025 19:04:12 +0000 (21:04 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Mon, 13 Oct 2025 16:04:41 +0000 (18:04 +0200)
When deserializing a serialized timer unit with RandomizedDelaySec= set,
systemd should use the last inactive exit timestamp instead of current
realtime to calculate the new next elapse, so the timer unit actually
runs in the given calendar window.

Provides coverage for:
  - https://github.com/systemd/systemd/issues/18678
  - https://github.com/systemd/systemd/pull/27752

(cherry picked from commit f4c3c107d9be4e922a080fc292ed3889c4e0f4a5)
(cherry picked from commit 469c22f72d16afb5c5440332ac77eb3128bc005e)

test/units/TEST-53-TIMER.RandomizedDelaySec-reload.sh [new file with mode: 0755]
test/units/util.sh

diff --git a/test/units/TEST-53-TIMER.RandomizedDelaySec-reload.sh b/test/units/TEST-53-TIMER.RandomizedDelaySec-reload.sh
new file mode 100755 (executable)
index 0000000..08f4f46
--- /dev/null
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# When deserializing a serialized timer unit with RandomizedDelaySec= set, systemd should use the last
+# inactive exit timestamp instead of current realtime to calculate the new next elapse, so the timer unit
+# actually runs in the given calendar window.
+#
+# Provides coverage for:
+#   - https://github.com/systemd/systemd/issues/18678
+#   - https://github.com/systemd/systemd/pull/27752
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/test-control.sh
+. "$(dirname "$0")"/util.sh
+
+UNIT_NAME="timer-RandomizedDelaySec-$RANDOM"
+TARGET_TS="$(date --date="tomorrow 00:10")"
+TARGET_TS_S="$(date --date="$TARGET_TS" "+%s")"
+# Maximum possible next elapse timestamp: $TARGET_TS (OnCalendar=) + 22 hours (RandomizedDelaySec=)
+MAX_NEXT_ELAPSE_REALTIME_S="$((TARGET_TS_S + 22 * 60 * 60))"
+MAX_NEXT_ELAPSE_REALTIME="$(date --date="@$MAX_NEXT_ELAPSE_REALTIME_S")"
+
+# Let's make sure to return the date & time back to the original state once we're done with our time
+# shenigans. One way to do this would be to use hwclock, but the RTC in VMs can be unreliable or slow to
+# respond, causing unexpected test fails/timeouts.
+#
+# Instead, let's save the realtime timestamp before we start with the test together with a current monotonic
+# timestamp, after the test ends take the difference between the current monotonic timestamp and the "start"
+# one, add it to the originally saved realtime timestamp, and finally use that timestamp to set the system
+# time. This should advance the system time by the amount of time the test actually ran, and hence restore it
+# to some sane state after the time jumps performed by the test. It won't be perfect, but it should be close
+# enough for our needs.
+START_REALTIME="$(date "+%s")"
+START_MONOTONIC="$(cut -d . -f 1 /proc/uptime)"
+at_exit() {
+    : "Restore the system date to a sane state"
+    END_MONOTONIC="$(cut -d . -f 1 /proc/uptime)"
+    date --set="@$((START_REALTIME + END_MONOTONIC - START_MONOTONIC))"
+}
+trap at_exit EXIT
+
+# Set some predictable time so we can schedule the first timer elapse in a deterministic-ish way
+date --set="23:00"
+
+# Setup
+cat >"/run/systemd/system/$UNIT_NAME.timer" <<EOF
+[Timer]
+# Run this timer daily, ten minutes after midnight
+OnCalendar=*-*-* 00:10:00
+RandomizedDelaySec=22h
+AccuracySec=1ms
+EOF
+
+cat >"/run/systemd/system/$UNIT_NAME.service" <<EOF
+[Service]
+ExecStart=echo "Hello world"
+EOF
+
+systemctl daemon-reload
+
+check_elapse_timestamp() {
+    systemctl status "$UNIT_NAME.timer"
+    systemctl show -p InactiveExitTimestamp "$UNIT_NAME.timer"
+
+    NEXT_ELAPSE_REALTIME="$(systemctl show -P NextElapseUSecRealtime "$UNIT_NAME.timer")"
+    NEXT_ELAPSE_REALTIME_S="$(date --date="$NEXT_ELAPSE_REALTIME" "+%s")"
+    : "Next elapse timestamp should be $TARGET_TS <= $NEXT_ELAPSE_REALTIME <= $MAX_NEXT_ELAPSE_REALTIME"
+    assert_ge "$NEXT_ELAPSE_REALTIME_S" "$TARGET_TS_S"
+    assert_le "$NEXT_ELAPSE_REALTIME_S" "$MAX_NEXT_ELAPSE_REALTIME_S"
+}
+
+# Restart the timer unit and check the initial next elapse timestamp
+: "Initial next elapse timestamp"
+systemctl restart "$UNIT_NAME.timer"
+check_elapse_timestamp
+
+# Bump the system date to 1 minute after the original calendar timer would've expired (without any random
+# delay!) - systemd should recalculate the next elapse timestamp with a new randomized delay, but it should
+# use the original inactive exit timestamp as a "base", so the final timestamp should not end up beyond the
+# original calendar timestamp + randomized delay range.
+#
+# Similarly, do the same check after doing daemon-reload, as that also forces systemd to recalculate the next
+# elapse timestamp (this goes through a slightly different codepath that actually contained the original
+# issue).
+: "Next elapse timestamp after time jump"
+date -s "tomorrow 00:11"
+check_elapse_timestamp
+
+: "Next elapse timestamp after daemon-reload"
+systemctl daemon-reload
+check_elapse_timestamp
+
+# Cleanup
+systemctl stop "$UNIT_NAME".{timer,service}
+rm -f "/run/systemd/system/$UNIT_NAME".{timer,service}
+systemctl daemon-reload
index bc3c1651a7d3ebefca26d9c18f4761d2050fb93a..696a23ea57433b346b0584a880dfa92abb3326e1 100755 (executable)
@@ -57,6 +57,15 @@ assert_le() {(
     fi
 )}
 
+assert_ge() {(
+    set +ex
+
+    if [[ "${1:?}" -lt "${2:?}" ]]; then
+        echo "FAIL: '$1' < '$2'" >&2
+        exit 1
+    fi
+)}
+
 assert_in() {(
     set +ex