Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4290,8 +4290,8 @@ async function maybeInvalidateCiCacheOnLegacyCiEvent(
* Wake linked PRs on an issue-side signal (#2259). Labeling/unlabeling (e.g. maintainer-only) or
* assigning/unassigning on a linked ISSUE can flip a linked-issue hard-rule verdict, but that only gets
* re-evaluated when the PR ITSELF receives a webhook or the staleness-ordered sweep eventually reaches it —
* which can lag for many cycles on a repo with more than a few open PRs. Enqueue staggered per-PR re-gate jobs
* for EVERY open PR that links this issue instead of doing the expensive live re-review inline.
* which can lag for many cycles on a repo with more than a few open PRs. Enqueue a bounded, staggered batch of
* per-PR re-gate jobs instead of doing the expensive live re-review inline.
* Uses its OWN coalesce window (issueLinkedPrReReviewCoalesced,
* DISTINCT from CI-completion's — #2371): the two triggers are not interchangeable, so a shared window let an
* unrelated CI re-review silently suppress a genuinely different issue-side signal. Within the issue-side
Expand Down Expand Up @@ -4320,11 +4320,12 @@ async function maybeReReviewOnLinkedIssueChange(
if (isConvergenceRepoAllowed(env, repoFullName)) {
const openPullRequests = await listOpenPullRequests(env, repoFullName);
// Issue-side label/assignment changes can flip linked-issue hard-rule verdicts from mergeable to close.
// Wake every linked PR promptly: a silent tail can keep stale passing gate/check state after the issue flips
// to an ineligible hard-rule state. Stagger the jobs so the expensive re-gates do not run inline or stampede
// the queue consumer.
// Wake a bounded prompt batch: unbounded issue-side fan-out can turn one webhook into thousands of
// foreground re-gates, exhausting queue, REST, and AI-review budgets. The regular stale sweep continues to
// converge any tail while preserving the same SWEEP_MAX_PRS source budget used by rate-aware fan-out.
const linkingPrs = openPullRequests
.filter((pr) => pr.linkedIssues.includes(issueNumber))
.slice(0, SWEEP_MAX_PRS)
.map((pr) => ({ number: pr.number, createdAt: pr.createdAt ?? null }));
for (const [index, pr] of linkingPrs.entries()) {
const prNumber = pr.number;
Expand Down
10 changes: 5 additions & 5 deletions test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2956,7 +2956,7 @@ describe("queue processors", () => {
]);
});

it("REGRESSION: issue-side linked PR wake re-gates every linked PR instead of leaving a stale tail", async () => {
it("REGRESSION: issue-side linked PR wake keeps fan-out bounded when many PRs link the same issue", async () => {
const sent: Array<{ message: import("../../src/types").JobMessage; options?: QueueSendOptions }> = [];
const env = createTestEnv({
GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
Expand All @@ -2981,7 +2981,7 @@ describe("queue processors", () => {

await processJob(env, {
type: "github-webhook",
deliveryId: "issue-label-all-linked-fanout",
deliveryId: "issue-label-bounded-linked-fanout",
eventName: "issues",
payload: {
action: "labeled",
Expand All @@ -2993,13 +2993,13 @@ describe("queue processors", () => {
});

expect(fetchCount).toBe(0);
expect(sent).toHaveLength(SWEEP_MAX_PRS + 2);
expect(sent).toHaveLength(SWEEP_MAX_PRS);
expect(sent.map(({ message }) => message)).toEqual(
Array.from({ length: SWEEP_MAX_PRS + 2 }, (_, index) =>
Array.from({ length: SWEEP_MAX_PRS }, (_, index) =>
expect.objectContaining({ type: "agent-regate-pr", repoFullName: "owner/agent-repo", prNumber: index + 1, installationId: 9001 }),
),
);
expect(sent.map(({ options }) => options)).toEqual([undefined, { delaySeconds: 10 }, { delaySeconds: 20 }, { delaySeconds: 30 }, { delaySeconds: 40 }]);
expect(sent.map(({ options }) => options)).toEqual([undefined, { delaySeconds: 10 }, { delaySeconds: 20 }]);
});

it("REGRESSION (#2371): a coalesced issue-side signal schedules a trailing re-review so an add-then-remove sequence is never lost", async () => {
Expand Down