Merge pull request 'Start the gitea bundle's binds without awaiting them, so its handshake is answered (issue 1793)' (#429) from fix/1793-bind-after-handshake into main
This commit was merged in pull request #429.
This commit is contained in:
@@ -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
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// **The bundle's import ends while a bind is refused** (novox/hq issue 1793). The launcher imports index.js and then answers
|
||||
// node-tools' handshake, which has 10 s before the bundle is killed and never started again. A bind awaited at the top
|
||||
// of index.ts, refused for longer, ended the bundle at 00:28:37 on 2026-10-22. Run in a child: the polls' timers keep a
|
||||
// process alive, and the child says "imported" and exits as soon as the import resolves.
|
||||
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
test("importing the bundle resolves while the broker keeps refusing the binds", async () => {
|
||||
const index = pathToFileURL(join(here, "..", "dist", "index.js")).href;
|
||||
const script = `
|
||||
const { useBroker } = await import("@novox/mesh-sdk/messaging");
|
||||
useBroker(() => ({
|
||||
subscribe: async () => { throw new Error("binding gitea's consumer novox_gitea: nats: no responders available for request"); },
|
||||
publish: async () => {}, request: async () => { throw new Error("no responders"); },
|
||||
handle: async () => () => {}, close: async () => {},
|
||||
}));
|
||||
await import(${JSON.stringify(index)});
|
||||
console.log("imported");
|
||||
process.exit(0);`;
|
||||
const env = { ...process.env, MESH_GITEA_TOKEN: "x", MESH_GITEA_URL: "http://127.0.0.1:1" };
|
||||
for (const k of ["MESH_GITEA_STATE_DIR", "MESH_GITEA_ADMIN_USER", "MESH_GITEA_ADMIN_PASSWORD", "MESH_SERVED_MODULE"]) delete env[k];
|
||||
const child = spawn(process.execPath, ["--input-type=module", "-e", script], { cwd: join(here, ".."), env, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let out = "";
|
||||
let err = "";
|
||||
child.stdout.on("data", (d) => (out += d));
|
||||
child.stderr.on("data", (d) => (err += d));
|
||||
const ended = new Promise<void>((r) => child.on("exit", () => r()));
|
||||
const timer = setTimeout(() => child.kill("SIGKILL"), 3000);
|
||||
await ended;
|
||||
clearTimeout(timer);
|
||||
assert.match(out, /imported/, `the import did not resolve within 3 s while a bind was refused; stderr: ${err.slice(0, 600)}`);
|
||||
});
|
||||
Reference in New Issue
Block a user