Skip to content

Commit 55d511a

Browse files
fix(queue): isolate per-repo failures in generateSignalSnapshots (#9293)
Use Promise.allSettled so one repo's data-gathering or persist error no longer skips siblings in the same multi-repo invocation, then surface an aggregate error listing the failed repos (same shape as #8355). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7876a05 commit 55d511a

2 files changed

Lines changed: 237 additions & 170 deletions

File tree

src/queue/signal-snapshot.ts

Lines changed: 201 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -62,176 +62,208 @@ export async function generateSignalSnapshots(
6262
(repo) =>
6363
repo.isInstalled && (!repoFullName || repo.fullName === repoFullName),
6464
);
65-
for (const repo of repositories) {
66-
const trendSince = new Date(
67-
Date.now() - QUEUE_TREND_HISTORY_DAYS * 24 * 60 * 60 * 1000,
68-
).toISOString();
69-
const [
70-
issues,
71-
pullRequests,
72-
recentMergedPullRequests,
73-
labels,
74-
queueCounts,
75-
bounties,
76-
totalsHistory,
77-
queueHealthHistory,
78-
] = await Promise.all([
79-
listIssueSignalSample(env, repo.fullName),
80-
listOpenPullRequests(env, repo.fullName),
81-
listRecentMergedPullRequests(env, repo.fullName),
82-
listRepoLabels(env, repo.fullName),
83-
loadOpenQueueCounts(env, repo.fullName),
84-
listBountiesByRepo(env, repo.fullName),
85-
listRepoGithubTotalsSnapshotHistory(env, repo.fullName, {
86-
sinceIso: trendSince,
87-
limit: 120,
88-
}),
89-
listSignalSnapshots(env, "queue-health", repo.fullName),
90-
]);
91-
const collisions = buildCollisionReport(
92-
repo.fullName,
93-
issues,
94-
pullRequests,
95-
recentMergedPullRequests,
96-
);
97-
const queueHealth = buildQueueHealth(
98-
repo,
99-
issues,
100-
pullRequests,
101-
collisions,
102-
queueCounts,
103-
);
104-
const configQuality = buildConfigQuality(
105-
repo,
106-
issues,
107-
pullRequests,
108-
repo.fullName,
109-
);
110-
const labelAudit = buildLabelAudit(
111-
repo,
112-
labels,
113-
issues,
114-
pullRequests,
115-
repo.fullName,
116-
);
117-
const maintainerLane = buildMaintainerLaneReport(
118-
repo,
119-
issues,
120-
pullRequests,
121-
repo.fullName,
122-
collisions,
123-
queueCounts,
124-
);
125-
const maintainerCutReadiness = buildMaintainerCutReadiness(
126-
repo,
127-
issues,
128-
pullRequests,
129-
repo.fullName,
130-
queueCounts,
131-
collisions,
132-
);
133-
const contributorIntakeHealth = buildContributorIntakeHealth(
134-
repo,
135-
issues,
136-
pullRequests,
137-
repo.fullName,
138-
collisions,
139-
queueCounts,
140-
);
141-
const issueQuality = buildIssueQualityReport(
142-
repo,
143-
issues,
144-
pullRequests,
145-
repo.fullName,
146-
bounties,
147-
collisions,
148-
recentMergedPullRequests,
149-
);
150-
await replaceCollisionEdges(
151-
env,
152-
repo.fullName,
153-
buildCollisionEdges(collisions),
154-
);
155-
const generatedAt = new Date().toISOString();
156-
await persistSignalSnapshot(env, {
157-
id: crypto.randomUUID(),
158-
signalType: "queue-health",
159-
targetKey: repo.fullName,
160-
repoFullName: repo.fullName,
161-
payload: queueHealth as unknown as Record<string, never>,
162-
generatedAt,
163-
});
164-
await upsertRepoQueueTrendSnapshot(env, {
165-
repoFullName: repo.fullName,
166-
payload: buildQueueTrendReport({
167-
repoFullName: repo.fullName,
168-
totalsSnapshots: totalsHistory,
169-
queueHealthSnapshots: queueHealthHistory,
170-
currentQueueHealth: queueHealth,
171-
generatedAt,
172-
}) as unknown as Record<string, never>,
173-
generatedAt,
174-
});
175-
await persistSignalSnapshot(env, {
176-
id: crypto.randomUUID(),
177-
signalType: "config-quality",
178-
targetKey: repo.fullName,
179-
repoFullName: repo.fullName,
180-
payload: configQuality as unknown as Record<string, never>,
181-
generatedAt,
182-
});
183-
await persistSignalSnapshot(env, {
184-
id: crypto.randomUUID(),
185-
signalType: "label-audit",
186-
targetKey: repo.fullName,
187-
repoFullName: repo.fullName,
188-
payload: labelAudit as unknown as Record<string, never>,
189-
generatedAt,
190-
});
191-
await persistSignalSnapshot(env, {
192-
id: crypto.randomUUID(),
193-
signalType: "maintainer-lane",
194-
targetKey: repo.fullName,
195-
repoFullName: repo.fullName,
196-
payload: maintainerLane as unknown as Record<string, never>,
197-
generatedAt,
198-
});
199-
await persistSignalSnapshot(env, {
200-
id: crypto.randomUUID(),
201-
signalType: "maintainer-cut-readiness",
202-
targetKey: repo.fullName,
203-
repoFullName: repo.fullName,
204-
payload: maintainerCutReadiness as unknown as Record<string, never>,
205-
generatedAt,
206-
});
207-
await persistSignalSnapshot(env, {
208-
id: crypto.randomUUID(),
209-
signalType: "contributor-intake-health",
210-
targetKey: repo.fullName,
211-
repoFullName: repo.fullName,
212-
payload: contributorIntakeHealth as unknown as Record<string, never>,
213-
generatedAt,
214-
});
215-
await persistSignalSnapshot(env, {
216-
id: crypto.randomUUID(),
217-
signalType: "issue-quality",
218-
targetKey: repo.fullName,
219-
repoFullName: repo.fullName,
220-
payload: issueQuality as unknown as Record<string, never>,
221-
generatedAt,
222-
});
223-
const repoOutcomePatterns = await computeRepoOutcomePatterns(
224-
env,
225-
repo.fullName,
226-
repo,
65+
// #9293: Promise.allSettled (not a bare sequential for-loop) so one repo's data-gathering or
66+
// persist failure never silently skips every subsequent repo in the same multi-repo invocation —
67+
// same isolation shape as job-dispatch.ts's "backfill-registered-repos" fan-out (#8355). Every
68+
// repo is attempted exactly once; successes persist regardless of a sibling's outcome; failures
69+
// are collected and rethrown as one aggregate error so the invocation stays observably failed.
70+
const settled = await Promise.allSettled(
71+
repositories.map((repo) => generateSignalSnapshotForRepo(env, repo)),
72+
);
73+
const failedRepoFullNames: string[] = [];
74+
settled.forEach((result, index) => {
75+
if (result.status === "rejected") {
76+
const failedRepoFullName = repositories[index]!.fullName;
77+
failedRepoFullNames.push(failedRepoFullName);
78+
console.error(
79+
JSON.stringify({
80+
level: "error",
81+
event: "generate_signal_snapshots_repo_failed",
82+
repoFullName: failedRepoFullName,
83+
reason: String(result.reason),
84+
}),
85+
);
86+
}
87+
});
88+
if (failedRepoFullNames.length > 0) {
89+
throw new Error(
90+
`generate-signal-snapshots: ${failedRepoFullNames.length}/${repositories.length} repo(s) failed: ${failedRepoFullNames.join(", ")}`,
22791
);
228-
await persistSignalSnapshot(env, {
229-
id: crypto.randomUUID(),
230-
signalType: REPO_OUTCOME_PATTERNS_SIGNAL,
231-
targetKey: repo.fullName,
92+
}
93+
}
94+
95+
async function generateSignalSnapshotForRepo(
96+
env: Env,
97+
repo: Awaited<ReturnType<typeof listRepositories>>[number],
98+
): Promise<void> {
99+
const trendSince = new Date(
100+
Date.now() - QUEUE_TREND_HISTORY_DAYS * 24 * 60 * 60 * 1000,
101+
).toISOString();
102+
const [
103+
issues,
104+
pullRequests,
105+
recentMergedPullRequests,
106+
labels,
107+
queueCounts,
108+
bounties,
109+
totalsHistory,
110+
queueHealthHistory,
111+
] = await Promise.all([
112+
listIssueSignalSample(env, repo.fullName),
113+
listOpenPullRequests(env, repo.fullName),
114+
listRecentMergedPullRequests(env, repo.fullName),
115+
listRepoLabels(env, repo.fullName),
116+
loadOpenQueueCounts(env, repo.fullName),
117+
listBountiesByRepo(env, repo.fullName),
118+
listRepoGithubTotalsSnapshotHistory(env, repo.fullName, {
119+
sinceIso: trendSince,
120+
limit: 120,
121+
}),
122+
listSignalSnapshots(env, "queue-health", repo.fullName),
123+
]);
124+
const collisions = buildCollisionReport(
125+
repo.fullName,
126+
issues,
127+
pullRequests,
128+
recentMergedPullRequests,
129+
);
130+
const queueHealth = buildQueueHealth(
131+
repo,
132+
issues,
133+
pullRequests,
134+
collisions,
135+
queueCounts,
136+
);
137+
const configQuality = buildConfigQuality(
138+
repo,
139+
issues,
140+
pullRequests,
141+
repo.fullName,
142+
);
143+
const labelAudit = buildLabelAudit(
144+
repo,
145+
labels,
146+
issues,
147+
pullRequests,
148+
repo.fullName,
149+
);
150+
const maintainerLane = buildMaintainerLaneReport(
151+
repo,
152+
issues,
153+
pullRequests,
154+
repo.fullName,
155+
collisions,
156+
queueCounts,
157+
);
158+
const maintainerCutReadiness = buildMaintainerCutReadiness(
159+
repo,
160+
issues,
161+
pullRequests,
162+
repo.fullName,
163+
queueCounts,
164+
collisions,
165+
);
166+
const contributorIntakeHealth = buildContributorIntakeHealth(
167+
repo,
168+
issues,
169+
pullRequests,
170+
repo.fullName,
171+
collisions,
172+
queueCounts,
173+
);
174+
const issueQuality = buildIssueQualityReport(
175+
repo,
176+
issues,
177+
pullRequests,
178+
repo.fullName,
179+
bounties,
180+
collisions,
181+
recentMergedPullRequests,
182+
);
183+
await replaceCollisionEdges(
184+
env,
185+
repo.fullName,
186+
buildCollisionEdges(collisions),
187+
);
188+
const generatedAt = new Date().toISOString();
189+
await persistSignalSnapshot(env, {
190+
id: crypto.randomUUID(),
191+
signalType: "queue-health",
192+
targetKey: repo.fullName,
193+
repoFullName: repo.fullName,
194+
payload: queueHealth as unknown as Record<string, never>,
195+
generatedAt,
196+
});
197+
await upsertRepoQueueTrendSnapshot(env, {
198+
repoFullName: repo.fullName,
199+
payload: buildQueueTrendReport({
232200
repoFullName: repo.fullName,
233-
payload: repoOutcomePatterns as unknown as Record<string, never>,
201+
totalsSnapshots: totalsHistory,
202+
queueHealthSnapshots: queueHealthHistory,
203+
currentQueueHealth: queueHealth,
234204
generatedAt,
235-
});
236-
}
205+
}) as unknown as Record<string, never>,
206+
generatedAt,
207+
});
208+
await persistSignalSnapshot(env, {
209+
id: crypto.randomUUID(),
210+
signalType: "config-quality",
211+
targetKey: repo.fullName,
212+
repoFullName: repo.fullName,
213+
payload: configQuality as unknown as Record<string, never>,
214+
generatedAt,
215+
});
216+
await persistSignalSnapshot(env, {
217+
id: crypto.randomUUID(),
218+
signalType: "label-audit",
219+
targetKey: repo.fullName,
220+
repoFullName: repo.fullName,
221+
payload: labelAudit as unknown as Record<string, never>,
222+
generatedAt,
223+
});
224+
await persistSignalSnapshot(env, {
225+
id: crypto.randomUUID(),
226+
signalType: "maintainer-lane",
227+
targetKey: repo.fullName,
228+
repoFullName: repo.fullName,
229+
payload: maintainerLane as unknown as Record<string, never>,
230+
generatedAt,
231+
});
232+
await persistSignalSnapshot(env, {
233+
id: crypto.randomUUID(),
234+
signalType: "maintainer-cut-readiness",
235+
targetKey: repo.fullName,
236+
repoFullName: repo.fullName,
237+
payload: maintainerCutReadiness as unknown as Record<string, never>,
238+
generatedAt,
239+
});
240+
await persistSignalSnapshot(env, {
241+
id: crypto.randomUUID(),
242+
signalType: "contributor-intake-health",
243+
targetKey: repo.fullName,
244+
repoFullName: repo.fullName,
245+
payload: contributorIntakeHealth as unknown as Record<string, never>,
246+
generatedAt,
247+
});
248+
await persistSignalSnapshot(env, {
249+
id: crypto.randomUUID(),
250+
signalType: "issue-quality",
251+
targetKey: repo.fullName,
252+
repoFullName: repo.fullName,
253+
payload: issueQuality as unknown as Record<string, never>,
254+
generatedAt,
255+
});
256+
const repoOutcomePatterns = await computeRepoOutcomePatterns(
257+
env,
258+
repo.fullName,
259+
repo,
260+
);
261+
await persistSignalSnapshot(env, {
262+
id: crypto.randomUUID(),
263+
signalType: REPO_OUTCOME_PATTERNS_SIGNAL,
264+
targetKey: repo.fullName,
265+
repoFullName: repo.fullName,
266+
payload: repoOutcomePatterns as unknown as Record<string, never>,
267+
generatedAt,
268+
});
237269
}

0 commit comments

Comments
 (0)