core: add RestartRandomizedDelaySec= service option

We already support exponential backoff for automatic restarts via
RestartSec=/RestartSteps=/RestartMaxDelaySec=, but there is no way to
randomize the restart delay. When many instances of a service fail at
the same time (e.g. because a shared resource briefly went away) they
are all restarted in lockstep, creating a thundering herd problem.

So this commit adds a simple `RestartRandomizedDelaySec=` service
option which is similar to the timer `RandomizedDelaySec=` and
adds a randomized restart delay.
This commit is contained in:
Michael Vogt
2026-06-20 12:31:28 +02:00
committed by Lennart Poettering
parent 23b0bd230b
commit 046de3caea
10 changed files with 134 additions and 3 deletions
+72
View File
@@ -218,4 +218,76 @@ assert_eq "$(systemctl show "$UNIT_NAME" -P NRestarts)" "1"
rm /run/systemd/system/"$UNIT_NAME"
# Test RestartRandomizedDelaySec=
export UNIT_NAME="TEST-03-JOBS-restart-randomized-delay.service"
cat >"/run/systemd/system/$UNIT_NAME" <<EOF
[Service]
Type=simple
ExecStart=false
Restart=on-failure
RestartSec=1
RestartRandomizedDelaySec=1
StartLimitIntervalSec=0
EOF
systemctl daemon-reload
# The option should be parsed and exposed on the bus in usec.
assert_eq "$(systemctl show "$UNIT_NAME" -P RestartRandomizedDelayUSec)" "1s"
# The chosen delay is logged at debug level when the unit enters auto-restart, so we can read it without
# waiting for the delay to elapse.
PREV_LOG_LEVEL="$(systemctl log-level)"
restart_randomized_delay_cleanup() {
set +e
systemctl log-level "$PREV_LOG_LEVEL"
systemctl stop "$UNIT_NAME"
rm -f /run/systemd/system/"$UNIT_NAME"
systemctl daemon-reload
}
trap restart_randomized_delay_cleanup EXIT
systemctl log-level debug
get_restart_interval() {
# Enter auto-restart once, read the logged "<total>|<delay>", then stop again so it never has to elapse.
systemctl start --no-block "$UNIT_NAME"
timeout 10 bash -c 'while [[ "$(systemctl show "'"$UNIT_NAME"'" -P SubState)" != "auto-restart" ]]; do sleep .2; done'
systemctl stop "$UNIT_NAME"
journalctl --sync
# needed because of -o pipefail
{ journalctl -q --no-pager -o cat -b -u "$UNIT_NAME" --grep="Next restart interval calculated as" || true; } |
sed -n 's/.*calculated as: \(.*\) (randomized delay: \(.*\))$/\1|\2/p' | tail -n1
}
# Several samples + "not all equal": two draws could rarely render identically (~1e-6) and falsely fail.
DELAYS=()
TOTALS=()
for _ in {1..4}; do
IFS='|' read -r total delay <<<"$(get_restart_interval)"
TOTALS+=("$total")
DELAYS+=("$delay")
done
systemctl log-level "$PREV_LOG_LEVEL"
: "Chosen randomized restart delays: ${DELAYS[*]} (totals: ${TOTALS[*]})"
for delay in "${DELAYS[@]}"; do
assert_neq "$delay" ""
# Within bound: a value below 1s never renders a bare "<digit>s" token (only ms/us).
if [[ "$delay" =~ [0-9]s ]]; then
echo "FAIL: randomized restart delay '$delay' exceeds the configured 1s bound" >&2
exit 1
fi
done
# Total must vary, proving the jitter is folded into the armed timer (not merely logged).
all_equal=1
for total in "${TOTALS[@]}"; do
[[ "$total" == "${TOTALS[0]}" ]] || all_equal=0
done
assert_eq "$all_equal" "0"
touch /testok