]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core/timer: Introduce RandomOffsetSec= knob 36437/head
authorAdrian Vovk <adrianvovk@gmail.com>
Wed, 19 Feb 2025 00:16:57 +0000 (19:16 -0500)
committerAdrian Vovk <adrianvovk@gmail.com>
Wed, 19 Feb 2025 00:16:57 +0000 (19:16 -0500)
This is like RandomDelaySec, but it doesn't reset whenever the manager
restarts.

Fixes https://github.com/systemd/systemd/issues/21166

man/org.freedesktop.systemd1.xml
man/systemd.timer.xml
src/core/dbus-timer.c
src/core/load-fragment-gperf.gperf.in
src/core/timer.c
src/core/timer.h
tools/dbus_ignorelist

index 5e1c3e2c08eddbc57b877bef904c70b0ca95d8e4..f3148fb52f81a2cabfccdcd673c9afa00859202e 100644 (file)
@@ -8906,6 +8906,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 = ...;
@@ -8935,6 +8937,8 @@ node /org/freedesktop/systemd1/unit/systemd_2dtmpfiles_2dclean_2etimer {
 
     <!--property RandomizedDelayUSec is not documented!-->
 
+    <!--property RandomizedOffsetUSec is not documented!-->
+
     <!--property FixedRandomDelay is not documented!-->
 
     <!--property Persistent is not documented!-->
@@ -8979,6 +8983,8 @@ node /org/freedesktop/systemd1/unit/systemd_2dtmpfiles_2dclean_2etimer {
 
     <variablelist class="dbus-property" generated="True" extra-ref="RandomizedDelayUSec"/>
 
+    <variablelist class="dbus-property" generated="True" extra-ref="RandomizedOffsetUSec"/>
+
     <variablelist class="dbus-property" generated="True" extra-ref="FixedRandomDelay"/>
 
     <variablelist class="dbus-property" generated="True" extra-ref="Persistent"/>
index 8a81d5066db742924c1c31bd215d5e727deac367..cf5d1030a6f503e6af753ae650571e372b76d6fb 100644 (file)
 
         <para>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.</para>
+        resource congestion on the local system.</para>
 
         <para>Note the relation to <varname>AccuracySec=</varname> above: the latter allows the service
         manager to coalesce timer events within a specified time range in order to minimize wakeups, while
         <xi:include href="version-info.xml" xpointer="v247"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>RandomizedOffsetSec=</varname></term>
+
+        <listitem><para>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
+        <varname>FixedRandomDelay=</varname>, see above. The offset is added on top of the next determined
+        elapsing time. This setting only has an effect on timers configured with <varname>OnCalendar=</varname>,
+        and it can be combined with <varname>RandomizedDelaySec=</varname>.</para>
+
+        <para>Much like <varname>RandomizedDelaySec=</varname>, 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
+        <varname>RandomizedDelaySec=</varname>, this setting applies its offset with no regard to manager
+        startup time. This maintains the periodicity of configured <varname>OnCalendar=</varname> events
+        across manager restarts.</para>
+
+        <para>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 <varname>OnCalendar=</varname> to
+        <literal>weekly</literal>, and then configuring a <varname>RandomizedDelaySec=</varname> of
+        <literal>5 days</literal> with <varname>FixedRandomDelay=</varname> 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 <varname>RandomizedOffsetSec=</varname>, which
+        will maintain the configured weekly cadence of timer events, even across reboots.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>DeferReactivation=</varname></term>
 
index b9d0c16acd87580b40043492a795323c73062b98..21efc77e613a52936445ec944ebc1853cb49db36 100644 (file)
@@ -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);
index 5104c107198cc25ce21bd0ad7ce64f6bc9d7efa1..85db3f295c448e44466fad22b1b943402bd1f21f 100644 (file)
@@ -577,7 +577,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
index b37a67f3107e4f26ee2c0083d774336194d3ca25..4fb7e0cd01920d61c68995564eb71401dffdd234 100644 (file)
@@ -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));
 
index 14a9931dffe5775092a215fd46d9519febbb9dc9..e642c9d515ba226f034f2b71307f3485878c7fcc 100644 (file)
@@ -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;
index 0fc572d204050986476e054b961c0e0ec2d5fcbf..5159fde1352826d0052735c7f44c664f4e93a1f9 100644 (file)
@@ -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