# 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@` # 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 = "."; 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.`: 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.`: 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 ; }