Skip to content

Commit 2aa6158

Browse files
authored
refactor(queue): extract job-dispatch.ts from processors.ts (#4971)
Final step of #4013. Moves the top-level processJob dispatcher (a pure switch over JobMessage["type"]) into its own file. This is the most interdependent piece of the split -- job-dispatch.ts imports most of its handlers back from processors.ts (which stays large; nothing else in processors.ts functions (and processGitHubWebhook) are exported for this one-directional import-back. processors.ts re-exports processJob for src/index.ts, src/server.ts, and the test suite. Also removes ~28 imports in processors.ts that were orphaned once processJob (their only remaining caller) moved out, and adds two regression tests for job-dispatch branches that were untested even in the original file (retry-orb-relay dispatch; a notify-evaluate payload with neither the events array nor the legacy singular event field).
1 parent c8bb830 commit 2aa6158

4 files changed

Lines changed: 392 additions & 326 deletions

File tree

src/queue/job-dispatch.ts

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
// #4013 step 10 (final): the top-level job dispatcher, extracted last because it is the single most
2+
// interdependent piece of processors.ts -- a pure switch over JobMessage["type"] that fans out to dozens of
3+
// handlers still defined in processors.ts (and elsewhere), most of which have no other reason to move. This
4+
// file therefore imports heavily FROM processors.ts (one-directional: processors.ts no longer calls
5+
// processJob itself, so there is no cycle) rather than the other way around. processJob is one of only 2
6+
// exports in the original file with a REAL external caller (src/index.ts, src/server.ts), plus the entire
7+
// test suite's `import { processJob } from "../../src/queue/processors"` -- processors.ts keeps a re-export
8+
// shim for it.
9+
import type { DetectedNotificationEvent, JobMessage } from "../types";
10+
import { refreshRegistry } from "../registry/sync";
11+
import { listRepositories, rollupProductUsageDaily } from "../db/repositories";
12+
import {
13+
backfillOpenPullRequestDetails,
14+
backfillRegisteredRepositories,
15+
backfillRepositorySegment,
16+
enqueueRepositoryOpenDataBackfill,
17+
refreshContributorActivity,
18+
refreshInstallationHealth,
19+
} from "../github/backfill";
20+
import { refreshScoringModelSnapshot } from "../scoring/model";
21+
import { fileUpstreamDriftIssues, refreshUpstreamDrift } from "../upstream/ruleset";
22+
import { generateWeeklyValueReport } from "../services/weekly-value-report";
23+
import { isRecapEnabled, resolveMaintainerRecapManifestOverride, runMaintainerRecapJob } from "../review/maintainer-recap-wire";
24+
import { performRepoDocRefresh } from "../github/repo-doc-refresh-runner";
25+
import { executeAgentRun } from "../services/agent-orchestrator";
26+
import { deliverNotification, evaluateNotificationEvent } from "../notifications/service";
27+
import { isOpsEnabled, runOpsAlerts } from "../review/ops-wire";
28+
import { isSweepWatchdogEnabled, runSweepLivenessWatchdog } from "../review/sweep-watchdog";
29+
import { isPrReconciliationEnabled, runOpenPrReconciliation } from "../review/pr-reconciliation";
30+
import { isSelfTuneEnabled, runSelfTune } from "../review/selftune-wire";
31+
import { runSelfTuneBreaker } from "../review/outcomes-wire";
32+
import { isRagEnabled } from "../review/rag-wire";
33+
import { processSubmitDraft } from "../services/draft";
34+
import { retryFailedRelays } from "../orb/relay";
35+
import { generateSignalSnapshots } from "./signal-snapshot";
36+
import { runRetentionPrune } from "./retention";
37+
// The 15 handlers below have no reason to move -- each is only reachable via this dispatcher (or, for
38+
// mapWithConcurrency, ALSO used by other still-in-processors.ts code), so they stay put and are exported
39+
// there purely for this one-directional import-back (processors.ts itself never calls processJob).
40+
import {
41+
buildBurdenForecasts,
42+
buildContributorDecisionPacks,
43+
buildContributorEvidence,
44+
fanOutAgentRegateSweepJobs,
45+
fanOutBacklogConvergenceSweepJobs,
46+
fanOutRepoDocRefreshSweepJobs,
47+
fanOutRepoSignalSnapshotJobs,
48+
mapWithConcurrency,
49+
processGitHubWebhook,
50+
regatePullRequest,
51+
reReviewStoredPullRequest,
52+
repairDataFidelity,
53+
runRagIndexJob,
54+
runReviewRecapJob,
55+
sweepRepoBacklogConvergence,
56+
sweepRepoRegate,
57+
} from "./processors";
58+
59+
// A batched notify-evaluate job (#selfhost-maintenance-self-pin) can carry many events from one webhook (a
60+
// popular newly-opened issue can have dozens of watchers) -- an unbounded Promise.all over all of them would
61+
// let a single job spend as many concurrent DB/eval calls as it likes, bypassing the queue's own
62+
// backgroundConcurrency cap (which defaults to 1) entirely from inside one job's execution. Bounded worker-pool
63+
// fan-out, same shape as GLOBAL_OPEN_ITEM_LIVE_CHECK_CONCURRENCY in processors.ts. Moved here (rather than
64+
// imported back) since processJob was its only caller.
65+
const NOTIFY_EVALUATE_EVENT_CONCURRENCY = 5;
66+
67+
export async function processJob(env: Env, message: JobMessage): Promise<void> {
68+
switch (message.type) {
69+
case "refresh-registry":
70+
await refreshRegistry(env);
71+
return;
72+
case "backfill-registered-repos":
73+
if (!message.repoFullName && message.requestedBy !== "test") {
74+
const repositories = (await listRepositories(env)).filter(
75+
(repo) => repo.isRegistered,
76+
);
77+
if (repositories.length > 0) {
78+
const delayStepSeconds =
79+
message.mode === "full" || message.mode === "resume" ? 45 : 15;
80+
await Promise.all(
81+
repositories.map((repo, index) => {
82+
const repoMessage: JobMessage = {
83+
type: "backfill-registered-repos",
84+
requestedBy: message.requestedBy,
85+
repoFullName: repo.fullName,
86+
...(message.force === undefined
87+
? {}
88+
: { force: message.force }),
89+
...(message.mode === undefined ? {} : { mode: message.mode }),
90+
};
91+
const delaySeconds = Math.min(index * delayStepSeconds, 900);
92+
return delaySeconds > 0
93+
? env.JOBS.send(repoMessage, { delaySeconds })
94+
: env.JOBS.send(repoMessage);
95+
}),
96+
);
97+
return;
98+
}
99+
}
100+
if (message.repoFullName && message.requestedBy !== "test") {
101+
await enqueueRepositoryOpenDataBackfill(env, {
102+
repoFullName: message.repoFullName,
103+
requestedBy: message.requestedBy,
104+
...(message.force === undefined ? {} : { force: message.force }),
105+
...(message.mode === undefined ? {} : { mode: message.mode }),
106+
});
107+
return;
108+
}
109+
await backfillRegisteredRepositories(env, {
110+
...(message.repoFullName ? { repoFullName: message.repoFullName } : {}),
111+
requestedBy: message.requestedBy,
112+
...(message.force === undefined ? {} : { force: message.force }),
113+
...(message.mode === undefined ? {} : { mode: message.mode }),
114+
});
115+
return;
116+
case "backfill-repo-segment":
117+
await backfillRepositorySegment(env, {
118+
repoFullName: message.repoFullName,
119+
segment: message.segment,
120+
requestedBy: message.requestedBy,
121+
...(message.mode === undefined ? {} : { mode: message.mode }),
122+
...(message.cursor === undefined ? {} : { cursor: message.cursor }),
123+
...(message.force === undefined ? {} : { force: message.force }),
124+
});
125+
return;
126+
case "backfill-pr-details":
127+
await backfillOpenPullRequestDetails(env, {
128+
repoFullName: message.repoFullName,
129+
...(message.mode === undefined ? {} : { mode: message.mode }),
130+
...(message.cursor === undefined ? {} : { cursor: message.cursor }),
131+
});
132+
return;
133+
case "refresh-installation-health":
134+
await refreshInstallationHealth(env);
135+
return;
136+
case "generate-signal-snapshots":
137+
if (!message.repoFullName && message.requestedBy !== "test") {
138+
await fanOutRepoSignalSnapshotJobs(env, message.requestedBy);
139+
return;
140+
}
141+
await generateSignalSnapshots(env, message.repoFullName);
142+
return;
143+
case "refresh-scoring-model":
144+
await refreshScoringModelSnapshot(env);
145+
return;
146+
case "refresh-upstream-drift":
147+
await refreshUpstreamDrift(env);
148+
return;
149+
case "file-upstream-drift-issues":
150+
await fileUpstreamDriftIssues(env);
151+
return;
152+
case "build-contributor-evidence":
153+
await buildContributorEvidence(env, message.login, message.logins);
154+
return;
155+
case "build-contributor-decision-packs":
156+
await buildContributorDecisionPacks(env, message.login);
157+
return;
158+
case "refresh-contributor-activity":
159+
await refreshContributorActivity(
160+
env,
161+
message.login,
162+
message.repoFullName ? { repoFullName: message.repoFullName } : {},
163+
);
164+
return;
165+
case "build-burden-forecasts":
166+
await buildBurdenForecasts(env, message.repoFullName);
167+
return;
168+
case "repair-data-fidelity":
169+
await repairDataFidelity(env, message.requestedBy);
170+
return;
171+
case "rollup-product-usage":
172+
await rollupProductUsageDaily(env, {
173+
...(message.day ? { day: message.day } : {}),
174+
...(message.days === undefined ? {} : { days: message.days }),
175+
});
176+
return;
177+
case "prune-retention":
178+
await runRetentionPrune(
179+
env,
180+
message.requestedBy,
181+
message.dryRun ?? false,
182+
);
183+
return;
184+
case "generate-weekly-value-report":
185+
await generateWeeklyValueReport(env, {
186+
variant: message.variant ?? "operator",
187+
...(message.days === undefined ? {} : { days: message.days }),
188+
});
189+
return;
190+
case "generate-review-recap":
191+
await runReviewRecapJob(env, message.repoFullName, message.windowDays);
192+
return;
193+
case "generate-maintainer-recap": {
194+
// Convergence (maintainer recap digest, flag GITTENSORY_MAINTAINER_RECAP, #1963/#2248, config-as-code
195+
// override #2250). Defense-in-depth: the cron only ENQUEUES this when enabled, but a stale in-flight job
196+
// that lands after a flag-flip (env OR manifest) must still no-op, so disabled does zero work here too.
197+
const maintainerRecapOverride = await resolveMaintainerRecapManifestOverride(env);
198+
if (isRecapEnabled(env, maintainerRecapOverride)) await runMaintainerRecapJob(env, message.windowDays, maintainerRecapOverride);
199+
return;
200+
}
201+
case "agent-regate-sweep":
202+
if (!message.repoFullName && message.requestedBy !== "test") {
203+
await fanOutAgentRegateSweepJobs(env, message.requestedBy);
204+
return;
205+
}
206+
await sweepRepoRegate(env, message.repoFullName, message.requestedBy);
207+
return;
208+
case "backlog-convergence-sweep":
209+
if (!message.repoFullName && message.requestedBy !== "test") {
210+
await fanOutBacklogConvergenceSweepJobs(env, message.requestedBy);
211+
return;
212+
}
213+
await sweepRepoBacklogConvergence(env, message.repoFullName, message.requestedBy);
214+
return;
215+
case "repo-doc-refresh-sweep":
216+
if (!message.repoFullName && message.requestedBy !== "test") {
217+
await fanOutRepoDocRefreshSweepJobs(env, message.requestedBy);
218+
return;
219+
}
220+
if (message.repoFullName) await performRepoDocRefresh(env, message.repoFullName);
221+
return;
222+
case "agent-regate-pr":
223+
// One bounded re-gate unit fanned out by the sweep (#audit-sweep-fanout): re-review + stamp a single PR.
224+
await regatePullRequest(
225+
env,
226+
message.repairHeadSha,
227+
message.repoFullName,
228+
message.prNumber,
229+
message.installationId,
230+
message.deliveryId,
231+
message.force,
232+
message.prCreatedAt,
233+
);
234+
return;
235+
case "run-agent":
236+
await executeAgentRun(env, message.runId);
237+
return;
238+
case "notify-evaluate": {
239+
// Legacy payload compat: a row enqueued before the batched-events deploy (#selfhost-maintenance-self-pin)
240+
// still carries the OLD singular `event` field on disk, not `events` -- a rolling deploy can process such
241+
// a row after the new code ships, so normalize both shapes rather than assuming every persisted payload
242+
// already matches the current type (which only the type checker, not the durable queue, enforces).
243+
const legacyMessage = message as unknown as { events?: DetectedNotificationEvent[]; event?: DetectedNotificationEvent };
244+
const events = Array.isArray(legacyMessage.events) ? legacyMessage.events : legacyMessage.event ? [legacyMessage.event] : [];
245+
const deliveries = (
246+
await mapWithConcurrency(events, NOTIFY_EVALUATE_EVENT_CONCURRENCY, (event) => evaluateNotificationEvent(env, event))
247+
).flat();
248+
await Promise.all(
249+
deliveries.map((delivery) =>
250+
env.JOBS.send({
251+
type: "notify-deliver",
252+
requestedBy: "notify-evaluate",
253+
deliveryId: delivery.id,
254+
}),
255+
),
256+
);
257+
return;
258+
}
259+
case "notify-deliver":
260+
await deliverNotification(env, message.deliveryId);
261+
return;
262+
case "ops-alerts":
263+
// Convergence (ops / observability, flag GITTENSORY_REVIEW_OPS). Defense-in-depth: the cron only ENQUEUES this
264+
// when the flag is ON, but a stale in-flight job that lands after a flag-flip must still no-op, so
265+
// flag-OFF does zero work here too. Read-only telemetry — never throws into the queue.
266+
if (isOpsEnabled(env)) await runOpsAlerts(env);
267+
return;
268+
case "sweep-liveness-watchdog":
269+
// Self-heal (flag GITTENSORY_SWEEP_WATCHDOG). Defense-in-depth: the cron only ENQUEUES this when the flag
270+
// is ON, but a stale in-flight job that lands after a flag-flip must still no-op, so flag-OFF does zero
271+
// work here too. Fails safe internally — never throws into the queue.
272+
if (isSweepWatchdogEnabled(env)) await runSweepLivenessWatchdog(env);
273+
return;
274+
case "reconcile-open-prs":
275+
// Self-heal (flag GITTENSORY_PR_RECONCILIATION). Defense-in-depth: the cron only ENQUEUES this when the
276+
// flag is ON, but a stale in-flight job that lands after a flag-flip must still no-op, so flag-OFF does
277+
// zero work here too. Fails safe internally — never throws into the queue.
278+
if (isPrReconciliationEnabled(env)) await runOpenPrReconciliation(env);
279+
return;
280+
case "selftune":
281+
// Convergence (self-improve / auto-tune, flag GITTENSORY_REVIEW_SELFTUNE). Defense-in-depth: the cron only
282+
// ENQUEUES this when the flag is ON, but a stale in-flight job that lands after a flag-flip must still
283+
// no-op, so flag-OFF does zero work here too. TIGHTENING-ONLY + shadow-soak + audited; never throws into
284+
// the queue (runSelfTune fails safe).
285+
if (isSelfTuneEnabled(env)) {
286+
await runSelfTune(env);
287+
// GAP-4 accuracy circuit-breaker: read the gate-eval confusion matrix over the recorded pr_outcome
288+
// ground truth and ENGAGE holdonly (would-merge → hold) for any repo whose merge precision dropped
289+
// below the floor, plus AUTO-CLEAR a recovered breaker. Fail-safe: with no pr_outcome history the eval
290+
// reads neutral → nothing engages → byte-identical. (applyAutoTune / maybeAutoClearHoldOnly, previously
291+
// unwired — zero call-sites.)
292+
await runSelfTuneBreaker(env);
293+
}
294+
return;
295+
case "rag-index-repo":
296+
// Convergence (RAG / codebase index, flag GITTENSORY_REVIEW_RAG). Defense-in-depth: the cron + webhook only
297+
// ENQUEUE this when the flag is ON, but a stale in-flight job that lands after a flag-flip must still no-op,
298+
// so flag-OFF does zero work here too. indexRepo / reindexChangedPaths are fully fail-safe (never throw).
299+
if (isRagEnabled(env))
300+
await runRagIndexJob(
301+
env,
302+
message.requestedBy,
303+
message.repoFullName,
304+
message.paths,
305+
);
306+
return;
307+
case "recapture-preview":
308+
// Delayed visual self-poll: re-review the PR to re-capture the AFTER preview shot once its deploy is live.
309+
await reReviewStoredPullRequest(
310+
env,
311+
message.deliveryId,
312+
message.installationId,
313+
message.repoFullName,
314+
message.prNumber,
315+
message.attempt,
316+
);
317+
break;
318+
case "github-webhook":
319+
await processGitHubWebhook(
320+
env,
321+
message.deliveryId,
322+
message.eventName,
323+
message.payload,
324+
);
325+
return;
326+
case "submit-draft":
327+
// Public OAuth draft-submission (GITTENSORY_REVIEW_DRAFT). No-ops internally when the flag is off.
328+
await processSubmitDraft(env, message.draftId);
329+
return;
330+
case "retry-orb-relay":
331+
// Orb relay retry (#relay-retry): re-attempt events that failed to reach a brokered self-host container
332+
// (container was temporarily down). Enqueued by the cron ONLY when ORB_BROKER_ENABLED is set; a stale
333+
// in-flight job that arrives after the flag clears is still safe — retryFailedRelays fails open (no-op on
334+
// an empty table). Never throws.
335+
await retryFailedRelays(env);
336+
return;
337+
}
338+
}

0 commit comments

Comments
 (0)