From: Morteza Pourkazemi Date: Sun, 26 Jul 2026 22:38:11 +0000 (+0200) Subject: Add ActivatingConcurrencyMax for slice startup pacing X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=HEAD;p=thirdparty%2Fsystemd.git 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. --- diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index abeaaac4381..929239c7d12 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 6990a27a98c..63d603cec45 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 075ae59d419..99949467dc0 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 57a56f299a2..84a885110cb 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 dfd5b6cdba1..acebc69e749 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 2f83973b885..5854960439c 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 0936e422958..5dd9f41dad5 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 e3b1a0f00ba..c9ef6628577 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 82588739bc8..8d4635cf450 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 <