Add ActivatingConcurrencyMax for slice startup pacing

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.
This commit is contained in:
Morteza Pourkazemi
2026-08-03 14:29:07 +09:00
committed by Yu Watanabe
parent 6044c31038
commit 5f0d98e728
9 changed files with 183 additions and 13 deletions
+7
View 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>
+40
View File
@@ -139,6 +139,46 @@
<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>
+4
View 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;
}
+1
View 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') }}
+34
View 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),
+2
View 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);
+23 -10
View 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) {
+13 -3
View 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;
+59
View File
@@ -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