Merge pull request 'Bind the gitea bundle's event consumers again until they hold, so verdicts reach the forge (issue 1793)' (#428) from fix/1793-verdict-reaches-the-forge into main
This commit was merged in pull request #428.
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
// **A consumer that cannot be bound yet is bound again, never fatal** (novox/hq issue 1793). When the tool runtime
|
||||||
|
// starts this bundle, the runtime's answer to "bind this module's consumer" can be "no responders" for a few seconds (the
|
||||||
|
// runtime restarted by a delivery, the bus not yet serving the stream). That rejection, left unhandled at the top of
|
||||||
|
// index.ts, ended the bundle: nothing heard `mesh-controller.checked` again, and a verdict that landed later never
|
||||||
|
// reached the forge's statuses. A bind that fails is said and asked again with a growing wait until it holds.
|
||||||
|
|
||||||
|
/** How long to wait before the nth new attempt (1-based): 2 s, 5 s, 10 s, then every 30 s. */
|
||||||
|
export function bindWait(attempt: number): number {
|
||||||
|
return [2_000, 5_000, 10_000][attempt - 1] ?? 30_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds with `bind`, asking again after each failure, until it holds. `sleep` is the wait; `say` how a failure is said.
|
||||||
|
* Never rejects; resolves once the bind holds.
|
||||||
|
*/
|
||||||
|
export async function bindUntilHeld(what: string, bind: () => Promise<unknown>,
|
||||||
|
sleep: (ms: number) => Promise<void> = (ms) => new Promise((r) => setTimeout(r, ms)),
|
||||||
|
say: (line: string) => void = (line) => console.error(line)): Promise<void> {
|
||||||
|
for (let attempt = 1; ; attempt++) {
|
||||||
|
try {
|
||||||
|
await bind();
|
||||||
|
if (attempt > 1) say(`[gitea] ${what} is bound after ${attempt} attempts`);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
const wait = bindWait(attempt);
|
||||||
|
say(`[gitea] ${what} could not be bound (${err instanceof Error ? err.message : String(err)}); asking again in ${wait / 1000}s`);
|
||||||
|
await sleep(wait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ import { CHECK_CONTEXT, REPO_CHECK_CONTEXT, commentFor, composedGate, groupGateS
|
|||||||
import { GateWriter, clonedAs, type BuildStarted } from "./gate.js";
|
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 { 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 { keepSiblings, noteComposed, noteVerdict, readSiblings, siblingsFile, siblingsOf, weighSiblings } from "./siblings.js";
|
||||||
|
import { bindUntilHeld } from "./bind.js";
|
||||||
import { announcementOf, keepTook, lateVerdictComment, outOfDateWords, sameCommit, tookFile, weighTrunks } from "./trunk.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
|
// Without a way to a token — configured, or mintable with the admin account (token.ts) — there is
|
||||||
@@ -590,7 +591,8 @@ if (gitea) {
|
|||||||
tick(() => pollRepos(client), 60_000);
|
tick(() => pollRepos(client), 60_000);
|
||||||
tick(() => pollMerged(client), () => pace.every());
|
tick(() => pollMerged(client), () => pace.every());
|
||||||
tick(() => pollPulls(client), () => pace.every());
|
tick(() => pollPulls(client), () => pace.every());
|
||||||
await on("mesh-controller.checked", (event) => {
|
// 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;
|
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 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).
|
// A verdict of a check asked before the one now pending is an older run's, and dropped (novox/hq issue 456).
|
||||||
@@ -598,7 +600,7 @@ if (gitea) {
|
|||||||
return gates.verdict(c.owner, c.repo, c.commit, () => setVerdict(client, event), c.id);
|
return gates.verdict(c.owner, c.repo, c.commit, () => setVerdict(client, event), c.id);
|
||||||
}
|
}
|
||||||
return setVerdict(client, event);
|
return setVerdict(client, event);
|
||||||
});
|
}));
|
||||||
await on("node-build-agent.started", buildStarted);
|
await bindUntilHeld("node-build-agent.started", () => on("node-build-agent.started", buildStarted));
|
||||||
console.log("[gitea] watching for new repositories and merged pull requests");
|
console.log("[gitea] watching for new repositories and merged pull requests");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// **A consumer that cannot be bound yet is bound again** (novox/hq issue 1793): the runtime's "no responders" at the
|
||||||
|
// bundle's start, seen 2026-10-22 00:28:37 on mesh-catalog#427's verdict, ended the bundle and the verdict was never
|
||||||
|
// written on the forge.
|
||||||
|
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { 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;
|
||||||
|
const waits: number[] = [];
|
||||||
|
const said: string[] = [];
|
||||||
|
await bindUntilHeld("mesh-controller.checked", async () => {
|
||||||
|
calls++;
|
||||||
|
if (calls < 4) throw new Error("binding gitea's consumer novox_gitea: nats: no responders available for request");
|
||||||
|
}, async (ms) => { waits.push(ms); }, (l) => said.push(l));
|
||||||
|
assert.equal(calls, 4);
|
||||||
|
assert.deepEqual(waits, [2_000, 5_000, 10_000]);
|
||||||
|
assert.equal(said.length, 4);
|
||||||
|
assert.match(said[0], /no responders available for request.*asking again in 2s/);
|
||||||
|
assert.match(said[3], /is bound after 4 attempts/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a bind that holds the first time says nothing and waits for nothing", async () => {
|
||||||
|
const said: string[] = [];
|
||||||
|
await bindUntilHeld("x", async () => {}, async () => assert.fail("waited"), (l) => said.push(l));
|
||||||
|
assert.deepEqual(said, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user