]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Add ActivatingConcurrencyMax for slice startup pacing main
authorMorteza Pourkazemi <mortezaPRK@users.noreply.github.com>
Sun, 26 Jul 2026 22:38:11 +0000 (00:38 +0200)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Mon, 3 Aug 2026 05:29:07 +0000 (14:29 +0900)
Introduce Slice.ActivatingConcurrencyMax to limit how many units
within a slice hierarchy may be in activating state concurrently.

Expose the setting over D-Bus, support transient/property parsing,
enforce it during unit start dispatch, and re-check queued starts when
units leave activating state.

Document the new slice option and add a PID1 concurrency test covering
queued startup behavior.

man/org.freedesktop.systemd1.xml
man/systemd.slice.xml
src/core/dbus-slice.c
src/core/load-fragment-gperf.gperf.in
src/core/slice.c
src/core/slice.h
src/core/unit.c
src/systemctl/systemctl-show.c
test/units/TEST-07-PID1.concurrency.sh

index abeaaac4381840c7c6d21fae24eddf199dfc21e8..929239c7d1286abc7c2310c6f6c6d40935a4498a 100644 (file)
@@ -11469,6 +11469,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
       @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
       readonly u ConcurrencySoftMax = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+      readonly u ActivatingConcurrencyMax = ...;
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
       readonly u NCurrentlyActive = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
       readonly s Slice = '...';
@@ -11666,6 +11668,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
 
     <!--property ConcurrencySoftMax is not documented!-->
 
+    <!--property ActivatingConcurrencyMax is not documented!-->
+
     <!--property NCurrentlyActive is not documented!-->
 
     <!--property Slice is not documented!-->
@@ -11854,6 +11858,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
 
     <variablelist class="dbus-property" generated="True" extra-ref="ConcurrencySoftMax"/>
 
+    <variablelist class="dbus-property" generated="True" extra-ref="ActivatingConcurrencyMax"/>
+
     <variablelist class="dbus-property" generated="True" extra-ref="NCurrentlyActive"/>
 
     <variablelist class="dbus-property" generated="True" extra-ref="Slice"/>
@@ -13265,6 +13271,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
       <varname>IOPressureWatch</varname>,
       <varname>CPUSetPartition</varname>, and
       <varname>OOMRules</varname> were added in version 261.</para>
+      <para><varname>ActivatingConcurrencyMax</varname> was added in version 262.</para>
     </refsect2>
     <refsect2>
       <title>Scope Unit Objects</title>
index 6990a27a98c1428769851ced90dccd57b96bd859..63d603cec459885530ca6444686a253f12f02102 100644 (file)
 
         <xi:include href="version-info.xml" xpointer="v258"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>ActivatingConcurrencyMax=</varname></term>
+
+        <listitem><para>Configures a limit on the maximum number of units assigned to this
+        slice (or any descendent slices) that may be in the <emphasis>activating</emphasis> state
+        at the same time. Unlike <varname>ConcurrencySoftMax=</varname> which limits units in the
+        <emphasis>active</emphasis> state, this option limits units while they are starting up.
+        Once a unit leaves the <emphasis>activating</emphasis> state (whether to
+        <emphasis>active</emphasis>, <emphasis>failed</emphasis>, or any other state), it no longer
+        counts toward this limit, allowing the next queued unit to begin starting.</para>
+
+        <para>This is particularly useful for managing the "thundering herd" problem during system
+        boot, where many long-running services (such as container workloads) attempt to start
+        simultaneously. By setting <varname>ActivatingConcurrencyMax=</varname>, you can pace the
+        startup process to limit CPU and I/O pressure, while still allowing all services to
+        eventually reach the <emphasis>active</emphasis> state.</para>
+
+        <para>When the limit is reached, further activation requests are queued and will be
+        dispatched automatically once running activations complete. No error is returned to the
+        caller. Note that if a unit becomes stuck in the activating state (for example, due to
+        a hung process or missing dependency), it will continue to occupy a slot until it
+        leaves that state. Configure appropriate timeouts (e.g.,
+        <varname>TimeoutStartSec=</varname>) on individual units to prevent indefinite blocking.</para>
+
+        <para>Setting <varname>ActivatingConcurrencyMax=0</varname> blocks all activation
+        requests in the slice hierarchy indefinitely. Queued units will never start until
+        the limit is raised. This can be used to intentionally freeze slice startup,
+        matching the behavior of <varname>ConcurrencySoftMax=0</varname>.</para>
+
+        <para>If the special value <literal>infinity</literal> is specified, no concurrency limit
+        is enforced. This is the default.</para>
+
+        <para>Note that this option has a hierarchical effect: a limit set for a slice unit will
+        apply to both the units immediately within the slice and all units further down the slice
+        tree. Note that slice units themselves never enter the activating state, so nested slices
+        do not count toward the limit.</para>
+
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
     </variablelist>
 
   </refsect1>
