Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/module-eval/boot-wait.nix

317 lines
9.7 KiB
Nix

# Which units of a container's guest hold its boot open, and which of those can
# hold it past the host's start limit for the container.
#
# A swarm service container is `Type=notify`: the host's `container@<name>`
# start job lasts until the guest's job queue is empty, and ends in a kill at
# `timeoutStartSec`. A guest unit that is pulled in by the boot targets and
# whose start job blocks therefore counts its start timeout against that limit.
# Pure functions over a guest's `config.systemd` and the host's
# `timeoutStartSec` string; ./swarm-services-switch.nix feeds them each named
# container.
{ lib }:
let
# A time span as seconds; `null` is infinity, which `TimeoutStartSec` spells
# `0`, `"infinity"` or leaves to `Type=oneshot`'s default.
spanSeconds =
v:
if builtins.isInt v then
if v == 0 then null else v
else if v == "infinity" then
null
else
lib.foldl' (acc: t: acc + tokenSeconds t) 0 (lib.splitString " " (lib.trim v));
unitSeconds = {
s = 1;
sec = 1;
second = 1;
seconds = 1;
m = 60;
min = 60;
minute = 60;
minutes = 60;
h = 3600;
hr = 3600;
hour = 3600;
hours = 3600;
};
tokenSeconds =
token:
let
m = builtins.match "([0-9]+)([a-z]*)" token;
unit = if m == null then null else lib.elemAt m 1;
in
if m == null then
throw "boot-wait: cannot read '${token}' as a time span"
else if unit == "" then
lib.toInt (lib.head m)
else
lib.toInt (lib.head m)
* (unitSeconds.${unit} or (throw "boot-wait: unknown time unit '${unit}' in '${token}'"));
# Where a boot starts: what `multi-user.target` requires, and the targets
# the stock units pull in alongside it.
rootTargets = [
"sysinit.target"
"basic.target"
"multi-user.target"
"sockets.target"
"timers.target"
"paths.target"
"local-fs.target"
];
kinds = {
services = "service";
timers = "timer";
sockets = "socket";
paths = "path";
targets = "target";
};
# Every unit the guest configures, as `{ name = "<n>.<type>"; unit; }`.
unitsOf =
systemd:
lib.concatLists (
lib.mapAttrsToList (
kind: suffix:
lib.mapAttrsToList (n: unit: {
name = "${n}.${suffix}";
inherit unit;
}) (systemd.${kind} or { })
) kinds
);
# The units a boot starts: the closure of `rootTargets` over what a unit
# `wants`/`requires`/`bindsTo` and over the units naming it in
# `wantedBy`/`requiredBy`. `after`/`before` alone pull nothing in.
bootSet =
systemd:
let
units = unitsOf systemd;
byName = lib.listToAttrs (map (u: lib.nameValuePair u.name u.unit) units);
pulls = u: (u.wants or [ ]) ++ (u.requires or [ ]) ++ (u.bindsTo or [ ]);
pulledBy = u: (u.wantedBy or [ ]) ++ (u.requiredBy or [ ]);
children =
key:
(pulls (byName.${key} or { }))
++ (map (u: u.name) (lib.filter (u: lib.elem key (pulledBy u.unit)) units));
in
map (n: n.key) (
builtins.genericClosure {
startSet = map (key: { inherit key; }) rootTargets;
operator = n: map (key: { inherit key; }) (children n.key);
}
);
# Whether the service's start job blocks on the unit's own work rather than
# ending at the fork: a oneshot, a notifying or forking daemon, or any unit
# with a start hook (which runs inside the job).
holdsJob =
unit:
let
sc = unit.serviceConfig or { };
in
lib.elem (sc.Type or "simple") [
"oneshot"
"notify"
"notify-reload"
"forking"
"dbus"
]
|| sc ? ExecStartPre
|| sc ? ExecStartPost
|| (unit.preStart or "") != ""
|| (unit.postStart or "") != "";
# What a service's start job can last, and where the number comes from:
# `stated` is the unit's own `TimeoutStartSec`; `default` is the manager's
# `DefaultTimeoutStartSec` (90 s unless the guest sets one), which a unit that
# states none gets; `unbounded` is a oneshot that states none, which systemd
# leaves without a timeout. `seconds = null` is infinity.
budgetOf =
guest: unit:
let
sc = unit.serviceConfig or { };
managerDefault = (guest.systemd.settings.Manager or { }).DefaultTimeoutStartSec or 90;
in
if sc ? TimeoutStartSec then
{
source = "stated";
seconds = spanSeconds sc.TimeoutStartSec;
}
else if (sc.Type or "simple") == "oneshot" then
{
source = "unbounded";
seconds = null;
}
else
{
source = "default";
seconds = spanSeconds managerDefault;
};
# The services in `guest`'s boot wait whose start job can last `limit` seconds
# or more: `{ key; seconds; source; }`, `seconds = null` for infinity. A job
# that ends at `limit` or later can outlast the container's start, because the
# guest has already spent time booting.
overBudget =
{ guest, limit }:
let
services = guest.systemd.services or { };
in
lib.concatMap (
key:
let
n = lib.removeSuffix ".service" key;
unit = services.${n};
budget = budgetOf guest unit;
in
lib.optional
(
lib.hasSuffix ".service" key
&& services ? ${n}
&& holdsJob unit
&& (budget.seconds == null || budget.seconds >= limit)
)
{
inherit key;
inherit (budget) seconds source;
}
) (bootSet guest.systemd);
# Seconds, `null` being infinity.
addSeconds = a: b: if a == null || b == null then null else a + b;
longer = a: b: b != null && (a == null || a > b);
# The longest chain of the guest's boot-wait units that systemd runs one
# after another, because one names the other in `after` or `before`: a path
# through that ordering where each job-holding service adds its budget (see
# `budgetOf`) and every other unit adds nothing. Only an ordering between two
# units of the boot wait is an edge. `weightless` takes `{ key; seconds;
# source; }` of a job-holding service and says whether it adds 0 s.
#
# Returns `{ cycle; seconds; units; }`. With no cycle in the ordering,
# `cycle = [ ]`, `seconds` is the path's sum and `units` its weighted
# services in order, as `{ key; seconds; source; }`. With a cycle, `cycle` is
# the unit names around it and the other two are `null`: no start order
# exists to add up.
longestChain =
{
guest,
weightless ? (_: false),
}:
let
byName = lib.listToAttrs (map (u: lib.nameValuePair u.name u.unit) (unitsOf guest.systemd));
nodes = lib.filter (key: byName ? ${key}) (bootSet guest.systemd);
isNode = lib.genAttrs nodes (_: true);
ordered = key: field: lib.filter (k: isNode ? ${k}) (byName.${key}.${field} or [ ]);
# `preds.<k>`: the units whose start job ends before `k`'s begins.
preds = lib.genAttrs nodes (
key: lib.unique (ordered key "after" ++ lib.filter (k: lib.elem key (ordered k "before")) nodes)
);
succs = lib.genAttrs nodes (key: lib.filter (k: lib.elem key preds.${k}) nodes);
weightOf =
key:
let
unit = byName.${key};
entry = {
inherit key;
inherit (budgetOf guest unit) seconds source;
};
in
if lib.hasSuffix ".service" key && holdsJob unit && !(weightless entry) then entry else null;
# Taking away, again and again, the units with no predecessor or no
# successor left leaves the units on a cycle and those between cycles.
peeled =
left:
lib.filter (
k: lib.any (p: lib.elem p left) preds.${k} && lib.any (s: lib.elem s left) succs.${k}
) left;
onCycles =
(lib.last (
builtins.genericClosure {
startSet = [
{
key = 0;
left = nodes;
}
];
operator =
s:
let
next = peeled s.left;
in
lib.optional (lib.length next < lib.length s.left) {
key = s.key + 1;
left = next;
};
}
)).left;
# Every unit left has a successor left, so following successors from any
# of them reaches a cycle within that many steps and stays on it.
follow = k: lib.head (lib.filter (s: lib.elem s onCycles) succs.${k});
cycle = map (x: x.key) (
builtins.genericClosure {
startSet = [ { key = lib.foldl' (k: _: follow k) (lib.head onCycles) onCycles; } ];
operator = x: [ { key = follow x.key; } ];
}
);
none = {
seconds = 0;
units = [ ];
};
heavier = acc: c: if longer c.seconds acc.seconds then c else acc;
# `best.<k>`: the heaviest chain ending in `k`.
best = lib.genAttrs nodes (
key:
let
before = lib.foldl' heavier none (map (p: best.${p}) preds.${key});
w = weightOf key;
in
if w == null then
before
else
{
seconds = addSeconds before.seconds w.seconds;
units = before.units ++ [ w ];
}
);
in
if onCycles != [ ] then
{
inherit cycle;
seconds = null;
units = null;
}
else
{
cycle = [ ];
inherit (lib.foldl' heavier none (map (k: best.${k}) nodes)) seconds units;
};
# `a 30s + b 30s = 60s`, an infinite span as `unbounded`.
describeChain =
chain:
let
show = s: if s == null then "unbounded" else "${toString s}s";
in
"${
lib.concatMapStringsSep " + " (u: "${u.key} ${show u.seconds}") chain.units
} = ${show chain.seconds}";
in
{
inherit
spanSeconds
bootSet
holdsJob
budgetOf
overBudget
longestChain
describeChain
;
}