mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
One commit with exactly the tree of the reviewed head35b4509c0a, on main12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
157 lines
6.2 KiB
TypeScript
157 lines
6.2 KiB
TypeScript
// mesh-catalog's entrypoint — the module graph's consumer (novox/hq ADR 0070, ADR 0072).
|
|
//
|
|
// The build-machine role announces what it built; this places it in the graph and announces what that means.
|
|
// The control plane hooks the *meaning* — a module was upgraded — rather than the build output, so
|
|
// it never has to interpret an artifact or ask this module anything.
|
|
//
|
|
// **Ordering is not computed here.** When a registration makes something else stale, the modules
|
|
// whose own dependencies are all current are announced as needing a rebuild; the rest stay stale
|
|
// and appear the next time round, once whatever they were waiting for is registered. A chain and a
|
|
// diamond need no special handling, and nothing holds a plan.
|
|
|
|
import { emit } from "@novox/mesh-sdk/events";
|
|
import { broker } from "@novox/mesh-sdk/messaging";
|
|
import { invokeTool } from "@novox/mesh-sdk/tools";
|
|
import { state } from "@novox/mesh-sdk/state";
|
|
import { Graph } from "./store.js";
|
|
import {
|
|
answerOf, builtFrom, placeable, BUILDER, BUILDS_SINCE_TOOL, BUILDS_STATE, MANIFESTS_STATE,
|
|
type Built, type BuildsSince, type LogEntry,
|
|
} from "./heard.js";
|
|
|
|
const graph = Graph.fromEnv();
|
|
|
|
// The schema is not brought up here. The mesh prepares this module's state before it starts this
|
|
// version, and does not start it if that failed (novox/hq ADR 0135) — see prepare/index.ts. Doing it
|
|
// at start made a schema that could not be reached a crash loop instead of a stop, with the graph
|
|
// keeping a gap and nothing saying so. The reason it used to be here — that a step blocking the apply
|
|
// would block the very apply that brings the overlay up — stopped being true when a step's failure
|
|
// became this module's business and not the machine's (ADR 0136).
|
|
|
|
/**
|
|
* What a build means for the graph, wherever it came from.
|
|
*
|
|
* Two emitters say the same thing and neither is a mistake: the build machine says it as it happens,
|
|
* and the control plane says what it already held when this module asks what it missed
|
|
* (novox/hq ADR 0134). A replay is marked as one in its body, so nothing acts on a module that moved
|
|
* months ago — see `replay` in heard.ts.
|
|
*/
|
|
const placeTheBuild = async (event: { body: unknown }): Promise<void> => {
|
|
const body = (event.body ?? {}) as Built;
|
|
const placing = placeable(body);
|
|
if (placing.placed === "no") {
|
|
// A check, a try, a dry run and a failure are the build seat's ordinary outcomes: passed over. One
|
|
// that names no module or commit is a fault in the builder, said rather than dropped, so it does
|
|
// not look like a missing event.
|
|
if (placing.fault) console.error(`mesh-catalog: a build event was not placed: ${placing.why}`, body);
|
|
return;
|
|
}
|
|
const { module, commit } = placing;
|
|
|
|
const { upgraded, previous } = await graph.register({
|
|
module,
|
|
commit,
|
|
repository: body.repository ?? "",
|
|
path: body.path ?? "",
|
|
ref: body.ref ?? "",
|
|
manifest: body.manifest ?? {},
|
|
}, body.made ?? [], body.against ?? []);
|
|
|
|
// **A replay is registered and announced to nobody.** See `replay` above: the graph gains what
|
|
// it was missing, and the mesh is told nothing happened, because nothing did.
|
|
if (body.replay) return;
|
|
|
|
await emit("registered", {
|
|
module, commit, upgraded,
|
|
});
|
|
|
|
// **A rebuild that changed nothing is not an upgrade.** Announcing it would ripple outward
|
|
// through modules that did not change, forever (ADR 0072).
|
|
if (!upgraded) return;
|
|
|
|
await emit("upgraded", {
|
|
module, commit, previous,
|
|
});
|
|
|
|
// What can be built now — stale, and waiting on nothing that is itself stale.
|
|
for (const next of await graph.buildable()) {
|
|
await emit("rebuild-needed", {
|
|
module: next.module,
|
|
builtAt: next.commit,
|
|
because: next.because,
|
|
});
|
|
}
|
|
};
|
|
|
|
// **From the builder's log** (novox/hq ADR 0571 clauses 1 and 9). The builder keeps every build's end in a log of its
|
|
// own and answers the builds that ended since a time by its tool. This module asks at its start for what it missed (all
|
|
// the log holds, taken in as history: registered, announced to nobody), and again whenever the builder's state of done
|
|
// keys changes, for what ended since the last build it took in. Registering a build already held changes nothing, and a
|
|
// build is taken in once per run of this module.
|
|
const taken = new Set<string>();
|
|
let since = new Date(0).toISOString();
|
|
let caughtUp = false;
|
|
let asking: Promise<void> | undefined;
|
|
let askAgain = false;
|
|
|
|
const manifestOf = async (digest: string | undefined): Promise<unknown> => {
|
|
if (!digest) return {};
|
|
const held = await state<{ manifest?: string }>(MANIFESTS_STATE).get(digest);
|
|
if (!held?.value.manifest) return {};
|
|
return JSON.parse(held.value.manifest);
|
|
};
|
|
|
|
const takeIn = async (e: LogEntry, replay: boolean): Promise<void> => {
|
|
const key = `${e.module}/${e.fp}`;
|
|
if (taken.has(key)) return;
|
|
const built = builtFrom(e, await manifestOf(e.manifest), replay);
|
|
if (!built) return;
|
|
await placeTheBuild({ body: built });
|
|
taken.add(key);
|
|
};
|
|
|
|
const catchUp = async (): Promise<void> => {
|
|
for (;;) {
|
|
const answer = answerOf<BuildsSince>(
|
|
await invokeTool(broker(), BUILDER, BUILDS_SINCE_TOOL, { since }),
|
|
);
|
|
for (const e of answer.builds ?? []) {
|
|
await takeIn(e, !caughtUp);
|
|
if (e.at > since) since = e.at;
|
|
}
|
|
if (!answer.more) break;
|
|
}
|
|
caughtUp = true;
|
|
};
|
|
|
|
/** One ask at a time; a change heard while asking is asked for again once, so none is missed. */
|
|
const ask = (): Promise<void> => {
|
|
if (asking) {
|
|
askAgain = true;
|
|
return asking;
|
|
}
|
|
asking = (async () => {
|
|
try {
|
|
do {
|
|
askAgain = false;
|
|
await catchUp();
|
|
} while (askAgain);
|
|
} catch (err) {
|
|
console.error("mesh-catalog: the builder's builds since", since, "could not be taken in:", err);
|
|
} finally {
|
|
asking = undefined;
|
|
}
|
|
})();
|
|
return asking;
|
|
};
|
|
|
|
// Asked AFTER watching begins, so a build ending while the first ask runs asks again rather than being missed: the watch
|
|
// hands every current key first (the start's ask), then every change.
|
|
await state(BUILDS_STATE).watch(async (change) => {
|
|
if (!change.current) await ask();
|
|
});
|
|
await ask();
|
|
|
|
// **And say that this catalogue has caught up** (novox/hq 04-ISSUES/050).
|
|
await emit("catching-up", {});
|