gitea: a merge poll looks only at repositories that moved, one pass at a time
With the poll the only announcer of a merge, its cost showed: every 30 s it asked every repository for its pull requests, a pass outlasted the tick, and passes piled up beside each other — a merge was announced four and a half minutes late, and two passes at once could each announce it. A pass now asks only repositories updated since a minute before the last look, and the next pass starts when this one ends. hq issue 250.
This commit is contained in:
@@ -17,6 +17,8 @@ export interface GiteaRepo {
|
||||
description?: string;
|
||||
html_url: string;
|
||||
default_branch?: string;
|
||||
/** When anything last moved in it — a push, and so a merge. */
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/** An issue, with its labels flattened to names. */
|
||||
@@ -356,6 +358,7 @@ export class GiteaClient {
|
||||
description: r.description || undefined,
|
||||
html_url: r.html_url,
|
||||
default_branch: r.default_branch,
|
||||
updated_at: r.updated_at ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -598,3 +601,11 @@ export class GiteaAdmin {
|
||||
GiteaAdmin.fail(`/admin/users/${username}`, res);
|
||||
}
|
||||
}
|
||||
|
||||
/** The repositories that moved at or after a moment: every one when there is no moment yet, and one whose
|
||||
* update time is not known, so a forge that does not say is asked as before (novox/hq issue 250). */
|
||||
export function movedSince(repos: GiteaRepo[], floor: string): GiteaRepo[] {
|
||||
if (!floor) return repos;
|
||||
const at = Date.parse(floor);
|
||||
return repos.filter((r) => !r.updated_at || !(Date.parse(r.updated_at) < at));
|
||||
}
|
||||
|
||||
+16
-4
@@ -14,7 +14,7 @@
|
||||
// the forge for an immediacy nobody asked for is not.
|
||||
|
||||
import { emit } from "@novox/mesh-sdk/events";
|
||||
import { GiteaClient } from "./client.js";
|
||||
import { GiteaClient, movedSince } from "./client.js";
|
||||
|
||||
// Without a way to a token — configured, or mintable with the admin account (token.ts) — there is
|
||||
// nothing to watch; log and stay quiet rather than crash the runtime. With one, the first poll mints
|
||||
@@ -84,8 +84,16 @@ function keepAnnounced(): void {
|
||||
writeFileSync(tmp, JSON.stringify({ announced: [...announced].slice(-2000), since }));
|
||||
renameSync(tmp, mergedRecord);
|
||||
}
|
||||
// **Only the repositories that moved** (novox/hq issue 250). A merge is a push, and a push moves the
|
||||
// repository's update time; asking every repository for its pull requests on every tick took longer than the
|
||||
// tick itself, so ticks piled up and a merge was announced minutes late. A repository unchanged since a
|
||||
// minute before the last look is skipped — the minute absorbs the forge's clock against this one.
|
||||
let lastLook = "";
|
||||
const MARGIN_MS = 60_000;
|
||||
async function pollMerged(client: GiteaClient): Promise<void> {
|
||||
const repos = await client.listAllRepos();
|
||||
const began = new Date().toISOString();
|
||||
const floor = lastLook && primedMerges ? new Date(Date.parse(lastLook) - MARGIN_MS).toISOString() : "";
|
||||
const repos = movedSince(await client.listAllRepos(), floor);
|
||||
let changed = false;
|
||||
for (const repo of repos) {
|
||||
const pulls = await client.listPullRequests(repo.owner, repo.name, { state: "closed", sort: "recentupdate", limit: "20" });
|
||||
@@ -124,6 +132,7 @@ async function pollMerged(client: GiteaClient): Promise<void> {
|
||||
if (!primedMerges) since = new Date().toISOString();
|
||||
if (!primedMerges || changed) keepAnnounced();
|
||||
primedMerges = true;
|
||||
lastLook = began;
|
||||
}
|
||||
|
||||
if (gitea) {
|
||||
@@ -132,6 +141,9 @@ if (gitea) {
|
||||
// yet, the admin account refused on a restored forge) is one fact, and a recovery is worth a line.
|
||||
let failing: string | null = null;
|
||||
const tick = (fn: () => Promise<void>, everyMs: number): void => {
|
||||
// **One pass at a time** (novox/hq issue 250): the next pass is scheduled when this one has ended, so a
|
||||
// pass that outlasts its interval delays the next instead of running beside it — two passes at once
|
||||
// could each announce the same merge before either recorded it.
|
||||
const run = (): void =>
|
||||
void fn()
|
||||
.then(() => {
|
||||
@@ -142,8 +154,8 @@ if (gitea) {
|
||||
const why = err instanceof Error ? err.message : String(err);
|
||||
if (why !== failing) console.error(`[gitea] not watching until this clears — ${why}`);
|
||||
failing = why;
|
||||
});
|
||||
setInterval(run, everyMs);
|
||||
})
|
||||
.finally(() => setTimeout(run, everyMs));
|
||||
run();
|
||||
};
|
||||
tick(() => pollRepos(client), 60_000);
|
||||
|
||||
@@ -22,3 +22,15 @@ test("every page of a pull request's files is read, though the forge caps a page
|
||||
assert.equal(got.truncated, false);
|
||||
assert.equal(new Set(got.paths).size, total);
|
||||
});
|
||||
|
||||
test("a pass asks only the repositories that moved since the last look, every one before the first", async () => {
|
||||
const { movedSince } = await import("../client.ts");
|
||||
const repos = [
|
||||
{ full_name: "a/old", name: "old", owner: "a", private: false, html_url: "", updated_at: "2026-10-05T10:00:00Z" },
|
||||
{ full_name: "a/new", name: "new", owner: "a", private: false, html_url: "", updated_at: "2026-10-05T16:10:02Z" },
|
||||
{ full_name: "a/unknown", name: "unknown", owner: "a", private: false, html_url: "" },
|
||||
];
|
||||
assert.deepEqual(movedSince(repos, "").map((r) => r.name), ["old", "new", "unknown"]);
|
||||
assert.deepEqual(movedSince(repos, "2026-10-05T16:09:00.000Z").map((r) => r.name), ["new", "unknown"]);
|
||||
assert.deepEqual(movedSince(repos, "2026-10-05T18:10:02+02:00").map((r) => r.name), ["new", "unknown"], "an offset is a moment, not a string");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user