index 075ae59d419c900008bca78d307ef5f52ae2ca85..99949467dc0d1e6c6b34acc38f79cdfd66855430 100644 (file)
@@ -34,6 +34,7 @@ const sd_bus_vtable bus_slice_vtable[] = {
          * systemctl set-property), hence they aren't marked as constant */
         SD_BUS_PROPERTY("ConcurrencyHardMax", "u", bus_property_get_unsigned, offsetof(Slice, concurrency_hard_max), 0),
         SD_BUS_PROPERTY("ConcurrencySoftMax", "u", bus_property_get_unsigned, offsetof(Slice, concurrency_soft_max), 0),
+        SD_BUS_PROPERTY("ActivatingConcurrencyMax", "u", bus_property_get_unsigned, offsetof(Slice, activating_concurrency_max), 0),
         SD_BUS_PROPERTY("NCurrentlyActive", "u", property_get_currently_active, 0, 0),
         SD_BUS_VTABLE_END
 };
@@ -59,6 +60,9 @@ static int bus_slice_set_transient_property(
         if (streq(name, "ConcurrencySoftMax"))
                 return bus_set_transient_unsigned(u, name, &s->concurrency_soft_max, message, flags, reterr_error);
 
+        if (streq(name, "ActivatingConcurrencyMax"))
+                return bus_set_transient_unsigned(u, name, &s->activating_concurrency_max, message, flags, reterr_error);
+
         return 0;
 }
 
index 57a56f299a24d44c94c3e1e661708bafc46ffe70..84a885110cb83e650098b4be08427a3a111d66b7 100644 (file)
@@ -634,6 +634,7 @@ Path.TriggerLimitIntervalSec,                 config_parse_sec,
 Path.TriggerLimitBurst,                       config_parse_unsigned,                              0,                                  offsetof(Path, trigger_limit.burst)
 Slice.ConcurrencySoftMax,                     config_parse_concurrency_max,                       0,                                  offsetof(Slice, concurrency_soft_max)
 Slice.ConcurrencyHardMax,                     config_parse_concurrency_max,                       0,                                  offsetof(Slice, concurrency_hard_max)
+Slice.ActivatingConcurrencyMax,               config_parse_concurrency_max,                       0,                                  offsetof(Slice, activating_concurrency_max)
 {{ CGROUP_CONTEXT_CONFIG_ITEMS('Slice') }}
 {{ CGROUP_CONTEXT_CONFIG_ITEMS('Scope') }}
 {{ KILL_CONTEXT_CONFIG_ITEMS('Scope') }}
