From 03a14bcaa51d0730b6bfc633157cf43a0462d8af Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 16 Feb 2026 13:32:11 +0100 Subject: [PATCH] Add <~cond> condition modifier for tightly coupled services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conditions in Finit are dependencies: if A is asserted, service B is allowed to run. When A goes through FLUX (e.g., upstream reloads), dependents are PAUSED and then simply resumed when the condition is reasserted -- this is the correct behavior for barrier-style deps like . However, some setups have tightly coupled services where dependents must be reloaded/restarted when an upstream service reloads, not just resumed. E.g., the FRR routing stack on Infix OS: netd ← zebra ← {staticd,ripd} When netd reloads (SIGHUP), zebra and its dependents must be restarted to pick up the new configuration. The new '~' condition prefix marks a dependency as flux-sensitive: service name:zebra ... When the upstream condition goes FLUX and returns to ON, the dependent is reloaded (SIGHUP) or restarted (noreload '!') instead of merely resumed. Transitivity follows naturally through the condition chain. Closes #416 Closes #476 Signed-off-by: Joachim Wiberg --- doc/conditions.md | 37 +++++++++++++++++++++++++++++++++---- doc/config/services.md | 8 ++++++++ src/conf.c | 13 +++++++++++++ src/service.c | 13 +++++++++++++ src/svc.h | 1 + test/dep-chain-reload.sh | 17 +++++++++-------- 6 files changed, 77 insertions(+), 12 deletions(-) diff --git a/doc/conditions.md b/doc/conditions.md index 93df4169..aac0eadf 100644 --- a/doc/conditions.md +++ b/doc/conditions.md @@ -14,11 +14,11 @@ specified separated by comma. Multiple conditions are logically AND'ed during evaluation, i.e. all conditions must be satisfied in order for a service to run. -A special syntax, using a leading `!` in run/task/service conditions, -denote if a: +Two special prefixes can be used inside the angle brackets: - - service does not support `SIGHUP` - - run/task should not block runlevel changes (i.e., bootstrap) + - `!` -- service does not support `SIGHUP` (noreload), or run/task + should not block runlevel changes (i.e., bootstrap) + - `~` -- propagate reload from this dependency, see below Finit guarantees by default that all run/tasks run (at least) once per runlevel. For most tasks this is a good default, for example @@ -53,6 +53,30 @@ moving to the next runlevel after bootstrap, so we set ``: task [S0123456789] name:pwrfail initctl poweroff -- Power failure, shutting down +Propagating Reload in Dependencies +----------------------------------- + +By default, when a service reloads during `initctl reload`, dependent +services are paused (`SIGSTOP`) and simply resumed (`SIGCONT`) when the +condition is reasserted. This is correct for barrier-style dependencies +like ``, where dependents just need syslogd running and do +not care if it reloads its config. + +For services that need to react when their upstream reloads, the `~` +prefix propagates the reload from the dependency: + + service name:svc_b /sbin/svc_b -- Needs A (barrier) + service name:svc_c /sbin/svc_c -- Propagate reload from B + +Here, `<~pid/svc_b>` means: propagate a reload of `svc_b` to `svc_c`. +When `svc_b` reloads, `svc_c` will be restarted (because of `!`, +noreload) instead of merely resumed. If `svc_c` supported `SIGHUP` +(no `!` prefix), it would be sent `SIGHUP` instead. + +This is similar to systemd's `PropagatesReloadTo=` directive, but +declared on the consumer side rather than the provider side. + + Triggering ---------- @@ -281,6 +305,11 @@ This STOP/CONT handling minimizes the number of unnecessary service restarts that would otherwise occur because a depending service was sent `SIGHUP` for example. +Services with the `~` prefix are an exception to this rule: when their +conditions return to `on` after being in `flux`, the reload is propagated +-- the service is reloaded (SIGHUP) or restarted (noreload `!`) instead +of simply being resumed. + Therefore, any plugin that supplies Finit with conditions must ensure that their state is updated after each reconfiguration. This can be done by binding to the `HOOK_SVC_RECONF` hook. For an example of how diff --git a/doc/config/services.md b/doc/config/services.md index 38ba0be4..a315b372 100644 --- a/doc/config/services.md +++ b/doc/config/services.md @@ -47,6 +47,14 @@ waits for another service, `zebra`, to have created its PID file in *all* files in `/var/run`, for each file named `*.pid`, or `*/pid`, Finit opens it and find the matching `NAME:ID` using the PID. +The condition can be prefixed with `!` and/or `~`: + + - `` -- `ospfd` does not support `SIGHUP` (noreload) + - `<~pid/zebra>` -- propagate reload from `zebra` to `ospfd` + - `` -- both: noreload and propagate reload + +For details, see the [Finit Conditions](../conditions.md) document. + Some services do not maintain a PID file and rather than patching each application Finit provides a workaround. A `pid` keyword can be set to have Finit automatically create (when starting) and later remove diff --git a/src/conf.c b/src/conf.c index b4393e65..d0abb61d 100644 --- a/src/conf.c +++ b/src/conf.c @@ -745,8 +745,21 @@ void conf_parse_cond(svc_t *svc, char *cond) return; } + /* + * The '~' prefix means a reload of the upstream service is + * propagated to this service -- it will be reloaded (SIGHUP) + * or restarted (noreload), not just paused and resumed. + * Syntax: or <~pid/foo> + * + * Strip '~' from each condition and set the service-level + * flag. This allows '~' on any condition in the list. + */ svc->cond[0] = 0; for (i = 0, c = strtok(ptr, ","); c; c = strtok(NULL, ","), i++) { + if (c[0] == '~') { + c++; + svc->flux_reload = 1; + } devmon_add_cond(c); if (i) strlcat(svc->cond, ",", sizeof(svc->cond)); diff --git a/src/service.c b/src/service.c index eefae25b..09c9823f 100644 --- a/src/service.c +++ b/src/service.c @@ -3147,6 +3147,19 @@ restart: case COND_ON: kill(svc->pid, SIGCONT); svc_set_state(svc, SVC_RUNNING_STATE); + + /* + * Propagate reload (~): upstream reloaded, so + * we must also reload/restart, not just resume. + */ + if (svc->flux_reload && !svc_is_changed(svc)) { + if (svc_is_noreload(svc)) + service_stop(svc); + else + service_reload(svc); + break; + } + /* Reassert condition if we go from waiting and no change */ if (!svc_is_changed(svc)) { if (svc->notify == SVC_NOTIFY_PID) { diff --git a/src/svc.h b/src/svc.h index d433a515..f21a8ee6 100644 --- a/src/svc.h +++ b/src/svc.h @@ -146,6 +146,7 @@ typedef struct svc { int starting; /* ... waiting for pidfile to be re-asserted */ int runlevels; int sighup; /* This service supports SIGHUP :) */ + int flux_reload; /* Propagate reload from dependency, '~' prefix */ int forking; /* This is a service/sysv daemon that forks, wait for it ... */ svc_block_t block; /* Reason that this service is currently stopped */ char cond[MAX_COND_LEN]; diff --git a/test/dep-chain-reload.sh b/test/dep-chain-reload.sh index b5cfea49..dd67ac19 100755 --- a/test/dep-chain-reload.sh +++ b/test/dep-chain-reload.sh @@ -4,17 +4,18 @@ # Four services in a chain: A → B → C → D, all using notify:pid. # B and C are placed in separate sub-config files so we can use # 'initctl touch' on them independently. B supports SIGHUP (reload), -# while C and D use the '!' condition prefix (noreload), matching -# a common pattern in real-world setups (e.g., FRR daemons on Infix). +# while C and D use '!' (noreload) and '~' (propagate reload), +# matching a common pattern in real-world setups (e.g., FRR routing +# daemons on Infix OS). # -# Chain: A ← B ← C ← D +# Chain: A ← B ← C ← D # # Test 1 - touch C + reload (the "Infix" scenario): # C is in the middle of the chain. After 'initctl touch svc_c.conf' # + 'initctl reload': # - A and B should be unaffected (same PID) # - C is stopped/started (config was touched, noreload) -# - D must be restarted (reverse dep of C) +# - D must be restarted (reload propagated from C) # # Test 2 - crash (kill -9): # When B is killed with SIGKILL the crash path (RUNNING → HALTED) @@ -26,7 +27,7 @@ # svc_b.conf' + 'initctl reload': # - A should be unaffected (same PID) # - B is SIGHUP'd (same PID, config was touched) -# - C and D must be restarted (transitive reverse deps) +# - C and D must be restarted (reload propagated, '~' prefix) set -eu @@ -47,13 +48,13 @@ test_setup() { run "cat >> $FINIT_CONF" < name:svc_d serv -np -i svc_d -- Needs C +service log:stdout notify:pid name:svc_d serv -np -i svc_d -- Needs C EOF run "cat >> $FINIT_RCSD/svc_b.conf" < name:svc_b serv -np -i svc_b -- Needs A +service log:stdout notify:pid name:svc_b serv -np -i svc_b -- Needs A EOF run "cat >> $FINIT_RCSD/svc_c.conf" < name:svc_c serv -np -i svc_c -- Needs B +service log:stdout notify:pid name:svc_c serv -np -i svc_c -- Needs B EOF }