Start the gitea bundle's binds without awaiting them, so its handshake is answered while a refused bind is asked again (issue 1793)
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | 49a28…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 49a28f85378b merged into trunk c770f7947d95 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 49a28f85378b merged into trunk c770f7947d95 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…

This commit is contained in:
2026-10-22 00:45:54 +02:00
parent c770f7947d
commit 49a28f8537
3 changed files with 55 additions and 14 deletions
+14
View File
@@ -28,3 +28,17 @@ export async function bindUntilHeld(what: string, bind: () => Promise<unknown>,
}
}
}
/**
* **Starts the binds without making the bundle wait for them** (novox/hq issue 1793). The launcher imports the bundle
* and then answers the runtime's handshake, which the runtime gives 10 s and then kills the bundle without starting it
* again; a bind awaited at the top of the module that stays refused longer ends the bundle all the same. So this
* returns at once: the binds run in the order given, each until it holds, and `done` is called after the last.
*/
export function bindInBackground(binds: [what: string, bind: () => Promise<unknown>][], done: () => void,
sleep?: (ms: number) => Promise<void>, say?: (line: string) => void): void {
void (async () => {
for (const [what, bind] of binds) await bindUntilHeld(what, bind, sleep, say);
done();
})();
}
+15 -13
View File
@@ -47,7 +47,7 @@ import { CHECK_CONTEXT, REPO_CHECK_CONTEXT, commentFor, composedGate, groupGateS
import { GateWriter, clonedAs, type BuildStarted } from "./gate.js";
import { HOOK_PATH, HOOK_PORT, Pace, claimHead, ensureSystemHook, eventsTool, keptSecret, receiver, type ForgeEvent } from "./webhook.js";
import { keepSiblings, noteComposed, noteVerdict, readSiblings, siblingsFile, siblingsOf, weighSiblings } from "./siblings.js";
import { bindUntilHeld } from "./bind.js";
import { bindInBackground } from "./bind.js";
import { announcementOf, keepTook, lateVerdictComment, outOfDateWords, sameCommit, tookFile, weighTrunks } from "./trunk.js";
// Without a way to a token — configured, or mintable with the admin account (token.ts) — there is
@@ -591,16 +591,18 @@ if (gitea) {
tick(() => pollRepos(client), 60_000);
tick(() => pollMerged(client), () => pace.every());
tick(() => pollPulls(client), () => pace.every());
// Bound again until it holds (bind.ts, issue 1793): a refusal at the start must not end the bundle.
await bindUntilHeld("mesh-controller.checked", () => on("mesh-controller.checked", (event) => {
const c = (event.body ?? {}) as Checked;
// A head's own verdict takes its turn after every write of that head's gate already asked for (gate.ts).
// A verdict of a check asked before the one now pending is an older run's, and dropped (novox/hq issue 456).
if (!c.group && c.owner && c.repo && c.commit) {
return gates.verdict(c.owner, c.repo, c.commit, () => setVerdict(client, event), c.id);
}
return setVerdict(client, event);
}));
await bindUntilHeld("node-build-agent.started", () => on("node-build-agent.started", buildStarted));
console.log("[gitea] watching for new repositories and merged pull requests");
// Bound again until they hold, and not awaited (bind.ts, issue 1793): the runtime kills a bundle that has not answered its
// handshake in 10 s, so the import must end while a refused bind is still asked again.
bindInBackground([
["mesh-controller.checked", () => on("mesh-controller.checked", (event) => {
const c = (event.body ?? {}) as Checked;
// A head's own verdict takes its turn after every write of that head's gate already asked for (gate.ts).
// A verdict of a check asked before the one now pending is an older run's, and dropped (novox/hq issue 456).
if (!c.group && c.owner && c.repo && c.commit) {
return gates.verdict(c.owner, c.repo, c.commit, () => setVerdict(client, event), c.id);
}
return setVerdict(client, event);
})],
["node-build-agent.started", () => on("node-build-agent.started", buildStarted)],
], () => console.log("[gitea] watching for new repositories and merged pull requests"));
}
+26 -1
View File
@@ -5,7 +5,7 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { bindUntilHeld, bindWait } from "../dist/bind.js";
import { bindInBackground, bindUntilHeld, bindWait } from "../dist/bind.js";
test("a bind refused with no responders is asked again until it holds, and says so", async () => {
let calls = 0;
@@ -31,3 +31,28 @@ test("a bind that holds the first time says nothing and waits for nothing", asyn
test("the wait grows to 30 s and stays", () => {
assert.deepEqual([1, 2, 3, 4, 50].map(bindWait), [2_000, 5_000, 10_000, 30_000, 30_000]);
});
test("the binds start without being waited for: the caller returns while a bind keeps refusing, and they go in order", async () => {
const order: string[] = [];
let finished = false;
let refusals = 0;
const gate = new Promise<void>((r) => setTimeout(r, 0));
bindInBackground([
["first", async () => { refusals++; if (refusals < 3) throw new Error("nats: no responders available for request"); order.push("first"); }],
["second", async () => { order.push("second"); }],
], () => { finished = true; }, async () => { await gate; }, () => {});
// Returned at once, with the first bind refused and nothing done.
assert.equal(finished, false);
assert.deepEqual(order, []);
await new Promise((r) => setTimeout(r, 20));
assert.deepEqual(order, ["first", "second"]);
assert.equal(finished, true);
});
test("a bind that never holds does not stop the caller from returning", () => {
let returned = false;
bindInBackground([["x", async () => { throw new Error("no responders"); }]], () => assert.fail("done"),
() => new Promise(() => {}), () => {});
returned = true;
assert.equal(returned, true);
});