mirror of
https://github.com/morgan9e/systemd
synced 2026-04-14 00:14:32 +09:00
When we moved the time to 1 minute after the timer would've elapsed, systemd could pick RandomizedDelaySec= <= 1 minute which would then cause the timer to elapse immediately and the InactiveExitTimestamp= to get recalculated including a new next elapse time that would be for the next "window": systemd[1]: timer-RandomizedDelaySec-30785.timer: Adding 3.634672s random time. systemd[1]: timer-RandomizedDelaySec-30785.timer: Realtime timer elapses at Fri 2025-11-07 00:10:03 UTC. systemd[1]: timer-RandomizedDelaySec-30785.timer: Timer elapsed. systemd[1]: timer-RandomizedDelaySec-30785.timer: Changed waiting -> running systemd[1]: Found unit timer-RandomizedDelaySec-30785.timer at /run/systemd/system/timer-RandomizedDelaySec-30785.timer (regular file) systemd[1]: Preset files say disable timer-RandomizedDelaySec-30785.timer. systemd[1]: timer-RandomizedDelaySec-30785.timer: Got notified about unit deactivation. systemd[1]: timer-RandomizedDelaySec-30785.timer: Adding 8h 39min 26.166418s random time. systemd[1]: timer-RandomizedDelaySec-30785.timer: Realtime timer elapses at Sat 2025-11-08 08:49:26 UTC. systemd[1]: timer-RandomizedDelaySec-30785.timer: Changed running -> waiting ... TEST-53-TIMER.sh[1008]: InactiveExitTimestamp=Thu 2025-11-06 23:00:00 UTC TEST-53-TIMER.sh[1010]: ++ systemctl show -P NextElapseUSecRealtime timer-RandomizedDelaySec-30785.timer TEST-53-TIMER.sh[905]: + NEXT_ELAPSE_REALTIME='Sat 2025-11-08 08:49:26 UTC' TEST-53-TIMER.sh[1011]: ++ date '--date=Sat 2025-11-08 08:49:26 UTC' +%s TEST-53-TIMER.sh[905]: + NEXT_ELAPSE_REALTIME_S=1762591766 TEST-53-TIMER.sh[905]: + : 'Next elapse timestamp should be Fri 2025-11-07 00:10:00 UTC <= Sat 2025-11-08 08:49:26 UTC <= Fri 2025-11-07 22:10:00 UTC' TEST-53-TIMER.sh[905]: + assert_ge 1762591766 1762474200 TEST-53-TIMER.sh[1012]: + set +ex TEST-53-TIMER.sh[905]: + assert_le 1762591766 1762553400 TEST-53-TIMER.sh[1013]: + set +ex TEST-53-TIMER.sh[1013]: FAIL: '1762591766' > '1762553400' Technically, the race is still there, but the window for it should be _much_ smaller now (< 1s on a reasonably fast system). Let's hope that's enough. Resolves: #39594
98 lines
4.0 KiB
Bash
Executable File
98 lines
4.0 KiB
Bash
Executable File
#!/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" "+%a %Y-%m-%d %H:%M:%S %Z")"
|
|
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" "+%a %Y-%m-%d %H:%M:%S %Z")"
|
|
|
|
# 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 exactly the original calendar timer time (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:10"
|
|
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
|