]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core/socket: introduce DeferTrigger= and DeferTriggerMaxSec=
authorMike Yuan <me@yhndnzj.com>
Fri, 16 May 2025 16:10:46 +0000 (18:10 +0200)
committerMike Yuan <me@yhndnzj.com>
Mon, 30 Jun 2025 11:10:43 +0000 (13:10 +0200)
Alternative to b50f6dbe574b0421db7dbf200ad951186382277d

The commit naively returned early from socket_enter_running(), which however
is quite problematic, as the socket will be woken up over and over again
without doing a thing, until we eventually hit Poll/TriggerLimit*=.
On top of that it requires hacks to hold the start job for initrd-switch-root.service
up. Overall I doubt that is the right approach.

Let's instead hook this into our job engine, and try to activate
the service again when some other units are stopped. If all installed
jobs have been run yet we're still seeing the conflict or the manually
selected timeout is reached, fail the socket as before.

16 files changed:
man/org.freedesktop.systemd1.xml
man/systemd.socket.xml
src/basic/unit-def.c
src/basic/unit-def.h
src/core/dbus-socket.c
src/core/job.c
src/core/load-fragment-gperf.gperf.in
src/core/load-fragment.c
src/core/load-fragment.h
src/core/manager.c
src/core/manager.h
src/core/socket.c
src/core/socket.h
src/core/unit.c
src/core/unit.h
src/shared/bus-unit-util.c

index 95f9a4f4494b903dfd0fdec90cc608a0b6b66863..3cf0e18709671bdded4ddba1e6b5e5ac6e2d860d 100644 (file)
@@ -4962,6 +4962,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
       readonly t PollLimitIntervalUSec = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly u PollLimitBurst = ...;
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly s DeferTrigger = '...';
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly t DeferTriggerMaxUSec = ...;
       readonly u UID = ...;
       readonly u GID = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -5626,6 +5630,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
 
     <!--property TriggerLimitBurst is not documented!-->
 
+    <!--property DeferTrigger is not documented!-->
+
+    <!--property DeferTriggerMaxUSec is not documented!-->
+
     <!--property UID is not documented!-->
 
     <!--property GID is not documented!-->
@@ -6244,6 +6252,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
 
     <variablelist class="dbus-property" generated="True" extra-ref="PollLimitBurst"/>
 
+    <variablelist class="dbus-property" generated="True" extra-ref="DeferTrigger"/>
+
+    <variablelist class="dbus-property" generated="True" extra-ref="DeferTriggerMaxUSec"/>
+
     <variablelist class="dbus-property" generated="True" extra-ref="UID"/>
 
     <variablelist class="dbus-property" generated="True" extra-ref="GID"/>
@@ -12112,8 +12124,10 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
       <para><varname>ProtectHostnameEx</varname>,
       <varname>PassPIDFD</varname>,
       <varname>AcceptFileDescriptors</varname>,
-      <varname>DelegateNamespaces</varname>, and
-      <function>RemoveSubgroup()</function> were added in version 258.</para>
+      <varname>DelegateNamespaces</varname>,
+      <function>RemoveSubgroup()</function>,
+      <varname>DeferTrigger</varname>, and
+      <varname>DeferTriggerMaxUSec</varname> were added in version 258.</para>
     </refsect2>
     <refsect2>
       <title>Mount Unit Objects</title>
index 91552d691f9e716a40ab74ac5f4b9cce1f9a0121..213ceffcdbd745e4b1a7b4f17ffc031d8c90f4d4 100644 (file)
         <xi:include href="version-info.xml" xpointer="v255"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>DeferTrigger=</varname></term>
+
+        <listitem><para>Takes a boolean argument, or <literal>patient</literal>. May only be used when <varname>Accept=no</varname>.
+        If enabled, job mode <literal>lenient</literal> instead of <literal>replace</literal> is used when
+        triggering the service, which means currently activating/running units that conflict with the service
+        won't be disturbed/brought down. Furthermore, if a conflict exists, the socket unit will wait for
+        current job queue to complete and potentially defer the activation by then. An upper limit of total time
+        to wait can be configured via <varname>DeferTriggerMaxSec=</varname>. If set to <option>yes</option>,
+        the socket unit will fail if all jobs have finished or the timeout has been reached but the conflict remains.
+        If <option>patient</option>, always wait until <varname>DeferTriggerMaxSec=</varname> elapses.
+        Defaults to no.</para>
+
+        <para>This setting is particularly useful if the socket unit should stay active across switch-root/soft-reboot
+        operations while the triggered service is stopped.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>DeferTriggerMaxSec=</varname></term>
+
+        <listitem><para>Configures the maximum time to defer the triggering when <varname>DeferTrigger=</varname>
+        is enabled. If the service cannot be activated within the specified time, the socket will be considered
+        failed and get terminated. Takes a unit-less value in seconds, or a time span value such as "5min 20s".
+        Pass <literal>0</literal> or <literal>infinity</literal> to disable the timeout logic (the default).
+        </para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>PassFileDescriptorsToExec=</varname></term>
 
