Skip to content

Graphs

Fleet data is plain Nix, so a question about it is a function. yen.lib.graphs answers graph questions. It works on succ, an attrset from a node to its successors. Every function sorts, so the order the data is written in never changes an answer.

FunctionAnswer
reach succ nevery node reachable from n
reachers succ nevery node that reaches n
transpose succthe edges reversed
topo succa topological order, or { ok = false; stuck; } for a cycle
layers succwaves: each node after its successors
sccs succ, cycles succstrongly connected components, nodes on a cycle
shortest succ a b, paths succ a bthe shortest path, all paths
fromEdges essucc from [ { from; to; } ]

Hosts depend on hosts:

modules/fleet.nix
{ yen, lib, ... }:
{
yen.kinds.fleet = {
spawns.host = _: yen.host;
sinks.out.reduce = yen.lib.reduce.merge;
};
yen.kinds.host.fields.dependsOn = yen.lib.opt (yen.lib.is lib.isList) [ ]; # host refs
yen.fleet.main = { };
yen.host.db = { };
yen.host.cache = { };
yen.host.api = {
dependsOn = [
yen.host.db
yen.host.cache
];
};
yen.host.web = {
dependsOn = [ yen.host.api ];
};
yen.host.lb = {
dependsOn = [ yen.host.web ];
};
yen.flake = { out, ... }: out;
}
modules/deploy.nix
# `succ` is an attrset from a node to its successors. Here: a host to the hosts it depends on.
{ yen, lib, ... }:
let
G = yen.lib.graphs;
dependsOn = lib.mapAttrs (_: h: map (d: d.name) h.dependsOn) yen.host;
in
{
yen.aspects.deploy-plan = {
at = yen.kinds.fleet;
out.deploy = {
waves = G.layers dependsOn; # dependencies first: each wave after the waves it depends on
impact = lib.genAttrs (lib.attrNames yen.host) (G.reachers dependsOn); # who breaks if this host is down
path = G.shortest dependsOn "lb" "db";
};
errors = map (c: "deploy: a dependency cycle: ${lib.concatStringsSep " -> " c}") (
lib.filter (c: lib.length c > 1) (G.sccs dependsOn)
);
};
}
result
{
impact = {
api = [
"lb"
"web"
];
cache = [
"api"
"lb"
"web"
];
db = [
"api"
"lb"
"web"
];
lb = [ ];
web = [
"lb"
];
};
path = [
"lb"
"web"
"api"
"db"
];
waves = [
[
"cache"
"db"
]
[
"api"
]
[
"web"
]
[
"lb"
]
];
}

The deploy waves as a diagram, from the same data:

flowchart LR
  cache --> w0["wave 1"]
  db --> w0["wave 1"]
  api --> w1["wave 2"]
  web --> w2["wave 3"]
  lb --> w3["wave 4"]

A cycle is data, not a hang. sccs finds it, and the aspect gives an error record:

variants/cycle.nix
# db now depends on lb: a cycle.
{ yen, ... }:
{
yen.host.db.dependsOn = [ yen.host.lb ];
}
result
[
"deploy: a dependency cycle: api -> db -> lb -> web"
]
Contribute Community Sponsor