systemd/SOURCES/1045-test-check-the-next-elapse-timer-timestamp-after-des.patch

138 lines
6.2 KiB
Diff

From fdd763b42d864b4f7777fe9394a29c520c613d78 Mon Sep 17 00:00:00 2001
From: Frantisek Sumsal <frantisek@sumsal.cz>
Date: Tue, 23 Sep 2025 21:04:12 +0200
Subject: [PATCH] test: check the next elapse timer timestamp after
deserialization
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)
Related: RHEL-108744
---
.../testsuite.RandomizedDelaySec-reload.sh | 94 +++++++++++++++++++
test/test-functions | 2 +-
2 files changed, 95 insertions(+), 1 deletion(-)
create mode 100755 test/TEST-53-TIMER/testsuite.RandomizedDelaySec-reload.sh
diff --git a/test/TEST-53-TIMER/testsuite.RandomizedDelaySec-reload.sh b/test/TEST-53-TIMER/testsuite.RandomizedDelaySec-reload.sh
new file mode 100755
index 0000000000..54b90d3dd1
--- /dev/null
+++ b/test/TEST-53-TIMER/testsuite.RandomizedDelaySec-reload.sh
@@ -0,0 +1,94 @@
+#!/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
+
+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 --no-pager "$UNIT_NAME.timer"
+ systemctl show -p InactiveExitTimestamp --no-pager "$UNIT_NAME.timer"
+
+ NEXT_ELAPSE_REALTIME="$(systemctl show -p NextElapseUSecRealtime "$UNIT_NAME.timer" | cut -d = -f 2)"
+ NEXT_ELAPSE_REALTIME_S="$(date --date="$NEXT_ELAPSE_REALTIME" "+%s")"
+ : "Next elapse timestamp should be $TARGET_TS <= $NEXT_ELAPSE_REALTIME <= $MAX_NEXT_ELAPSE_REALTIME"
+ [[ "$NEXT_ELAPSE_REALTIME_S" -ge "$TARGET_TS_S" ]]
+ [[ "$NEXT_ELAPSE_REALTIME_S" -le "$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
diff --git a/test/test-functions b/test/test-functions
index 2345ab6e8a..f367994fe5 100644
--- a/test/test-functions
+++ b/test/test-functions
@@ -23,7 +23,7 @@ fi
PATH_TO_INIT=$ROOTLIBDIR/systemd
-BASICTOOLS="test sh bash setsid loadkeys setfont login sulogin gzip sleep echo mount umount cryptsetup date dmsetup modprobe sed cmp tee rm true false chmod chown ln xargs env mktemp mountpoint useradd userdel timeout jq wc awk diff dirname readlink"
+BASICTOOLS="test sh bash setsid loadkeys setfont login sulogin gzip sleep echo mount umount cryptsetup date dmsetup modprobe sed cmp tee rm true false chmod chown ln xargs env mktemp mountpoint useradd userdel timeout jq wc awk diff dirname readlink cut"
DEBUGTOOLS="df free ls stty cat ps ln ip route dmesg dhclient mkdir cp ping dhclient strace less grep id tty touch du sort hostname find"
STATEDIR="${BUILD_DIR:-.}/test/$(basename $(dirname $(realpath $0)))"