index dfd5b6cdba19f995d85911d11f15b46aba94c4bb..acebc69e7495fe73552b3a4605ecc4b272cea9e8 100644 (file)
@@ -28,6 +28,7 @@ static void slice_init(Unit *u) {
         u->ignore_on_isolate = true;
         s->concurrency_hard_max = UINT_MAX;
         s->concurrency_soft_max = UINT_MAX;
+        s->activating_concurrency_max = UINT_MAX;
 }
 
 static void slice_set_state(Slice *s, SliceState state) {
@@ -410,6 +411,25 @@ unsigned slice_get_currently_active(Slice *slice, Unit *ignore, bool with_pendin
         return n;
 }
 
+static unsigned slice_get_currently_activating(Slice *slice, Unit *ignore) {
+        Unit *u = ASSERT_PTR(UNIT(slice));
+        unsigned n = 0;
+        Unit *member;
+
+        UNIT_FOREACH_DEPENDENCY(member, u, UNIT_ATOM_SLICE_OF) {
+                if (member == ignore)
+                        continue;
+
+                if (unit_active_state(member) == UNIT_ACTIVATING)
+                        n++;
+
+                if (member->type == UNIT_SLICE)
+                        n += slice_get_currently_activating(SLICE(member), ignore);
+        }
+
+        return n;
+}
+
 bool slice_concurrency_soft_max_reached(Slice *slice, Unit *ignore) {
         assert(slice);
 
@@ -438,6 +458,20 @@ bool slice_concurrency_hard_max_reached(Slice *slice, Unit *ignore) {
         return false;
 }
 
+bool slice_activating_concurrency_max_reached(Slice *slice, Unit *ignore) {
+        assert(slice);
+
+        if (slice->activating_concurrency_max != UINT_MAX &&
+            slice_get_currently_activating(slice, ignore) >= slice->activating_concurrency_max)
+                return true;
+
+        Unit *parent = UNIT_GET_SLICE(UNIT(slice));
+        if (parent)
+                return slice_activating_concurrency_max_reached(SLICE(parent), ignore);
+
+        return false;
+}
+
 const UnitVTable slice_vtable = {
         .object_size = sizeof(Slice),
         .cgroup_context_offset = offsetof(Slice, cgroup_context),
index 2f83973b8855c5620d3387b862c2e9885b31fe4d..5854960439c9cabf78c2f8009d980ad6f9746ca6 100644 (file)
@@ -12,6 +12,7 @@ typedef struct Slice {
 
         unsigned concurrency_soft_max;
         unsigned concurrency_hard_max;
+        unsigned activating_concurrency_max;
 
         CGroupContext cgroup_context;
 
@@ -26,3 +27,4 @@ unsigned slice_get_currently_active(Slice *slice, Unit *ignore, bool with_pendin
 
 bool slice_concurrency_hard_max_reached(Slice *slice, Unit *ignore);
 bool slice_concurrency_soft_max_reached(Slice *slice, Unit *ignore);
+bool slice_activating_concurrency_max_reached(Slice *slice, Unit *ignore);
index 0936e422958962aa9874fbc6162dc6ccd69f8523..5dd9f41dad59c692eaddd59aa2bba99786f060b8 100644 (file)
@@ -2033,6 +2033,10 @@ int unit_start(Unit *u, ActivationDetails *details) {
                          * the queue */
                         if (slice_concurrency_soft_max_reached(slice, u))
                                 return -EAGAIN; /* Try again, keep in queue */
+
+                        /* Check activating concurrency limit to pace concurrent startups */
+                        if (slice_activating_concurrency_max_reached(slice, u))
+                                return -EAGAIN; /* Try again, keep in queue */
                 }
         }
 
@@ -2713,20 +2717,21 @@ static void unit_check_concurrency_limit(Unit *u) {
         if (!slice)
                 return;
 
-        /* If a unit was stopped, maybe it has pending siblings (or children thereof) that can be started now */
+        /* If a unit was stopped, maybe it has pending siblings (or children thereof) that can be started now.
+         * Walk up the slice hierarchy and re-dispatch for each ancestor that has a limit configured. */
 
-        if (SLICE(slice)->concurrency_soft_max != UINT_MAX) {
-                Unit *sibling;
-                UNIT_FOREACH_DEPENDENCY(sibling, slice, UNIT_ATOM_SLICE_OF) {
-                        if (sibling == u)
-                                continue;
+        for (Unit *s = slice; s; s = UNIT_GET_SLICE(s)) {
+                if (SLICE(s)->concurrency_soft_max != UINT_MAX ||
+                    SLICE(s)->activating_concurrency_max != UINT_MAX) {
+                        Unit *member;
+                        UNIT_FOREACH_DEPENDENCY(member, s, UNIT_ATOM_SLICE_OF) {
+                                if (member == u)
+                                        continue;
 
-                        unit_recursive_add_to_run_queue(sibling);
+                                unit_recursive_add_to_run_queue(member);
+                        }
                 }
         }
-
-        /* Also go up the tree. */
-        unit_check_concurrency_limit(slice);
 }
 
 void unit_notify(Unit *u, UnitActiveState os, UnitActiveState ns, bool reload_success) {
@@ -2882,7 +2887,15 @@ void unit_notify(Unit *u, UnitActiveState os, UnitActiveState ns, bool reload_su
                  * when something BindsTo= to a Type=oneshot unit, as these units go directly from starting to
                  * inactive, without ever entering started.) */
                 unit_submit_to_stop_when_bound_queue(u);
+
+                /* Maybe the activating concurrency limits now allow dispatching of another start job in this slice? */
+                unit_check_concurrency_limit(u);
         }
+
+        /* When a unit leaves the activating state (to deactivating, inactive, or active), it frees up a slot for
+         * ActivatingConcurrencyMax. Re-dispatch queued starts. */
+        if (os == UNIT_ACTIVATING && ns != UNIT_ACTIVATING)
+                unit_check_concurrency_limit(u);
 }
 
 int unit_watch_pidref(Unit *u, const PidRef *pid, bool exclusive) {
index e3b1a0f00ba2cfe8c704d2fd9fc09efae034f522..c9ef66285774f272cec88c7875265a5831a213c4 100644 (file)
@@ -273,6 +273,7 @@ typedef struct UnitStatusInfo {
         /* Slice */
         unsigned concurrency_hard_max;
         unsigned concurrency_soft_max;
+        unsigned activating_concurrency_max;
         unsigned n_currently_active;
 
         /* CGroup */
@@ -758,16 +759,21 @@ static void print_status_info(
         if (endswith(i->id, ".slice")) {
                 printf(" Act. Units: %u", i->n_currently_active);
 
-                if (i->concurrency_soft_max != UINT_MAX || i->concurrency_hard_max != UINT_MAX) {
+                if (i->concurrency_soft_max != UINT_MAX || i->concurrency_hard_max != UINT_MAX || i->activating_concurrency_max != UINT_MAX) {
                         fputs(" (", stdout);
 
                         if (i->concurrency_soft_max != UINT_MAX && i->concurrency_soft_max < i->concurrency_hard_max) {
                                 printf("soft limit: %u", i->concurrency_soft_max);
-                                if (i->concurrency_hard_max != UINT_MAX)
+                                if (i->concurrency_hard_max != UINT_MAX || i->activating_concurrency_max != UINT_MAX)
                                         fputs("; ", stdout);
                         }
-                        if (i->concurrency_hard_max != UINT_MAX)
+                        if (i->concurrency_hard_max != UINT_MAX) {
                                 printf("hard limit: %u", i->concurrency_hard_max);
+                                if (i->activating_concurrency_max != UINT_MAX)
+                                        fputs("; ", stdout);
+                        }
+                        if (i->activating_concurrency_max != UINT_MAX)
+                                printf("activating limit: %u", i->activating_concurrency_max);
 
                         putchar(')');
                 }
@@ -2260,6 +2266,7 @@ static int show_one(
                 { "What",                           "s",               NULL,           offsetof(UnitStatusInfo, what)                              },
                 { "ConcurrencyHardMax",             "u",               NULL,           offsetof(UnitStatusInfo, concurrency_hard_max)              },
                 { "ConcurrencySoftMax",             "u",               NULL,           offsetof(UnitStatusInfo, concurrency_soft_max)              },
+                { "ActivatingConcurrencyMax",       "u",               NULL,           offsetof(UnitStatusInfo, activating_concurrency_max)        },
                 { "NCurrentlyActive",               "u",               NULL,           offsetof(UnitStatusInfo, n_currently_active)                },
                 { "MemoryCurrent",                  "t",               NULL,           offsetof(UnitStatusInfo, memory_current)                    },
                 { "MemoryPeak",                     "t",               NULL,           offsetof(UnitStatusInfo, memory_peak)                       },
@@ -2336,6 +2343,9 @@ static int show_one(
                 .ip_egress_bytes = UINT64_MAX,
                 .io_read_bytes = UINT64_MAX,
                 .io_write_bytes = UINT64_MAX,
+                .concurrency_soft_max = UINT_MAX,
+                .concurrency_hard_max = UINT_MAX,
+                .activating_concurrency_max = UINT_MAX,
         };
         bool collect_found_properties;
         int r;
index 82588739bc846d7aaac11d54691d81492c5a03ad..8d4635cf4508925d4f380605cc655c30b124b215 100755 (executable)
@@ -117,6 +117,65 @@ systemctl start sleepforever3@a.service
 systemctl stop concurrency1.slice
 systemctl reset-failed
 
+# Test ActivatingConcurrencyMax
+cat >/run/systemd/system/concurrency-activating.slice <<EOF
+[Slice]
+ActivatingConcurrencyMax=2
+EOF
+
+cat >/run/systemd/system/slow-start@.service <<EOF
+[Service]
+Slice=concurrency-activating.slice
+# Simulate slow startup
+ExecStartPre=/usr/bin/sleep 2
+ExecStart=/usr/bin/sleep infinity
+EOF
+
+systemctl daemon-reload
+
+# Start 3 services - only 2 should activate concurrently
+systemctl --no-block start slow-start@a.service
+systemctl --no-block start slow-start@b.service
+systemctl --no-block start slow-start@c.service
+
+# Wait for jobs to be dispatched and first two to enter activating
+for _ in {1..20}; do
+    jobs=$(systemctl list-jobs | grep -c "slow-start@.*start" || true)
+    if [[ "$jobs" -eq 3 ]]; then
+        activating=$(systemctl list-units --state=activating 'slow-start@*' --no-legend | wc -l)
+        if [[ "$activating" -eq 2 ]]; then
+            break
+        fi
+    fi
+    sleep 0.1
+done
+
+# Check that exactly 2 are activating (c should be queued)
+test "$(systemctl list-jobs | grep -c "slow-start@.*start")" -eq 3
+test "$(systemctl show -p ActiveState slow-start@a.service --value)" = "activating"
+test "$(systemctl show -p ActiveState slow-start@b.service --value)" = "activating"
+test "$(systemctl show -p ActiveState slow-start@c.service --value)" = "inactive"
+
+# Wait for a and b to finish starting, then c to start and finish
+# a,b take ~2s to activate, then c starts and takes ~2s more
+sleep 5
+
+# Now all should be active (c started when a/b finished activating)
+systemctl is-active slow-start@a.service
+systemctl is-active slow-start@b.service
+systemctl is-active slow-start@c.service
+
+# Cleanup
+systemctl stop concurrency-activating.slice
+systemctl reset-failed
+rm /run/systemd/system/concurrency-activating.slice
+rm /run/systemd/system/slow-start@.service
+
+systemctl daemon-reload
+
+# Final cleanup of original tests
+systemctl reset-failed
+
 rm /run/systemd/system/concurrency1.slice
 rm /run/systemd/system/concurrency1-concurrency2.slice
 rm /run/systemd/system/concurrency1-concurrency3.slice