sum the boot-wait budgets of units that start one after another in a container, and check it
This commit is contained in:
parent
d6e983995d
commit
a0c8892b99
1 changed files with 423 additions and 28 deletions
|
|
@ -124,43 +124,48 @@ let
|
|||
|| (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.
|
||||
#
|
||||
# `source` says 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.
|
||||
overBudget =
|
||||
{ guest, limit }:
|
||||
let
|
||||
services = guest.systemd.services or { };
|
||||
managerDefault = (guest.systemd.settings.Manager or { }).DefaultTimeoutStartSec or 90;
|
||||
in
|
||||
lib.concatMap (
|
||||
key:
|
||||
let
|
||||
n = lib.removeSuffix ".service" key;
|
||||
unit = services.${n};
|
||||
sc = unit.serviceConfig or { };
|
||||
budget =
|
||||
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;
|
||||
};
|
||||
budget = budgetOf guest unit;
|
||||
in
|
||||
lib.optional
|
||||
(
|
||||
|
|
@ -174,12 +179,139 @@ let
|
|||
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
|
||||
;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue