core/socket: introduce DeferTrigger= (#37505)

Follow-up for #37222 and #37421
This commit is contained in:
Yu Watanabe
2025-07-03 06:33:23 +09:00
committed by GitHub
26 changed files with 473 additions and 30 deletions
+16 -2
View 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>
+7 -4
View File
@@ -2197,6 +2197,7 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
<listitem>
<para>When queuing a new job, this option controls how to deal with
already queued jobs. It takes one of <literal>fail</literal>,
<literal>lenient</literal>,
<literal>replace</literal>,
<literal>replace-irreversibly</literal>,
<literal>isolate</literal>,
@@ -2209,10 +2210,12 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
<command>isolate</command> command is used which implies the
<literal>isolate</literal> job mode.</para>
<para>If <literal>fail</literal> is specified and a requested
operation conflicts with a pending job (more specifically:
causes an already pending start job to be reversed into a stop
job or vice versa), cause the operation to fail.</para>
<para>If <literal>fail</literal> is specified and a requested operation on weak dependencies
conflicts with a pending job (more specifically: causes an already pending start job to be reversed
into a stop job or vice versa), cause the operation to fail.</para>
<para>If <literal>lenient</literal> is specified and a requested operation conflicts with any
active/activating unit, cause the operation to fail.</para>
<para>If <literal>replace</literal> (the default) is
specified, any conflicting pending job will be replaced, as
+31
View File
@@ -956,6 +956,37 @@
<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>
+2
View 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",
@@ -341,6 +342,7 @@ DEFINE_STRING_TABLE_LOOKUP(notify_access, NotifyAccess);
static const char* const job_mode_table[_JOB_MODE_MAX] = {
[JOB_FAIL] = "fail",
[JOB_LENIENT] = "lenient",
[JOB_REPLACE] = "replace",
[JOB_REPLACE_IRREVERSIBLY] = "replace-irreversibly",
[JOB_ISOLATE] = "isolate",
+2
View 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,
@@ -282,6 +283,7 @@ typedef enum NotifyAccess {
typedef enum JobMode {
JOB_FAIL, /* Fail if a conflicting job is already queued */
JOB_LENIENT, /* Fail if any conflicting unit is active (even weaker than JOB_FAIL) */
JOB_REPLACE, /* Replace an existing conflicting job */
JOB_REPLACE_IRREVERSIBLY, /* Like JOB_REPLACE + produce irreversible jobs */
JOB_ISOLATE, /* Start a unit, and stop all others */
+10
View 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);
+4
View 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;
+2
View 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)
+1
View 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;
+1
View 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);
+29
View 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;
+4
View 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;
+131 -12
View 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;
}
@@ -2412,12 +2517,11 @@ static void socket_enter_running(Socket *s, int cfd_in) {
if (s->max_connections_per_source > 0) {
r = socket_acquire_peer(s, cfd, &p);
if (r < 0) {
if (ERRNO_IS_DISCONNECT(r))
return;
if (ERRNO_IS_NEG_DISCONNECT(r))
return;
if (r < 0)
/* We didn't have enough resources to acquire peer information, let's fail. */
goto fail;
}
if (r > 0 && p->n_ref > s->max_connections_per_source) {
_cleanup_free_ char *t = NULL;
@@ -2599,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;
@@ -3286,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);
@@ -3415,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 */
@@ -3611,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),
@@ -3660,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,
+14
View 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);
+21 -8
View File
@@ -565,7 +565,7 @@ static int transaction_is_destructive(Transaction *tr, JobMode mode, sd_bus_erro
assert(!j->transaction_prev);
assert(!j->transaction_next);
if (j->unit->job && (mode == JOB_FAIL || j->unit->job->irreversible) &&
if (j->unit->job && (IN_SET(mode, JOB_FAIL, JOB_LENIENT) || j->unit->job->irreversible) &&
job_type_is_conflicting(j->unit->job->type, j->type))
return sd_bus_error_setf(e, BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE,
"Transaction for %s/%s is destructive (%s has '%s' job queued, but '%s' is included in transaction).",
@@ -576,7 +576,7 @@ static int transaction_is_destructive(Transaction *tr, JobMode mode, sd_bus_erro
return 0;
}
static void transaction_minimize_impact(Transaction *tr) {
static int transaction_minimize_impact(Transaction *tr, JobMode mode, sd_bus_error *e) {
Job *head;
assert(tr);
@@ -584,13 +584,16 @@ static void transaction_minimize_impact(Transaction *tr) {
/* Drops all unnecessary jobs that reverse already active jobs
* or that stop a running service. */
if (!IN_SET(mode, JOB_FAIL, JOB_LENIENT))
return 0;
rescan:
HASHMAP_FOREACH(head, tr->jobs) {
LIST_FOREACH(transaction, j, head) {
bool stops_running_service, changes_existing_job;
/* If it matters, we shouldn't drop it */
if (j->matters_to_anchor)
if (j->matters_to_anchor && mode != JOB_LENIENT)
continue;
/* Would this stop a running service?
@@ -607,6 +610,13 @@ rescan:
if (!stops_running_service && !changes_existing_job)
continue;
if (j->matters_to_anchor) {
assert(mode == JOB_LENIENT);
return sd_bus_error_setf(e, BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE,
"%s/%s would stop a running unit or change existing job, bailing",
j->unit->id, job_type_to_string(j->type));
}
if (stops_running_service)
log_unit_debug(j->unit,
"%s/%s would stop a running service.",
@@ -626,6 +636,8 @@ rescan:
goto rescan;
}
}
return 0;
}
static int transaction_apply(
@@ -734,11 +746,12 @@ int transaction_activate(
/* First step: figure out which jobs matter */
transaction_find_jobs_that_matter_to_anchor(tr->anchor_job, generation++);
/* Second step: Try not to stop any running services if
* we don't have to. Don't try to reverse running
* jobs if we don't have to. */
if (mode == JOB_FAIL)
transaction_minimize_impact(tr);
/* Second step: Try not to stop any running services if we don't have to. Don't try to reverse
* running jobs if we don't have to. */
r = transaction_minimize_impact(tr, mode, e);
if (r < 0)
return r; /* Note that we don't log here, because for JOB_LENIENT conflicts are very much expected
and shouldn't appear to be fatal for the unit. Only inform the caller via bus error. */
/* Third step: Drop redundant jobs */
transaction_drop_redundant(tr);
+28 -1
View 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);
@@ -6332,7 +6359,7 @@ void unit_next_freezer_state(Unit *u, FreezerAction action, FreezerState *ret_ne
* if its parent is frozen. So we instead "demote" a normal freeze into a freeze
* initiated by parent if the parent is frozen */
if (IN_SET(current, FREEZER_RUNNING, FREEZER_THAWING,
FREEZER_FREEZING_BY_PARENT, FREEZER_FROZEN_BY_PARENT)) /* Should usually be refused by unit_freezer_action */
FREEZER_FREEZING_BY_PARENT, FREEZER_FROZEN_BY_PARENT)) /* Should usually be refused by unit_freezer_action */
next = current;
else if (current == FREEZER_FREEZING) {
if (IN_SET(parent, FREEZER_RUNNING, FREEZER_THAWING))
+9
View 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);
+4 -2
View 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"))
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
# shellcheck disable=SC2016
set -eux
set -o pipefail
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
UNIT_NAME="TEST-07-PID1-socket-defer-$RANDOM"
at_exit() {
systemctl stop "$UNIT_NAME*"
rm -f /run/systemd/system/"$UNIT_NAME".socket \
/run/systemd/system/"$UNIT_NAME"{,-conflict1,-conflict2,-forever}.service
}
trap at_exit EXIT
mkdir -p "/tmp/$UNIT_NAME"
cat >/run/systemd/system/"$UNIT_NAME-conflict1.service" <<EOF
[Service]
Type=oneshot
ExecStart=bash -c 'until [[ -f /tmp/$UNIT_NAME/flag ]]; do sleep 1; done'
TimeoutStartSec=infinity
EOF
cat >/run/systemd/system/"$UNIT_NAME-conflict2.service" <<EOF
[Service]
ExecStart=bash -c 'while [[ -f /tmp/$UNIT_NAME/flag ]]; do sleep 1; done'
EOF
cat >/run/systemd/system/"$UNIT_NAME-forever.service" <<EOF
[Service]
Type=oneshot
ExecStart=sleep infinity
TimeoutSec=infinity
EOF
cat >/run/systemd/system/"$UNIT_NAME.socket" <<EOF
[Socket]
ListenStream=/tmp/$UNIT_NAME/test
FlushPending=yes
EOF
cat >/run/systemd/system/"$UNIT_NAME.service" <<EOF
[Unit]
Conflicts=$UNIT_NAME-conflict1.service
Conflicts=$UNIT_NAME-conflict2.service
[Service]
ExecStart=sleep infinity
EOF
wait_for_start() {
timeout 30 bash -c "until systemctl -q is-active '$UNIT_NAME.service'; do sleep .5; done"
assert_eq "$(systemctl show "$UNIT_NAME.socket" -P SubState)" "running"
}
wait_for_stop() {
timeout 30 bash -c "while systemctl -q is-active '$UNIT_NAME.service'; do sleep .5; done"
assert_eq "$(systemctl show "$UNIT_NAME.socket" -P SubState)" "listening"
}
# DeferTrigger=no: job mode replace
systemctl start "$UNIT_NAME.socket"
[[ -S "/tmp/$UNIT_NAME/test" ]]
assert_eq "$(systemctl show "$UNIT_NAME.socket" -P SubState)" "listening"
(! systemctl -q is-active "$UNIT_NAME.service")
socat -u - UNIX-CONNECT:"/tmp/$UNIT_NAME/test" &
wait_for_start
kill %%
touch "/tmp/$UNIT_NAME/flag"
systemctl start "$UNIT_NAME-conflict2.service"
wait_for_stop
socat -u - UNIX-CONNECT:"/tmp/$UNIT_NAME/test" &
wait_for_start
kill %%
(! systemctl -q is-active "$UNIT_NAME-conflict2.service")
# DeferTrigger=yes
echo "DeferTrigger=yes" >>/run/systemd/system/"$UNIT_NAME.socket"
systemctl daemon-reload
assert_eq "$(systemctl show "$UNIT_NAME.socket" -P SubState)" "running"
rm "/tmp/$UNIT_NAME/flag"
systemctl start --no-block "$UNIT_NAME-conflict1.service"
wait_for_stop
assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start"
# Wait in "deferred" state
socat -u - UNIX-CONNECT:"/tmp/$UNIT_NAME/test" &
timeout 30 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .5; done"
(! systemctl -q is-active "$UNIT_NAME.service")
kill %%
assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start"
systemctl daemon-reload
timeout 10 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .2; done"
(! systemctl -q is-active "$UNIT_NAME.service")
assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start"
# Activate after conflicting unit exits
touch "/tmp/$UNIT_NAME/flag"
timeout 30 bash -c "while systemctl -q is-active '$UNIT_NAME-conflict1.service'; do sleep .2; done"
wait_for_start
# An irrelevant job running, during which one of the conflicting units exits
systemctl start --no-block "$UNIT_NAME-forever.service"
systemctl start "$UNIT_NAME-conflict2.service"
wait_for_stop
socat -u - UNIX-CONNECT:"/tmp/$UNIT_NAME/test" &
timeout 30 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .5; done"
(! systemctl -q is-active "$UNIT_NAME.service")
kill %%
rm "/tmp/$UNIT_NAME/flag"
timeout 30 bash -c "while systemctl -q is-active '$UNIT_NAME-conflict2.service'; do sleep .2; done"
wait_for_start
assert_eq "$(systemctl show "$UNIT_NAME-forever.service" -P SubState)" "start"
# Fail if DeferTriggerMaxSec= is reached
systemctl start --no-block "$UNIT_NAME-conflict1.service"
wait_for_stop
assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start"
socat -u - UNIX-CONNECT:"/tmp/$UNIT_NAME/test" &
timeout 30 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .5; done"
(! systemctl -q is-active "$UNIT_NAME.service")
kill %%
echo "DeferTriggerMaxSec=20s" >>/run/systemd/system/"$UNIT_NAME.socket"
systemctl daemon-reload
timeout 10 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .2; done"
(! systemctl -q is-active "$UNIT_NAME.service")
sleep 10
timeout 30 bash -c "until systemctl -q is-failed '$UNIT_NAME.socket'; do sleep .5; done"
assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start"
-1
View File
@@ -18,4 +18,3 @@ After=initrd-root-fs.target initrd-fs.target initrd.target
[Service]
Type=oneshot
ExecStart=systemctl --no-block isolate initrd-switch-root.target
ExecStart=systemctl --no-block --job-mode=replace-irreversibly start initrd-switch-root.service
+1
View File
@@ -20,6 +20,7 @@ Service=systemd-journald.service
ReceiveBuffer=128M
ListenNetlink=audit 1
PassCredentials=yes
DeferTrigger=yes
[Install]
WantedBy=sockets.target
+1
View File
@@ -26,6 +26,7 @@ Service=systemd-journald.service
SocketMode=0666
Symlinks=/dev/log
Timestamping=us
DeferTrigger=yes
# Increase both the send and receive buffer, so that things don't
# block early. Note that journald internally uses the this socket both
+1
View File
@@ -27,3 +27,4 @@ ReceiveBuffer=8M
Service=systemd-journald.service
SocketMode=0666
Timestamping=us
DeferTrigger=yes
+1
View File
@@ -20,3 +20,4 @@ ListenSequentialPacket=/run/udev/control
SocketMode=0600
PassCredentials=yes
RemoveOnStop=yes
DeferTrigger=yes
+1
View File
@@ -24,3 +24,4 @@ Service=systemd-udevd.service
ReceiveBuffer=128M
ListenNetlink=kobject-uevent 1
PassCredentials=yes
DeferTrigger=yes
+1
View File
@@ -20,3 +20,4 @@ ListenStream=/run/udev/io.systemd.Udev
FileDescriptorName=varlink
SocketMode=0600
RemoveOnStop=yes
DeferTrigger=yes