]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
systemctl: use new EnqueueUnitJobMany() when available
authorLuca Boccassi <luca.boccassi@gmail.com>
Tue, 19 May 2026 17:43:28 +0000 (18:43 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Mon, 22 Jun 2026 20:19:16 +0000 (21:19 +0100)
Fixes https://github.com/systemd/systemd/issues/7877
Replaces https://github.com/systemd/systemd/pull/7947

man/systemctl.xml
src/systemctl/systemctl-start-unit.c

index ce485aee14fa75f8fd0dc9e8c6ec4ab749fc6679..d75ae0188e88c01579a1ab58d2af4d1008cb417c 100644 (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
index 1b6dbd8b14040ea0eee45bbf59e10e2c893b43c4..9bc924a07d5d27f7f8a0d57a60f47cb539f2e428 100644 (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) {