diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index d1d7a9f6e0f..0b5e8693219 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -708,6 +708,24 @@ + + Upholds= + + Configures dependencies similar to Wants=, but as long a this unit + is up, all units listed in Upholds= are started whenever found to be inactive or + failed, and no job is queued for them. While a Wants= dependency on another unit + has a one-time effect when this units started, a Upholds= dependency on it has a + continuous effect, constantly restarting the unit if necessary. This is an alternative to the + Restart= setting of service units, to ensure they are kept running whatever + happens. + + When Upholds=b.service is used on a.service, this + dependency will show as UpheldBy=a.service in the property listing of + b.service. The UpheldBy= dependency cannot be specified + directly. + + + Conflicts= diff --git a/src/basic/unit-def.c b/src/basic/unit-def.c index f83e398a4b1..2667e61dc46 100644 --- a/src/basic/unit-def.c +++ b/src/basic/unit-def.c @@ -265,10 +265,12 @@ static const char* const unit_dependency_table[_UNIT_DEPENDENCY_MAX] = { [UNIT_WANTS] = "Wants", [UNIT_BINDS_TO] = "BindsTo", [UNIT_PART_OF] = "PartOf", + [UNIT_UPHOLDS] = "Upholds", [UNIT_REQUIRED_BY] = "RequiredBy", [UNIT_REQUISITE_OF] = "RequisiteOf", [UNIT_WANTED_BY] = "WantedBy", [UNIT_BOUND_BY] = "BoundBy", + [UNIT_UPHELD_BY] = "UpheldBy", [UNIT_CONSISTS_OF] = "ConsistsOf", [UNIT_CONFLICTS] = "Conflicts", [UNIT_CONFLICTED_BY] = "ConflictedBy", diff --git a/src/basic/unit-def.h b/src/basic/unit-def.h index 92b40e8a3de..08651efa57a 100644 --- a/src/basic/unit-def.h +++ b/src/basic/unit-def.h @@ -211,6 +211,7 @@ typedef enum UnitDependency { UNIT_WANTS, UNIT_BINDS_TO, UNIT_PART_OF, + UNIT_UPHOLDS, /* Inverse of the above */ UNIT_REQUIRED_BY, /* inverse of 'requires' is 'required_by' */ @@ -218,6 +219,7 @@ typedef enum UnitDependency { UNIT_WANTED_BY, /* inverse of 'wants' */ UNIT_BOUND_BY, /* inverse of 'binds_to' */ UNIT_CONSISTS_OF, /* inverse of 'part_of' */ + UNIT_UPHELD_BY, /* inverse of 'uphold' */ /* Negative dependencies */ UNIT_CONFLICTS, /* inverse of 'conflicts' is 'conflicted_by' */ diff --git a/src/core/dbus-unit.c b/src/core/dbus-unit.c index 60f406a160a..8875ba082d1 100644 --- a/src/core/dbus-unit.c +++ b/src/core/dbus-unit.c @@ -2304,6 +2304,7 @@ static int bus_unit_set_transient_property( UNIT_WANTS, UNIT_BINDS_TO, UNIT_PART_OF, + UNIT_UPHOLDS, UNIT_CONFLICTS, UNIT_BEFORE, UNIT_AFTER, diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 25cd55e2f9e..17f2bea5b82 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -261,6 +261,7 @@ Unit.Requisite, config_parse_unit_deps, Unit.Wants, config_parse_unit_deps, UNIT_WANTS, 0 Unit.BindsTo, config_parse_unit_deps, UNIT_BINDS_TO, 0 Unit.BindTo, config_parse_unit_deps, UNIT_BINDS_TO, 0 +Unit.Upholds, config_parse_unit_deps, UNIT_UPHOLDS, 0 Unit.Conflicts, config_parse_unit_deps, UNIT_CONFLICTS, 0 Unit.Before, config_parse_unit_deps, UNIT_BEFORE, 0 Unit.After, config_parse_unit_deps, UNIT_AFTER, 0 diff --git a/src/core/manager.c b/src/core/manager.c index a9b51bb25dd..e5ec84ac5e0 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -1295,7 +1295,7 @@ static unsigned manager_dispatch_stop_when_unneeded_queue(Manager *m) { /* If stopping a unit fails continuously we might enter a stop loop here, hence stop acting on the * service being unnecessary after a while. */ - if (!ratelimit_below(&u->auto_stop_ratelimit)) { + if (!ratelimit_below(&u->auto_start_stop_ratelimit)) { log_unit_warning(u, "Unit not needed anymore, but not stopping since we tried this too often recently."); continue; } @@ -1309,6 +1309,44 @@ static unsigned manager_dispatch_stop_when_unneeded_queue(Manager *m) { return n; } +static unsigned manager_dispatch_start_when_upheld_queue(Manager *m) { + unsigned n = 0; + Unit *u; + int r; + + assert(m); + + while ((u = m->start_when_upheld_queue)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + Unit *culprit = NULL; + + assert(u->in_start_when_upheld_queue); + LIST_REMOVE(start_when_upheld_queue, m->start_when_upheld_queue, u); + u->in_start_when_upheld_queue = false; + + n++; + + if (!unit_is_upheld_by_active(u, &culprit)) + continue; + + log_unit_debug(u, "Unit is started because upheld by active unit %s.", culprit->id); + + /* If stopping a unit fails continuously we might enter a stop loop here, hence stop acting on the + * service being unnecessary after a while. */ + + if (!ratelimit_below(&u->auto_start_stop_ratelimit)) { + log_unit_warning(u, "Unit needs to be started because active unit %s upholds it, but not starting since we tried this too often recently.", culprit->id); + continue; + } + + r = manager_add_job(u->manager, JOB_START, u, JOB_FAIL, NULL, &error, NULL); + if (r < 0) + log_unit_warning_errno(u, r, "Failed to enqueue start job, ignoring: %s", bus_error_message(&error, r)); + } + + return n; +} + static void manager_clear_jobs_and_units(Manager *m) { Unit *u; @@ -1327,6 +1365,7 @@ static void manager_clear_jobs_and_units(Manager *m) { assert(!m->gc_unit_queue); assert(!m->gc_job_queue); assert(!m->stop_when_unneeded_queue); + assert(!m->start_when_upheld_queue); assert(hashmap_isempty(m->jobs)); assert(hashmap_isempty(m->units)); @@ -2954,6 +2993,9 @@ int manager_loop(Manager *m) { if (manager_dispatch_cgroup_realize_queue(m) > 0) continue; + if (manager_dispatch_start_when_upheld_queue(m) > 0) + continue; + if (manager_dispatch_stop_when_unneeded_queue(m) > 0) continue; diff --git a/src/core/manager.h b/src/core/manager.h index f58982a3647..bfb8b628e71 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -187,6 +187,9 @@ struct Manager { /* Units that might be subject to StopWhenUnneeded= clean-up */ LIST_HEAD(Unit, stop_when_unneeded_queue); + /* Units which are upheld by another other which we might need to act on */ + LIST_HEAD(Unit, start_when_upheld_queue); + sd_event *event; /* This maps PIDs we care about to units that are interested in. We allow multiple units to he interested in diff --git a/src/core/unit-dependency-atom.c b/src/core/unit-dependency-atom.c index 6bd961e92b0..60939ce9567 100644 --- a/src/core/unit-dependency-atom.c +++ b/src/core/unit-dependency-atom.c @@ -35,6 +35,12 @@ static const UnitDependencyAtom atom_map[_UNIT_DEPENDENCY_MAX] = { [UNIT_PART_OF] = UNIT_ATOM_ADD_DEFAULT_TARGET_DEPENDENCY_QUEUE, + [UNIT_UPHOLDS] = UNIT_ATOM_PULL_IN_START_IGNORED | + UNIT_ATOM_RETROACTIVE_START_REPLACE | + UNIT_ATOM_ADD_START_WHEN_UPHELD_QUEUE | + UNIT_ATOM_ADD_STOP_WHEN_UNNEEDED_QUEUE | + UNIT_ATOM_ADD_DEFAULT_TARGET_DEPENDENCY_QUEUE, + [UNIT_REQUIRED_BY] = UNIT_ATOM_PROPAGATE_STOP | UNIT_ATOM_PROPAGATE_RESTART | UNIT_ATOM_PROPAGATE_START_FAILURE | @@ -58,6 +64,10 @@ static const UnitDependencyAtom atom_map[_UNIT_DEPENDENCY_MAX] = { UNIT_ATOM_PINS_STOP_WHEN_UNNEEDED | UNIT_ATOM_DEFAULT_TARGET_DEPENDENCIES, + [UNIT_UPHELD_BY] = UNIT_ATOM_START_STEADILY | + UNIT_ATOM_DEFAULT_TARGET_DEPENDENCIES | + UNIT_ATOM_PINS_STOP_WHEN_UNNEEDED, + [UNIT_CONSISTS_OF] = UNIT_ATOM_PROPAGATE_STOP | UNIT_ATOM_PROPAGATE_RESTART, @@ -140,6 +150,14 @@ UnitDependency unit_dependency_from_unique_atom(UnitDependencyAtom atom) { case UNIT_ATOM_CANNOT_BE_ACTIVE_WITHOUT: return UNIT_BINDS_TO; + case UNIT_ATOM_PULL_IN_START_IGNORED | + UNIT_ATOM_RETROACTIVE_START_REPLACE | + UNIT_ATOM_ADD_START_WHEN_UPHELD_QUEUE | + UNIT_ATOM_ADD_STOP_WHEN_UNNEEDED_QUEUE | + UNIT_ATOM_ADD_DEFAULT_TARGET_DEPENDENCY_QUEUE: + case UNIT_ATOM_ADD_START_WHEN_UPHELD_QUEUE: + return UNIT_UPHOLDS; + case UNIT_ATOM_PROPAGATE_STOP | UNIT_ATOM_PROPAGATE_RESTART | UNIT_ATOM_PROPAGATE_START_FAILURE | @@ -157,6 +175,12 @@ UnitDependency unit_dependency_from_unique_atom(UnitDependencyAtom atom) { UNIT_ATOM_DEFAULT_TARGET_DEPENDENCIES: return UNIT_BOUND_BY; + case UNIT_ATOM_START_STEADILY | + UNIT_ATOM_DEFAULT_TARGET_DEPENDENCIES | + UNIT_ATOM_PINS_STOP_WHEN_UNNEEDED: + case UNIT_ATOM_START_STEADILY: + return UNIT_UPHELD_BY; + case UNIT_ATOM_PULL_IN_STOP | UNIT_ATOM_RETROACTIVE_STOP_ON_START: case UNIT_ATOM_PULL_IN_STOP: diff --git a/src/core/unit-dependency-atom.h b/src/core/unit-dependency-atom.h index daa296f6012..3e684f01c4f 100644 --- a/src/core/unit-dependency-atom.h +++ b/src/core/unit-dependency-atom.h @@ -32,6 +32,11 @@ typedef enum UnitDependencyAtom { /* Stop our unit if the other unit happens to inactive */ UNIT_ATOM_CANNOT_BE_ACTIVE_WITHOUT = UINT64_C(1) << 7, + /* Start this unit whenever we find it inactive and the other unit active */ + UNIT_ATOM_START_STEADILY = UINT64_C(1) << 9, + /* Whenever our unit becomes active, add other unit to start_when_upheld_queue */ + UNIT_ATOM_ADD_START_WHEN_UPHELD_QUEUE = UINT64_C(1) << 10, + /* If our unit unexpectedly becomes active, retroactively start the other unit too, in "replace" job * mode */ UNIT_ATOM_RETROACTIVE_START_REPLACE = UINT64_C(1) << 11, diff --git a/src/core/unit.c b/src/core/unit.c index 6892930d7bb..f401a1457c6 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -122,7 +122,7 @@ Unit* unit_new(Manager *m, size_t size) { u->last_section_private = -1; u->start_ratelimit = (RateLimit) { m->default_start_limit_interval, m->default_start_limit_burst }; - u->auto_stop_ratelimit = (RateLimit) { 10 * USEC_PER_SEC, 16 }; + u->auto_start_stop_ratelimit = (RateLimit) { 10 * USEC_PER_SEC, 16 }; for (CGroupIOAccountingMetric i = 0; i < _CGROUP_IO_ACCOUNTING_METRIC_MAX; i++) u->io_accounting_last[i] = UINT64_MAX; @@ -507,6 +507,22 @@ void unit_submit_to_stop_when_unneeded_queue(Unit *u) { u->in_stop_when_unneeded_queue = true; } +void unit_submit_to_start_when_upheld_queue(Unit *u) { + assert(u); + + if (u->in_start_when_upheld_queue) + return; + + if (!UNIT_IS_INACTIVE_OR_FAILED(unit_active_state(u))) + return; + + if (!unit_has_dependency(u, UNIT_ATOM_START_STEADILY, NULL)) + return; + + LIST_PREPEND(start_when_upheld_queue, u->manager->start_when_upheld_queue, u); + u->in_start_when_upheld_queue = true; +} + static void unit_clear_dependencies(Unit *u) { assert(u); @@ -719,6 +735,9 @@ Unit* unit_free(Unit *u) { if (u->in_stop_when_unneeded_queue) LIST_REMOVE(stop_when_unneeded_queue, u->manager->stop_when_unneeded_queue, u); + if (u->in_start_when_upheld_queue) + LIST_REMOVE(start_when_upheld_queue, u->manager->start_when_upheld_queue, u); + safe_close(u->ip_accounting_ingress_map_fd); safe_close(u->ip_accounting_egress_map_fd); @@ -2011,6 +2030,36 @@ bool unit_is_unneeded(Unit *u) { return true; } +bool unit_is_upheld_by_active(Unit *u, Unit **ret_culprit) { + Unit *other; + + assert(u); + + /* Checks if the unit needs to be started because it currently is not running, but some other unit + * that is active declared an Uphold= dependencies on it */ + + if (!UNIT_IS_INACTIVE_OR_FAILED(unit_active_state(u)) || u->job) { + if (ret_culprit) + *ret_culprit = NULL; + return false; + } + + UNIT_FOREACH_DEPENDENCY(other, u, UNIT_ATOM_START_STEADILY) { + if (other->job) + continue; + + if (UNIT_IS_ACTIVE_OR_RELOADING(unit_active_state(other))) { + if (ret_culprit) + *ret_culprit = other; + return true; + } + } + + if (ret_culprit) + *ret_culprit = NULL; + return false; +} + static void check_unneeded_dependencies(Unit *u) { Unit *other; assert(u); @@ -2021,6 +2070,16 @@ static void check_unneeded_dependencies(Unit *u) { unit_submit_to_stop_when_unneeded_queue(other); } +static void check_uphold_dependencies(Unit *u) { + Unit *other; + assert(u); + + /* Add all units this unit depends on to the queue that processes Uphold= behaviour. */ + + UNIT_FOREACH_DEPENDENCY(other, u, UNIT_ATOM_ADD_START_WHEN_UPHELD_QUEUE) + unit_submit_to_start_when_upheld_queue(other); +} + static void unit_check_binds_to(Unit *u) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; bool stop = false; @@ -2056,7 +2115,7 @@ static void unit_check_binds_to(Unit *u) { /* If stopping a unit fails continuously we might enter a stop * loop here, hence stop acting on the service being * unnecessary after a while. */ - if (!ratelimit_below(&u->auto_stop_ratelimit)) { + if (!ratelimit_below(&u->auto_start_stop_ratelimit)) { log_unit_warning(u, "Unit is bound to inactive unit %s, but not stopping since we tried this too often recently.", other->id); return; } @@ -2595,10 +2654,14 @@ void unit_notify(Unit *u, UnitActiveState os, UnitActiveState ns, UnitNotifyFlag retroactively_stop_dependencies(u); } - /* stop unneeded units regardless if going down was expected or not */ + /* Stop unneeded units regardless if going down was expected or not */ if (UNIT_IS_INACTIVE_OR_FAILED(ns)) check_unneeded_dependencies(u); + /* Start uphold units regardless if going up was expected or not */ + if (UNIT_IS_ACTIVE_OR_RELOADING(ns)) + check_uphold_dependencies(u); + if (ns != os && ns == UNIT_FAILED) { log_unit_debug(u, "Unit entered failed state."); @@ -2634,6 +2697,9 @@ void unit_notify(Unit *u, UnitActiveState os, UnitActiveState ns, UnitNotifyFlag /* Maybe we finished startup and are now ready for being stopped because unneeded? */ unit_submit_to_stop_when_unneeded_queue(u); + /* Maybe someone wants us to remain up? */ + unit_submit_to_start_when_upheld_queue(u); + /* Maybe we finished startup, but something we needed has vanished? Let's die then. (This happens when * something BindsTo= to a Type=oneshot unit, as these units go directly from starting to inactive, * without ever entering started.) */ @@ -2901,11 +2967,13 @@ int unit_add_dependency( [UNIT_REQUISITE] = UNIT_REQUISITE_OF, [UNIT_BINDS_TO] = UNIT_BOUND_BY, [UNIT_PART_OF] = UNIT_CONSISTS_OF, + [UNIT_UPHOLDS] = UNIT_UPHELD_BY, [UNIT_REQUIRED_BY] = UNIT_REQUIRES, [UNIT_REQUISITE_OF] = UNIT_REQUISITE, [UNIT_WANTED_BY] = UNIT_WANTS, [UNIT_BOUND_BY] = UNIT_BINDS_TO, [UNIT_CONSISTS_OF] = UNIT_PART_OF, + [UNIT_UPHELD_BY] = UNIT_UPHOLDS, [UNIT_CONFLICTS] = UNIT_CONFLICTED_BY, [UNIT_CONFLICTED_BY] = UNIT_CONFLICTS, [UNIT_BEFORE] = UNIT_AFTER, diff --git a/src/core/unit.h b/src/core/unit.h index 1a3aaa5020b..67367d5e4c6 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -229,9 +229,12 @@ typedef struct Unit { /* Target dependencies queue */ LIST_FIELDS(Unit, target_deps_queue); - /* Queue of units with StopWhenUnneeded set that shell be checked for clean-up. */ + /* Queue of units with StopWhenUnneeded= set that shall be checked for clean-up. */ LIST_FIELDS(Unit, stop_when_unneeded_queue); + /* Queue of units that have an Uphold= dependency from some other unit, and should be checked for starting */ + LIST_FIELDS(Unit, start_when_upheld_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 */ @@ -260,8 +263,8 @@ typedef struct Unit { int success_action_exit_status, failure_action_exit_status; char *reboot_arg; - /* Make sure we never enter endless loops with the check unneeded logic, or the BindsTo= logic */ - RateLimit auto_stop_ratelimit; + /* Make sure we never enter endless loops with the StopWhenUnneeded=, BindsTo=, Uphold= logic */ + RateLimit auto_start_stop_ratelimit; /* Reference to a specific UID/GID */ uid_t ref_uid; @@ -383,6 +386,7 @@ typedef struct Unit { bool in_cgroup_oom_queue:1; bool in_target_deps_queue:1; bool in_stop_when_unneeded_queue:1; + bool in_start_when_upheld_queue:1; bool sent_dbus_new_signal:1; @@ -740,6 +744,7 @@ void unit_add_to_cleanup_queue(Unit *u); void unit_add_to_gc_queue(Unit *u); void unit_add_to_target_deps_queue(Unit *u); void unit_submit_to_stop_when_unneeded_queue(Unit *u); +void unit_submit_to_start_when_upheld_queue(Unit *u); int unit_merge(Unit *u, Unit *other); int unit_merge_by_name(Unit *u, const char *other); @@ -869,6 +874,7 @@ bool unit_type_supported(UnitType t); bool unit_is_pristine(Unit *u); bool unit_is_unneeded(Unit *u); +bool unit_is_upheld_by_active(Unit *u, Unit **ret_culprit); pid_t unit_control_pid(Unit *u); pid_t unit_main_pid(Unit *u); diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service index 50b4b4b7f6f..1ce6967c659 100644 --- a/test/fuzz/fuzz-unit-file/directives.service +++ b/test/fuzz/fuzz-unit-file/directives.service @@ -103,6 +103,7 @@ StopWhenUnneeded= StopPropagatedFrom= SuccessAction= SuccessActionExitStatus= +Upholds= Wants= [Install] Alias=