index 45911fb190c0ded252f8ca22d2aab41be854bc0e..8bc5f56532eb1ce7106c576564027212a19897df 100644 (file)
@@ -250,6 +250,7 @@ static const char* const socket_state_table[_SOCKET_STATE_MAX] = {
         [SOCKET_START_CHOWN]      = "start-chown",
         [SOCKET_START_POST]       = "start-post",
         [SOCKET_LISTENING]        = "listening",
+        [SOCKET_DEFERRED]         = "deferred",
         [SOCKET_RUNNING]          = "running",
         [SOCKET_STOP_PRE]         = "stop-pre",
         [SOCKET_STOP_PRE_SIGTERM] = "stop-pre-sigterm",
index f82d072ed50ddf6c361a8121389cbf9b6c674187..fd55950ea63ec85ede7f2f81aa8558b3bab10b2f 100644 (file)
@@ -169,6 +169,7 @@ typedef enum SocketState {
         SOCKET_START_CHOWN,
         SOCKET_START_POST,
         SOCKET_LISTENING,
+        SOCKET_DEFERRED,
         SOCKET_RUNNING,
         SOCKET_STOP_PRE,
         SOCKET_STOP_PRE_SIGTERM,
index de96f00a15832a5f12eae0be1bbb2af968380da7..02039dd6db6a6b19c0675cd0988ed45845ba7ae2 100644 (file)
@@ -21,6 +21,7 @@ static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_result, socket_result, SocketRe
 static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_bind_ipv6_only, socket_address_bind_ipv6_only, SocketAddressBindIPv6Only);
 static BUS_DEFINE_PROPERTY_GET(property_get_fdname, "s", Socket, socket_fdname);
 static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_timestamping, socket_timestamping, SocketTimestamping);
