diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index abeaaac438..929239c7d1 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -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 {
+
+
@@ -11854,6 +11858,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
+
+
@@ -13265,6 +13271,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
IOPressureWatch,
CPUSetPartition, and
OOMRules were added in version 261.
+ ActivatingConcurrencyMax was added in version 262.
Scope Unit Objects
diff --git a/man/systemd.slice.xml b/man/systemd.slice.xml
index 6990a27a98..63d603cec4 100644
--- a/man/systemd.slice.xml
+++ b/man/systemd.slice.xml
@@ -139,6 +139,46 @@
+
+
+ ActivatingConcurrencyMax=
+
+ Configures a limit on the maximum number of units assigned to this
+ slice (or any descendent slices) that may be in the activating state
+ at the same time. Unlike ConcurrencySoftMax= which limits units in the
+ active state, this option limits units while they are starting up.
+ Once a unit leaves the activating state (whether to
+ active, failed, or any other state), it no longer
+ counts toward this limit, allowing the next queued unit to begin starting.
+
+ 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 ActivatingConcurrencyMax=, you can pace the
+ startup process to limit CPU and I/O pressure, while still allowing all services to
+ eventually reach the active state.
+
+ 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.,
+ TimeoutStartSec=) on individual units to prevent indefinite blocking.
+
+ Setting ActivatingConcurrencyMax=0 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 ConcurrencySoftMax=0.
+
+ If the special value infinity is specified, no concurrency limit
+ is enforced. This is the default.
+
+ 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.
+
+
+
diff --git a/src/core/dbus-slice.c b/src/core/dbus-slice.c
index 075ae59d41..99949467dc 100644
--- a/src/core/dbus-slice.c
+++ b/src/core/dbus-slice.c
@@ -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;
}
diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in
index 57a56f299a..84a885110c 100644
--- a/src/core/load-fragment-gperf.gperf.in
+++ b/src/core/load-fragment-gperf.gperf.in
@@ -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') }}
diff --git a/src/core/slice.c b/src/core/slice.c
index dfd5b6cdba..acebc69e74 100644
--- a/src/core/slice.c
+++ b/src/core/slice.c
@@ -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),
diff --git a/src/core/slice.h b/src/core/slice.h
index 2f83973b88..5854960439 100644
--- a/src/core/slice.h
+++ b/src/core/slice.h
@@ -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);
diff --git a/src/core/unit.c b/src/core/unit.c
index 0936e42295..5dd9f41dad 100644
--- a/src/core/unit.c
+++ b/src/core/unit.c
@@ -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) {
diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c
index e3b1a0f00b..c9ef662857 100644
--- a/src/systemctl/systemctl-show.c
+++ b/src/systemctl/systemctl-show.c
@@ -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;
diff --git a/test/units/TEST-07-PID1.concurrency.sh b/test/units/TEST-07-PID1.concurrency.sh
index 82588739bc..8d4635cf45 100755
--- a/test/units/TEST-07-PID1.concurrency.sh
+++ b/test/units/TEST-07-PID1.concurrency.sh
@@ -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 </run/systemd/system/slow-start@.service <