Stop asking the forge again for a mesh/check it refuses for good, and end the ask when its key is deleted (issue 774)
gitea asked an owed status again every 30 s forever, even on 403 and 404 (jschoubben/hal and jschoubben/novadock, 119 times each in the sweep), and a key the checker deleted did not end a loop already running. Quick fix: return from askAgain on 403/404. Thorough fix: the same, said once with its cause; a delete of the key marks its revision seen, so any running ask again ends before its next write; a newer revision still asks anew. The checker no longer owes statuses outside the mesh (mesh-delivery), which removes the trigger. Not done: raising the module's health, since gitea's TypeScript SDK has no seat publish for a condition (mesh-sdk).
This commit is contained in:
@@ -83,6 +83,12 @@ export function checkStatusOf(change: OwedChange): { owner: string; repo: string
|
||||
return { owner, repo, sha, revision, status };
|
||||
}
|
||||
|
||||
/** Whether the forge's refusal will not change by asking again: it forbids the write (403) or knows no such repository
|
||||
* or commit (404), as the client words it ("Gitea API <path>: <status> ..."). */
|
||||
export function refusedForGood(err: unknown): boolean {
|
||||
return /Gitea API \S+: (403|404) /.test(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
/** How many mesh/check writes this module has at the forge at once. */
|
||||
export const WRITES_AT_ONCE = 4;
|
||||
|
||||
@@ -115,6 +121,12 @@ export class CheckStatuses {
|
||||
try {
|
||||
asked = checkStatusOf(change);
|
||||
} catch (err) {
|
||||
// A deleted key is owed no more: its revision is seen, so a write of it still asked again ends (askAgain).
|
||||
if (change?.op === "delete" && typeof change.key === "string" && Number.isInteger(change.revision)) {
|
||||
const seen = this.seen.get(change.key);
|
||||
if (seen === undefined || change.revision > seen) this.remember(this.seen, change.key, change.revision);
|
||||
return "dropped";
|
||||
}
|
||||
this.say(`[gitea] ${CHECKER_OWED} ${change?.key} at revision ${change?.revision} writes nothing: ${err instanceof Error ? err.message : err}`);
|
||||
return "dropped";
|
||||
}
|
||||
@@ -172,6 +184,12 @@ export class CheckStatuses {
|
||||
private async askAgain(change: OwedChange, first: unknown): Promise<void> {
|
||||
let err = first;
|
||||
for (let attempt = 1; ; attempt++) {
|
||||
if (refusedForGood(err)) {
|
||||
// A repository the forge forbids or does not know answers the same at every ask (the sweep of 2026-10-30, issue
|
||||
// 774: hal and novadock, 119 asks each): said once, never asked again; a newer revision of the key asks anew.
|
||||
this.say(`[gitea] ${change.key}: mesh/check at revision ${change.revision} is refused for good (${err instanceof Error ? err.message : err}); not asked again`);
|
||||
return;
|
||||
}
|
||||
const wait = bindWait(attempt);
|
||||
this.say(`[gitea] ${change.key}: mesh/check at revision ${change.revision} could not be written (${err instanceof Error ? err.message : err}); asked again in ${wait / 1000}s`);
|
||||
await this.sleep(wait);
|
||||
|
||||
@@ -175,3 +175,33 @@ test("the writes a watch's start hands over go to the forge a few at a time, nev
|
||||
assert.equal(done, 50);
|
||||
assert.equal(most, WRITES_AT_ONCE);
|
||||
});
|
||||
|
||||
// The sweep of 2026-10-30 (issue 774): a status the forge refuses with 403 or 404 was asked again every 30 s forever (hal
|
||||
// and novadock, 119 times each), and a key the checker deleted kept its loop running. Now a 403 or 404 is said once and
|
||||
// never asked again, a 5xx is still asked again, and a delete ends a loop that is running.
|
||||
test("a refusal for good is said once and not asked again; a delete ends a running ask again", async () => {
|
||||
const { CheckStatuses, refusedForGood } = await import("../check.ts");
|
||||
for (const [err, forGood] of [["Gitea API /repos/jschoubben/hal/statuses/x: 403 forbidden", true],
|
||||
["Gitea API /repos/jschoubben/novadock/statuses/x: 404 not found", true], ["Gitea API /repos/x/statuses: 502 bad gateway", false],
|
||||
["Gitea API /repos/x: no answer in 10 s, the request was cut", false]] as [string, boolean][]) {
|
||||
assert.equal(refusedForGood(new Error(err)), forGood, err);
|
||||
}
|
||||
const said: string[] = [];
|
||||
const forbidden = { setCommitStatus: async () => { throw new Error("Gitea API /repos/jschoubben/hal/statuses/x: 403 forbidden"); } };
|
||||
const checks = new CheckStatuses(forbidden, (l) => said.push(l), async () => { throw new Error("asked again"); });
|
||||
await checks.take(put(key(sha, "jschoubben/hal"), owed(), 3));
|
||||
await checks.settled();
|
||||
assert.equal(said.length, 1);
|
||||
assert.match(said[0], /refused for good.*403.*not asked again/);
|
||||
// A 502 asks again; the key's delete meanwhile ends the loop before its next ask.
|
||||
const g = forge();
|
||||
let release: () => void = () => {};
|
||||
const gate = new Promise<void>((r) => { release = r; });
|
||||
const later = new CheckStatuses(g, () => {}, () => gate);
|
||||
g.fail(5);
|
||||
await later.take(put(key(), owed(), 8));
|
||||
await later.take({ state: "mesh-delivery-checker.owed", key: key(), op: "delete", revision: 9, current: false } as any);
|
||||
release();
|
||||
await later.settled();
|
||||
assert.deepEqual(g.sent, [], "a deleted key is not written");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user