+static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_defer_trigger, socket_defer_trigger, SocketDeferTrigger);
 
 static int property_get_listen(
                 sd_bus *bus,
@@ -115,6 +116,8 @@ const sd_bus_vtable bus_socket_vtable[] = {
         SD_BUS_PROPERTY("TriggerLimitBurst", "u", bus_property_get_unsigned, offsetof(Socket, trigger_limit.burst), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("PollLimitIntervalUSec", "t", bus_property_get_usec, offsetof(Socket, poll_limit.interval), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("PollLimitBurst", "u", bus_property_get_unsigned, offsetof(Socket, poll_limit.burst), SD_BUS_VTABLE_PROPERTY_CONST),
+        SD_BUS_PROPERTY("DeferTrigger", "s", property_get_defer_trigger, offsetof(Socket, defer_trigger), SD_BUS_VTABLE_PROPERTY_CONST),
+        SD_BUS_PROPERTY("DeferTriggerMaxUSec", "t", bus_property_get_usec, offsetof(Socket, defer_trigger_max_usec), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("UID", "u", bus_property_get_uid, offsetof(Unit, ref_uid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
         SD_BUS_PROPERTY("GID", "u", bus_property_get_gid, offsetof(Unit, ref_gid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
         SD_BUS_PROPERTY("PassFileDescriptorsToExec", "b", bus_property_get_bool, offsetof(Socket, pass_fds_to_exec), SD_BUS_VTABLE_PROPERTY_CONST),
@@ -148,6 +151,7 @@ static BUS_DEFINE_SET_TRANSIENT_STRING_WITH_CHECK(ifname, ifname_valid);
 static BUS_DEFINE_SET_TRANSIENT_TO_STRING_ALLOC(ip_tos, "i", int32_t, int, "%" PRIi32, ip_tos_to_string_alloc);
 static BUS_DEFINE_SET_TRANSIENT_TO_STRING(socket_protocol, "i", int32_t, int, "%" PRIi32, socket_protocol_to_string);
 static BUS_DEFINE_SET_TRANSIENT_PARSE(socket_timestamping, SocketTimestamping, socket_timestamping_from_string_harder);
+static BUS_DEFINE_SET_TRANSIENT_PARSE(socket_defer_trigger, SocketDeferTrigger, socket_defer_trigger_from_string);
 
 static int bus_socket_set_transient_property(
                 Socket *s,
@@ -274,6 +278,12 @@ static int bus_socket_set_transient_property(
         if (streq(name, "PollLimitIntervalUSec"))
                 return bus_set_transient_usec(u, name, &s->poll_limit.interval, message, flags, error);
 
+        if (streq(name, "DeferTrigger"))
+                return bus_set_transient_socket_defer_trigger(u, name, &s->defer_trigger, message, flags, error);
+
+        if (streq(name, "DeferTriggerMaxUSec"))
+                return bus_set_transient_usec_fix_0(u, name, &s->defer_trigger_max_usec, message, flags, error);
+
         if (streq(name, "SmackLabel"))
                 return bus_set_transient_string(u, name, &s->smack, message, flags, error);
 
index a60025cffc35c0b83903d05827078d67bbcafe9e..bac12542dcaea5c3d0e7dacbafc76c994bfc1c6f 100644 (file)
@@ -1108,6 +1108,10 @@ finish:
         unit_submit_to_stop_when_bound_queue(u);
         unit_submit_to_stop_when_unneeded_queue(u);
 
+        /* All jobs might have finished, let's see */
+        if (u->manager->may_dispatch_stop_notify_queue == 0)
+                u->manager->may_dispatch_stop_notify_queue = -1;
+
         manager_check_finished(u->manager);
 
         return 0;
index aacc76d05342c27ded5af9e1469d92c9a6fab889..96fe04aaf406e03b1bc38ed36ec66c2938707eff 100644 (file)
@@ -528,6 +528,8 @@ Socket.TriggerLimitIntervalSec,               config_parse_sec,
 Socket.TriggerLimitBurst,                     config_parse_unsigned,                              0,                                  offsetof(Socket, trigger_limit.burst)
 Socket.PollLimitIntervalSec,                  config_parse_sec,                                   0,                                  offsetof(Socket, poll_limit.interval)
 Socket.PollLimitBurst,                        config_parse_unsigned,                              0,                                  offsetof(Socket, poll_limit.burst)
+Socket.DeferTrigger,                          config_parse_socket_defer_trigger,                  0,                                  offsetof(Socket, defer_trigger)
+Socket.DeferTriggerMaxSec,                    config_parse_sec_fix_0,                             0,                                  offsetof(Socket, defer_trigger_max_usec)
 {% if ENABLE_SMACK %}
 Socket.SmackLabel,                            config_parse_unit_string_printf,                    0,                                  offsetof(Socket, smack)
 Socket.SmackLabelIPIn,                        config_parse_unit_string_printf,                    0,                                  offsetof(Socket, smack_ip_in)
index bed01029dc1d26074d12563b315e70fecf4a9072..a6cb3c7e88e6654c4a4ab78a4b9f5f4bc0127a3c 100644 (file)
@@ -159,6 +159,7 @@ DEFINE_CONFIG_PARSE_PTR(config_parse_exec_mount_propagation_flag, mount_propagat
 DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_numa_policy, mpol, int, -1);
 DEFINE_CONFIG_PARSE_ENUM(config_parse_status_unit_format, status_unit_format, StatusUnitFormat);
 DEFINE_CONFIG_PARSE_ENUM_FULL(config_parse_socket_timestamping, socket_timestamping_from_string_harder, SocketTimestamping);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_socket_defer_trigger, socket_defer_trigger, SocketDeferTrigger);
 
 bool contains_instance_specifier_superset(const char *s) {
         const char *p, *q;
index d87bff0996880ee16d3089084be2c091ca4f8df0..e2fe7dce1bb43c97d825e063a66bc13c71ef7dde 100644 (file)
@@ -148,6 +148,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_timeout_abort);
 CONFIG_PARSER_PROTOTYPE(config_parse_swap_priority);
 CONFIG_PARSER_PROTOTYPE(config_parse_mount_images);
 CONFIG_PARSER_PROTOTYPE(config_parse_socket_timestamping);
+CONFIG_PARSER_PROTOTYPE(config_parse_socket_defer_trigger);
 CONFIG_PARSER_PROTOTYPE(config_parse_extension_images);
 CONFIG_PARSER_PROTOTYPE(config_parse_bpf_foreign_program);
 CONFIG_PARSER_PROTOTYPE(config_parse_cgroup_socket_bind);
index 000f19d6f68e600fbef4e8fc1c5e34f23374cd01..aa43c9d79b19f07511d91fc0bc05244f4ab42eb0 100644 (file)
@@ -1597,6 +1597,32 @@ static unsigned manager_dispatch_stop_when_bound_queue(Manager *m) {
         return n;
 }
 
+static unsigned manager_dispatch_stop_notify_queue(Manager *m) {
+        unsigned n = 0;
+
+        assert(m);
+
+        if (m->may_dispatch_stop_notify_queue < 0)
+                m->may_dispatch_stop_notify_queue = hashmap_isempty(m->jobs);
+
+        if (!m->may_dispatch_stop_notify_queue)
+                return 0;
+
+        m->may_dispatch_stop_notify_queue = false;
+
+        LIST_FOREACH(stop_notify_queue, u, m->stop_notify_queue) {
+                assert(u->in_stop_notify_queue);
+
+                assert(UNIT_VTABLE(u)->stop_notify);
+                if (UNIT_VTABLE(u)->stop_notify(u)) {
+                        assert(!u->in_stop_notify_queue);
+                        n++;
+                }
+        }
+
+        return n;
+}
+
 static void manager_clear_jobs_and_units(Manager *m) {
         Unit *u;
 
@@ -3274,6 +3300,9 @@ int manager_loop(Manager *m) {
                 if (manager_dispatch_release_resources_queue(m) > 0)
                         continue;
 
+                if (manager_dispatch_stop_notify_queue(m) > 0)
+                        continue;
+
                 if (manager_dispatch_dbus_queue(m) > 0)
                         continue;
 
index 39f822f4e22178670aecc103243220557f0360a3..c267ebe7eee9aadf60a561c3679dd2639ee7ec3b 100644 (file)
@@ -211,6 +211,9 @@ typedef struct Manager {
         /* Units that have resources open, and where it might be good to check if they can be released now */
         LIST_HEAD(Unit, release_resources_queue);
 
+        /* Units that perform certain actions after some other unit deactivates */
+        LIST_HEAD(Unit, stop_notify_queue);
+
         sd_event *event;
 
         /* This maps PIDs we care about to units that are interested in them. We allow multiple units to be
@@ -358,6 +361,7 @@ typedef struct Manager {
 
         /* Flags */
         bool dispatching_load_queue;
+        int may_dispatch_stop_notify_queue; /* tristate */
 
         /* Have we already sent out the READY=1 notification? */
         bool ready_sent;
index 1765e6f9c6af6cf8b739f88ecc77ed28c3bd2994..727840e678ddd24c5d3b1e884889ec3d3f792b87 100644 (file)
@@ -12,6 +12,7 @@
 
 #include "alloc-util.h"
 #include "bpf-program.h"
+#include "bus-common-errors.h"
 #include "bus-error.h"
 #include "copy.h"
 #include "dbus-socket.h"
@@ -69,6 +70,7 @@ static const UnitActiveState state_translation_table[_SOCKET_STATE_MAX] = {
         [SOCKET_START_CHOWN]      = UNIT_ACTIVATING,
         [SOCKET_START_POST]       = UNIT_ACTIVATING,
         [SOCKET_LISTENING]        = UNIT_ACTIVE,
+        [SOCKET_DEFERRED]         = UNIT_ACTIVE,
         [SOCKET_RUNNING]          = UNIT_ACTIVE,
         [SOCKET_STOP_PRE]         = UNIT_DEACTIVATING,
         [SOCKET_STOP_PRE_SIGTERM] = UNIT_DEACTIVATING,
@@ -144,6 +146,8 @@ static void socket_init(Unit *u) {
         s->trigger_limit = RATELIMIT_OFF;
 
         s->poll_limit = RATELIMIT_OFF;
+
+        s->defer_trigger_max_usec = USEC_INFINITY;
 }
 
 static void socket_unwatch_control_pid(Socket *s) {
@@ -427,6 +431,9 @@ static int socket_verify(Socket *s) {
         if (s->accept && UNIT_ISSET(s->service))
                 return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Explicit service configuration for accepting socket units not supported. Refusing.");
 
+        if (s->accept && s->defer_trigger != SOCKET_DEFER_NO)
+                return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Socket unit is configured to be accepting with DeferTrigger= enabled. Refusing.");
+
         if (!strv_isempty(s->symlinks) && !socket_find_symlink_target(s))
                 return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Unit has symlinks set but none or more than one node in the file system. Refusing.");
 
@@ -672,8 +679,12 @@ static void socket_dump(Unit *u, FILE *f, const char *prefix) {
                         prefix, s->max_connections_per_source);
         else
                 fprintf(f,
-                        "%sFlushPending: %s\n",
-                         prefix, yes_no(s->flush_pending));
+                        "%sFlushPending: %s\n"
+                        "%sDeferTrigger: %s\n"
+                        "%sDeferTriggerMaxSec: %s\n",
+                        prefix, yes_no(s->flush_pending),
+                        prefix, socket_defer_trigger_to_string(s->defer_trigger),
+                        prefix, FORMAT_TIMESPAN(s->defer_trigger_max_usec, USEC_PER_SEC));
 
         if (s->priority >= 0)
                 fprintf(f,
@@ -1852,8 +1863,10 @@ static void socket_set_state(Socket *s, SocketState state) {
         old_state = s->state;
         s->state = state;
 
-        if (!SOCKET_STATE_WITH_PROCESS(state)) {
+        if (!SOCKET_STATE_WITH_PROCESS(state) && state != SOCKET_DEFERRED)
                 s->timer_event_source = sd_event_source_disable_unref(s->timer_event_source);
+
+        if (!SOCKET_STATE_WITH_PROCESS(state)) {
                 socket_unwatch_control_pid(s);
                 s->control_command = NULL;
                 s->control_command_id = _SOCKET_EXEC_COMMAND_INVALID;
@@ -1867,12 +1880,16 @@ static void socket_set_state(Socket *s, SocketState state) {
                     SOCKET_START_CHOWN,
                     SOCKET_START_POST,
                     SOCKET_LISTENING,
+                    SOCKET_DEFERRED,
                     SOCKET_RUNNING,
                     SOCKET_STOP_PRE,
                     SOCKET_STOP_PRE_SIGTERM,
                     SOCKET_STOP_PRE_SIGKILL))
                 socket_close_fds(s);
 
+        if (state != SOCKET_DEFERRED)
+                unit_remove_from_stop_notify_queue(UNIT(s));
+
         if (state != old_state)
                 log_unit_debug(UNIT(s), "Changed %s -> %s", socket_state_to_string(old_state), socket_state_to_string(state));
 
@@ -1888,6 +1905,11 @@ static int socket_coldplug(Unit *u) {
         if (s->deserialized_state == s->state)
                 return 0;
 
+        /* Patch "deferred" back to "listening" and let socket_enter_running() figure out what to do.
+         * This saves us the trouble of handling flipping of DeferTrigger= vs Accept= during reload. */
+        if (s->deserialized_state == SOCKET_DEFERRED)
+                s->deserialized_state = SOCKET_LISTENING;
+
         if (pidref_is_set(&s->control_pid) &&
             pidref_is_unwaited(&s->control_pid) > 0 &&
             SOCKET_STATE_WITH_PROCESS(s->deserialized_state)) {
@@ -2347,6 +2369,76 @@ static void socket_enter_start_pre(Socket *s) {
                 socket_enter_start_open(s);
 }
 
+static bool socket_may_defer(Socket *s) {
+        assert(s);
+
+        switch (s->defer_trigger) {
+
+        case SOCKET_DEFER_NO:
+                return false;
+
+        case SOCKET_DEFER_YES:
+                return !hashmap_isempty(UNIT(s)->manager->jobs);
+
+        case SOCKET_DEFER_PATIENT:
+                assert(s->defer_trigger_max_usec > 0);
+                return true;
+
+        default:
+                assert_not_reached();
+        }
+}
+
+static bool socket_stop_notify(Unit *u) {
+        Socket *s = ASSERT_PTR(SOCKET(u));
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(s->state == SOCKET_DEFERRED);
+
+        r = manager_add_job(u->manager, JOB_START, UNIT_DEREF(s->service), JOB_LENIENT, &error, /* ret = */ NULL);
+        if (r >= 0) { /* Yay! */
+                socket_set_state(s, SOCKET_RUNNING);
+                return true; /* changed */
+        }
+        if (sd_bus_error_has_name(&error, BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE)) {
+                if (s->defer_trigger == SOCKET_DEFER_PATIENT || !hashmap_isempty(u->manager->jobs))
+                        /* Wait for some more */
+                        return false;
+
+                log_unit_warning_errno(u, r, "Service conflicts with active units even after all jobs have completed, giving up.");
+        } else
+                log_unit_warning_errno(u, r, "Failed to queue service startup job: %s", bus_error_message(&error, r));
+
+        socket_enter_stop_pre(s, SOCKET_FAILURE_RESOURCES);
+        return true; /* changed */
+}
+
+static void socket_enter_deferred(Socket *s) {
+        int r;
+
+        assert(s);
+        assert(socket_may_defer(s));
+
+        /* So here's the thing: if there're currently units conflicting with the service we shall be
+         * triggering, and the previous transaction is still running (job pool is not empty), let's
+         * defer the activation a bit, and recheck upon any unit stop. IOW, the trigger in question
+         * becomes bound to the conflicting dependency, and not the socket IO because we never process them.
+         * Put a safety net around all this though, i.e. give up if the service still can't be started
+         * even after all existing jobs have completed, or DeferTriggerMaxSec= is reached. */
+
+        r = socket_arm_timer(s, /* relative = */ true, s->defer_trigger_max_usec);
+        if (r < 0) {
+                log_unit_warning_errno(UNIT(s), r, "Failed to install timer: %m");
+                return socket_enter_stop_pre(s, SOCKET_FAILURE_RESOURCES);
+        }
+
+        unit_add_to_stop_notify_queue(UNIT(s));
+
+        /* Disable IO event sources */
+        socket_set_state(s, SOCKET_DEFERRED);
+}
+
 static void socket_enter_running(Socket *s, int cfd_in) {
         /* Note that this call takes possession of the connection fd passed. It either has to assign it
          * somewhere or close it. */
@@ -2368,6 +2460,11 @@ static void socket_enter_running(Socket *s, int cfd_in) {
                 return;
         }
 
+        if (s->state == SOCKET_DEFERRED) {
+                assert(cfd < 0);
+                return;
+        }
+
         if (!ratelimit_below(&s->trigger_limit)) {
                 log_unit_warning(UNIT(s), "Trigger limit hit, refusing further activation.");
                 socket_enter_stop_pre(s, SOCKET_FAILURE_TRIGGER_LIMIT_HIT);
@@ -2392,9 +2489,17 @@ static void socket_enter_running(Socket *s, int cfd_in) {
                                 goto fail;
                         }
 
-                        r = manager_add_job(UNIT(s)->manager, JOB_START, UNIT_DEREF(s->service), JOB_REPLACE, &error, /* ret = */ NULL);
-                        if (r == -EDEADLK)
-                                return (void) log_unit_debug_errno(UNIT(s), r, "Failed to queue service startup job, ignoring: %s", bus_error_message(&error, r));
+                        if (s->defer_trigger != SOCKET_DEFER_NO) {
+                                r = manager_add_job(UNIT(s)->manager, JOB_START, UNIT_DEREF(s->service), JOB_LENIENT, &error, /* ret = */ NULL);
+                                if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE) && socket_may_defer(s))
+                                        /* We only check BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE here, not
+                                         * BUS_ERROR_TRANSACTION_JOBS_CONFLICTING or BUS_ERROR_TRANSACTION_ORDER_IS_CYCLIC,
+                                         * since those are errors in a single transaction, which are most likely
+                                         * caused by dependency issues in the unit configuration.
+                                         * Deferring activation probabaly won't help. */
+                                        return socket_enter_deferred(s);
+                        } else
+                                r = manager_add_job(UNIT(s)->manager, JOB_START, UNIT_DEREF(s->service), JOB_REPLACE, &error, /* ret = */ NULL);
                         if (r < 0)
                                 goto queue_error;
                 }
@@ -2598,7 +2703,7 @@ static int socket_stop(Unit *u) {
                 return 0;
         }
 
-        assert(IN_SET(s->state, SOCKET_LISTENING, SOCKET_RUNNING));
+        assert(IN_SET(s->state, SOCKET_LISTENING, SOCKET_DEFERRED, SOCKET_RUNNING));
 
         socket_enter_stop_pre(s, SOCKET_SUCCESS);
         return 1;
@@ -3285,6 +3390,11 @@ static int socket_dispatch_timer(sd_event_source *source, usec_t usec, void *use
                 socket_enter_stop_pre(s, SOCKET_FAILURE_TIMEOUT);
                 break;
 
+        case SOCKET_DEFERRED:
+                log_unit_warning(UNIT(s), "DeferTriggerMaxSec= elapsed. Stopping.");
+                socket_enter_stop_pre(s, SOCKET_FAILURE_TIMEOUT);
+                break;
+
         case SOCKET_STOP_PRE:
                 log_unit_warning(UNIT(s), "Stopping timed out. Terminating.");
                 socket_enter_signal(s, SOCKET_STOP_PRE_SIGTERM, SOCKET_FAILURE_TIMEOUT);
@@ -3414,7 +3524,7 @@ static void socket_trigger_notify(Unit *u, Unit *other) {
         Service *service = ASSERT_PTR(SERVICE(other));
 
         /* Don't propagate state changes from the service if we are already down */
-        if (!IN_SET(s->state, SOCKET_RUNNING, SOCKET_LISTENING))
+        if (!IN_SET(s->state, SOCKET_RUNNING, SOCKET_LISTENING, SOCKET_DEFERRED))
                 return;
 
         /* We don't care for the service state if we are in Accept=yes mode */
@@ -3610,6 +3720,14 @@ SocketTimestamping socket_timestamping_from_string_harder(const char *p) {
         return r ? SOCKET_TIMESTAMPING_NS : SOCKET_TIMESTAMPING_OFF; /* If boolean yes, default to ns accuracy */
 }
 
+static const char* const socket_defer_trigger_table[_SOCKET_DEFER_MAX] = {
+        [SOCKET_DEFER_NO]      = "no",
+        [SOCKET_DEFER_YES]     = "yes",
+        [SOCKET_DEFER_PATIENT] = "patient",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(socket_defer_trigger, SocketDeferTrigger, SOCKET_DEFER_YES);
+
 const UnitVTable socket_vtable = {
         .object_size = sizeof(Socket),
         .exec_context_offset = offsetof(Socket, exec_context),
@@ -3659,6 +3777,8 @@ const UnitVTable socket_vtable = {
 
         .trigger_notify = socket_trigger_notify,
 
+        .stop_notify = socket_stop_notify,
+
         .reset_failed = socket_reset_failed,
 
         .notify_handoff_timestamp = socket_handoff_timestamp,
index 70291fb0d266c96d5b67a76ab2d91fe80accb6d0..cec10dece94dcc45b63a89be2e855fcc46dbb184 100644 (file)
@@ -66,6 +66,14 @@ typedef enum SocketTimestamping {
         _SOCKET_TIMESTAMPING_INVALID = -EINVAL,
 } SocketTimestamping;
 
+typedef enum SocketDeferTrigger {
+        SOCKET_DEFER_NO,
+        SOCKET_DEFER_YES,
+        SOCKET_DEFER_PATIENT,
+        _SOCKET_DEFER_MAX,
+        _SOCKET_DEFER_INVALID = -EINVAL,
+} SocketDeferTrigger;
+
 typedef struct Socket {
         Unit meta;
 
@@ -165,6 +173,9 @@ typedef struct Socket {
 
         RateLimit trigger_limit;
         RateLimit poll_limit;
+
+        usec_t defer_trigger_max_usec;
+        SocketDeferTrigger defer_trigger;
 } Socket;
 
 SocketPeer *socket_peer_ref(SocketPeer *p);
@@ -205,4 +216,7 @@ const char* socket_timestamping_to_string(SocketTimestamping p) _const_;
 SocketTimestamping socket_timestamping_from_string(const char *p) _pure_;
 SocketTimestamping socket_timestamping_from_string_harder(const char *p) _pure_;
 
+const char* socket_defer_trigger_to_string(SocketDeferTrigger i) _const_;
+SocketDeferTrigger socket_defer_trigger_from_string(const char *s) _pure_;
+
 DEFINE_CAST(SOCKET, Socket);
index 2906675202830c4e88e4c80890cbff33ba3b0719..9b95834263f12e4738b7511bbbc8f10a5b4d8d0b 100644 (file)
@@ -617,6 +617,28 @@ void unit_submit_to_release_resources_queue(Unit *u) {
         u->in_release_resources_queue = true;
 }
 
+void unit_add_to_stop_notify_queue(Unit *u) {
+        assert(u);
+
+        if (u->in_stop_notify_queue)
+                return;
+
+        assert(UNIT_VTABLE(u)->stop_notify);
+
+        LIST_PREPEND(stop_notify_queue, u->manager->stop_notify_queue, u);
+        u->in_stop_notify_queue = true;
+}
+
+void unit_remove_from_stop_notify_queue(Unit *u) {
+        assert(u);
+
+        if (!u->in_stop_notify_queue)
+                return;
+
+        LIST_REMOVE(stop_notify_queue, u->manager->stop_notify_queue, u);
+        u->in_stop_notify_queue = false;
+}
+
 static void unit_clear_dependencies(Unit *u) {
         assert(u);
 
@@ -842,6 +864,8 @@ Unit* unit_free(Unit *u) {
         if (u->in_release_resources_queue)
                 LIST_REMOVE(release_resources_queue, u->manager->release_resources_queue, u);
 
+        unit_remove_from_stop_notify_queue(u);
+
         condition_free_list(u->conditions);
         condition_free_list(u->asserts);
 
@@ -2804,6 +2828,9 @@ void unit_notify(Unit *u, UnitActiveState os, UnitActiveState ns, bool reload_su
                 /* Maybe the concurrency limits now allow dispatching of another start job in this slice? */
                 unit_check_concurrency_limit(u);
 
+                /* Maybe someone else has been waiting for us to stop? */
+                m->may_dispatch_stop_notify_queue = true;
+
         } else if (UNIT_IS_ACTIVE_OR_RELOADING(ns)) {
                 /* Start uphold units regardless if going up was expected or not */
                 check_uphold_dependencies(u);
index d5088657485a0b8d9c56340dd32939b045020417..4c1051472615172732e1f640cbd5b321a52d9403 100644 (file)
@@ -346,6 +346,9 @@ typedef struct Unit {
         /* Queue of units that should be checked if they can release resources now */
         LIST_FIELDS(Unit, release_resources_queue);
 
+        /* Queue of units that should be informed when other units stop */
+        LIST_FIELDS(Unit, stop_notify_queue);
+
         /* PIDs we keep an eye on. Note that a unit might have many more, but these are the ones we care
          * enough about to process SIGCHLD for */
         Set *pids; /* → PidRef* */
@@ -446,6 +449,7 @@ typedef struct Unit {
         bool in_start_when_upheld_queue:1;
         bool in_stop_when_bound_queue:1;
         bool in_release_resources_queue:1;
+        bool in_stop_notify_queue:1;
 
         bool sent_dbus_new_signal:1;
 
@@ -670,6 +674,9 @@ typedef struct UnitVTable {
          * state or gains/loses a job */
         void (*trigger_notify)(Unit *u, Unit *trigger);
 
+        /* Invoked when some other units stop */
+        bool (*stop_notify)(Unit *u);
+
         /* Called whenever CLOCK_REALTIME made a jump */
         void (*time_change)(Unit *u);
 
@@ -847,6 +854,8 @@ void unit_submit_to_stop_when_unneeded_queue(Unit *u);
 void unit_submit_to_start_when_upheld_queue(Unit *u);
 void unit_submit_to_stop_when_bound_queue(Unit *u);
 void unit_submit_to_release_resources_queue(Unit *u);
+void unit_add_to_stop_notify_queue(Unit *u);
+void unit_remove_from_stop_notify_queue(Unit *u);
 
 int unit_merge(Unit *u, Unit *other);
 int unit_merge_by_name(Unit *u, const char *other);
index e51646a29468b6a4a5e6ea427492ba4c3c4beb2f..cc0449dd96e05bd92e5660ae12a175a773fdf72a 100644 (file)
@@ -2623,7 +2623,8 @@ static int bus_append_socket_property(sd_bus_message *m, const char *field, cons
                               "KeepAliveIntervalSec",
                               "DeferAcceptSec",
                               "TriggerLimitIntervalSec",
-                              "PollLimitIntervalSec"))
+                              "PollLimitIntervalSec",
+                              "DeferTriggerMaxSec"))
                 return bus_append_parse_sec_rename(m, field, eq);
 
         if (STR_IN_SET(field, "ReceiveBuffer",
@@ -2646,7 +2647,8 @@ static int bus_append_socket_property(sd_bus_message *m, const char *field, cons
                               "FileDescriptorName",
                               "SocketUser",
                               "SocketGroup",
-                              "Timestamping"))
+                              "Timestamping",
+                              "DeferTrigger"))
                 return bus_append_string(m, field, eq);
 
         if (streq(field, "Symlinks"))