diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index b9965543885..d7847a2e469 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -8924,6 +8924,8 @@ node /org/freedesktop/systemd1/unit/systemd_2dtmpfiles_2dclean_2etimer { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly t RandomizedDelayUSec = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly t RandomizedOffsetUSec = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b FixedRandomDelay = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b Persistent = ...; @@ -8953,6 +8955,8 @@ node /org/freedesktop/systemd1/unit/systemd_2dtmpfiles_2dclean_2etimer { + + @@ -8997,6 +9001,8 @@ node /org/freedesktop/systemd1/unit/systemd_2dtmpfiles_2dclean_2etimer { + + diff --git a/man/systemd.timer.xml b/man/systemd.timer.xml index f2035a541f4..cf5d1030a6f 100644 --- a/man/systemd.timer.xml +++ b/man/systemd.timer.xml @@ -266,13 +266,13 @@ Delay the timer by a randomly selected, evenly distributed amount of time between 0 and the specified time value. Defaults to 0, indicating that no randomized delay shall be applied. - Each timer unit will determine this delay randomly before each iteration, and the delay will simply - be added on top of the next determined elapsing time, unless modified with - FixedRandomDelay=, see below. + Each timer unit will determine this delay randomly before each iteration, unless modified with + FixedRandomDelay=, see below. The delay is added on top of the next determined + elapsing time or the service manager's startup time, whichever is later. This setting is useful to stretch dispatching of similarly configured timer events over a certain time interval, to prevent them from firing all at the same time, possibly resulting in - resource congestion. + resource congestion on the local system. Note the relation to AccuracySec= above: the latter allows the service manager to coalesce timer events within a specified time range in order to minimize wakeups, while @@ -292,12 +292,12 @@ FixedRandomDelay= - Takes a boolean argument. When enabled, the randomized offset specified by - RandomizedDelaySec= is reused for all firings of the same timer. For a given timer - unit, the offset depends on the machine ID, user identifier and timer name, which means that it is - stable between restarts of the manager. This effectively creates a fixed offset for an individual - timer, reducing the jitter in firings of this timer, while still avoiding firing at the same time as - other similarly configured timers. + Takes a boolean argument. When enabled, the randomized delay specified by + RandomizedDelaySec= is chosen deterministically, and remains stable between all + firings of the same timer, even if the manager is restarted. The delay is derived from the machine + ID, the manager's user identifier, and the timer unit's name. This effectively creates a unique fixed + offset for each timer, reducing the jitter in firings of an individual timer while still avoiding + firing at the same time as other similarly configured timers. This setting has no effect if RandomizedDelaySec= is set to 0. Defaults to . @@ -305,6 +305,36 @@ + + RandomizedOffsetSec= + + Offsets the timer by a stable, randomly-selected, and evenly distributed amount of + time between 0 and the specified time value. Defaults to 0, indicating that no such offset shall be + applied. The offset is chosen deterministically, and is derived the same way as + FixedRandomDelay=, see above. The offset is added on top of the next determined + elapsing time. This setting only has an effect on timers configured with OnCalendar=, + and it can be combined with RandomizedDelaySec=. + + Much like RandomizedDelaySec=, this setting is for distributing timer events + to prevent them from firing all at once. However, this setting is most useful to prevent resource + congestion on a remote service, from a fleet of similarly-configured clients. Unlike + RandomizedDelaySec=, this setting applies its offset with no regard to manager + startup time. This maintains the periodicity of configured OnCalendar= events + across manager restarts. + + For example, let's say you're running a backup service and have a fleet of laptops that wish + to make backups weekly. To distribute load on the backup service, each laptop should randomly pick + a weekday to upload its backups. This could be achieved by setting OnCalendar= to + weekly, and then configuring a RandomizedDelaySec= of + 5 days with FixedRandomDelay= enabled. Let's say that some + laptop randomly chooses a delay of 4 days. If this laptop is restarted more often than that, then the + timer will never fire: on each fresh boot, the 4 day delay is restarted and will not be finished by + the time of the next shutdown. Instead, you should use RandomizedOffsetSec=, which + will maintain the configured weekly cadence of timer events, even across reboots. + + + + DeferReactivation= diff --git a/src/core/dbus-timer.c b/src/core/dbus-timer.c index b9d0c16acd8..21efc77e613 100644 --- a/src/core/dbus-timer.c +++ b/src/core/dbus-timer.c @@ -113,7 +113,8 @@ const sd_bus_vtable bus_timer_vtable[] = { BUS_PROPERTY_DUAL_TIMESTAMP("LastTriggerUSec", offsetof(Timer, last_trigger), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("Result", "s", property_get_result, offsetof(Timer, result), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("AccuracyUSec", "t", bus_property_get_usec, offsetof(Timer, accuracy_usec), SD_BUS_VTABLE_PROPERTY_CONST), - SD_BUS_PROPERTY("RandomizedDelayUSec", "t", bus_property_get_usec, offsetof(Timer, random_usec), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("RandomizedDelayUSec", "t", bus_property_get_usec, offsetof(Timer, random_delay_usec), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("RandomizedOffsetUSec", "t", bus_property_get_usec, offsetof(Timer, random_offset_usec), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("FixedRandomDelay", "b", bus_property_get_bool, offsetof(Timer, fixed_random_delay), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("Persistent", "b", bus_property_get_bool, offsetof(Timer, persistent), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("WakeSystem", "b", bus_property_get_bool, offsetof(Timer, wake_system), SD_BUS_VTABLE_PROPERTY_CONST), @@ -214,7 +215,10 @@ static int bus_timer_set_transient_property( } if (streq(name, "RandomizedDelayUSec")) - return bus_set_transient_usec(u, name, &t->random_usec, message, flags, error); + return bus_set_transient_usec(u, name, &t->random_delay_usec, message, flags, error); + + if (streq(name, "RandomizedOffsetUSec")) + return bus_set_transient_usec(u, name, &t->random_offset_usec, message, flags, error); if (streq(name, "FixedRandomDelay")) return bus_set_transient_bool(u, name, &t->fixed_random_delay, message, flags, error); diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 7344e56c4a2..6164f9765cc 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -578,7 +578,8 @@ Timer.RemainAfterElapse, config_parse_bool, Timer.FixedRandomDelay, config_parse_bool, 0, offsetof(Timer, fixed_random_delay) Timer.DeferReactivation, config_parse_bool, 0, offsetof(Timer, defer_reactivation) Timer.AccuracySec, config_parse_sec, 0, offsetof(Timer, accuracy_usec) -Timer.RandomizedDelaySec, config_parse_sec, 0, offsetof(Timer, random_usec) +Timer.RandomizedDelaySec, config_parse_sec, 0, offsetof(Timer, random_delay_usec) +Timer.RandomizedOffsetSec, config_parse_sec, 0, offsetof(Timer, random_offset_usec) Timer.Unit, config_parse_trigger_unit, 0, 0 Path.PathExists, config_parse_path_spec, 0, 0 Path.PathExistsGlob, config_parse_path_spec, 0, 0 diff --git a/src/core/timer.c b/src/core/timer.c index b37a67f3107..4fb7e0cd019 100644 --- a/src/core/timer.c +++ b/src/core/timer.c @@ -347,18 +347,18 @@ static void timer_enter_elapsed(Timer *t, bool leave_around) { timer_enter_dead(t, TIMER_SUCCESS); } -static void add_random(Timer *t, usec_t *v) { +static void add_random_delay(Timer *t, usec_t *v) { usec_t add; assert(t); assert(v); - if (t->random_usec == 0) + if (t->random_delay_usec == 0) return; if (*v == USEC_INFINITY) return; - add = (t->fixed_random_delay ? timer_get_fixed_delay_hash(t) : random_u64()) % t->random_usec; + add = (t->fixed_random_delay ? timer_get_fixed_delay_hash(t) : random_u64()) % t->random_delay_usec; if (*v + add < *v) /* overflow */ *v = (usec_t) -2; /* Highest possible value, that is not USEC_INFINITY */ @@ -391,12 +391,19 @@ static void timer_enter_waiting(Timer *t, bool time_change) { continue; if (v->base == TIMER_CALENDAR) { - usec_t b, rebased; + usec_t b, rebased, random_offset = 0; + + if (t->random_offset_usec != 0) + random_offset = timer_get_fixed_delay_hash(t) % t->random_offset_usec; /* If DeferReactivation= is enabled, schedule the job based on the last time * the trigger unit entered inactivity. Otherwise, if we know the last time * this was triggered, schedule the job based relative to that. If we don't, - * just start from the activation time or realtime. */ + * just start from the activation time or realtime. + * + * Unless we have a real last-trigger time, we subtract the random_offset because + * any event that elapsed within the last random_offset has actually been delayed + * and thus hasn't truly elapsed yet. */ if (t->defer_reactivation && dual_timestamp_is_set(&trigger->inactive_enter_timestamp)) { @@ -408,14 +415,16 @@ static void timer_enter_waiting(Timer *t, bool time_change) { } 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)) - b = UNIT(t)->inactive_exit_timestamp.realtime; + b = UNIT(t)->inactive_exit_timestamp.realtime - random_offset; else - b = ts.realtime; + b = ts.realtime - random_offset; r = calendar_spec_next_usec(v->calendar_spec, b, &v->next_elapse); if (r < 0) continue; + v->next_elapse += random_offset; + /* To make the delay due to RandomizedDelaySec= work even at boot, if the scheduled * 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 @@ -505,7 +514,7 @@ static void timer_enter_waiting(Timer *t, bool time_change) { if (found_monotonic) { usec_t left; - add_random(t, &t->next_elapse_monotonic_or_boottime); + add_random_delay(t, &t->next_elapse_monotonic_or_boottime); left = usec_sub_unsigned(t->next_elapse_monotonic_or_boottime, triple_timestamp_by_clock(&ts, TIMER_MONOTONIC_CLOCK(t))); log_unit_debug(UNIT(t), "Monotonic timer elapses in %s.", FORMAT_TIMESPAN(left, 0)); @@ -546,7 +555,7 @@ static void timer_enter_waiting(Timer *t, bool time_change) { } if (found_realtime) { - add_random(t, &t->next_elapse_realtime); + add_random_delay(t, &t->next_elapse_realtime); log_unit_debug(UNIT(t), "Realtime timer elapses at %s.", FORMAT_TIMESTAMP(t->next_elapse_realtime)); diff --git a/src/core/timer.h b/src/core/timer.h index 14a9931dffe..e642c9d515b 100644 --- a/src/core/timer.h +++ b/src/core/timer.h @@ -41,7 +41,8 @@ struct Timer { Unit meta; usec_t accuracy_usec; - usec_t random_usec; + usec_t random_delay_usec; + usec_t random_offset_usec; LIST_HEAD(TimerValue, values); usec_t next_elapse_realtime; diff --git a/tools/dbus_ignorelist b/tools/dbus_ignorelist index 0fc572d2040..5159fde1352 100644 --- a/tools/dbus_ignorelist +++ b/tools/dbus_ignorelist @@ -2044,6 +2044,7 @@ org.freedesktop.systemd1.Timer.OnClockChange org.freedesktop.systemd1.Timer.OnTimezoneChange org.freedesktop.systemd1.Timer.Persistent org.freedesktop.systemd1.Timer.RandomizedDelayUSec +org.freedesktop.systemd1.Timer.RandomizedOffsetUSec org.freedesktop.systemd1.Timer.RemainAfterElapse org.freedesktop.systemd1.Timer.Result org.freedesktop.systemd1.Timer.TimersCalendar