systemctl: use new EnqueueUnitJobMany() when available

Fixes https://github.com/systemd/systemd/issues/7877
Replaces https://github.com/systemd/systemd/pull/7947
This commit is contained in:
Luca Boccassi
2026-06-22 21:19:16 +01:00
parent 7d3b32daef
commit 8b25e0f85e
2 changed files with 142 additions and 10 deletions
+7
View File
@@ -418,6 +418,13 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
<listitem>
<para>Start (activate) one or more units specified on the command line.</para>
<para>When more than one unit is specified, the units are enqueued in a single job transaction.
This ensures that ordering dependencies (<varname>After=</varname>/<varname>Before=</varname>)
configured between the listed units are honored, regardless of the order in which they are given
on the command line. The same applies to <command>stop</command>, <command>restart</command> and
the reload/restart variants below. If a single transaction cannot be put together, separate
transactions will be submitted separately instead as a fallback.</para>
<para>Note that unit glob patterns expand to names of units currently in memory. Units which are
not active and are not in a failed state usually are not in memory, and will not be matched by
any pattern. In addition, in case of instantiated units, systemd is often unaware of the instance
+135 -10
View File
@@ -2,7 +2,6 @@
#include "sd-bus.h"
#include "alloc-util.h"
#include "ansi-color.h"
#include "bus-common-errors.h"
#include "bus-error.h"
@@ -192,6 +191,95 @@ fail:
return r;
}
static int start_units_one_transaction(
sd_bus *bus,
char * const *names,
const char *job_type,
const char *mode,
sd_bus_error *error,
BusWaitForJobs *w,
BusWaitForUnits *wu,
char ***ret_stopped_units) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
int r;
assert(bus);
assert(!strv_isempty(names));
assert(job_type);
assert(mode);
assert(error);
log_debug("Executing dbus call org.freedesktop.systemd1.Manager EnqueueUnitJobMany(%s, %s) for %zu units",
job_type, mode, strv_length(names));
r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, "EnqueueUnitJobMany");
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append_strv(m, (char**) names);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append(m, "sst", job_type, mode, UINT64_C(0));
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_call(bus, m, /* usec= */ 0, error, &reply);
if (r < 0)
return r;
r = sd_bus_message_enter_container(reply, 'a', "(uosos)");
if (r < 0)
return bus_log_parse_error(r);
for (;;) {
const char *path, *unit_id, *jt;
uint32_t id;
r = sd_bus_message_read(reply, "(uosos)", &id, &path, &unit_id, NULL, &jt);
if (r < 0)
return bus_log_parse_error(r);
if (r == 0)
break;
log_debug("Enqueued job %" PRIu32 " for %s/%s as %s", id, unit_id, jt, path);
if (need_daemon_reload(bus, unit_id) > 0)
warn_unit_file_changed(unit_id);
if (w) {
log_debug("Adding %s to the set", path);
r = bus_wait_for_jobs_add(w, path);
if (r < 0)
return log_error_errno(r, "Failed to watch job for %s: %m", unit_id);
}
if (wu) {
r = bus_wait_for_units_add_unit(wu, unit_id, BUS_WAIT_FOR_INACTIVE|BUS_WAIT_NO_JOB, NULL, NULL);
if (r < 0)
return log_error_errno(r, "Failed to watch unit %s: %m", unit_id);
}
/* Only record units for which the daemon actually enqueued a stop job. The
* server may optimize away stops for units that are already inactive, in
* which case the corresponding entry has a different job type (or is
* omitted), and we must not warn about "unit can still be triggered by…"
* for those. */
if (ret_stopped_units && streq(jt, "stop")) {
r = strv_extend(ret_stopped_units, unit_id);
if (r < 0)
return log_oom();
}
}
r = sd_bus_message_exit_container(reply);
if (r < 0)
return bus_log_parse_error(r);
return 0;
}
static int enqueue_marked_jobs(
sd_bus *bus,
BusWaitForJobs *w) {
@@ -293,7 +381,7 @@ int verb_start(int argc, char *argv[], uintptr_t _data, void *userdata) {
_cleanup_(bus_wait_for_units_freep) BusWaitForUnits *wu = NULL;
_cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL;
const char *method, *job_type, *mode, *one_name, *suffix = NULL;
_cleanup_free_ char **stopped_units = NULL; /* Do not use _cleanup_strv_free_ */
_cleanup_strv_free_ char **stopped_units = NULL;
_cleanup_strv_free_ char **names = NULL;
bool is_enqueue_marked_jobs = false;
int r, ret = EXIT_SUCCESS;
@@ -401,22 +489,59 @@ int verb_start(int argc, char *argv[], uintptr_t _data, void *userdata) {
if (is_enqueue_marked_jobs)
ret = enqueue_marked_jobs(bus, w);
else {
bool fallback = true;
if (arg_verbose)
(void) journal_fork(arg_runtime_scope, names, arg_output, &journal_pid);
STRV_FOREACH(name, names) {
/* When operating on multiple units in one go, prefer the new EnqueueUnitJobMany() method
* which builds a single transaction. This ensures After= ordering is honored regardless
* of the order on the command line (see issue #8102). For show-transaction or dry-run we
* stick to per-unit calls. */
if (arg_action == ACTION_SYSTEMCTL &&
strv_length(names) > 1 &&
!arg_show_transaction &&
!arg_dry_run &&
!streq(mode, "isolate")) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
r = start_unit_one(bus, method, job_type, *name, mode, &error, w, wu);
if (ret == EXIT_SUCCESS && r < 0)
r = start_units_one_transaction(bus, names, job_type, mode, &error, w, wu,
streq(method, "StopUnit") ? &stopped_units : NULL);
if (r >= 0)
fallback = false;
else if (sd_bus_error_has_names(&error,
SD_BUS_ERROR_UNKNOWN_METHOD,
SD_BUS_ERROR_INVALID_ARGS)) {
log_debug_errno(r, "EnqueueUnitJobMany() unavailable or unsupported (%s), falling back to per-unit calls.",
bus_error_message(&error, r));
} else if (sd_bus_error_has_name(&error, BUS_ERROR_UNIT_MASKED) &&
STR_IN_SET(method, "TryRestartUnit", "ReloadOrTryRestartUnit")) {
/* try-restart variants silently ignore masked units in the per-unit
* path; preserve that by falling back so each unit is evaluated
* individually. */
log_debug_errno(r, "Batched %s aborted due to masked unit (%s), falling back to per-unit calls.",
method, bus_error_message(&error, r));
} else {
log_error_errno(r, "Failed to enqueue jobs: %s", bus_error_message(&error, r));
ret = translate_bus_error_to_exit_status(r, &error);
if (r >= 0 && streq(method, "StopUnit")) {
r = strv_push(&stopped_units, *name);
if (r < 0)
return log_oom();
fallback = false; /* Real error, don't fall back. */
}
}
if (fallback)
STRV_FOREACH(name, names) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
r = start_unit_one(bus, method, job_type, *name, mode, &error, w, wu);
if (ret == EXIT_SUCCESS && r < 0)
ret = translate_bus_error_to_exit_status(r, &error);
if (r >= 0 && streq(method, "StopUnit")) {
r = strv_extend(&stopped_units, *name);
if (r < 0)
return log_oom();
}
}
}
if (!arg_no_block) {