@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 = '...';
<!--property ConcurrencySoftMax is not documented!-->
+ <!--property ActivatingConcurrencyMax is not documented!-->
+
<!--property NCurrentlyActive is not documented!-->
<!--property Slice is not documented!-->
<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"/>
<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>
<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>
* 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
};
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;
}
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') }}
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) {
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);
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),
unsigned concurrency_soft_max;
unsigned concurrency_hard_max;
+ unsigned activating_concurrency_max;
CGroupContext cgroup_context;
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);
* 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 */
}
}
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) {
* 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) {
/* Slice */
unsigned concurrency_hard_max;
unsigned concurrency_soft_max;
+ unsigned activating_concurrency_max;
unsigned n_currently_active;
/* CGroup */
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(')');
}
{ "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) },
.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;
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