diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 95f9a4f449..3cf0e18709 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -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 { + + + + @@ -6244,6 +6252,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + + + @@ -12112,8 +12124,10 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ ProtectHostnameEx, PassPIDFD, AcceptFileDescriptors, - DelegateNamespaces, and - RemoveSubgroup() were added in version 258. + DelegateNamespaces, + RemoveSubgroup(), + DeferTrigger, and + DeferTriggerMaxUSec were added in version 258. Mount Unit Objects diff --git a/man/systemctl.xml b/man/systemctl.xml index 037410cd21..08cfb5421b 100644 --- a/man/systemctl.xml +++ b/man/systemctl.xml @@ -2197,6 +2197,7 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err When queuing a new job, this option controls how to deal with already queued jobs. It takes one of fail, + lenient, replace, replace-irreversibly, isolate, @@ -2209,10 +2210,12 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err isolate command is used which implies the isolate job mode. - If fail 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. + If fail 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. + + If lenient is specified and a requested operation conflicts with any + active/activating unit, cause the operation to fail. If replace (the default) is specified, any conflicting pending job will be replaced, as diff --git a/man/systemd.socket.xml b/man/systemd.socket.xml index 91552d691f..213ceffcdb 100644 --- a/man/systemd.socket.xml +++ b/man/systemd.socket.xml @@ -956,6 +956,37 @@ + + DeferTrigger= + + Takes a boolean argument, or patient. May only be used when Accept=no. + If enabled, job mode lenient instead of replace 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 DeferTriggerMaxSec=. If set to , + the socket unit will fail if all jobs have finished or the timeout has been reached but the conflict remains. + If , always wait until DeferTriggerMaxSec= elapses. + Defaults to no. + + This setting is particularly useful if the socket unit should stay active across switch-root/soft-reboot + operations while the triggered service is stopped. + + + + + + DeferTriggerMaxSec= + + Configures the maximum time to defer the triggering when DeferTrigger= + 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 0 or infinity to disable the timeout logic (the default). + + + + + PassFileDescriptorsToExec= diff --git a/src/basic/unit-def.c b/src/basic/unit-def.c index 89c7f93790..8bc5f56532 100644 --- a/src/basic/unit-def.c +++ b/src/basic/unit-def.c @@ -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", diff --git a/src/basic/unit-def.h b/src/basic/unit-def.h index a8779769e3..fd55950ea6 100644 --- a/src/basic/unit-def.h +++ b/src/basic/unit-def.h @@ -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 */ diff --git a/src/core/dbus-socket.c b/src/core/dbus-socket.c index de96f00a15..02039dd6db 100644 --- a/src/core/dbus-socket.c +++ b/src/core/dbus-socket.c @@ -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); diff --git a/src/core/job.c b/src/core/job.c index a60025cffc..bac12542dc 100644 --- a/src/core/job.c +++ b/src/core/job.c @@ -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; diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index aacc76d053..96fe04aaf4 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -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) diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index bed01029dc..a6cb3c7e88 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -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; diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index d87bff0996..e2fe7dce1b 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -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); diff --git a/src/core/manager.c b/src/core/manager.c index 000f19d6f6..aa43c9d79b 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -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; diff --git a/src/core/manager.h b/src/core/manager.h index 39f822f4e2..c267ebe7ee 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -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; diff --git a/src/core/socket.c b/src/core/socket.c index e5cff0de76..727840e678 100644 --- a/src/core/socket.c +++ b/src/core/socket.c @@ -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, diff --git a/src/core/socket.h b/src/core/socket.h index 70291fb0d2..cec10dece9 100644 --- a/src/core/socket.h +++ b/src/core/socket.h @@ -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); diff --git a/src/core/transaction.c b/src/core/transaction.c index 1fe3c950e2..1e5b88af4d 100644 --- a/src/core/transaction.c +++ b/src/core/transaction.c @@ -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); diff --git a/src/core/unit.c b/src/core/unit.c index 1d23d24cc8..9b95834263 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -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)) diff --git a/src/core/unit.h b/src/core/unit.h index d508865748..4c10514726 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -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); diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index e51646a294..cc0449dd96 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -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")) diff --git a/test/units/TEST-07-PID1.socket-defer.sh b/test/units/TEST-07-PID1.socket-defer.sh new file mode 100755 index 0000000000..ec04f4f7e4 --- /dev/null +++ b/test/units/TEST-07-PID1.socket-defer.sh @@ -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" </run/systemd/system/"$UNIT_NAME-conflict2.service" </run/systemd/system/"$UNIT_NAME-forever.service" </run/systemd/system/"$UNIT_NAME.socket" </run/systemd/system/"$UNIT_NAME.service" <>/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" diff --git a/units/initrd-cleanup.service b/units/initrd-cleanup.service index 9c7914e341..08ab503962 100644 --- a/units/initrd-cleanup.service +++ b/units/initrd-cleanup.service @@ -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 diff --git a/units/systemd-journald-audit.socket b/units/systemd-journald-audit.socket index cf9b6e8b84..d0b43c7f42 100644 --- a/units/systemd-journald-audit.socket +++ b/units/systemd-journald-audit.socket @@ -20,6 +20,7 @@ Service=systemd-journald.service ReceiveBuffer=128M ListenNetlink=audit 1 PassCredentials=yes +DeferTrigger=yes [Install] WantedBy=sockets.target diff --git a/units/systemd-journald-dev-log.socket b/units/systemd-journald-dev-log.socket index e2a9352719..aa2a406095 100644 --- a/units/systemd-journald-dev-log.socket +++ b/units/systemd-journald-dev-log.socket @@ -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 diff --git a/units/systemd-journald.socket b/units/systemd-journald.socket index e2ffb96bfc..7b49d43ee8 100644 --- a/units/systemd-journald.socket +++ b/units/systemd-journald.socket @@ -27,3 +27,4 @@ ReceiveBuffer=8M Service=systemd-journald.service SocketMode=0666 Timestamping=us +DeferTrigger=yes diff --git a/units/systemd-udevd-control.socket b/units/systemd-udevd-control.socket index 89304ab65a..942f6a5e8b 100644 --- a/units/systemd-udevd-control.socket +++ b/units/systemd-udevd-control.socket @@ -20,3 +20,4 @@ ListenSequentialPacket=/run/udev/control SocketMode=0600 PassCredentials=yes RemoveOnStop=yes +DeferTrigger=yes diff --git a/units/systemd-udevd-kernel.socket b/units/systemd-udevd-kernel.socket index d337b06ea0..b185716aae 100644 --- a/units/systemd-udevd-kernel.socket +++ b/units/systemd-udevd-kernel.socket @@ -24,3 +24,4 @@ Service=systemd-udevd.service ReceiveBuffer=128M ListenNetlink=kobject-uevent 1 PassCredentials=yes +DeferTrigger=yes diff --git a/units/systemd-udevd-varlink.socket b/units/systemd-udevd-varlink.socket index 94fee15b08..74353e7f1a 100644 --- a/units/systemd-udevd-varlink.socket +++ b/units/systemd-udevd-varlink.socket @@ -20,3 +20,4 @@ ListenStream=/run/udev/io.systemd.Udev FileDescriptorName=varlink SocketMode=0600 RemoveOnStop=yes +DeferTrigger=yes